commit 8c83ff764c9225c1353a7fd209df7ba2da6e1961 Author: Karl Determe Date: Wed Jul 22 14:48:40 2026 +0200 Initial commit of clean Core diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4118205 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,43 @@ +# Git +.git +.gitignore + +# Environments and Virtualenvs +venv/ +venv_old/ +.env +.env.local +.env.production +.env.development +.env.staging + +# Python +**/__pycache__/ +**/*.pyc +**/*.pyo +**/*.pyd +.Python +**/*.egg-info/ +.eggs/ +.pytest_cache/ + +# Databases +*.sqlite3 + +# Django media & static +streetup/media/ +streetup/private_media/ +streetup/static/ +streetup/django.log +*.log + +# VS Code / Editor config +.vscode/ +.idea/ +*.swp +*~ + +# Docker files +Dockerfile +docker-compose.yml +.dockerignore diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..995dd4e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,105 @@ +name: Build and Deploy StreetUp + +on: + workflow_dispatch: + inputs: + environment: + description: 'Environnement de déploiement (test ou production)' + required: true + default: 'test' + type: choice + options: + - test + - production + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Log in to the Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=test,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'test' }} + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production' }} + type=raw,value=test,enable=${{ github.event_name != 'workflow_dispatch' && github.ref == 'refs/heads/main' }} + type=raw,value=latest,enable=${{ github.event_name != 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push Docker image + uses: docker/build-push-action@v7 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Determine Target Environment + id: target + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + ENV_CHOICE="${{ github.event.inputs.environment }}" + else + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + ENV_CHOICE="production" + else + ENV_CHOICE="test" + fi + fi + + if [[ "$ENV_CHOICE" == "production" ]]; then + echo "env=production" >> $GITHUB_OUTPUT + echo "host=${{ secrets.SSH_HOST_PROD }}" >> $GITHUB_OUTPUT + echo "path=/home/kdeterme/streetup-prod" >> $GITHUB_OUTPUT + else + echo "env=test" >> $GITHUB_OUTPUT + echo "host=${{ secrets.SSH_HOST_TEST }}" >> $GITHUB_OUTPUT + echo "path=/home/kdeterme/streetup-test" >> $GITHUB_OUTPUT + fi + + - name: Deploy to Server via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ steps.target.outputs.host }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + cd ${{ steps.target.outputs.path }} + # Pull de la nouvelle image + docker compose pull + # Relance des conteneurs + docker compose up -d + # Exécution des migrations de base de données + docker compose exec -T web python manage.py migrate --noinput + # Collecte des fichiers statiques + docker compose exec -T web python manage.py collectstatic --noinput --clear diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f36b0d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.env +.venv +venv/ +venv_old/ +env/ +node_modules/ +.well-known/ +*.log +*.md +!streetup/common/help_docs/ +!streetup/common/help_docs/**/*.md + +media/ +private_media/ +/static/ +android/ +backups/ +exports/ +external_files/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +streetup/.env + +scripts/ + +carl_export/ +carl_export.* + +streetup/static/ + +deploy.sh +generated_passwords.txt +streetup/generated_passwords.txt +scratch/ +Caddyfile +Caddyfile.prod +.vscode/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..417e79e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,111 @@ +# Utilisation de l'image officielle Miniforge (basée sur Debian) +FROM condaforge/miniforge3:26.3.2-3 + +# Éviter l'écriture de fichiers .pyc et forcer le buffering immédiat des logs +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Installation des paquets système requis pour le runtime (ex: OpenGL et GLib pour OpenCV/YOLO) +RUN apt-get update && apt-get install -y --no-install-recommends \ + gnupg \ + ca-certificates \ + curl \ + && curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg \ + && (. /etc/os-release && echo "deb http://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" > /etc/apt/sources.list.d/pgdg.list) \ + && apt-get update && apt-get install -y --no-install-recommends \ + libgl1 \ + libglib2.0-0 \ + fonts-dejavu \ + fonts-liberation \ + postgresql-client-18 \ + rsync \ + openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# Création de l'environnement conda avec les packages cartographiques et poppler +# gdal, geos, proj et poppler sont installés depuis conda-forge +RUN mamba create -n streetup-env -y -c conda-forge \ + python=3.12 \ + gdal \ + geos \ + proj \ + poppler \ + && mamba clean -afy + +# Activation par défaut de l'environnement Conda dans le conteneur +ENV PATH=/opt/conda/envs/streetup-env/bin:$PATH +ENV CONDA_DEFAULT_ENV=streetup-env + +# Configuration explicite des variables GIS pour GeoDjango +ENV GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so +ENV GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so +ENV PROJ_DATA=/opt/conda/envs/streetup-env/share/proj + +# Argument de build pour activer l'installation des librairies IA (ultralytics, OpenCV...) +# Passer INSTALL_AI_LIBS=true dans le docker-compose pour les inclure +ARG INSTALL_AI_LIBS=false + +# Copie et installation des dépendances Python +COPY requirements/ /app/requirements/ +RUN pip install --no-cache-dir -r requirements/base.txt && \ + if [ "$INSTALL_AI_LIBS" = "true" ]; then \ + pip install --no-cache-dir -r requirements/ai.txt; \ + fi + +# 1. Copie des répertoires volumineux et statiques (très bonne mise en cache) +COPY streetup/road_detection/detection_models/ /app/road_detection/detection_models/ +COPY streetup/scripts/ /app/scripts/ +COPY streetup/common/ /app/common/ +COPY streetup/home/ /app/home/ + +# 2. Copie des modules de l'application qui changent rarement +COPY streetup/locale/ /app/locale/ +COPY streetup/templates/ /app/templates/ +COPY streetup/analytics/ /app/analytics/ +COPY streetup/api/ /app/api/ +COPY streetup/authorizations/ /app/authorizations/ +COPY streetup/contracts/ /app/contracts/ +COPY streetup/documents/ /app/documents/ +COPY streetup/maps/ /app/maps/ +COPY streetup/notifications/ /app/notifications/ +COPY streetup/projects/ /app/projects/ +COPY streetup/reporting/ /app/reporting/ +COPY streetup/schools/ /app/schools/ +COPY streetup/sign/ /app/sign/ +COPY streetup/stock/ /app/stock/ + +# 3. Copie des modules actifs ou modifiés fréquemment +COPY streetup/streetup/ /app/streetup/ +COPY streetup/mobile/ /app/mobile/ +COPY streetup/observations/ /app/observations/ +COPY streetup/controls/ /app/controls/ +COPY streetup/interventions/ /app/interventions/ +COPY streetup/assets/ /app/assets/ + +# 4. Copie des fichiers de code de road_detection (sans réimporter les modèles de détection) +COPY streetup/road_detection/__init__.py /app/road_detection/ +COPY streetup/road_detection/admin.py /app/road_detection/ +COPY streetup/road_detection/apps.py /app/road_detection/ +COPY streetup/road_detection/detection.py /app/road_detection/ +COPY streetup/road_detection/forms.py /app/road_detection/ +COPY streetup/road_detection/models.py /app/road_detection/ +COPY streetup/road_detection/urls.py /app/road_detection/ +COPY streetup/road_detection/views.py /app/road_detection/ +COPY streetup/road_detection/migrations/ /app/road_detection/migrations/ +COPY streetup/road_detection/templates/ /app/road_detection/templates/ + +# 5. Copie des fichiers Python et de configuration restants à la racine +COPY streetup/*.py /app/ +COPY streetup/generated_passwords.txt* /app/ + +# Exposer le port par défaut +EXPOSE 8000 + +# Entrypoint de production : Gunicorn avec workers/timeout configurables via variables d'environnement +# GUNICORN_WORKERS (défaut: 4) et GUNICORN_TIMEOUT (défaut: 120) peuvent être surchargés dans le docker-compose +# En développement, "command: python manage.py runserver..." dans docker-compose.yml court-circuite cet entrypoint +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh +CMD ["/docker-entrypoint.sh"] diff --git a/doc/DOCUMENTATION.html b/doc/DOCUMENTATION.html new file mode 100644 index 0000000..7bfcd86 --- /dev/null +++ b/doc/DOCUMENTATION.html @@ -0,0 +1,859 @@ + + + + + + Documentation StreetUp + + + +
+

Documentation de l'application StreetUp

Table des matières

1. Vue d'ensemble +2. Architecture de l'application +3. Modules Django +4. Schéma des modèles et relations +5. Détail des modèles par module +6. Flux de données +7. Diagrammes

---

Vue d'ensemble

StreetUp est un système complet de gestion des actifs de voirie (Street Asset Management System). L'application permet de gérer, suivre et maintenir différents types d'infrastructures de rue telles que :

L'application est construite avec Django et utilise PostGIS pour la gestion des données géospatiales.

---

Architecture de l'application

streetup/
+├── streetup/           # Configuration principale Django
+│   ├── settings.py     # Paramètres de l'application
+│   ├── urls.py         # Routes principales
+│   └── wsgi.py         # Point d'entrée WSGI
+│
+├── common/             # Module commun (thématiques, utilisateurs, documents)
+├── assets/             # Gestion des actifs (équipements de voirie)
+├── interventions/      # Gestion des interventions de maintenance
+├── contracts/          # Gestion des contrats et commandes
+├── projects/           # Gestion des projets
+├── observations/       # Signalements et observations terrain
+├── controls/           # Contrôles et rapports de chantier
+├── documents/          # Gestion documentaire avancée
+├── stock/              # Gestion du stock et des entrepôts
+├── api/                # API REST
+├── home/               # Page d'accueil
+└── mobile/             # Interface mobile
+

---

Modules Django

1. Module common - Données communes

Le module common contient les modèles partagés par l'ensemble de l'application.

ModèleDescription +--------------------- +ThematicThématiques (feux, éclairage, signalisation, etc.) +RoleRôles utilisateurs (manager, technicien, admin) +UserConfigConfiguration utilisateur et permissions +UserThematicsAccès utilisateur par thématique +UserContractAccessAccès utilisateur par contrat +DocumentClasse abstraite pour les documents +MunicipalityCommunes +AdrStreetNoms de rues +AdrPointPoints d'adresse

Liste des thématiques

Nom (FR)Nom (NL)CodeIcône BootstrapCouleur +---------------------------------------------------- +Centres de contrôlesControl centerscontrolcentersbi-building-gear#000000 +Eclairage publicOpenbare verlichtingpubliclightingbi-lightbulb#000000 +Feux tricoloresVerkeerslichtentrafficlightsbi-stoplights#000000 +ITSITSitsbi-lightning#000000 +NatureNatuurnaturebi-suit-club#00ff00 +Ouvrages d'artKunstwerkenstructuresbi-rainbow#000000 +Patrimoine artistiqueArtistiek erfgoedartworksbi-bank#0000aa +RoadsRoadsroadsbi-fullscreen-exit#000000 +SignalisationSignalisatiesignbi-signpost#000000

2. Module assets - Gestion des actifs

Le module assets gère tous les équipements et infrastructures de voirie, organisés par sous-domaine.

Classes abstraites de base

ModèleDescription +--------------------- +AbstractLocationLocalisation abstraite (code, nom, coordonnées) +AbstractAssetModelModèle d'équipement (marque, catégorie) +AbstractAssetÉquipement abstrait (statut, dates d'installation) +AbstractGeoAssetÉquipement géolocalisé +AssetCategoryCatégorie d'équipement

Feux de signalisation (Traffic Lights)

ModèleDescription +--------------------- +TrafficLightIntersectionCarrefour à feux +TrafficLightPolePoteau de feu +TrafficLightPoleModelModèle de poteau +TrafficLightLanternLanterne +TrafficLightLanternModelModèle de lanterne +TrafficLightCableCâble +TrafficLightCableModelModèle de câble +TrafficLightControllerContrôleur +TrafficLightControllerModelModèle de contrôleur +TrafficLightDetectorDétecteur +TrafficLightDetectorModelModèle de détecteur +TrafficLightContractContrat lié à un carrefour +TrafficLightIntersectionDocumentDocument associé

Éclairage public (Public Lighting)

ModèleDescription +--------------------- +PublicLightingStreetRue d'éclairage public +PublicLightingPolePoteau d'éclairage +PublicLightingPoleModelModèle de poteau +PublicLightingLightFixtureLuminaire +PublicLightingLightFixtureModelModèle de luminaire +PublicLightingStreetDocumentDocument associé

Signalisation routière (Road Signs)

ModèleDescription +--------------------- +SignStreetRue pour la signalisation +SignPolePoteau de signalisation +SignPoleModelModèle de poteau +SignPanelPanneau de signalisation +SignPanelModelModèle de panneau +SignPanelTypeType de panneau +SignStreetDocumentDocument associé

Ouvrages d'art (Structures)

ModèleDescription +--------------------- +StructureOuvrage d'art (tunnel, pont, viaduc...) +StructureLocationLocalisation dans l'ouvrage +StructureGeoAssetÉquipement géolocalisé dans l'ouvrage +StructureAssetModelModèle d'équipement +StructureContractContrat lié à l'ouvrage +StructureDocumentDocument associé

Nature / Espaces verts

ModèleDescription +--------------------- +NatureLocationZone naturelle +NatureGreenSurfaceSurface végétale +NatureTreeArbre +NatureAssetModelModèle d'équipement nature +NatureGreenSurfaceDocumentDocument associé

Centres de contrôle (Control Centers)

ModèleDescription +--------------------- +ControlCenterCentre de contrôle +ControlCenterLocationLocalisation dans le centre de contrôle +ControlCenterGeoAssetÉquipement géolocalisé +ControlCenterAssetModelModèle d'équipement +ControlCenterContractContrat lié au centre +ControlCenterGeoAssetContractContrat lié à un équipement +ControlCenterDocumentDocument du centre +ControlCenterAssetDocumentDocument d'équipement

Patrimoine artistique (Artworks)

ModèleDescription +--------------------- +ArtworkStreetRue pour le patrimoine artistique +ArtworkMonumentMonument (statue, sculpture, structure...) +ArtworkFountainFontaine artistique +ArtworkFountainDocumentDocument de fontaine +ArtworkMonumentDocumentDocument de monument

ITS - Systèmes de transport intelligents (Intelligent Transport Systems)

ModèleDescription +--------------------- +ITSLocationLocalisation ITS +ITSGeoAssetÉquipement ITS géolocalisé +ITSAssetModelModèle d'équipement ITS +ITSLocationDocumentDocument de localisation +ITSLocationContractContrat lié à une localisation +ITSGeoAssetContractContrat lié à un équipement

Chaussées, trottoirs et mobilier urbain (Roads)

ModèleDescription +--------------------- +RoadStreetRue +RoadwayChaussée +RoadwayModelModèle de chaussée +RoadwayFunctionFonctions de la chaussée (piéton, vélo, transport) +RoadSidewalkTrottoir +RoadSidewalkModelModèle de trottoir +RoadFurnitureMobilier urbain +RoadFurnitureModelModèle de mobilier urbain +RoadStreetDocumentDocument associé

3. Module interventions - Gestion des interventions

Ce module gère tout le cycle de vie des interventions de maintenance.

ModèleDescription +--------------------- +InterventionIntervention (corrective, préventive, améliorative) +InterventionSequenceNumérotation automatique des interventions +InterventionTimeLineHistorique des événements +InterventionPlanificationTimeLinePlanification +InterventionRealizationTimeLineRéalisation +InterventionOccupationTemps de travail +InterventionNoteNotes et commentaires +InterventionDocumentDocuments associés +InterventionAssetLien vers les équipements +InterventionLocationLien vers les localisations +InterventionLinkLiens entre interventions +InterventionContractPostPostes de contrat utilisés +InterventionPostPeriodQuantityQuantités validées par période +SymptomSymptôme/problème signalé +SymptomRecommendationRecommandation associée au symptôme +ReflexSheetFiche réflexe +SourceCategorySource de l'intervention

Opérations de maintenance préventive

ModèleDescription +--------------------- +OperationTemplateTemplate d'opérations +OperationTemplateItemItem du template +OperationOpération à réaliser +OperationStatusSetEnsemble de statuts possibles +OperationStatusStatut d'opération +MeasurementDefinitionDéfinition de mesure

4. Module contracts - Gestion des contrats

ModèleDescription +--------------------- +CompanySociété/Organisme +CompanyTeamÉquipe +CompanyMemberMembre d'équipe +ContractContrat de maintenance +ContractPostPoste de contrat (bordereau de prix) +ContractOrderCommande +ContractOrderQuoteDevis +ContractOrderQuoteItemLigne de devis +ContractOrderDocumentDocument de commande +ContractOrderProgressStateÉtat d'avancement +ContractOrderProgressStateItemItem d'état d'avancement

5. Module projects - Gestion des projets

ModèleDescription +--------------------- +ProjectProjet +ProjectSequenceNumérotation automatique +ProjectPhasePhase de projet +ProjectMemberMembre du projet +ProjectInterventionIntervention liée au projet +ProjectDocumentDocument du projet

6. Module observations - Observations terrain

ModèleDescription +--------------------- +ObservationObservation/signalement +ObservationPhotoPhoto de l'observation +ObservationCommentCommentaire

7. Module controls - Contrôles et rapports

ModèleDescription +--------------------- +ControlContrôle/rapport de chantier +CategoryCatégorie de contrôle +ControlCheckPoint de contrôle +DocumentCheckVérification documentaire +CheckListListe de contrôle +CheckListControlCheckItem de checklist +CheckListDocumentCheckDocument de checklist +ControlReportRapport généré +ControlReferencePlanPlan de référence +RemarkRemarque +RemarkImageImage de remarque +RemarkTagTag de remarque +ControlReportTemplateTemplate de rapport +ControlQuantityQuantité dans une remarque

8. Module documents - Gestion documentaire avancée

ModèleDescription +--------------------- +DocumentTypeType de document +DocumentTagTag de document +DocumentFolderDossier +ManagedDocumentDocument géré +DocumentVersionVersion de document +DocumentValidationValidation de document +DocumentSharePartage de document +DocumentFolderSharePartage de dossier +DocumentAccessLinkLien d'accès +DocumentFolderAccessLinkLien d'accès dossier +DocumentAttachmentPièce jointe +DocumentFolderAttachmentAttachement de dossier

9. Module stock - Gestion du stock

ModèleDescription +--------------------- +WarehouseEntrepôt +WarehouseLocationEmplacement dans l'entrepôt +CategoryCatégorie de produit +ProductProduit +StockQuantité en stock +StockMovementMouvement de stock +InventoryInventaire +InventoryItemLigne d'inventaire +SupplierFournisseur +PurchaseOrderCommande d'achat +PurchaseOrderItemLigne de commande +StockAlertAlerte de stock +PreparationOrderOrdre de préparation +PreparationOrderItemLigne de préparation

---

Schéma des modèles et relations

Diagramme des relations principales

┌─────────────────────────────────────────────────────────────────────────────────────┐
+│                                    THÉMATIQUES                                       │
+│                                                                                       │
+│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐              │
+│  │   Feux de   │   │  Éclairage  │   │Signalisation│   │  Ouvrages   │              │
+│  │signalisation│   │   public    │   │  routière   │   │   d'art     │              │
+│  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘   └──────┬──────┘              │
+│         │                 │                 │                 │                      │
+│         ▼                 ▼                 ▼                 ▼                      │
+│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐              │
+│  │Intersection │   │   Street    │   │   Street    │   │  Structure  │              │
+│  │  (Location) │   │  (Location) │   │  (Location) │   │  (Location) │              │
+│  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘   └──────┬──────┘              │
+│         │                 │                 │                 │                      │
+│         ▼                 ▼                 ▼                 ▼                      │
+│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐              │
+│  │    Pôles    │   │    Pôles    │   │    Pôles    │   │   Assets    │              │
+│  │  Lanternes  │   │  Luminaires │   │  Panneaux   │   │             │              │
+│  │  Câbles     │   │             │   │             │   │             │              │
+│  │ Contrôleurs │   │             │   │             │   │             │              │
+│  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘   └──────┬──────┘              │
+└─────────┼─────────────────┼─────────────────┼─────────────────┼──────────────────────┘
+          │                 │                 │                 │
+          └────────────┬────┴─────────────────┴─────────────────┘
+                       │
+                       ▼
+          ┌───────────────────────┐
+          │     INTERVENTIONS     │
+          │                       │
+          │  - Correctives        │
+          │  - Préventives        │
+          │  - Amélioratives      │
+          └───────────┬───────────┘
+                      │
+          ┌───────────┴───────────┐
+          │                       │
+          ▼                       ▼
+┌─────────────────┐     ┌─────────────────┐
+│    CONTRATS     │     │     PROJETS     │
+│                 │     │                 │
+│  - Commandes    │     │  - Phases       │
+│  - Postes       │     │  - Membres      │
+│  - Devis        │     │  - Documents    │
+└────────┬────────┘     └─────────────────┘
+         │
+         ▼
+┌─────────────────┐
+│  OBSERVATIONS   │
+│                 │
+│  - Photos       │
+│  - Commentaires │
+└─────────────────┘
+

Relations détaillées par domaine

1. Relations Thematic (Thématiques)

Thematic
+    │
+    ├──► AssetCategory (1:N)
+    │        │
+    │        └──► Assets, Symptoms, etc.
+    │
+    ├──► Project (M:N via thematics)
+    │
+    ├──► Control (M:N via thematics)
+    │
+    ├──► DocumentFolder (M:N)
+    │
+    ├──► Intervention (1:N)
+    │
+    ├──► Observation (1:N)
+    │
+    ├──► Symptom (1:N)
+    │
+    └──► OperationTemplate (1:N)
+

2. Relations Company (Société)

Company
+    │
+    ├──► Contract (1:N) ───► ContractPost (1:N)
+    │        │                    │
+    │        │                    └──► InterventionContractPost
+    │        │
+    │        ├──► ContractOrder (1:N)
+    │        │        │
+    │        │        ├──► ContractOrderQuote
+    │        │        │        │
+    │        │        │        └──► ContractOrderQuoteItem (1:N)
+    │        │        │
+    │        │        ├──► ContractOrderDocument (1:N)
+    │        │        │
+    │        │        └──► ContractOrderProgressState (1:N)
+    │        │
+    │        └──► Intervention (1:N)
+    │
+    ├──► CompanyTeam (1:N)
+    │        │
+    │        └──► CompanyMember (1:N)
+    │                 │
+    │                 └──► Intervention (assigned_member)
+    │                 │
+    │                 └──► InterventionOccupation (1:N)
+    │
+    └──► Symptom (1:N - provider)
+

3. Relations Intervention

Intervention
+    │
+    ├──► InterventionTimeLine (1:N) - Historique des statuts
+    │
+    ├──► InterventionPlanificationTimeLine (1:N) - Planification
+    │
+    ├──► InterventionRealizationTimeLine (1:N) - Réalisation
+    │
+    ├──► InterventionOccupation (1:N) - Temps de travail
+    │
+    ├──► InterventionNote (1:N) - Notes
+    │
+    ├──► InterventionDocument (1:N) - Documents
+    │
+    ├──► InterventionContractPost (1:N) - Postes de contrat
+    │        │
+    │        └──► InterventionPostPeriodQuantity (1:N)
+    │
+    ├──► InterventionAsset (1:N) - GenericForeignKey vers assets
+    │
+    ├──► InterventionLocation (1:N) - GenericForeignKey vers locations
+    │
+    ├──► InterventionLink (M:N) - Liens entre interventions
+    │
+    ├──► Operation (1:N) - Opérations préventives
+    │
+    ├──► Observation (1:N) - Observations liées
+    │
+    └──► Control (1:N) - Contrôles liés
+

4. Relations Operations (Maintenance préventive)

OperationTemplate
+    │
+    ├──► OperationTemplateItem (1:N)
+    │        │
+    │        ├──► OperationStatusSet (N:1)
+    │        │
+    │        └──► MeasurementDefinition (N:1)
+    │
+    └──► Intervention.equipment_operation_templates (M:N)

Operation + │ + ├──► Intervention (N:1) + │ + ├──► OperationStatusSet (N:1) + │ + ├──► OperationStatus (N:1) + │ + ├──► MeasurementDefinition (N:1) + │ + └──► Asset (GenericForeignKey) +

5. Relations Traffic Lights (Feux de signalisation)

TrafficLightIntersection
+    │
+    ├──► TrafficLightPole (1:N)
+    │        │
+    │        ├──► TrafficLightLantern (1:N)
+    │        │
+    │        └──► TrafficLightDetector (1:N)
+    │
+    ├──► TrafficLightCable (1:N)
+    │
+    ├──► TrafficLightController (1:N)
+    │
+    ├──► TrafficLightIntersectionDocument (1:N)
+    │
+    ├──► TrafficLightContract (1:N)
+    │
+    └──► Municipality (N:1)
+

6. Relations Project (Projets)

Project
+    │
+    ├──► ProjectPhase (1:N)
+    │        │
+    │        ├──► ProjectIntervention (1:N)
+    │        │
+    │        └──► Control (1:N)
+    │
+    ├──► ProjectMember (1:N) ───► User
+    │
+    ├──► ProjectDocument (1:N)
+    │
+    ├──► Thematic (M:N)
+    │
+    └──► ContractOrder (1:N via project)
+

7. Relations Control (Contrôles)

Control
+    │
+    ├──► ControlCheck (1:N)
+    │
+    ├──► DocumentCheck (1:N)
+    │
+    ├──► ControlReport (1:N)
+    │
+    ├──► ControlReferencePlan (1:N)
+    │        │
+    │        └──► Remark (1:N)
+    │                 │
+    │                 ├──► RemarkImage (1:N)
+    │                 │
+    │                 ├──► RemarkTag (1:N)
+    │                 │
+    │                 └──► ControlQuantity (1:N)
+    │
+    ├──► ControlReportTemplate (1:N)
+    │
+    ├──► Project (N:1)
+    │
+    ├──► ProjectPhase (N:1)
+    │
+    └──► Intervention (N:1)
+

8. Relations Documents

ManagedDocument
+    │
+    ├──► DocumentVersion (1:N)
+    │        │
+    │        └──► DocumentValidation (1:N)
+    │
+    ├──► DocumentType (M:N)
+    │
+    ├──► DocumentTag (M:N)
+    │
+    ├──► DocumentFolder (M:N)
+    │
+    ├──► DocumentShare (1:N) ───► User
+    │
+    ├──► DocumentAccessLink (1:N)
+    │
+    └──► DocumentAttachment (1:N) - GenericForeignKey

DocumentFolder + │ + ├──► DocumentFolderShare (1:N) ───► User + │ + ├──► DocumentFolderAccessLink (1:N) + │ + ├──► DocumentFolderAttachment (1:N) - GenericForeignKey + │ + ├──► Thematic (M:N) + │ + └──► parent_folders (M:N - self-referential) +

9. Relations Stock

Warehouse
+    │
+    ├──► WarehouseLocation (1:N)
+    │        │
+    │        ├──► Stock (1:N)
+    │        │
+    │        ├──► StockMovement (N:1 source/destination)
+    │        │
+    │        └──► InventoryItem (N:1)
+    │
+    ├──► Inventory (1:N)
+    │        │
+    │        └──► InventoryItem (1:N)
+    │
+    ├──► PurchaseOrder (1:N)
+    │        │
+    │        └──► PurchaseOrderItem (1:N)
+    │
+    └──► StockAlert (1:N)

Product + │ + ├──► Category (N:1) + │ + ├──► Stock (1:N) + │ + ├──► StockMovement (1:N) + │ + ├──► PurchaseOrderItem (1:N) + │ + ├──► InventoryItem (1:N) + │ + ├──► StockAlert (1:N) + │ + └──► PreparationOrderItem (1:N) +

---

Détail des modèles par module

Statuts d'intervention

STATUS_CHOICES = {
+    'in_preparation': 'En préparation',      # Ordre: 1
+    'to_be_approved': 'A approuver',          # Ordre: 2
+    'to_be_planned': 'A planifier',           # Ordre: 5
+    'to_be_processed': 'A traiter',           # Ordre: 11
+    'in_progress': 'Démarré',                 # Ordre: 21
+    'on_pause': 'En pause',                   # Ordre: 22
+    'finished': 'Terminé',                    # Ordre: 25
+    'processed': 'A valider',                 # Ordre: 31
+    'to_be_corrected': 'A corriger',          # Ordre: 41
+    'corrected': 'Corrigé',                   # Ordre: 45
+    'validated': 'Validé',                    # Ordre: 51
+    'invoiced': 'Facturé',                    # Ordre: 61
+    'closed': 'Finalisé',                     # Ordre: 71
+    'canceled': 'Annulé',                     # Ordre: 200
+}
+

Types de maintenance

MAINTAIN_CHOICES = [
+    ('corrective', 'Maintenance corrective'),
+    ('preventive', 'Maintenance préventive'),
+    ('ameliorative', 'Maintenance améliorative'),
+]
+

Types d'opération

TYPE_CHOICES = [
+    ('fixing', 'Dépannage'),
+    ('repair', 'Réparation'),
+    ('replace', 'Remplacement'),
+    ('care', 'Entretien'),
+    ('control', 'Contrôle'),
+    ('inspection', 'Inspection'),
+    ('build', 'Construction'),
+]
+

Priorités

PRIORITY_CHOICES = {
+    '1': 'Urgent',
+    '2': 'Elevé',
+    '3': 'Normal',
+    '4': 'Bas',
+}
+

---

Flux de données

Flux de création d'intervention

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
+│ Observation │────►│   Symptom   │────►│ Intervention│
+│   (terrain) │     │ (problème)  │     │  (création) │
+└─────────────┘     └─────────────┘     └──────┬──────┘
+                                               │
+                    ┌──────────────────────────┼──────────────────────────┐
+                    │                          │                          │
+                    ▼                          ▼                          ▼
+           ┌─────────────┐            ┌─────────────┐            ┌─────────────┐
+           │   Contrat   │            │   Location  │            │   Company   │
+           │  (liaison)  │            │   (lieu)    │            │ (assignation)│
+           └─────────────┘            └─────────────┘            └─────────────┘
+

Flux de maintenance préventive

┌─────────────────┐
+│OperationTemplate│
+│   (templates)   │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐     ┌─────────────────┐
+│TemplateItems    │────►│   Intervention  │
+│ (opérations)    │     │  (préventive)   │
+└─────────────────┘     └────────┬────────┘
+                                 │
+                                 ▼
+                        ┌─────────────────┐
+                        │   Operations    │
+                        │ (à effectuer)   │
+                        └────────┬────────┘
+                                 │
+                    ┌────────────┼────────────┐
+                    │            │            │
+                    ▼            ▼            ▼
+             ┌───────────┐ ┌───────────┐ ┌───────────┐
+             │  Status   │ │  Mesure   │ │  Asset    │
+             │(validation)│ │  (valeur) │ │  (lié)    │
+             └───────────┘ └───────────┘ └───────────┘
+

Flux de gestion des contrats

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
+│   Company   │────►│   Contract  │────►│ ContractPost│
+│ (entreprise)│     │  (marché)   │     │ (bordereau) │
+└─────────────┘     └──────┬──────┘     └──────┬──────┘
+                           │                    │
+                           ▼                    │
+                    ┌─────────────┐             │
+                    │ContractOrder│             │
+                    │ (commande)  │             │
+                    └──────┬──────┘             │
+                           │                    │
+              ┌────────────┼────────────┐       │
+              │            │            │       │
+              ▼            ▼            ▼       │
+       ┌───────────┐ ┌───────────┐ ┌───────────┐
+       │   Quote   │ │ Documents │ │ Progress  │
+       │  (devis)  │ │           │ │  State    │
+       └───────────┘ └───────────┘ └───────────┘
+                                          │
+                                          ▼
+                                   ┌─────────────┐
+                                   │Intervention │
+                                   │ContractPost │◄──────┘
+                                   └─────────────┘
+

---

Diagrammes

Architecture globale

┌─────────────────────────────────────────────────────────────────────────────────────┐
+│                                   STREETUP                                           │
+│                        Système de gestion des actifs de voirie                       │
+├─────────────────────────────────────────────────────────────────────────────────────┤
+│                                                                                       │
+│  ┌───────────────────────────────────────────────────────────────────────────────┐  │
+│  │                              COUCHE PRÉSENTATION                               │  │
+│  │                                                                                 │  │
+│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐           │  │
+│  │  │    Web UI   │  │  Mobile UI  │  │   API REST  │  │   Reports   │           │  │
+│  │  │  (Django)   │  │  (Django)   │  │   (Django)  │  │    (PDF)    │           │  │
+│  │  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘           │  │
+│  └───────────────────────────────────────────────────────────────────────────────┘  │
+│                                         │                                            │
+│  ┌───────────────────────────────────────────────────────────────────────────────┐  │
+│  │                               COUCHE MÉTIER                                    │  │
+│  │                                                                                 │  │
+│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│  │
+│  │  │  Assets  │ │Interven- │ │ Contracts│ │ Projects │ │ Controls │ │  Stock   ││  │
+│  │  │          │ │  tions   │ │          │ │          │ │          │ │          ││  │
+│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘│  │
+│  │                                                                                 │  │
+│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐                                        │  │
+│  │  │Observa-  │ │Documents │ │  Common  │                                        │  │
+│  │  │  tions   │ │          │ │          │                                        │  │
+│  │  └──────────┘ └──────────┘ └──────────┘                                        │  │
+│  └───────────────────────────────────────────────────────────────────────────────┘  │
+│                                         │                                            │
+│  ┌───────────────────────────────────────────────────────────────────────────────┐  │
+│  │                              COUCHE DONNÉES                                    │  │
+│  │                                                                                 │  │
+│  │  ┌─────────────────────────────────────────────────────────────────────────┐  │  │
+│  │  │                     PostgreSQL + PostGIS                                 │  │  │
+│  │  │                                                                          │  │  │
+│  │  │   ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐          │  │  │
+│  │  │   │  Users  │ │Thematics│ │ Assets  │ │Interven-│ │Contracts│          │  │  │
+│  │  │   │         │ │         │ │ + Geo   │ │  tions  │ │         │          │  │  │
+│  │  │   └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘          │  │  │
+│  │  └─────────────────────────────────────────────────────────────────────────┘  │  │
+│  └───────────────────────────────────────────────────────────────────────────────┘  │
+│                                                                                       │
+└─────────────────────────────────────────────────────────────────────────────────────┘
+

Hiérarchie des classes d'actifs

                            AbstractLocation
+                                   │
+           ┌───────────────────────┼───────────────────────┐
+           │                       │                       │
+           ▼                       ▼                       ▼
+  TrafficLightIntersection  PublicLightingStreet    SignStreet
+           │                       │                       │
+           │                       │                       │
+           ▼                       ▼                       ▼
+      AbstractAsset          AbstractAsset           AbstractAsset
+           │                       │                       │
+           ├─► AbstractGeoAsset    ├─► AbstractGeoAsset    ├─► AbstractGeoAsset
+           │        │              │        │              │        │
+           │        ▼              │        ▼              │        ▼
+           │  TrafficLightPole     │  PLPole/Fixture       │  SignPole/Panel
+           │                       │                       │
+           └─► TrafficLightLantern │                       │
+           └─► TrafficLightCable   │                       │
+           └─► TrafficLightController                      │
+                                   │                       │
+                            StructureLocation        NatureLocation
+                                   │                       │
+                                   ▼                       ▼
+                            StructureGeoAsset      NatureTree/Surface
+

Cycle de vie d'une intervention

┌─────────────────┐
+│  in_preparation │  ◄─── Création initiale
+│   (En prép.)    │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐
+│  to_be_approved │  ◄─── Soumission pour approbation
+│  (A approuver)  │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐
+│  to_be_planned  │  ◄─── Approuvé, à planifier
+│  (A planifier)  │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐
+│ to_be_processed │  ◄─── Planifié, à traiter
+│   (A traiter)   │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐     ┌─────────────────┐
+│   in_progress   │────►│    on_pause     │
+│   (Démarré)     │◄────│   (En pause)    │
+└────────┬────────┘     └─────────────────┘
+         │
+         ▼
+┌─────────────────┐
+│    finished     │  ◄─── Travaux terminés
+│   (Terminé)     │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐     ┌─────────────────┐
+│    processed    │────►│ to_be_corrected │
+│  (A valider)    │◄────│  (A corriger)   │
+└────────┬────────┘     └────────┬────────┘
+         │                       │
+         │              ┌────────▼────────┐
+         │              │    corrected    │
+         │              │   (Corrigé)     │
+         │              └────────┬────────┘
+         │                       │
+         ▼◄──────────────────────┘
+┌─────────────────┐
+│    validated    │  ◄─── Validation finale
+│    (Validé)     │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐
+│    invoiced     │  ◄─── Facturation
+│   (Facturé)     │
+└────────┬────────┘
+         │
+         ▼
+┌─────────────────┐
+│     closed      │  ◄─── Clôture définitive
+│   (Finalisé)    │
+└─────────────────┘

│ + ├───────────────────────────────────┐ + ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ +│ canceled │ ◄─── Peut être │ (Tout statut │ +│ (Annulé) │ depuis │ précédent) │ +└─────────────────┘ └─────────────────┘ +

---

Notes techniques

Base de données

Authentification et permissions

Gestion des fichiers

API

---

Conclusion

StreetUp est une application Django complète et modulaire qui permet de gérer l'ensemble du cycle de vie des actifs de voirie. La structure en modules permet une maintenance facile et une évolution progressive des fonctionnalités.

Les principaux points forts de l'architecture sont :

1. Modularité : Chaque domaine métier est isolé dans son propre module Django +2. Extensibilité : Les classes abstraites permettent d'ajouter facilement de nouveaux types d'actifs +3. Traçabilité : Historique complet des interventions et des modifications +4. Géolocalisation : Support natif des données géospatiales avec PostGIS +5. Gestion documentaire : Système complet de versionnement et de partage de documents +6. Intégration : API REST pour l'intégration avec d'autres systèmes + +

+ + \ No newline at end of file diff --git a/docker-compose.panoramax.yml b/docker-compose.panoramax.yml new file mode 100644 index 0000000..d81c62f --- /dev/null +++ b/docker-compose.panoramax.yml @@ -0,0 +1,135 @@ +services: + panoramax_db: + image: postgis/postgis:16-3.4 + container_name: panoramax_db + restart: unless-stopped + environment: + POSTGRES_DB: panoramax + POSTGRES_USER: panoramax_user + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - ${SRV_DIR:-/srv/panoramax}/db_data:/var/lib/postgresql/data + networks: + - panoramax_network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U panoramax_user -d panoramax" ] + interval: 10s + timeout: 5s + retries: 5 + + panoramax_api: + image: panoramax/api:latest + container_name: panoramax_api + restart: unless-stopped + depends_on: + panoramax_db: + condition: service_healthy + env_file: + - .env + environment: + - DB_HOST=panoramax_db + - DB_PORT=5432 + - DB_NAME=panoramax + - DB_USERNAME=panoramax_user + - DB_PASSWORD=${DB_PASSWORD} + # Désactive le traitement interne pour déléguer au picture-worker + - PICTURE_PROCESS_THREADS_LIMIT=0 + # Permet à Gunicorn de faire confiance aux en-têtes de proxy (comme X-Forwarded-Proto) envoyés par Caddy + - GUNICORN_CMD_ARGS=--forwarded-allow-ips=* + # Visibilité par défaut publique pour les métadonnées (images protégées au niveau du proxy) + - API_DEFAULT_PICTURE_VISIBILITY=anyone + # ports: + # Optionnel : Exposer localement pour le débug si besoin (décommenter si nécessaire) + # - "127.0.0.1:5001:5000" + # - "5000:5000" + networks: + - panoramax_network + - streetup_network + deploy: + resources: + limits: + cpus: '2.0' + memory: 2G + + panoramax_worker: + image: panoramax/api:latest + restart: unless-stopped + command: picture-worker + depends_on: + panoramax_db: + condition: service_healthy + panoramax_api: + condition: service_started + panoramax_blur: + condition: service_healthy + env_file: + - .env + environment: + - DB_HOST=panoramax_db + - DB_PORT=5432 + - DB_NAME=panoramax + - DB_USERNAME=panoramax_user + - DB_PASSWORD=${DB_PASSWORD} + # Visibilité par défaut publique pour les métadonnées (images protégées au niveau du proxy) + - API_DEFAULT_PICTURE_VISIBILITY=anyone + networks: + - panoramax_network + deploy: + replicas: 4 + resources: + limits: + cpus: '2.0' + memory: 4G + + panoramax_blur: + image: panoramax/sgblur:latest + container_name: panoramax_blur + restart: unless-stopped + environment: + - WEB_CONCURRENCY=4 # Limite le nombre de workers concurrents pour préserver le CPU + - DETECT_URL=http://panoramax_detect:8001 + depends_on: + panoramax_detect: + condition: service_healthy + networks: + - panoramax_network + deploy: + resources: + limits: + cpus: '2.0' + memory: 1G + healthcheck: + test: python -c "import requests; requests.get('http://localhost:8000/').raise_for_status()" + interval: 10s + timeout: 5s + retries: 5 + start_period: 2s + + panoramax_detect: + image: panoramax/sgblur:latest + container_name: panoramax_detect + entrypoint: ["uvicorn", "src.detect.detect_api:app", "--host", "0.0.0.0", "--port", "8001", "--workers", "4"] + restart: unless-stopped + environment: + - OMP_NUM_THREADS=1 # 1 seul thread PyTorch par processus pour éviter la surcharge de synchronisation + - MKL_NUM_THREADS=1 # 1 seul thread MKL par processus + - MKL_DYNAMIC=FALSE # Désactive la mise à l'échelle dynamique des threads de MKL + networks: + - panoramax_network + deploy: + resources: + limits: + cpus: '4.0' + memory: 6G + healthcheck: + test: python -c "import requests; requests.get('http://localhost:8001/').raise_for_status()" + interval: 10s + timeout: 5s + retries: 5 + start_period: 2s + +networks: + panoramax_network: + driver: bridge + streetup_network: + external: true diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..fcb4a9b --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,102 @@ +services: + db: + image: postgis/postgis:18-3.6 + container_name: ${CONTAINER_PREFIX}_db + restart: unless-stopped + ports: + - "127.0.0.1:${DB_HOST_PORT:-5438}:5432" # Conserve l'accès local sécurisé pour l'hôte (port personnalisable) + environment: + POSTGRES_DB: streetup + POSTGRES_USER: streetup_admin + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - ${SRV_DIR:-/srv/streetup}/db_data:/var/lib/postgresql + networks: + - streetup_network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U streetup_admin -d streetup" ] + interval: 10s + timeout: 5s + retries: 5 + + web: + # Utilise l'image avec le tag approprié (latest en prod, test en environnement de test) + image: ghcr.io/kdeterme/streetup:${IMAGE_TAG:-latest} + container_name: ${CONTAINER_PREFIX}_web + restart: unless-stopped + command: /docker-entrypoint.sh + networks: + - streetup_network + volumes: + - ${SRV_DIR:-/srv/streetup}/media:/app/media + - ${SRV_DIR:-/srv/streetup}/private_media:/app/private_media + - ${SRV_DIR:-/srv/streetup}/static:/app/static + - ${SRV_DIR:-/srv/streetup}/backups:/app/backups + - ${BACKUP_HOST_SSH_KEY:-${HOME}/.ssh/id_ed25519}:/app/backup_key:ro + env_file: + - .env + environment: + - DB_HOST=db + - DB_PORT=5432 + - GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so + - GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so + - PROJ_DATA=/opt/conda/envs/streetup-env/share/proj + - LOG_FILE_PATH=/app/django.log + depends_on: + db: + condition: service_healthy + + rclone-backup: + image: rclone/rclone:latest + container_name: ${CONTAINER_PREFIX}_backup_rclone + restart: unless-stopped + volumes: + - ${SRV_DIR:-/srv/streetup}/media:/data/media:ro + - ${SRV_DIR:-/srv/streetup}/private_media:/data/private_media:ro + - ./rclone:/config/rclone + environment: + - BACKUP_INTERVAL=86400 + - DESTINATION=gdrive:streetup_backup + entrypoint: > + sh -c ' + echo "=== Démarrage du daemon de sauvegarde Rclone ==="; + while true; do + echo "=== Début de la synchronisation : $$(date) ==="; + rclone sync /data/media $${DESTINATION}/media --verbose; + rclone sync /data/private_media $${DESTINATION}/private_media --verbose; + echo "=== Fin de la synchronisation : $$(date) ==="; + echo "Sommeil de $${BACKUP_INTERVAL} secondes..."; + sleep $${BACKUP_INTERVAL}; + done + ' + + caddy: + image: caddy:latest + container_name: ${CONTAINER_PREFIX}_caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ${CONTAINER_PREFIX}_caddy_data:/data + - ${CONTAINER_PREFIX}_caddy_config:/config + - ${SRV_DIR:-/srv/streetup}/static:/srv/static:ro + - ${SRV_DIR:-/srv/streetup}/media:/srv/media:ro + - /srv/streetup-test/static:/srv/static_test:ro + - /srv/streetup-test/media:/srv/media_test:ro + depends_on: + - web + networks: + - streetup_network + +volumes: + streetup_prod_caddy_data: + streetup_prod_caddy_config: + streetup_test_caddy_data: + streetup_test_caddy_config: + +networks: + streetup_network: + external: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c66f976 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +services: + db: + image: postgis/postgis:18-3.6 + container_name: streetup_db + restart: unless-stopped + ports: + - "5434:5432" # Mappé sur le port 5434 de l'hôte pour éviter tout conflit avec PostgreSQL local + environment: + POSTGRES_DB: streetup + POSTGRES_USER: admin + POSTGRES_PASSWORD: replace_by_your_pwd + volumes: + - streetup_db_data:/var/lib/postgresql + - ./streetup/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U admin -d streetup" ] + interval: 10s + timeout: 5s + retries: 5 + + web: + build: + context: . + dockerfile: Dockerfile + args: + # Mettre "true" pour installer ultralytics, OpenCV et PyTorch dans l'image + INSTALL_AI_LIBS: "false" + container_name: streetup_web + restart: unless-stopped + command: python manage.py runserver 0.0.0.0:8000 + ports: + - "8000:8000" + volumes: + - ./streetup:/app # Volume mount pour le hot-reload local + - streetup_media:/app/media + - streetup_private_media:/app/private_media + env_file: + - ./streetup/.env # Chargement du fichier d'environnement local + environment: + # Surcharges d'environnement internes au conteneur pour cibler la base du conteneur "db" + - DB_HOST=db + - DB_PORT=5432 + # Surcharges des chemins GIS internes au conteneur pour écraser ceux de l'hôte issus du .env + - GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so + - GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so + - PROJ_DATA=/opt/conda/envs/streetup-env/share/proj + # Correction du chemin des logs pour rester dans le conteneur + - LOG_FILE_PATH=/app/django.log + depends_on: + db: + condition: service_healthy + + caddy: + image: caddy:latest + container_name: streetup_caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - streetup_caddy_data:/data + - streetup_caddy_config:/config + depends_on: + - web + +volumes: + streetup_db_data: + streetup_media: + streetup_private_media: + streetup_caddy_data: + streetup_caddy_config: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..db749ca --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Point d'entrée du conteneur web en production. +# En développement, ce script est court-circuité par la directive +# "command: python manage.py runserver ..." dans docker-compose.yml. +set -e + +exec gunicorn streetup.wsgi:application \ + --bind 0.0.0.0:8000 \ + --workers "${GUNICORN_WORKERS:-4}" \ + --threads "${GUNICORN_THREADS:-2}" \ + --timeout "${GUNICORN_TIMEOUT:-120}" diff --git a/panoramax.env.example b/panoramax.env.example new file mode 100644 index 0000000..85beda8 --- /dev/null +++ b/panoramax.env.example @@ -0,0 +1,77 @@ +# ============================================================================== +# Configuration d'environnement pour Panoramax (GeoVisio) +# ============================================================================== +# Copiez ce fichier sous le nom '.env' (dans le dossier /srv/panoramax/) sur votre +# serveur de production et renseignez les valeurs appropriées. +# ============================================================================== + +# ------------------------------------------------------------------------------ +# 1. Sécurité et Base de données +# ------------------------------------------------------------------------------ +# Le mot de passe de la base de données PostGIS dédiée de Panoramax. +# Doit correspondre à la variable de la base de données pour que l'API et +# le worker puissent s'y connecter. +DB_PASSWORD=choisissez_un_mot_de_passe_robuste + +# Clé secrète de Flask utilisée pour signer les sessions et jetons (tokens). +# Requis pour générer le jeton d'administration permanent. +# Générez une clé robuste avec : openssl rand -hex 32 +FLASK_SECRET_KEY=choisissez_une_cle_secrete_aleatoire + +# ------------------------------------------------------------------------------ +# 2. Stockage S3 Privé (OVHcloud Roubaix - rbx) +# ------------------------------------------------------------------------------ +# Identifiants de l'utilisateur ayant le rôle 'ObjectStore operator' sur OVHcloud. +# IMPORTANT : Le bucket S3 doit être configuré en ACCÈS PRIVÉ (non public). +AWS_ACCESS_KEY_ID=votre_access_key_id_ovh +AWS_SECRET_ACCESS_KEY=votre_secret_access_key_ovh + +# URL d'accès PyFilesystem vers le stockage S3 d'OVHcloud (Roubaix). +# Remplacez 'nom-du-bucket' par le nom de votre bucket S3 privé créé sur OVHcloud. +# Notez que le endpoint_url doit être encodé pour l'URL (https%3A%2F%2Fs3.rbx.io.cloud.ovh.net) +FS_URL= + +# ------------------------------------------------------------------------------ +# 3. URLs de l'API et Performance (Option B : Liens Privés + Signature) +# ------------------------------------------------------------------------------ +# URL publique principale de l'API Panoramax. +API_PUBLIC_URL= + +# Avec l'Option B (Sécurité maximale), nous désactivons le service direct de photos +# publiques par Panoramax. Nous laissons ces variables commentées. +# +# C'est StreetUp (Django) qui utilisera les mêmes identifiants AWS ci-dessus pour +# générer des URLs de lecture S3 temporaires signées (S3 Presigned URLs) +# valables 30 minutes (1800 secondes) pour les utilisateurs connectés. +# +# API_PERMANENT_PICTURES_PUBLIC_URL= +# API_DERIVATES_PICTURES_PUBLIC_URL= + +# Stratégie de génération des vignettes/tuiles. +# 'PREPROCESS' force le picture-worker à précalculer et envoyer toutes les vignettes +# sur le S3 dès le versement de la photo par StreetUp. +PICTURE_PROCESS_DERIVATES_STRATEGY=PREPROCESS + +# ------------------------------------------------------------------------------ +# 4. Authentification & Sécurité d'écriture +# ------------------------------------------------------------------------------ +# Désactivation de l'authentification OAuth externe (laissé vide). +# Aucun utilisateur externe ne pourra se connecter via navigateur. +OAUTH_PROVIDER= + +# Force l'authentification obligatoire pour tous les téléversements (uploads). +# Seul StreetUp, muni du jeton d'administration généré en console, pourra verser +# des images et créer des séquences après avoir validé leur qualité. +API_FORCE_AUTH_ON_UPLOAD=true + +# Visibilité par défaut pour les nouvelles images ('anyone', 'logged-only' ou 'owner-only'). +# Par défaut, nous utilisons 'anyone' (le catalogue de métadonnées reste accessible +# au public), et nous protégeons l'accès direct aux images (S3) au niveau du proxy. +API_DEFAULT_PICTURE_VISIBILITY=anyone + +# ------------------------------------------------------------------------------ +# 5. Floutage (Anonymisation) des visages et plaques +# ------------------------------------------------------------------------------ +# Renseignez l'URL du service de floutage (SGBlur) exécuté localement. +# Si cette variable est commentée ou vide, le floutage automatique est désactivé. +API_BLUR_URL= diff --git a/requirements/ai.txt b/requirements/ai.txt new file mode 100644 index 0000000..6cf8bcc --- /dev/null +++ b/requirements/ai.txt @@ -0,0 +1,3 @@ +# Les librairies IA en local (ultralytics, PyTorch, etc.) ont été supprimées. +# L'application utilise désormais un appel à un microservice externe pour la détection. +# Ce fichier reste vide ou pour d'éventuelles dépendances futures du client d'API. diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..dccd382 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,44 @@ +asgiref==3.11.0 +certifi==2025.11.12 +charset-normalizer==3.4.4 +crispy-bootstrap5==2025.6 +Django==5.2.8 +djangorestframework==3.16.1 +djangorestframework-simplejwt==5.5.1 +django-crispy-forms==2.5 +et_xmlfile==2.0.0 +geopandas==1.1.1 +idna==3.11 +numpy==2.3.5 +openpyxl==3.1.5 +opencv-python-headless>=4.10.0 +packaging==25.0 +python-docx==1.2.0 +pandas==2.3.3 +pdf2image==1.17.0 +pillow==12.0.0 +psycopg==3.2.13 +psycopg-binary==3.2.13 +pyogrio==0.12.1 +pyproj==3.7.2 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.1 +pytz==2025.2 +requests==2.32.5 +shapely==2.1.2 +six==1.17.0 +sqlparse==0.5.3 +tzdata==2025.2 +urllib3==2.5.0 +fpdf2==2.8.5 +qrcode[pil]==8.2 +bleach==6.1.0 +num2words==0.5.12 +httpx==0.28.1 +rapidfuzz==3.14.5 +aiohttp +gunicorn>=23.0.0 +ezdxf==1.4.4 +matplotlib==3.10.9 +boto3>=1.34.0 +markdown diff --git a/streetup/.env.example b/streetup/.env.example new file mode 100644 index 0000000..b0c5fd1 --- /dev/null +++ b/streetup/.env.example @@ -0,0 +1,82 @@ +DEBUG=True + +SECRET_KEY='...' + +ALLOWED_HOSTS= +CSRF_TRUSTED_ORIGINS= + +ROOT_URL= + +# Configuration de la base de données +# - Pour un usage local (hors Docker) : DB_HOST=127.0.0.1, DB_PORT=5432 +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASSWORD= + +# Librairies GIS (cartographiques) +# - Windows (exemple OSGeo4W) : +# GDAL_LIBRARY_PATH='C:\OSGeo4W\\bin\gdal309.dll' +# GEOS_LIBRARY_PATH='C:\OSGeo4W\\bin\geos_c.dll' +# - Linux (Conda local) : +# GDAL_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgdal.so +# GEOS_LIBRARY_PATH=/chemin/vers/miniconda3/envs/.../lib/libgeos_c.so +# - Docker (Miniforge/Conda interne - défini par défaut dans le Dockerfile) : +# GDAL_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgdal.so +# GEOS_LIBRARY_PATH=/opt/conda/envs/streetup-env/lib/libgeos_c.so +# PROJ_DATA=/opt/conda/envs/streetup-env/share/proj + +EMAIL_HOST= +EMAIL_PORT= +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= + +LOG_FILE_PATH=".../django.log" + +PYGEOAPI_BASE_URL= + +# Backup settings +BACKUP_ENABLED=false +BACKUP_DIR= +BACKUP_RETENTION_DAYS=30 +BACKUP_TIMEOUT_SECONDS=3600 +BACKUP_SSH_KEY= + + +# MinIO / S3 Backup settings +BACKUP_MINIO_ENABLED= +BACKUP_MINIO_ENDPOINT_URL= +BACKUP_MINIO_BUCKET= +BACKUP_MINIO_REGION= +BACKUP_MINIO_ACCESS_KEY= +BACKUP_MINIO_SECRET_KEY= + + +# Cartography Styles and Tiles (e.g. MapTiler or OpenFreeMap) +MAP_STYLE_LIGHT= +MAP_STYLE_STANDARD= +MAP_RASTER_TILE_URL= +MAP_ATTRIBUTION= + +# Panoramax API Configuration +PANORAMAX_API_URL= +PANORAMAX_API_KEY= + +# AWS S3 Settings for Panoramax private bucket redirects (Option B) +PANORAMAX_AWS_ACCESS_KEY_ID= +PANORAMAX_AWS_SECRET_ACCESS_KEY= +PANORAMAX_AWS_S3_ENDPOINT_URL= +PANORAMAX_AWS_S3_BUCKET= +PANORAMAX_AWS_S3_PREFIX= + +# Organization Brand Configuration +PRODUCT_NAME= +ORGANIZATION_NAME= +ORGANIZATION_NAME_FR= +ORGANIZATION_NAME_NL= +ORGANIZATION_LOGO_HEADER= +ORGANIZATION_LOGO_PNG= +ORGANIZATION_LOGO_SVG= +ORGANIZATION_PRIMARY_COLOR_HEX= + diff --git a/streetup/analytics/__init__.py b/streetup/analytics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/analytics/admin.py b/streetup/analytics/admin.py new file mode 100644 index 0000000..c2666bf --- /dev/null +++ b/streetup/analytics/admin.py @@ -0,0 +1,185 @@ +""" +Interface d'administration pour les modèles Analytics. +""" + +from django.contrib import admin +from django.utils.html import format_html +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from .models import PageView, UserSession, DailyPageViewSummary + + +@admin.register(PageView) +class PageViewAdmin(admin.ModelAdmin): + """Interface d'administration pour les vues de pages.""" + + list_display = [ + 'timestamp', 'user_link', 'path', 'method', + 'response_time_colored', 'ip_address' + ] + list_filter = [ + 'method', 'timestamp', + ('user', admin.RelatedOnlyFieldListFilter), + ] + search_fields = ['path', 'user__username', 'ip_address', 'full_url'] + readonly_fields = [ + 'user', 'session_key', 'path', 'full_url', 'method', + 'timestamp', 'ip_address', 'user_agent', 'referer', 'response_time_ms' + ] + date_hierarchy = 'timestamp' + ordering = ['-timestamp'] + + # Pagination + list_per_page = 50 + + def user_link(self, obj): + """Lien vers la page de l'utilisateur.""" + if obj.user: + url = reverse('admin:auth_user_change', args=[obj.user.id]) + return format_html('{}', url, obj.user.username) + return _("Anonyme") + user_link.short_description = _("Utilisateur") + + def response_time_colored(self, obj): + """Affiche le temps de réponse avec une couleur selon la performance.""" + if obj.response_time_ms is None: + return '-' + + # Définir les seuils de couleur + if obj.response_time_ms < 100: + color = 'green' + elif obj.response_time_ms < 500: + color = 'orange' + else: + color = 'red' + + return format_html( + '{} ms', + color, + obj.response_time_ms + ) + response_time_colored.short_description = _("Temps de réponse") + response_time_colored.admin_order_field = 'response_time_ms' + + def has_add_permission(self, request): + """Désactiver l'ajout manuel.""" + return False + + def has_change_permission(self, request, obj=None): + """Désactiver la modification.""" + return False + + +@admin.register(UserSession) +class UserSessionAdmin(admin.ModelAdmin): + """Interface d'administration pour les sessions utilisateurs.""" + + list_display = [ + 'login_time', 'user_link', 'duration_formatted', + 'page_views_count', 'status_colored', 'ip_address' + ] + list_filter = [ + 'is_active', 'login_time', + ('user', admin.RelatedOnlyFieldListFilter), + ] + search_fields = ['user__username', 'session_key', 'ip_address'] + readonly_fields = [ + 'user', 'session_key', 'login_time', 'last_activity', + 'logout_time', 'ip_address', 'user_agent', 'page_views_count', + 'is_active', 'duration_formatted' + ] + date_hierarchy = 'login_time' + ordering = ['-login_time'] + + # Pagination + list_per_page = 50 + + def user_link(self, obj): + """Lien vers la page de l'utilisateur.""" + url = reverse('admin:auth_user_change', args=[obj.user.id]) + return format_html('{}', url, obj.user.username) + user_link.short_description = _("Utilisateur") + + def status_colored(self, obj): + """Affiche le statut avec une couleur.""" + if obj.is_active: + return format_html( + '● {}', + _("Active") + ) + else: + return format_html( + '○ {}', + _("Terminée") + ) + status_colored.short_description = _("Statut") + status_colored.admin_order_field = 'is_active' + + def duration_formatted(self, obj): + """Affiche la durée de la session formatée.""" + seconds = obj.duration_seconds + if seconds < 60: + return f"{int(seconds)}s" + elif seconds < 3600: + minutes = int(seconds / 60) + return f"{minutes}min" + else: + hours = int(seconds / 3600) + minutes = int((seconds % 3600) / 60) + return f"{hours}h {minutes}min" + duration_formatted.short_description = _("Durée") + + def has_add_permission(self, request): + """Désactiver l'ajout manuel.""" + return False + + def has_change_permission(self, request, obj=None): + """Désactiver la modification.""" + return False + + +@admin.register(DailyPageViewSummary) +class DailyPageViewSummaryAdmin(admin.ModelAdmin): + """Interface d'administration pour les résumés quotidiens.""" + + list_display = [ + 'date', 'total_page_views', 'unique_users', + 'unique_sessions', 'total_sessions_started', + 'avg_response_colored', + ] + list_filter = ['date'] + search_fields = ['date'] + readonly_fields = [ + 'date', 'total_page_views', 'unique_users', 'unique_sessions', + 'unique_ips', 'avg_response_time_ms', 'max_response_time_ms', + 'top_paths', 'top_users', 'method_counts', + 'total_sessions_started', 'created_at', 'updated_at', + ] + date_hierarchy = 'date' + ordering = ['-date'] + list_per_page = 50 + + def avg_response_colored(self, obj): + """Affiche le temps de réponse moyen avec couleur.""" + if obj.avg_response_time_ms is None: + return '-' + val = obj.avg_response_time_ms + if val < 100: + color = 'green' + elif val < 500: + color = 'orange' + else: + color = 'red' + return format_html( + '{} ms', + color, + f'{val:.0f}', + ) + avg_response_colored.short_description = _("Temps de rép. moyen") + avg_response_colored.admin_order_field = 'avg_response_time_ms' + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False diff --git a/streetup/analytics/apps.py b/streetup/analytics/apps.py new file mode 100644 index 0000000..c06daff --- /dev/null +++ b/streetup/analytics/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AnalyticsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'analytics' + verbose_name = 'Analytics & Monitoring' diff --git a/streetup/analytics/management/__init__.py b/streetup/analytics/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/analytics/management/commands/__init__.py b/streetup/analytics/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/analytics/management/commands/cleanup_analytics.py b/streetup/analytics/management/commands/cleanup_analytics.py new file mode 100644 index 0000000..5c1381f --- /dev/null +++ b/streetup/analytics/management/commands/cleanup_analytics.py @@ -0,0 +1,204 @@ +""" +Commande Django pour agréger les statistiques de pages vues +et purger les données détaillées de plus d'un mois. + +Workflow: + 1. Pour chaque jour ayant des PageView de plus de 30 jours, + calculer les agrégats et créer/mettre à jour un DailyPageViewSummary. + 2. Supprimer les PageView et UserSession de plus de 30 jours. + +Usage: + python manage.py cleanup_analytics + python manage.py cleanup_analytics --days 60 + python manage.py cleanup_analytics --dry-run +""" + +from datetime import timedelta + +from django.core.management.base import BaseCommand +from django.db.models import Count, Avg, Max, Q +from django.db.models.functions import TruncDate +from django.utils import timezone + +from analytics.models import PageView, UserSession, DailyPageViewSummary + + +class Command(BaseCommand): + help = ( + "Agrège les statistiques quotidiennes de pages vues dans " + "DailyPageViewSummary puis purge les enregistrements détaillés " + "de PageView et UserSession de plus de N jours (défaut: 30)." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--days", + type=int, + default=30, + help="Nombre de jours de rétention des données détaillées (défaut: 30).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Affiche ce qui serait fait sans modifier la base de données.", + ) + + def handle(self, *args, **options): + retention_days = options["days"] + dry_run = options["dry_run"] + cutoff = timezone.now() - timedelta(days=retention_days) + cutoff_date = cutoff.date() + + if dry_run: + self.stdout.write(self.style.WARNING("[DRY-RUN] Aucune modification ne sera effectuée.\n")) + + self.stdout.write( + self.style.WARNING( + f"\n=== Nettoyage analytics — rétention {retention_days} jours " + f"(avant le {cutoff_date}) ===\n" + ) + ) + + # ── 1. Agrégation ───────────────────────────────────────────── + old_page_views = PageView.objects.filter(timestamp__lt=cutoff) + total_old = old_page_views.count() + + if total_old == 0: + self.stdout.write("Aucune PageView à agréger.\n") + else: + self.stdout.write(f"{total_old} PageView(s) à agréger et purger.\n") + self._aggregate_page_views(old_page_views, dry_run) + + # ── 2. Purge des PageView ───────────────────────────────────── + if not dry_run and total_old > 0: + deleted_pv, _ = old_page_views.delete() + self.stdout.write(self.style.SUCCESS(f" ✓ {deleted_pv} PageView(s) supprimée(s).\n")) + + # ── 3. Purge des UserSession inactives ──────────────────────── + old_sessions = UserSession.objects.filter( + last_activity__lt=cutoff, + is_active=False, + ) + total_sessions = old_sessions.count() + if total_sessions == 0: + self.stdout.write("Aucune UserSession inactive à purger.\n") + else: + if dry_run: + self.stdout.write(f" [DRY-RUN] {total_sessions} UserSession(s) inactives seraient supprimées.\n") + else: + deleted_us, _ = old_sessions.delete() + self.stdout.write(self.style.SUCCESS(f" ✓ {deleted_us} UserSession(s) inactives supprimées.\n")) + + self.stdout.write(self.style.SUCCESS("\n=== Nettoyage terminé ===\n")) + + # ────────────────────────────────────────────────────────────────── + # Méthodes internes + # ────────────────────────────────────────────────────────────────── + + def _aggregate_page_views(self, queryset, dry_run): + """ + Calcule les résumés quotidiens à partir du queryset fourni + et crée / met à jour les DailyPageViewSummary correspondants. + """ + days = ( + queryset + .annotate(day=TruncDate("timestamp")) + .values("day") + .annotate(count=Count("id")) + .order_by("day") + ) + + for entry in days: + day = entry["day"] + day_qs = queryset.filter(timestamp__date=day) + summary_data = self._compute_daily_summary(day, day_qs) + + if dry_run: + self.stdout.write( + f" [DRY-RUN] {day} : {summary_data['total_page_views']} vues, " + f"{summary_data['unique_users']} utilisateurs uniques" + ) + else: + DailyPageViewSummary.objects.update_or_create( + date=day, + defaults=summary_data, + ) + self.stdout.write( + self.style.SUCCESS( + f" ✓ Résumé {day} : {summary_data['total_page_views']} vues, " + f"{summary_data['unique_users']} utilisateurs uniques" + ) + ) + + def _compute_daily_summary(self, day, day_qs): + """Calcule les métriques agrégées pour un jour donné.""" + # Compteurs de base + total_page_views = day_qs.count() + unique_users = ( + day_qs.filter(user__isnull=False) + .values("user") + .distinct() + .count() + ) + unique_sessions = ( + day_qs.exclude(session_key="") + .values("session_key") + .distinct() + .count() + ) + unique_ips = ( + day_qs.filter(ip_address__isnull=False) + .values("ip_address") + .distinct() + .count() + ) + + # Temps de réponse + response_stats = day_qs.filter( + response_time_ms__isnull=False + ).aggregate( + avg=Avg("response_time_ms"), + mx=Max("response_time_ms"), + ) + avg_response = response_stats["avg"] + max_response = response_stats["mx"] + + # Top 20 pages + top_paths = dict( + day_qs.values_list("path") + .annotate(cnt=Count("id")) + .order_by("-cnt")[:20] + ) + + # Top 20 utilisateurs + top_users = dict( + day_qs.filter(user__isnull=False) + .values_list("user__username") + .annotate(cnt=Count("id")) + .order_by("-cnt")[:20] + ) + + # Répartition par méthode HTTP + method_counts = dict( + day_qs.values_list("method") + .annotate(cnt=Count("id")) + .order_by("-cnt") + ) + + # Sessions démarrées ce jour-là + total_sessions_started = UserSession.objects.filter( + login_time__date=day + ).count() + + return { + "total_page_views": total_page_views, + "unique_users": unique_users, + "unique_sessions": unique_sessions, + "unique_ips": unique_ips, + "avg_response_time_ms": round(avg_response, 2) if avg_response else None, + "max_response_time_ms": max_response, + "top_paths": top_paths, + "top_users": top_users, + "method_counts": method_counts, + "total_sessions_started": total_sessions_started, + } diff --git a/streetup/analytics/middleware.py b/streetup/analytics/middleware.py new file mode 100644 index 0000000..67761f4 --- /dev/null +++ b/streetup/analytics/middleware.py @@ -0,0 +1,196 @@ +""" +Middleware pour le tracking des visites de pages et des sessions utilisateurs. + +Ce middleware enregistre automatiquement les visites de pages et met à jour +les informations de session pour le monitoring de l'application. +""" + +import time +import logging +from django.utils import timezone +from django.contrib.auth.signals import user_logged_in, user_logged_out + +# Import tardif pour éviter les dépendances circulaires +# Ces imports sont effectués au niveau du module mais après la configuration Django +from analytics.models import PageView, UserSession + + +class AnalyticsMiddleware: + """ + Middleware qui capture les visites de pages pour le monitoring. + + Pour chaque requête, enregistre: + - La page visitée + - L'utilisateur (si authentifié) + - La session + - Le timestamp + - Les métadonnées (IP, user agent, etc.) + """ + + # URLs à ignorer pour ne pas polluer les statistiques + IGNORED_PATHS = [ + '/static/', + '/media/', + '/private-media/', + '/favicon.ico', + '/__debug__/', + '/admin/jsi18n/', # Traductions JS de l'admin + ] + + # Méthodes HTTP à tracker (ignorer les HEAD, OPTIONS, etc.) + TRACKED_METHODS = ['GET', 'POST'] + + def __init__(self, get_response): + self.get_response = get_response + + # Connecter les signaux pour tracker les connexions/déconnexions + user_logged_in.connect(self._on_user_login) + user_logged_out.connect(self._on_user_logout) + + def __call__(self, request): + # Vérifier si cette requête doit être trackée + if not self._should_track(request): + return self.get_response(request) + + # Enregistrer le temps de début + start_time = time.time() + + # Traiter la requête + response = self.get_response(request) + + # Calculer le temps de réponse + response_time_ms = int((time.time() - start_time) * 1000) + + # Enregistrer la visite de page (de manière asynchrone pour ne pas ralentir la réponse) + try: + self._record_page_view(request, response_time_ms) + except Exception as e: + # En cas d'erreur, ne pas faire planter la requête + logger = logging.getLogger('analytics') + logger.error(f"Erreur lors de l'enregistrement de la page view: {e}") + + return response + + def _should_track(self, request): + """Détermine si la requête doit être trackée.""" + # Ignorer certains chemins + for path in self.IGNORED_PATHS: + if request.path.startswith(path): + return False + + # Ignorer certaines méthodes HTTP + if request.method not in self.TRACKED_METHODS: + return False + + # Ignorer les requêtes AJAX de type polling/refresh + if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest': + # On peut choisir de tracker ou non les requêtes AJAX + # Pour l'instant, on les ignore pour ne pas polluer les stats + return False + + return True + + def _get_client_ip(self, request): + """Récupère l'adresse IP du client.""" + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + else: + ip = request.META.get('REMOTE_ADDR') + return ip + + def _get_user_agent(self, request): + """Récupère le user agent en le tronquant si nécessaire.""" + user_agent = request.META.get('HTTP_USER_AGENT') + return user_agent[:2000] if user_agent else None + + def _get_referer(self, request): + """Récupère le referer en le tronquant si nécessaire.""" + referer = request.META.get('HTTP_REFERER') + return referer[:2048] if referer else None + + def _record_page_view(self, request, response_time_ms): + """Enregistre la visite de page dans la base de données.""" + # Créer l'enregistrement de page view + page_view = PageView.objects.create( + user=request.user if request.user.is_authenticated else None, + session_key=request.session.session_key or '', + path=request.path, + full_url=request.build_absolute_uri(), + method=request.method, + ip_address=self._get_client_ip(request), + user_agent=self._get_user_agent(request), + referer=self._get_referer(request), + response_time_ms=response_time_ms, + ) + + # Mettre à jour la session utilisateur si l'utilisateur est authentifié + if request.user.is_authenticated and request.session.session_key: + self._update_user_session(request) + + def _update_user_session(self, request): + """Met à jour les informations de session utilisateur.""" + session_key = request.session.session_key + if not session_key: + return + + try: + # Récupérer ou créer la session + user_session, created = UserSession.objects.get_or_create( + session_key=session_key, + defaults={ + 'user': request.user, + 'login_time': timezone.now(), + 'last_activity': timezone.now(), + 'ip_address': self._get_client_ip(request), + 'user_agent': self._get_user_agent(request), + } + ) + + # Mettre à jour l'activité et le compteur de pages vues + if not created: + user_session.last_activity = timezone.now() + user_session.page_views_count += 1 + user_session.save(update_fields=['last_activity', 'page_views_count']) + except Exception as e: + # En cas d'erreur (par exemple, clé de session dupliquée), ignorer + logger = logging.getLogger('analytics') + logger.warning(f"Erreur lors de la mise à jour de la session: {e}") + + def _on_user_login(self, sender, request, user, **kwargs): + """ + Signal handler appelé quand un utilisateur se connecte. + Crée une nouvelle session utilisateur. + """ + # S'assurer que la session a une clé + if not request.session.session_key: + request.session.create() + + session_key = request.session.session_key + + # Créer ou mettre à jour la session + UserSession.objects.update_or_create( + session_key=session_key, + defaults={ + 'user': user, + 'login_time': timezone.now(), + 'last_activity': timezone.now(), + 'ip_address': self._get_client_ip(request), + 'user_agent': self._get_user_agent(request), + 'is_active': True, + } + ) + + def _on_user_logout(self, sender, request, user, **kwargs): + """ + Signal handler appelé quand un utilisateur se déconnecte. + Marque la session comme inactive. + """ + if request.session.session_key: + try: + user_session = UserSession.objects.get( + session_key=request.session.session_key + ) + user_session.mark_inactive() + except UserSession.DoesNotExist: + pass diff --git a/streetup/analytics/migrations/0001_initial.py b/streetup/analytics/migrations/0001_initial.py new file mode 100644 index 0000000..22d2f4e --- /dev/null +++ b/streetup/analytics/migrations/0001_initial.py @@ -0,0 +1,84 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DailyPageViewSummary', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(db_index=True, unique=True, verbose_name='Date')), + ('total_page_views', models.IntegerField(default=0, verbose_name='Total pages vues')), + ('unique_users', models.IntegerField(default=0, verbose_name='Utilisateurs uniques')), + ('unique_sessions', models.IntegerField(default=0, verbose_name='Sessions uniques')), + ('unique_ips', models.IntegerField(default=0, verbose_name='IPs uniques')), + ('avg_response_time_ms', models.FloatField(blank=True, null=True, verbose_name='Temps de réponse moyen (ms)')), + ('max_response_time_ms', models.IntegerField(blank=True, null=True, verbose_name='Temps de réponse max (ms)')), + ('top_paths', models.JSONField(blank=True, default=dict, help_text='Les 20 pages les plus visitées avec leur nombre de vues', verbose_name='Top pages visitées')), + ('top_users', models.JSONField(blank=True, default=dict, help_text='Les 20 utilisateurs les plus actifs avec leur nombre de vues', verbose_name='Top utilisateurs')), + ('method_counts', models.JSONField(blank=True, default=dict, help_text='Nombre de requêtes par méthode HTTP (GET, POST, ...)', verbose_name='Répartition par méthode HTTP')), + ('total_sessions_started', models.IntegerField(default=0, verbose_name='Sessions démarrées')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Créé le')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Mis à jour le')), + ], + options={ + 'verbose_name': 'Résumé quotidien', + 'verbose_name_plural': 'Résumés quotidiens', + 'ordering': ['-date'], + }, + ), + migrations.CreateModel( + name='PageView', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_key', models.CharField(db_index=True, max_length=40, verbose_name='Clé de session')), + ('path', models.CharField(db_index=True, max_length=2048, verbose_name='Chemin URL')), + ('full_url', models.CharField(blank=True, max_length=4096, null=True, verbose_name='URL complète')), + ('method', models.CharField(default='GET', max_length=10, verbose_name='Méthode HTTP')), + ('timestamp', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date et heure')), + ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='Adresse IP')), + ('user_agent', models.TextField(blank=True, null=True, verbose_name='User Agent')), + ('referer', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Page précédente')), + ('response_time_ms', models.IntegerField(blank=True, help_text='Durée de traitement de la requête en millisecondes', null=True, verbose_name='Temps de réponse (ms)')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_views', to=settings.AUTH_USER_MODEL, verbose_name='Utilisateur')), + ], + options={ + 'verbose_name': 'Vue de page', + 'verbose_name_plural': 'Vues de pages', + 'ordering': ['-timestamp'], + 'indexes': [models.Index(fields=['user', 'timestamp'], name='analytics_p_user_id_9d5969_idx'), models.Index(fields=['session_key', 'timestamp'], name='analytics_p_session_982099_idx'), models.Index(fields=['path', 'timestamp'], name='analytics_p_path_51c7b7_idx'), models.Index(fields=['timestamp'], name='analytics_p_timesta_835321_idx')], + }, + ), + migrations.CreateModel( + name='UserSession', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_key', models.CharField(db_index=True, max_length=40, unique=True, verbose_name='Clé de session')), + ('login_time', models.DateTimeField(db_index=True, verbose_name='Heure de connexion')), + ('last_activity', models.DateTimeField(db_index=True, verbose_name='Dernière activité')), + ('logout_time', models.DateTimeField(blank=True, null=True, verbose_name='Heure de déconnexion')), + ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='Adresse IP')), + ('user_agent', models.TextField(blank=True, null=True, verbose_name='User Agent')), + ('page_views_count', models.IntegerField(default=0, verbose_name='Nombre de pages vues')), + ('is_active', models.BooleanField(db_index=True, default=True, verbose_name='Session active')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='analytics_sessions', to=settings.AUTH_USER_MODEL, verbose_name='Utilisateur')), + ], + options={ + 'verbose_name': 'Session utilisateur', + 'verbose_name_plural': 'Sessions utilisateurs', + 'ordering': ['-login_time'], + 'indexes': [models.Index(fields=['user', 'login_time'], name='analytics_u_user_id_eec74f_idx'), models.Index(fields=['is_active', 'last_activity'], name='analytics_u_is_acti_794ed6_idx')], + }, + ), + ] diff --git a/streetup/analytics/migrations/__init__.py b/streetup/analytics/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/analytics/models.py b/streetup/analytics/models.py new file mode 100644 index 0000000..6b28261 --- /dev/null +++ b/streetup/analytics/models.py @@ -0,0 +1,260 @@ +""" +Modèles pour le monitoring de l'utilisation de l'application. + +Ce module fournit des modèles pour tracker: +- Les visites de pages (PageView) +- Les sessions utilisateurs (UserSession) +- Les résumés quotidiens (DailyPageViewSummary) +- Les statistiques d'utilisation +""" + +from django.db import models, transaction +from django.contrib.auth.models import User +from django.conf import settings +from django.utils import timezone + + +class PageView(models.Model): + """ + Enregistre chaque visite de page par un utilisateur. + Permet de tracker les pages visitées et la fréquence d'utilisation. + """ + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="Utilisateur", + related_name="page_views" + ) + session_key = models.CharField( + max_length=40, + db_index=True, + verbose_name="Clé de session" + ) + path = models.CharField( + max_length=2048, + verbose_name="Chemin URL", + db_index=True + ) + full_url = models.CharField( + max_length=4096, + blank=True, + null=True, + verbose_name="URL complète" + ) + method = models.CharField( + max_length=10, + default='GET', + verbose_name="Méthode HTTP" + ) + timestamp = models.DateTimeField( + auto_now_add=True, + verbose_name="Date et heure", + db_index=True + ) + ip_address = models.GenericIPAddressField( + null=True, + blank=True, + verbose_name="Adresse IP" + ) + user_agent = models.TextField( + blank=True, + null=True, + verbose_name="User Agent" + ) + referer = models.CharField( + max_length=2048, + blank=True, + null=True, + verbose_name="Page précédente" + ) + response_time_ms = models.IntegerField( + null=True, + blank=True, + verbose_name="Temps de réponse (ms)", + help_text="Durée de traitement de la requête en millisecondes" + ) + + class Meta: + verbose_name = "Vue de page" + verbose_name_plural = "Vues de pages" + ordering = ["-timestamp"] + indexes = [ + models.Index(fields=['user', 'timestamp']), + models.Index(fields=['session_key', 'timestamp']), + models.Index(fields=['path', 'timestamp']), + models.Index(fields=['timestamp']), + ] + + def __str__(self): + user_str = self.user.username if self.user else "Anonyme" + return f"{user_str} - {self.path} - {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}" + + +class UserSession(models.Model): + """ + Enrichit les informations de session Django avec des métadonnées supplémentaires. + Permet de tracker les sessions actives et l'historique de connexion. + """ + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + verbose_name="Utilisateur", + related_name="analytics_sessions" + ) + session_key = models.CharField( + max_length=40, + unique=True, + db_index=True, + verbose_name="Clé de session" + ) + login_time = models.DateTimeField( + verbose_name="Heure de connexion", + db_index=True + ) + last_activity = models.DateTimeField( + verbose_name="Dernière activité", + db_index=True + ) + logout_time = models.DateTimeField( + null=True, + blank=True, + verbose_name="Heure de déconnexion" + ) + ip_address = models.GenericIPAddressField( + null=True, + blank=True, + verbose_name="Adresse IP" + ) + user_agent = models.TextField( + blank=True, + null=True, + verbose_name="User Agent" + ) + page_views_count = models.IntegerField( + default=0, + verbose_name="Nombre de pages vues" + ) + is_active = models.BooleanField( + default=True, + verbose_name="Session active", + db_index=True + ) + + class Meta: + verbose_name = "Session utilisateur" + verbose_name_plural = "Sessions utilisateurs" + ordering = ["-login_time"] + indexes = [ + models.Index(fields=['user', 'login_time']), + models.Index(fields=['is_active', 'last_activity']), + ] + + def __str__(self): + status = "Active" if self.is_active else "Terminée" + return f"{self.user.username} - {self.login_time.strftime('%Y-%m-%d %H:%M')} - {status}" + + @property + def duration(self): + """Retourne la durée de la session.""" + end_time = self.logout_time if self.logout_time else self.last_activity + return end_time - self.login_time + + @property + def duration_seconds(self): + """Retourne la durée de la session en secondes.""" + return self.duration.total_seconds() + + def mark_inactive(self): + """Marque la session comme inactive de manière atomique.""" + with transaction.atomic(): + # Utiliser select_for_update pour éviter les race conditions + session = UserSession.objects.select_for_update().filter(pk=self.pk).first() + if session and session.is_active: + session.is_active = False + if not session.logout_time: + session.logout_time = timezone.now() + session.save(update_fields=['is_active', 'logout_time']) + + # Mettre à jour l'instance actuelle + self.is_active = session.is_active + self.logout_time = session.logout_time + + +class DailyPageViewSummary(models.Model): + """ + Résumé quotidien des statistiques de pages vues. + + Ce modèle stocke les agrégats par jour pour conserver des statistiques + d'utilisation même après la purge des données détaillées de PageView. + """ + date = models.DateField( + unique=True, + db_index=True, + verbose_name="Date" + ) + total_page_views = models.IntegerField( + default=0, + verbose_name="Total pages vues" + ) + unique_users = models.IntegerField( + default=0, + verbose_name="Utilisateurs uniques" + ) + unique_sessions = models.IntegerField( + default=0, + verbose_name="Sessions uniques" + ) + unique_ips = models.IntegerField( + default=0, + verbose_name="IPs uniques" + ) + avg_response_time_ms = models.FloatField( + null=True, + blank=True, + verbose_name="Temps de réponse moyen (ms)" + ) + max_response_time_ms = models.IntegerField( + null=True, + blank=True, + verbose_name="Temps de réponse max (ms)" + ) + top_paths = models.JSONField( + default=dict, + blank=True, + verbose_name="Top pages visitées", + help_text="Les 20 pages les plus visitées avec leur nombre de vues" + ) + top_users = models.JSONField( + default=dict, + blank=True, + verbose_name="Top utilisateurs", + help_text="Les 20 utilisateurs les plus actifs avec leur nombre de vues" + ) + method_counts = models.JSONField( + default=dict, + blank=True, + verbose_name="Répartition par méthode HTTP", + help_text="Nombre de requêtes par méthode HTTP (GET, POST, ...)" + ) + total_sessions_started = models.IntegerField( + default=0, + verbose_name="Sessions démarrées" + ) + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Créé le" + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Mis à jour le" + ) + + class Meta: + verbose_name = "Résumé quotidien" + verbose_name_plural = "Résumés quotidiens" + ordering = ["-date"] + + def __str__(self): + return f"{self.date} — {self.total_page_views} vues, {self.unique_users} utilisateurs" diff --git a/streetup/analytics/templates/analytics/dashboard.html b/streetup/analytics/templates/analytics/dashboard.html new file mode 100644 index 0000000..031d349 --- /dev/null +++ b/streetup/analytics/templates/analytics/dashboard.html @@ -0,0 +1,291 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+
+
+
+

{% translate "Tableau de bord Analytics" %}

+
+ 7j + 30j + 90j +
+
+
+
+ + +
+
+
+
+
{{ stats.total_users }}
+
{% translate "Utilisateurs totaux" %}
+
+
+
+
+
+
+
{{ stats.active_users }}
+
{% translate "Utilisateurs actifs" %} ({{ days }}j)
+
+
+
+
+
+
+
{{ stats.total_sessions }}
+
{% translate "Sessions totales" %} ({{ days }}j)
+
+
+
+
+
+
+
{{ stats.sessions_per_user }}
+
{% translate "Sessions par utilisateur" %}
+
+
+
+
+ +
+
+
+
+
{{ stats.total_page_views }}
+
{% translate "Pages vues" %} ({{ days }}j)
+
+
+
+
+
+
+
{{ stats.active_sessions }}
+
{% translate "Sessions actives" %}
+
+
+
+
+
+
+
{{ stats.avg_response_time|floatformat:0 }} ms
+
{% translate "Temps de réponse moyen" %}
+
+
+
+
+ + +
+
+
+
+
{% translate "Activité quotidienne" %}
+
+
+
+ +
+
+
+
+
+
+
+
{% translate "Activité horaire (24h)" %}
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
{% translate "Pages les plus visitées" %}
+ {% translate "Voir tout" %} +
+
+
+ + + + + + + + + {% for page in top_pages %} + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Page" %}{% translate "Visites" %}
{{ page.path }}{{ page.count }}
{% translate "Aucune donnée" %}
+
+
+
+
+
+
+
+
{% translate "Utilisateurs les plus actifs" %}
+ {% translate "Voir tout" %} +
+
+
+ + + + + + + + + + {% for user in top_users %} + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Utilisateur" %}{% translate "Vues" %}{% translate "Sessions" %}
{{ user.user__username }}{{ user.views }}{{ user.sessions }}
{% translate "Aucune donnée" %}
+
+
+
+
+
+
+{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/streetup/analytics/templates/analytics/page_views_list.html b/streetup/analytics/templates/analytics/page_views_list.html new file mode 100644 index 0000000..3dd0659 --- /dev/null +++ b/streetup/analytics/templates/analytics/page_views_list.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+
+

{% translate "Vues de pages" %}

+ + {% translate "Retour au tableau de bord" %} + +
+
+ +
+
+
+ + + + + + + + + + + + {% for pv in page_views %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Date/Heure" %}{% translate "Utilisateur" %}{% translate "Page" %}{% translate "Méthode" %}{% translate "Temps" %}
{{ pv.timestamp|date:"d/m/Y H:i:s" }}{% if pv.user %}{{ pv.user.username }}{% else %}{% translate "Anonyme" %}{% endif %}{{ pv.path }}{{ pv.method }}{% if pv.response_time_ms %}{{ pv.response_time_ms }} ms{% else %}-{% endif %}
{% translate "Aucune vue de page trouvée" %}
+
+
+
+
+{% endblock %} diff --git a/streetup/analytics/templates/analytics/sessions_list.html b/streetup/analytics/templates/analytics/sessions_list.html new file mode 100644 index 0000000..87ac645 --- /dev/null +++ b/streetup/analytics/templates/analytics/sessions_list.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+
+

{% translate "Sessions utilisateurs" %}

+ + {% translate "Retour au tableau de bord" %} + +
+
+ +
+
+
+ + + + + + + + + + + + + {% for session in sessions %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Utilisateur" %}{% translate "Connexion" %}{% translate "Dernière activité" %}{% translate "Durée" %}{% translate "Pages vues" %}{% translate "Statut" %}
{{ session.user.username }}{{ session.login_time|date:"d/m/Y H:i" }}{{ session.last_activity|date:"d/m/Y H:i" }}{{ session.duration }}{{ session.page_views_count }} + {% if session.is_active %} + {% translate "Active" %} + {% else %} + {% translate "Terminée" %} + {% endif %} +
{% translate "Aucune session trouvée" %}
+
+
+
+
+{% endblock %} diff --git a/streetup/analytics/tests.py b/streetup/analytics/tests.py new file mode 100644 index 0000000..ebf8f57 --- /dev/null +++ b/streetup/analytics/tests.py @@ -0,0 +1,286 @@ +""" +Tests pour le système Analytics. +""" + +from django.test import TestCase, Client, RequestFactory +from django.contrib.auth.models import User +from django.utils import timezone +from datetime import timedelta + +from .models import PageView, UserSession +from .middleware import AnalyticsMiddleware + + +class PageViewModelTest(TestCase): + """Tests pour le modèle PageView.""" + + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + + def test_create_page_view(self): + """Test de création d'une vue de page.""" + page_view = PageView.objects.create( + user=self.user, + session_key='test_session_123', + path='/test/page/', + method='GET', + ip_address='127.0.0.1', + response_time_ms=100 + ) + + self.assertEqual(page_view.user, self.user) + self.assertEqual(page_view.path, '/test/page/') + self.assertEqual(page_view.response_time_ms, 100) + self.assertIsNotNone(page_view.timestamp) + + def test_page_view_str(self): + """Test de la représentation string d'une PageView.""" + page_view = PageView.objects.create( + user=self.user, + session_key='test_session_123', + path='/test/page/', + ) + + str_repr = str(page_view) + self.assertIn('testuser', str_repr) + self.assertIn('/test/page/', str_repr) + + def test_page_view_without_user(self): + """Test de création d'une vue de page sans utilisateur (anonyme).""" + page_view = PageView.objects.create( + user=None, + session_key='anonymous_session', + path='/public/page/', + ) + + self.assertIsNone(page_view.user) + str_repr = str(page_view) + self.assertIn('Anonyme', str_repr) + + +class UserSessionModelTest(TestCase): + """Tests pour le modèle UserSession.""" + + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + + def test_create_user_session(self): + """Test de création d'une session utilisateur.""" + now = timezone.now() + session = UserSession.objects.create( + user=self.user, + session_key='test_session_456', + login_time=now, + last_activity=now, + ip_address='127.0.0.1', + is_active=True + ) + + self.assertEqual(session.user, self.user) + self.assertEqual(session.session_key, 'test_session_456') + self.assertTrue(session.is_active) + self.assertIsNone(session.logout_time) + + def test_session_duration(self): + """Test du calcul de la durée d'une session.""" + now = timezone.now() + later = now + timedelta(hours=2) + + session = UserSession.objects.create( + user=self.user, + session_key='test_session_789', + login_time=now, + last_activity=later, + is_active=True + ) + + duration = session.duration + self.assertEqual(duration.total_seconds(), 7200) # 2 heures + self.assertEqual(session.duration_seconds, 7200) + + def test_mark_inactive(self): + """Test de marquage d'une session comme inactive.""" + session = UserSession.objects.create( + user=self.user, + session_key='test_session_abc', + login_time=timezone.now(), + last_activity=timezone.now(), + is_active=True + ) + + self.assertTrue(session.is_active) + self.assertIsNone(session.logout_time) + + session.mark_inactive() + session.refresh_from_db() + + self.assertFalse(session.is_active) + self.assertIsNotNone(session.logout_time) + + def test_session_str(self): + """Test de la représentation string d'une UserSession.""" + session = UserSession.objects.create( + user=self.user, + session_key='test_session_def', + login_time=timezone.now(), + last_activity=timezone.now(), + is_active=True + ) + + str_repr = str(session) + self.assertIn('testuser', str_repr) + self.assertIn('Active', str_repr) + + +class AnalyticsMiddlewareTest(TestCase): + """Tests pour le middleware Analytics.""" + + def setUp(self): + self.factory = RequestFactory() + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + self.middleware = AnalyticsMiddleware(lambda r: None) + + def test_should_track_normal_request(self): + """Test que les requêtes normales sont trackées.""" + request = self.factory.get('/test/page/') + self.assertTrue(self.middleware._should_track(request)) + + def test_should_not_track_static_files(self): + """Test que les fichiers statiques ne sont pas trackés.""" + request = self.factory.get('/static/css/style.css') + self.assertFalse(self.middleware._should_track(request)) + + def test_should_not_track_media_files(self): + """Test que les fichiers media ne sont pas trackés.""" + request = self.factory.get('/media/images/photo.jpg') + self.assertFalse(self.middleware._should_track(request)) + + def test_should_not_track_favicon(self): + """Test que le favicon n'est pas tracké.""" + request = self.factory.get('/favicon.ico') + self.assertFalse(self.middleware._should_track(request)) + + def test_should_not_track_head_method(self): + """Test que les requêtes HEAD ne sont pas trackées.""" + request = self.factory.head('/test/page/') + self.assertFalse(self.middleware._should_track(request)) + + def test_should_track_post_method(self): + """Test que les requêtes POST sont trackées.""" + request = self.factory.post('/test/form/') + self.assertTrue(self.middleware._should_track(request)) + + def test_get_client_ip(self): + """Test de récupération de l'adresse IP du client.""" + # IP directe + request = self.factory.get('/') + request.META['REMOTE_ADDR'] = '192.168.1.100' + ip = self.middleware._get_client_ip(request) + self.assertEqual(ip, '192.168.1.100') + + # IP via proxy (X-Forwarded-For) + request = self.factory.get('/') + request.META['HTTP_X_FORWARDED_FOR'] = '203.0.113.1, 192.168.1.1' + ip = self.middleware._get_client_ip(request) + self.assertEqual(ip, '203.0.113.1') + + +class AnalyticsViewsTest(TestCase): + """Tests pour les vues Analytics.""" + + def setUp(self): + self.client = Client() + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + self.superuser = User.objects.create_superuser( + username='superuser', + password='testpass123' + ) + + def test_dashboard_requires_superuser(self): + """Test que le dashboard nécessite les droits superuser.""" + # Utilisateur non authentifié + response = self.client.get('/analytics/') + self.assertEqual(response.status_code, 302) # Redirection vers login + + # Utilisateur authentifié mais non superuser + self.client.login(username='testuser', password='testpass123') + response = self.client.get('/analytics/') + self.assertEqual(response.status_code, 302) # Redirection + + # Utilisateur superuser + self.client.login(username='superuser', password='testpass123') + response = self.client.get('/analytics/') + self.assertEqual(response.status_code, 200) # Accès autorisé + + def test_dashboard_displays_stats(self): + """Test que le dashboard affiche les statistiques.""" + # Créer quelques données de test + PageView.objects.create( + user=self.user, + session_key='session1', + path='/test/', + ) + + self.client.login(username='superuser', password='testpass123') + response = self.client.get('/analytics/') + + self.assertEqual(response.status_code, 200) + self.assertIn('stats', response.context) + self.assertIn('chart_data', response.context) + + def test_dashboard_days_filter(self): + """Test du filtre de période du dashboard.""" + self.client.login(username='superuser', password='testpass123') + + # 7 jours + response = self.client.get('/analytics/?days=7') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['days'], 7) + + # 30 jours + response = self.client.get('/analytics/?days=30') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['days'], 30) + + +class AnalyticsIntegrationTest(TestCase): + """Tests d'intégration du système Analytics.""" + + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + + def test_page_view_tracking_flow(self): + """Test du flux complet de tracking d'une page view.""" + # Se connecter + client = Client() + client.login(username='testuser', password='testpass123') + + # Naviguer sur une page (le middleware devrait créer une PageView) + # Note: En environnement de test, le middleware peut ne pas être actif + # Ce test vérifie plutôt la structure + + # Vérifier qu'on peut créer manuellement une PageView + initial_count = PageView.objects.count() + PageView.objects.create( + user=self.user, + session_key=client.session.session_key or 'test_session', + path='/test/page/', + ) + + final_count = PageView.objects.count() + self.assertEqual(final_count, initial_count + 1) diff --git a/streetup/analytics/urls.py b/streetup/analytics/urls.py new file mode 100644 index 0000000..1d3b4e5 --- /dev/null +++ b/streetup/analytics/urls.py @@ -0,0 +1,14 @@ +""" +URLs pour l'application Analytics. +""" + +from django.urls import path +from . import views + +app_name = 'analytics' + +urlpatterns = [ + path('', views.dashboard, name='dashboard'), + path('sessions/', views.sessions_list, name='sessions_list'), + path('page-views/', views.page_views_list, name='page_views_list'), +] diff --git a/streetup/analytics/views.py b/streetup/analytics/views.py new file mode 100644 index 0000000..f3f0c9e --- /dev/null +++ b/streetup/analytics/views.py @@ -0,0 +1,224 @@ +""" +Vues pour le tableau de bord d'analytics. +""" + +from django.shortcuts import render +from django.contrib.auth.decorators import user_passes_test +from django.utils import timezone +from django.db.models import Count, Avg, Q, F +from django.db.models.functions import TruncDate, TruncHour +from datetime import timedelta +import json + +from .models import PageView, UserSession +from django.contrib.auth.models import User + + +def superuser_required(view_func): + """Décorateur qui vérifie que l'utilisateur est un superadmin.""" + decorated_view = user_passes_test( + lambda u: u.is_active and u.is_superuser, + login_url='admin:login' + )(view_func) + return decorated_view + + +@superuser_required +def dashboard(request): + """ + Vue principale du tableau de bord d'analytics. + Affiche les statistiques d'utilisation de l'application. + """ + # Période à analyser (par défaut: 30 derniers jours) + days = int(request.GET.get('days', 30)) + start_date = timezone.now() - timedelta(days=days) + + # Statistiques générales + stats = { + 'total_users': User.objects.filter(is_active=True).count(), + 'active_users': _get_active_users_count(start_date), + 'total_sessions': UserSession.objects.filter(login_time__gte=start_date).count(), + 'active_sessions': UserSession.objects.filter(is_active=True).count(), + 'total_page_views': PageView.objects.filter(timestamp__gte=start_date).count(), + 'avg_response_time': PageView.objects.filter( + timestamp__gte=start_date, + response_time_ms__isnull=False + ).aggregate(avg=Avg('response_time_ms'))['avg'] or 0, + } + + # Calculer les sessions par utilisateur (éviter la division par zéro) + stats['sessions_per_user'] = ( + round(stats['total_sessions'] / stats['active_users'], 2) + if stats['active_users'] > 0 + else 0 + ) + + # Pages les plus visitées + top_pages = PageView.objects.filter( + timestamp__gte=start_date + ).values('path').annotate( + count=Count('id') + ).order_by('-count')[:10] + + # Utilisateurs les plus actifs + top_users = PageView.objects.filter( + timestamp__gte=start_date, + user__isnull=False + ).values('user__username', 'user__id').annotate( + views=Count('id'), + sessions=Count('session_key', distinct=True) + ).order_by('-views')[:10] + + # Activité par jour + daily_activity = PageView.objects.filter( + timestamp__gte=start_date + ).annotate( + date=TruncDate('timestamp') + ).values('date').annotate( + views=Count('id'), + users=Count('user', distinct=True), + sessions=Count('session_key', distinct=True) + ).order_by('date') + + # Préparer les données pour les graphiques + chart_data = { + 'labels': [item['date'].strftime('%d/%m') for item in daily_activity], + 'views': [item['views'] for item in daily_activity], + 'users': [item['users'] for item in daily_activity], + 'sessions': [item['sessions'] for item in daily_activity], + } + + # Activité par heure (dernières 24h) + yesterday = timezone.now() - timedelta(days=1) + hourly_activity = PageView.objects.filter( + timestamp__gte=yesterday + ).annotate( + hour=TruncHour('timestamp') + ).values('hour').annotate( + count=Count('id') + ).order_by('hour') + + hourly_chart_data = { + 'labels': [item['hour'].strftime('%H:%M') for item in hourly_activity], + 'values': [item['count'] for item in hourly_activity], + } + + context = { + 'stats': stats, + 'top_pages': top_pages, + 'top_users': top_users, + 'chart_data': json.dumps(chart_data), + 'hourly_chart_data': json.dumps(hourly_chart_data), + 'days': days, + } + + return render(request, 'analytics/dashboard.html', context) + + +@superuser_required +def sessions_list(request): + """ + Liste détaillée des sessions utilisateurs. + """ + # Paramètres de filtrage + days = int(request.GET.get('days', 30)) + user_id = request.GET.get('user') + active_only = request.GET.get('active_only', False) + + start_date = timezone.now() - timedelta(days=days) + + # Requête de base + sessions = UserSession.objects.filter( + login_time__gte=start_date + ).select_related('user') + + # Filtres + if user_id: + sessions = sessions.filter(user_id=user_id) + + if active_only: + sessions = sessions.filter(is_active=True) + + # Ordre + sessions = sessions.order_by('-login_time') + + # Pagination (simple, 100 par page) + sessions = sessions[:100] + + # Liste des utilisateurs pour le filtre + users = User.objects.filter( + analytics_sessions__login_time__gte=start_date + ).distinct().order_by('username') + + context = { + 'sessions': sessions, + 'users': users, + 'selected_user_id': user_id, + 'active_only': active_only, + 'days': days, + } + + return render(request, 'analytics/sessions_list.html', context) + + +@superuser_required +def page_views_list(request): + """ + Liste détaillée des vues de pages. + """ + # Paramètres de filtrage + days = int(request.GET.get('days', 7)) # Par défaut 7 jours car il peut y avoir beaucoup de données + user_id = request.GET.get('user') + path = request.GET.get('path') + + start_date = timezone.now() - timedelta(days=days) + + # Requête de base + page_views = PageView.objects.filter( + timestamp__gte=start_date + ).select_related('user') + + # Filtres + if user_id: + page_views = page_views.filter(user_id=user_id) + + if path: + page_views = page_views.filter(path__icontains=path) + + # Ordre + page_views = page_views.order_by('-timestamp') + + # Pagination (simple, 100 par page) + page_views = page_views[:100] + + # Liste des utilisateurs et chemins pour les filtres + users = User.objects.filter( + page_views__timestamp__gte=start_date + ).distinct().order_by('username') + + popular_paths = PageView.objects.filter( + timestamp__gte=start_date + ).values('path').annotate( + count=Count('id') + ).order_by('-count')[:20] + + context = { + 'page_views': page_views, + 'users': users, + 'popular_paths': popular_paths, + 'selected_user_id': user_id, + 'selected_path': path, + 'days': days, + } + + return render(request, 'analytics/page_views_list.html', context) + + +def _get_active_users_count(start_date): + """ + Compte le nombre d'utilisateurs actifs depuis start_date. + Un utilisateur est considéré actif s'il a au moins une page view. + """ + return User.objects.filter( + page_views__timestamp__gte=start_date + ).distinct().count() diff --git a/streetup/api/__init__.py b/streetup/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/api/admin.py b/streetup/api/admin.py new file mode 100644 index 0000000..67aa7ab --- /dev/null +++ b/streetup/api/admin.py @@ -0,0 +1,92 @@ +# admin.py +from django import forms +from django.utils import timezone +from django.utils.html import escape, mark_safe +from django.contrib import admin +from django.contrib.admin.widgets import AdminSplitDateTime + +from .models import PersonalAccessToken, _sha256 # _sha256 dispo dans models.py + +import secrets + +_HTML5_FMT = "%Y-%m-%dT%H:%M" # sans secondes ni timezone (exigence de datetime-local) + +class PersonalAccessTokenForm(forms.ModelForm): + # Non obligatoire: on le remplit nous-mêmes si vide + token_hash = forms.CharField(required=False, widget=forms.TextInput(attrs={"readonly": "readonly"})) + # Aide à la saisie: durée si expires_at non remplit + expires_at = forms.DateTimeField( + required=False, + input_formats=[_HTML5_FMT], # pour le POST + widget=forms.DateTimeInput( + attrs={"type": "datetime-local"}, + format=_HTML5_FMT, # pour l’affichage initial + ), + ) + ttl_days = forms.IntegerField(required=False, min_value=1, initial=30, help_text="Utilisé si 'expires_at' est vide.") + + class Meta: + model = PersonalAccessToken + fields = ("user", "name", "collections", "expires_at", "revoked", "token_hash") + widgets = { + # JSONField par défaut convient; sinon remplacer par un Textarea + # "collections": forms.Textarea(attrs={"rows": 2}), + } + + def clean(self): + cleaned = super().clean() + expires_at = cleaned.get("expires_at") + ttl_days = cleaned.get("ttl_days") + if not expires_at: + # Si pas de date fournie, calcule via ttl_days (ou 30 par défaut) + days = ttl_days or 30 + cleaned["expires_at"] = timezone.now() + timezone.timedelta(days=days) + return cleaned + + +@admin.register(PersonalAccessToken) +class PersonalAccessTokenAdmin(admin.ModelAdmin): + form = PersonalAccessTokenForm + list_display = ("user", "name", "expires_at", "revoked", "created_at") + list_filter = ("revoked",) + search_fields = ("user__username", "user__email", "name") + readonly_fields = ("token_hash",) # visible mais non éditable + + actions = ["revoke_tokens"] + + @admin.action(description="Révoquer les tokens sélectionnés") + def revoke_tokens(self, request, queryset): + updated = queryset.update(revoked=True) + self.message_user(request, f"{updated} token(s) révoqué(s).") + + def save_model(self, request, obj, form, change): + """ + À la création, si token_hash est vide: + - Génère un raw token 'pat_...' + - Hash en SHA-256 (même logique que le modèle) + - Sauve l'objet + - Affiche le raw token une seule fois dans un message admin + """ + raw_token_to_display = None + + if not change and not obj.token_hash: + # Génère un raw token (même préfixe que la méthode issue) + raw = "pat_" + secrets.token_urlsafe(32) + obj.token_hash = _sha256(raw) # même algo que le modèle + raw_token_to_display = raw + + # Remise en forme des collections: set trié, comme dans issue() + if obj.collections: + obj.collections = sorted(set(obj.collections)) + + # Si l'admin n'a pas rempli expires_at, le form l'a calculé via ttl_days + + super().save_model(request, obj, form, change) + + if raw_token_to_display: + # Affiche le raw token (copiable) + l'expiration + message = f"Token généré pour {obj.user}: {raw_token_to_display}\nExpire le {obj.expires_at:%Y-%m-%d %H:%M}" + self.message_user( + request, + mark_safe("
"+escape(message)+"
") + ) diff --git a/streetup/api/apps.py b/streetup/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/streetup/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/streetup/api/auth.py b/streetup/api/auth.py new file mode 100644 index 0000000..4d3ed36 --- /dev/null +++ b/streetup/api/auth.py @@ -0,0 +1,124 @@ + +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.http import HttpRequest + +from rest_framework.authentication import SessionAuthentication, get_authorization_header + +from .models import PersonalAccessToken + + +# === AUTH HYBRIDE (JWT court OU PAT opaque) === +# - JWT: via djangorestframework-simplejwt (RS256) +# - PAT: tokens opaques en DB (lookup + expiration + revoke) + +User = get_user_model() + +# Import JWTAuthentication uniquement si simplejwt est installé/configuré +try: + from rest_framework_simplejwt.authentication import JWTAuthentication + + _jwt_auth = JWTAuthentication() +except Exception: # pragma: no cover + _jwt_auth = None + + +def collections_for(user) -> list[str]: + """ + Mapping des permissions Django -> collections accessibles via token. + Inclut les collections pygeoapi et les endpoints de reporting. + """ + collections = ["technical_galleries"] + + # Accès reporting : superuser ou can_access_view('reporting') + try: + from common.models import UserConfig + user_config = UserConfig.objects.get(user=user) + if user_config.can_access_view("reporting"): + collections.append("reporting") + except Exception: + pass + + if user.is_superuser: + if "reporting" not in collections: + collections.append("reporting") + + return collections + + +class HybridTokenAuthentication(SessionAuthentication): + """ + Authentifie les requêtes API de 3 façons: + 1) Session (cookie) — via SessionAuthentication (hérité) + 2) Authorization: Bearer + 3) ?token= + + Remplit request.collections (list[str]) quand un JWT/PAT est utilisé. + """ + + def authenticate(self, request: HttpRequest): + # 1) D'abord, si un header Bearer / query ?token est présent, tenter JWT/PAT + raw_token: str | None = None + auth = get_authorization_header(request).decode("utf-8") + if auth.startswith("Bearer "): + raw_token = auth.split(" ", 1)[1] + else: + raw_token = request.GET.get("token") + + # Si pas trouvé dans header/query, tenter depuis le chemin: /api/geo/pat_xxx/... + if not raw_token: + path = request.path or "" + # On cherche un segment 'pat_' immédiatement après /api/geo/ + # Exemple: /api/geo/pat_abCDe.../collections/technical_galleries/items + try: + after_geo = path.split("/geo/", 1)[1] # peut lever IndexError + first_seg = after_geo.split("/", 1)[0] + if first_seg.startswith("pat_"): + raw_token = first_seg + except Exception: + pass + + if raw_token: + # Essai JWT + if _jwt_auth is not None: + try: + validated = _jwt_auth.get_validated_token(raw_token) + user = _jwt_auth.get_user(validated) + request.collections = validated.get("collections", []) + return (user, validated) + except Exception: + pass + + # Essai PAT + pat = PersonalAccessToken.lookup(raw_token) + if pat is None: + # Échec explicite si un token a été fourni mais est invalide + return None + request.collections = pat.collections + return (pat.user, None) + + # 2) Sinon, retomber sur la session (auth interactive) + result = super().authenticate(request) + if result is not None: + # Pour les vues geo, on peut projeter les collections depuis la session + user, x = result + request.collections = collections_for(user) + return result + + +# === JWT issue utilitaire === +# (JWT court, claims minimales, signé RS256 via SimpleJWT) +try: + from rest_framework_simplejwt.tokens import AccessToken + + def mint_jwt(user, lifetime_minutes: int = 20) -> str: + t = AccessToken() + t["sub"] = str(user.pk) + t["aud"] = "pygeoapi" + t["collections"] = collections_for(user) + t.set_exp(from_time=timezone.now(), lifetime=timezone.timedelta(minutes=int(lifetime_minutes))) + return str(t) + +except Exception: # pragma: no cover + def mint_jwt(user, lifetime_minutes: int = 20) -> str: # fallback clair + raise RuntimeError("SimpleJWT n'est pas installé/configuré") \ No newline at end of file diff --git a/streetup/api/migrations/0001_initial.py b/streetup/api/migrations/0001_initial.py new file mode 100644 index 0000000..eb37a4e --- /dev/null +++ b/streetup/api/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PersonalAccessToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=120)), + ('token_hash', models.CharField(db_index=True, max_length=64, unique=True)), + ('collections', models.JSONField(default=list)), + ('expires_at', models.DateTimeField()), + ('revoked', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='personal_access_tokens', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-created_at',), + }, + ), + ] diff --git a/streetup/api/migrations/__init__.py b/streetup/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/api/models.py b/streetup/api/models.py new file mode 100644 index 0000000..6815b91 --- /dev/null +++ b/streetup/api/models.py @@ -0,0 +1,61 @@ +from __future__ import annotations +import hashlib +import secrets +from django.conf import settings +from django.contrib.auth import get_user_model +from django.db import models +from django.utils import timezone + +User = get_user_model() + + +def _sha256(s: str) -> str: + return hashlib.sha256(s.encode("utf-8")).hexdigest() + + +class PersonalAccessToken(models.Model): + """ + Token opaque (PAT) stocké haché pour liens publics ou intégrations. + Révocable instantanément + expiration. + """ + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="personal_access_tokens") + name = models.CharField(max_length=120, blank=True) + token_hash = models.CharField(max_length=64, unique=True, db_index=True) + collections = models.JSONField(default=list) # ex: ["technical-galleries", "assets"] + expires_at = models.DateTimeField() + revoked = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ("-created_at",) + + def __str__(self) -> str: + return f"PAT({self.user}, {self.name or 'unnamed'})" + + @property + def is_expired(self) -> bool: + return timezone.now() >= self.expires_at + + @property + def is_active(self) -> bool: + return (not self.revoked) and (not self.is_expired) + + @classmethod + def issue(cls, *, user, collections: list[str], ttl_hours: int = 720, name: str = "share link") -> tuple[str, "PersonalAccessToken"]: + raw = "pat_" + secrets.token_urlsafe(32) + obj = cls.objects.create( + user=user, + name=name, + token_hash=_sha256(raw), + collections=sorted(set(collections)), + expires_at=timezone.now() + timezone.timedelta(hours=int(ttl_hours)), + ) + return raw, obj + + @classmethod + def lookup(cls, raw: str) -> "PersonalAccessToken | None": + try: + obj = cls.objects.get(token_hash=_sha256(raw)) + return obj if obj.is_active else None + except cls.DoesNotExist: + return None diff --git a/streetup/api/tests.py b/streetup/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/streetup/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/streetup/api/urls.py b/streetup/api/urls.py new file mode 100644 index 0000000..36f9a80 --- /dev/null +++ b/streetup/api/urls.py @@ -0,0 +1,30 @@ +from django.urls import path, re_path, include +from .views import ( + GeoProxyView, + InterventionListCreateView, + InterventionRetrieveUpdateDestroyView, + IssueJWTView, + SharePATView, +) + +app_name = "zapi" + +urlpatterns = [ + # API root (optionnel: une vue d'info/health) + # path("", APIRootView.as_view(), name="root"), + + # ---- Assets -> proxy pygeoapi ---- + # /api/geo/... (catch-all) + re_path(r"^geo/.*$", GeoProxyView.as_view(), name="geo-proxy"), + + # ---- Interventions (DRF) ---- + path("interventions/", InterventionListCreateView.as_view(), name="intervention-list"), + path("interventions//", InterventionRetrieveUpdateDestroyView.as_view(), name="intervention-detail"), + + # ---- Reporting ---- + path("reporting/", include("reporting.urls")), + + # ---- Tokens utilitaires ---- + path("tokens/jwt/", IssueJWTView.as_view(), name="issue-jwt"), + path("tokens/share/", SharePATView.as_view(), name="share-pat"), +] diff --git a/streetup/api/views.py b/streetup/api/views.py new file mode 100644 index 0000000..e1b417c --- /dev/null +++ b/streetup/api/views.py @@ -0,0 +1,534 @@ +from __future__ import annotations +import os +from typing import Any, Optional +import re +from html import parser as html_parser +import httpx +from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode, unquote + +from rest_framework import generics, permissions, serializers, status +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView + +from django.http import HttpResponse + +from .models import PersonalAccessToken +from .auth import HybridTokenAuthentication, collections_for, mint_jwt + + +# === CONFIG === +PYGEOAPI_BASE_URL = os.environ.get("PYGEOAPI_BASE_URL") + + +# === VUES TOKENS === + +class IssueJWTView(APIView): + permission_classes = [IsAuthenticated] + authentication_classes = [HybridTokenAuthentication] + + def post(self, request, *args, **kwargs): + token = mint_jwt(request.user, lifetime_minutes=int(request.data.get("lifetime_minutes", 20))) + return Response({"access_token": token, "token_type": "Bearer"}) + + +class SharePATView(APIView): + permission_classes = [IsAuthenticated] + authentication_classes = [HybridTokenAuthentication] + + def post(self, request, *args, **kwargs): + req_cols = request.data.get("collections", []) + ttl_h = int(request.data.get("ttl_hours", 720)) + allowed = sorted(set(collections_for(request.user)) & set(req_cols)) if req_cols else collections_for(request.user) + raw, obj = PersonalAccessToken.issue( + user=request.user, + collections=allowed, + ttl_hours=ttl_h, + name=request.data.get("name", "share link"), + ) + base = request.build_absolute_uri("/api/geo/").rstrip("/") + url_path = f"{base}/{raw}/" # <-- forme /api/geo/pat_xxx/ + url_query = request.build_absolute_uri("/api/geo/").rstrip("/") + f"?token={raw}" + return Response({ + "token": raw, + "url_path": url_path, + "url_query": url_query, + "expires_at": obj.expires_at, + }) + + +# === PROXY GEO -> PYGEOAPI === + +class GeoProxyView(APIView): + """ + Passerelle /api/geo/... -> pygeoapi. + - Auth: HybridTokenAuthentication (JWT, PAT, ou Session) + - AutZ: filtrage par collection si l'URL cible le détail d'une collection + """ + permission_classes = [IsAuthenticated] + authentication_classes = [HybridTokenAuthentication] + + def _strip_prefix(self, full_path: str) -> str: + # /api/geo/... -> /collections/... (chemin attendu par pygeoapi) + path = full_path + if path.startswith("/api"): + path = path[len("/api"):] + if path.startswith("/geo"): + path = path[len("/geo"):] + if not path.startswith("/"): + path = "/" + path + + # Si le 1er segment est un PAT (pat_...), on l'enlève du chemin + parts = [p for p in path.split("/") if p] + if parts and parts[0].startswith("pat_"): + parts = parts[1:] + path = "/" + "/".join(parts) + + return path + + def _check_collection(self, url_or_path: str, allowed: list[str] | None) -> tuple[bool, str | None]: + """ + Retourne: + (True, None) -> accès OK (pas une URL de collection spécifique) + (True, cid) -> accès OK à la collection cid + (False, cid) -> accès refusé à la collection cid + """ + # 1) Extraire le path pur (ignore ?f=json, #hash, etc.) + s = urlsplit(url_or_path) + path = s.path or url_or_path + path = unquote(path) + + # 2) Trouver /collections/ n'importe où dans le path + needle = "/collections/" + i = path.lower().find(needle) + if i == -1: + # Pas une URL /collections/{cid} -> OK + return True, None + + # 3) Récupérer le segment cid (ce qui suit immédiatement /collections/) + after = path[i + len(needle):] + if not after: + # /collections (listing) -> OK + return True, None + + cid = after.split("/", 1)[0] + if not cid: + # /collections/ sans id -> traiter comme listing + return True, None + + # 4) Vérifier l'autorisation (si filtrage actif) + if allowed is None: + return True, cid + return (cid in allowed, cid) + + def request_upstream(self, request, upstream: str, *, path: str, allowed: list[str] | None = None): + headers = {k: v for k, v in request.headers.items() if k.lower() not in {"host", "content-length"}} + try: + r = httpx.request( + method=request.method, + url=upstream, + headers=headers, + params=request.GET, + content=(getattr(request, "body", None) if request.method in {"POST", "PUT", "PATCH"} else None), + timeout=15.0, + ) + except httpx.RequestError as exc: + return Response({"detail": f"Upstream error: {exc}"}, status=status.HTTP_502_BAD_GATEWAY) + + # --- NEW: renvoi correct selon le Content-Type --- + content_type = (r.headers.get("content-type") or "").split(";")[0].strip().lower() + + pat = _extract_pat_from_request(request) + mode = _choose_pat_mode(request) + + allowed_set: set[str] | None + if allowed is None: + allowed_set = None + else: + allowed_set = {c for c in allowed if c} + path_no_query = path.split("?", 1)[0] + path_for_filter = (path_no_query.rstrip("/") or "/") if path_no_query else "/" + + # Headers Location (redirections) + def _copy_headers(resp): + # Ne pas copier Content-Encoding pour éviter la double compression (Django GZipMiddleware + pygeoapi) + for h in ("Content-Disposition", "Cache-Control", "ETag", "Last-Modified"): + if h in r.headers: + resp[h] = r.headers[h] + loc = r.headers.get("Location") + if loc and pat: + resp["Location"] = _rewrite_api_geo_everywhere(loc, pat, mode=mode) + return resp + + # JSON + try: + data = r.json() + if allowed_set is not None and path_for_filter == "/collections" and isinstance(data, dict): + data = _filter_collections_json(data, allowed_set) + def _walk(o): + if isinstance(o, dict): + return {k: _walk(v) for k, v in o.items()} + if isinstance(o, list): + return [_walk(v) for v in o] + if isinstance(o, str) and "/api/geo" in o: + return _rewrite_api_geo_everywhere(o, pat, mode=mode) + return o + data = _walk(data) + resp = Response(data, status=r.status_code) + return _copy_headers(resp) + except ValueError: + pass + + # HTML / texte + if content_type in {"text/html", "text/plain", "application/xml", "text/xml"}: + text = r.text + if allowed_set is not None and path_for_filter == "/collections": + text = _filter_collections_html(text, allowed_set) + if pat and "/api/geo" in text: + text = _rewrite_api_geo_everywhere(text, pat, mode=mode) + resp = HttpResponse(text, status=r.status_code, content_type=r.headers.get("content-type")) + return _copy_headers(resp) + + # Binaire inchangé + resp = HttpResponse(r.content, status=r.status_code, content_type=r.headers.get("content-type")) + return _copy_headers(resp) + + def get(self, request, *args, **kwargs): + path = self._strip_prefix(request.get_full_path()) + ok, cid = self._check_collection(path, getattr(request, "collections", [])) + if not ok: + return Response({"detail": f"Forbidden collection: {cid}"}, status=status.HTTP_403_FORBIDDEN) + upstream = f"{PYGEOAPI_BASE_URL}{path}" + return self.request_upstream(request, upstream, path=path, allowed=getattr(request, "collections", None)) + + # Optionnel: POST/PUT/PATCH/DELETE si tu as des opérations d'écriture pygeoapi + def post(self, request, *args, **kwargs): + path = self._strip_prefix(request.get_full_path()) + ok, cid = self._check_collection(path, getattr(request, "collections", [])) + if not ok: + return Response({"detail": f"Forbidden collection: {cid}"}, status=status.HTTP_403_FORBIDDEN) + upstream = f"{PYGEOAPI_BASE_URL}{path}" + return self.request_upstream(request, upstream, path=path, allowed=getattr(request, "collections", None)) + + +# === DRF pur === + +from interventions.models import Intervention + +class InterventionSerializer(serializers.ModelSerializer): + class Meta: + model = Intervention + fields = ("code","title","status","creation_time") + + +class InterventionListCreateView(generics.ListCreateAPIView): + """ + GET /api/interventions/?status=to_be_processed&status=in_progress + POST /api/interventions/ + """ + permission_classes = [IsAuthenticated] + authentication_classes = [HybridTokenAuthentication] + # TODO: remplacer par le queryset réel: + queryset = [] + serializer_class = InterventionSerializer + + def get_queryset(self): + qs = super().get_queryset() + statuses = self.request.query_params.getlist("status") + if statuses: + # TODO: adapter au champ status réel + # qs = qs.filter(status__in=statuses) + pass + return qs + + +class InterventionRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [IsAuthenticated] + authentication_classes = [HybridTokenAuthentication] + # TODO: remplacer par le queryset réel: + queryset = [] + serializer_class = InterventionSerializer + + + + + + +def _extract_pat_from_request(request): + auth = request.headers.get("Authorization", "") + if auth.lower().startswith("bearer pat_"): + return auth.split(None, 1)[1].strip() + tok = request.GET.get("token") + if tok and tok.startswith("pat_"): + return tok + path = request.path or "" + try: + after_geo = path.split("/geo/", 1)[1] + seg0 = after_geo.split("/", 1)[0] + if seg0.startswith("pat_"): + return seg0 + except Exception: + pass + return None + +def _choose_pat_mode(request) -> str: + # Forçage manuel + forced = request.GET.get("_pat_mode") + if forced in {"path", "query"}: + return forced + # Si PAT en query -> réécritures en query, sinon path + if request.GET.get("token", "").startswith("pat_"): + return "query" + return "path" + + + +# Capture TOUTE l’URL /api/geo… (absolue ou relative), y compris ?query et #fragment, +# mais en évitant le cas où un /pat_… est déjà juste après /api/geo/ +_PATTERN_API_GEO_URL = re.compile( + r'((?:(?:https?://|//)[^/\'"\s>]+)?' # schéma + host éventuel + r'/api/geo' # base + r'(?:/(?!pat_[^/]+)[^\'"\s>]*)?' # suite de chemin (sans /pat_… juste après) + r'(?:\?[^#\'"\s>]*)?' # query facultative + r'(?:#[^\'"\s>]*)?' # fragment facultatif + r')' +) + +def _rewrite_api_geo_everywhere(text: str, pat: str, mode: str = "path") -> str: + """ + Réécrit toutes les URLs /api/geo… : + - mode="path" -> injecte /pat_xxx/ après /api/geo + - mode="query" -> ajoute ?token=pat_xxx (ou &token=…) + Ne double pas si déjà présent (/pat_… ou token=…). + """ + if not pat or not text: + return text + + def _repl(m): + url = m.group(1) + parts = urlsplit(url) + # parts.path commence par /api/geo… + after = parts.path.split("/api/geo", 1)[1] # "", "/...", etc. + + if mode == "path": + # si déjà /pat_… juste après /api/geo, ne rien faire + if after.startswith("/") and after[1:].startswith("pat_"): + return url + new_path = parts.path.replace("/api/geo", f"/api/geo/{pat}", 1) + return urlunsplit((parts.scheme, parts.netloc, new_path, parts.query, parts.fragment)) + + # mode "query" + # si token déjà présent, ne rien faire + q = parse_qsl(parts.query or "", keep_blank_values=True) + if any(k == "token" for k, _ in q): + return url + q.append(("token", pat)) + new_query = urlencode(q, doseq=True) + return urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment)) + + return _PATTERN_API_GEO_URL.sub(_repl, text) + + + +def _filter_collections_json(data: dict[str, Any], allowed: set[str]) -> dict[str, Any]: + """Filtre la réponse JSON /collections selon les collections autorisées.""" + collections = data.get("collections") + if isinstance(collections, list): + filtered = [] + for item in collections: + if not isinstance(item, dict): + continue + cid = item.get("id") + if cid in allowed: + filtered.append(item) + data["collections"] = filtered + + links = data.get("links") + if isinstance(links, list): + filtered_links = [] + for link in links: + if not isinstance(link, dict): + filtered_links.append(link) + continue + href = link.get("href", "") + cid = _extract_collection_id_from_href(str(href)) + if cid is None or cid in allowed: + filtered_links.append(link) + data["links"] = filtered_links + + return data + + + +def _extract_collection_id_from_href(href: str) -> str | None: + if not href: + return None + ref = href.strip() + if not ref: + return None + # Normalise la partie chemin + path = ref + if "?" in path: + path = path.split("?", 1)[0] + if "#" in path: + path = path.split("#", 1)[0] + if "collections/" in path: + segment = path.split("collections/", 1)[1] + else: + segment = path + segment = segment.lstrip("./") + while segment.startswith("../"): + segment = segment[3:] + segment = segment.lstrip("/") + if not segment: + return None + cid = segment.split("/", 1)[0] + if not cid: + return None + # on évite de considérer des liens génériques (items, schema, etc.) + if cid in {"items", "query", "schema", "coverage", "tiles", "map"}: + return None + return cid + + +# ------------------------------------------------------------------- +# HTML Filtering +# ------------------------------------------------------------------- + +_COLLECTION_HREF_RE = re.compile(r"/collections/([^/?#]+)", re.I) + +class _CollectionHTMLFilter(html_parser.HTMLParser): + CONTAINER_TAGS = {"tr", "li", "article", "div", "section"} + # HTML void elements (no end tag is emitted by the parser) + VOID_TAGS = { + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr" + } + + def __init__(self, allowed: Optional[set[str]]): + super().__init__(convert_charrefs=False) + self.allowed = allowed + self.stack = [{"tag": None, "buffer": [], "ids": set(), "container": False}] + + def _push(self, tag: Optional[str], container: bool = False): + self.stack.append({"tag": tag, "buffer": [], "ids": set(), "container": container}) + + def _pop(self): + return self.stack.pop() + + def _append(self, s: str): + self.stack[-1]["buffer"].append(s) + + def get_html(self) -> str: + return "".join(self.stack[0]["buffer"]) + + def handle_decl(self, decl: str): + self._append(f"") + + def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]): + t = tag.lower() + + # Build attributes + if attrs: + a = " ".join( + f'{k}="{(v or "").replace("\"", """)}"' if v is not None else f"{k}" + for k, v in attrs + ) + open_tag = f"<{tag} {a}>" + else: + open_tag = f"<{tag}>" + + # Void elements: append and DO NOT push (no end tag will arrive) + if t in self.VOID_TAGS: + self._append(open_tag) + return + + # Non-void: push a new frame and append the start tag inside that frame + container = t in self.CONTAINER_TAGS + self._push(tag, container) + self._append(open_tag) + + # Detect collection IDs on + if t == "a": + href = None + for k, v in attrs: + if k.lower() == "href": + href = v or "" + break + if href: + m = _COLLECTION_HREF_RE.search(href) + if m: + cid = m.group(1) + # mark current and nearest container + self.stack[-1]["ids"].add(cid) + for frame in reversed(self.stack): + if frame["container"]: + frame["ids"].add(cid) + break + + def handle_endtag(self, tag: str): + t = tag.lower() + + # If the stack top isn't the same tag, avoid popping (robustness) + if not self.stack or self.stack[-1]["tag"] is None or self.stack[-1]["tag"].lower() != t: + # Still emit the closing tag for best-effort rendering + self._append(f"") + return + + # Close normally + self._append(f"") + frame = self._pop() + + if frame["container"] and self.allowed is not None: + ids = frame["ids"] + keep = (not ids) or any(cid in self.allowed for cid in ids) + if not keep: + # Drop the entire container block + return + + # Keep the content (opening+children+closing) by appending to parent + self._append("".join(frame["buffer"])) + + def handle_startendtag(self, tag: str, attrs: list[tuple[str, Optional[str]]]): + # Treat as a void-like tag + if attrs: + a = " ".join( + f'{k}="{(v or "").replace("\"", """)}"' if v is not None else f"{k}" + for k, v in attrs + ) + self._append(f"<{tag} {a} />") + else: + self._append(f"<{tag} />") + + def handle_data(self, data: str): + self._append(data) + + def handle_comment(self, data: str): + self._append(f"") + + def handle_entityref(self, name: str): + self._append(f"&{name};") + + def handle_charref(self, name: str): + self._append(f"&#{name};") + + +def _filter_collections_html(html: str, allowed: Optional[set[str]]) -> str: + if not html: + return "\n" + + if allowed is None: + out = html + if "\n" + out + return out + + parser = _CollectionHTMLFilter(allowed) + parser.feed(html) + parser.close() + print(parser.stack) + out = parser.get_html() + + if "\n" + out + return out \ No newline at end of file diff --git a/streetup/assets/__init__.py b/streetup/assets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/assets/admin.py b/streetup/assets/admin.py new file mode 100644 index 0000000..4641362 --- /dev/null +++ b/streetup/assets/admin.py @@ -0,0 +1,653 @@ +from django.contrib.gis import admin +from django.contrib.contenttypes.admin import GenericTabularInline +from django import forms + +from documents.models import DocumentFolder, DocumentFolderAttachment + +from .models import TrafficLightIntersection, TrafficLightIntersectionDocument +from .models import TrafficLightPoleModel, TrafficLightPole, TrafficLightCableModel, TrafficLightCable, TrafficLightElectricalCabinet, TrafficLightElectricalCabinetModel +from .models import TrafficLightLanternModel, TrafficLightLantern, TrafficLightControllerModel, TrafficLightController, TrafficLightDetector, TrafficLightDetectorModel +from .models import TrafficLightContract, TrafficLightRadarModel, TrafficLightRadar +from .models import ( + TrafficLightRadarPoleModel, TrafficLightRadarPole, TrafficLightElectricalCabinetContentModel, + TrafficLightElectricalCabinetContent, TrafficLightAccessoryModel, TrafficLightAccessory, + TrafficLightProgramming, TrafficLightControllerHardware, TrafficLightControllerLogFile, + TrafficLightNetworkHardware, TrafficLightVoltageHardware +) +from .models import SignStreet +from .models import AssetCategory, AssetHistory +from .models import GreenSurfacesComment, GreenSurfacesCommentPhoto +from .models import CleanGlassContainer, CleanGlassContainerModel, CleanLitterBin, CleanLitterBinModel, CleanLocation, CleanLocationContract + + + + +class AssetAdmin(admin.ModelAdmin): + def get_search_fields(self, request): + search_fields = list(super().get_search_fields(request) or []) + model_fields = {f.name for f in self.model._meta.get_fields()} + for field in ["code", "name_fr", "name_nl"]: + if field in model_fields and field not in search_fields: + search_fields.append(field) + return search_fields + + def get_list_filter(self, request): + list_filter = list(super().get_list_filter(request) or []) + model_fields = {f.name for f in self.model._meta.get_fields()} + if "model" in model_fields and "model" not in list_filter: + list_filter.append("model") + return list_filter + + +class AssetGISAdmin(admin.GISModelAdmin): + gis_widget_kwargs = { + 'attrs': { + "default_lon" : 4.3517, # Bruxelles + "default_lat" : 50.8503, + "default_zoom" : 12 + }, + } + + def get_search_fields(self, request): + search_fields = list(super().get_search_fields(request) or []) + model_fields = {f.name for f in self.model._meta.get_fields()} + for field in ["code", "name_fr", "name_nl"]: + if field in model_fields and field not in search_fields: + search_fields.append(field) + return search_fields + + def get_list_filter(self, request): + list_filter = list(super().get_list_filter(request) or []) + model_fields = {f.name for f in self.model._meta.get_fields()} + if "model" in model_fields and "model" not in list_filter: + list_filter.append("model") + return list_filter + + +@admin.register(TrafficLightContract) +class TrafficLightContractAdmin(AssetAdmin): + search_fields = ("intersection__code", "contract__contract_number") + list_display = ("intersection", "contract") + list_filter = ("contract",) + +class TrafficLightContractAdminInline(admin.TabularInline): + model = TrafficLightContract + extra = 1 + +@admin.register(TrafficLightIntersection) +class TrafficLightIntersectionAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + list_display = ("code", "name_fr", "municipality") + list_filter = ("municipality",) + inlines = [TrafficLightContractAdminInline,] + + + +admin.site.register(TrafficLightIntersectionDocument, AssetAdmin) +admin.site.register(TrafficLightPoleModel, AssetAdmin) +admin.site.register(TrafficLightPole, AssetAdmin) +admin.site.register(TrafficLightCableModel, AssetAdmin) +admin.site.register(TrafficLightCable, AssetAdmin) +admin.site.register(TrafficLightLanternModel, AssetAdmin) +admin.site.register(TrafficLightLantern, AssetAdmin) +admin.site.register(TrafficLightControllerModel, AssetAdmin) +admin.site.register(TrafficLightDetector, AssetAdmin) +admin.site.register(TrafficLightDetectorModel, AssetAdmin) +admin.site.register(TrafficLightElectricalCabinetModel, AssetAdmin) +admin.site.register(TrafficLightElectricalCabinet, AssetAdmin) +admin.site.register(TrafficLightRadarModel, AssetAdmin) +admin.site.register(TrafficLightRadar, AssetAdmin) +admin.site.register(TrafficLightRadarPoleModel, AssetAdmin) +admin.site.register(TrafficLightRadarPole, AssetAdmin) +admin.site.register(TrafficLightElectricalCabinetContentModel, AssetAdmin) +admin.site.register(TrafficLightElectricalCabinetContent, AssetAdmin) +admin.site.register(TrafficLightAccessoryModel, AssetAdmin) +admin.site.register(TrafficLightAccessory, AssetAdmin) +admin.site.register(TrafficLightProgramming, AssetAdmin) +admin.site.register(TrafficLightControllerHardware, AssetAdmin) +admin.site.register(TrafficLightControllerLogFile, AssetAdmin) +admin.site.register(TrafficLightNetworkHardware, AssetAdmin) +admin.site.register(TrafficLightVoltageHardware, AssetAdmin) + + +@admin.register(TrafficLightController) +class TrafficLightControllerAdmin(AssetAdmin): + search_fields = ("intersection__code",) + list_display = ("intersection", "connected_to_control_center", "controller_maintenance_contract") + list_filter = ("connected_to_control_center", "controller_maintenance_contract") + + + +@admin.register(AssetCategory) +class AssetCategoryAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + list_display = ("code", "get_name", "thematic", "parent") + list_filter = ("thematic", "parent") + autocomplete_fields = ("parent",) + filter_horizontal = ("allowed_models",) + change_list_template = "admin/assets/assetcategory/change_list.html" + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if "parent" in form.base_fields: + # Limiter les choix du parent aux catégories sans parent (1 niveau max) + form.base_fields["parent"].queryset = AssetCategory.objects.filter(parent__isnull=True) + return form + + def get_urls(self): + from django.urls import path as urlpath + urls = super().get_urls() + custom_urls = [ + urlpath( + "import-excel/", + self.admin_site.admin_view(self.import_excel_view), + name="assets_assetcategory_import_excel", + ), + urlpath( + "download-template/", + self.admin_site.admin_view(self.download_template_view), + name="assets_assetcategory_download_template", + ), + ] + return custom_urls + urls + + # ------------------------------------------------------------------ + def import_excel_view(self, request): + import io + import tempfile + import os + + from django.contrib import messages + from django.shortcuts import render, redirect + from django.urls import reverse + + from assets.management.commands.import_assets_from_excel import Command as ImportCommand + + from assets.management.commands.import_assets_from_excel import COLUMNS, COLUMN_HINTS + + column_guide = [ + {"key": key, "desc": COLUMN_HINTS.get(key, "")} + for key, _label in COLUMNS + ] + + context = { + **self.admin_site.each_context(request), + "title": "Import d'assets depuis Excel", + "opts": self.model._meta, + "column_guide": column_guide, + } + + if request.method == "POST": + uploaded = request.FILES.get("excel_file") + if not uploaded: + messages.error(request, "Aucun fichier sélectionné.") + return render(request, "admin/assets/assetcategory/import_excel.html", context) + + if not uploaded.name.endswith(".xlsx"): + messages.error(request, "Seuls les fichiers .xlsx sont acceptés.") + return render(request, "admin/assets/assetcategory/import_excel.html", context) + + dry_run = "dry_run" in request.POST + update = "update" in request.POST + + # Save file to a temp path so the command can open it + suffix = ".xlsx" + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: + for chunk in uploaded.chunks(): + tmp.write(chunk) + tmp_path = tmp.name + + cmd = ImportCommand() + + # Capture stdout / stderr via StringIO-compatible wrapper + class _Writer: + def __init__(self): + self.lines = [] + def write(self, msg): + self.lines.append(str(msg)) + def flush(self): + pass + + out = _Writer() + err = _Writer() + cmd.stdout = out + cmd.stderr = err + + import_error = None + try: + cmd._import(tmp_path, dry_run=dry_run, update=update) + except Exception as e: + import_error = str(e) + finally: + try: + os.unlink(tmp_path) + except OSError: + pass + + output_lines = out.lines + err.lines + context["output_lines"] = output_lines + context["dry_run"] = dry_run + context["import_error"] = import_error + + if import_error: + messages.error(request, f"Erreur lors de l'import : {import_error}") + elif dry_run: + messages.warning(request, "Dry-run terminé – aucune donnée modifiée.") + else: + messages.success(request, "Import terminé avec succès.") + + return render(request, "admin/assets/assetcategory/import_excel.html", context) + + return render(request, "admin/assets/assetcategory/import_excel.html", context) + + # ------------------------------------------------------------------ + def download_template_view(self, request): + import io + from django.http import HttpResponse + from assets.management.commands.import_assets_from_excel import generate_template + + buf = io.BytesIO() + # generate_template expects a file path, so use a temp file + import tempfile + import os + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + try: + generate_template(tmp_path) + with open(tmp_path, "rb") as f: + data = f.read() + finally: + try: + os.unlink(tmp_path) + except OSError: + pass + + response = HttpResponse( + data, + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + response["Content-Disposition"] = 'attachment; filename="assets_import_template.xlsx"' + return response + + +@admin.register(AssetHistory) +class AssetHistoryAdmin(AssetAdmin): + list_display = ("timestamp", "content_type", "object_id", "user", "field_name", "action_type") + list_filter = ("action_type", "content_type", "timestamp") + search_fields = ("user__username", "field_name", "object_id") + readonly_fields = ("content_type", "object_id", "user", "timestamp", "field_name", "old_value", "new_value", "action_type") + date_hierarchy = "timestamp" + + def has_add_permission(self, request): + # Prevent manual creation + return False + + def has_change_permission(self, request, obj=None): + # Read-only + return False + + +# ============================================================================ +# Public Lighting +# ============================================================================ +from .models import ( + PublicLightingStreet, PublicLightingStreetDocument, PublicLightingPoleModel, + PublicLightingPole, PublicLightingLightFixtureModel, PublicLightingLightFixture, + PublicLightingElectricalCabinetModel, PublicLightingElectricalCabinet +) + +admin.site.register(PublicLightingStreet, AssetAdmin) +admin.site.register(PublicLightingStreetDocument, AssetAdmin) +admin.site.register(PublicLightingPoleModel, AssetAdmin) +admin.site.register(PublicLightingPole, AssetAdmin) +admin.site.register(PublicLightingLightFixtureModel, AssetAdmin) +admin.site.register(PublicLightingLightFixture, AssetAdmin) +admin.site.register(PublicLightingElectricalCabinetModel, AssetAdmin) +admin.site.register(PublicLightingElectricalCabinet, AssetAdmin) + + +# ============================================================================ +# Artworks +# ============================================================================ +from .models import ( + ArtworkStreet, ArtworkMonument, ArtworkFountain, + ArtworkFountainDocument, ArtworkMonumentDocument +) + +admin.site.register(ArtworkStreet, AssetAdmin) +admin.site.register(ArtworkMonument, AssetAdmin) +admin.site.register(ArtworkFountain, AssetAdmin) +admin.site.register(ArtworkFountainDocument, AssetAdmin) +admin.site.register(ArtworkMonumentDocument, AssetAdmin) + + +from .models import Structure, StructureLocation, StructureDocument, StructureAssetModel, StructureGeoAsset, StructureContract + + +def make_folder_attachment_inline(thematic_code): + """ + Factory pour créer un inline DocumentFolderAttachment filtré par thématique. + + Args: + thematic_code: Le code de la thématique (ex: 'structures', 'control_centers') + """ + class FolderAttachmentForm(forms.ModelForm): + class Meta: + model = DocumentFolderAttachment + fields = ['folder', 'is_default'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Filtrer les dossiers par thématique + self.fields['folder'].queryset = DocumentFolder.objects.filter( + thematics__code=thematic_code + ).distinct().order_by('name') + + class DocumentFolderAttachmentInline(GenericTabularInline): + model = DocumentFolderAttachment + form = FolderAttachmentForm + extra = 1 + fields = ('folder', 'is_default') + verbose_name = "Dossier documentaire" + verbose_name_plural = "Dossiers documentaires" + + return DocumentFolderAttachmentInline + + +# Inlines spécifiques par thématique +StructureFolderAttachmentInline = make_folder_attachment_inline('structures') +ControlCenterFolderAttachmentInline = make_folder_attachment_inline('control_centers') +TrafficLightFolderAttachmentInline = make_folder_attachment_inline('traffic_lights') + +@admin.register(Structure) +class StructureAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + list_display = ("code", "name_fr", "structure_type") + list_filter = ("structure_type",) + inlines = [StructureFolderAttachmentInline] + +@admin.register(StructureLocation) +class StructureLocationAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + +admin.site.register(StructureDocument, AssetAdmin) +admin.site.register(StructureAssetModel, AssetAdmin) +admin.site.register(StructureGeoAsset, AssetAdmin) + +admin.site.register(StructureContract, AssetAdmin) + + + +from .models import ControlCenter, ControlCenterLocation, ControlCenterDocument, ControlCenterAssetModel, ControlCenterGeoAsset, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument + +@admin.register(ControlCenter) +class ControlCenterAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + inlines = [ControlCenterFolderAttachmentInline] + +@admin.register(ControlCenterLocation) +class ControlCenterLocationAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + +admin.site.register(ControlCenterDocument, AssetAdmin) +admin.site.register(ControlCenterAssetModel, AssetAdmin) +admin.site.register(ControlCenterGeoAsset, AssetAdmin) +admin.site.register(ControlCenterContract, AssetAdmin) +admin.site.register(ControlCenterGeoAssetContract, AssetAdmin) +admin.site.register(ControlCenterAssetDocument, AssetAdmin) + + +@admin.register(SignStreet) +class SignStreetAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + list_display = ("code", "name_fr", "name_nl", "municipality") + list_filter = ("municipality",) + + +# ============================================================================ +# Sign +# ============================================================================ +from .models import ( + SignStreetDocument, SignPoleModel, SignPole, SignPanelModel, + SignPanelType, SignPanel, ComplementaryPoliceRegulation, SignParkingSpot +) + +admin.site.register(SignStreetDocument, AssetAdmin) +admin.site.register(SignPoleModel, AssetAdmin) +admin.site.register(SignPole, AssetAdmin) +admin.site.register(SignPanelModel, AssetAdmin) +admin.site.register(SignPanelType, AssetAdmin) +admin.site.register(SignPanel, AssetAdmin) +admin.site.register(ComplementaryPoliceRegulation, AssetAdmin) +admin.site.register(SignParkingSpot, AssetAdmin) + + +from .models import ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract + +admin.site.register(ITSAssetModel, AssetAdmin) + +@admin.register(ITSGeoAsset) +class ITSGeoAssetAdmin(AssetAdmin): + search_fields = ("code", "model__name_fr", "location__code") + list_display = ("code", "model", "location") + list_filter = ("model", "location") + +@admin.register(ITSGeoAssetContract) +class ITSGeoAssetContractAdmin(AssetAdmin): + search_fields = ("asset__code", "contract__contract_number") + list_display = ("asset", "contract") + list_filter = ("contract",) + + +from .models import ITSLocation, ITSLocationDocument, ITSLocationContract + +admin.site.register(ITSLocation, AssetAdmin) +admin.site.register(ITSLocationDocument, AssetAdmin) +admin.site.register(ITSLocationContract, AssetAdmin) + + +from .models import NatureAssetModel, NatureRWIAsset, NatureRWIAssetDocument +from .models import ( + NatureLocation, NatureGreenSurface, NatureGreenSurfaceDocument, + NatureTree, NatureShrub, NatureBench, NatureRWIZ +) + +admin.site.register(NatureAssetModel, AssetAdmin) +admin.site.register(NatureLocation, AssetAdmin) +admin.site.register(NatureGreenSurface, AssetAdmin) +admin.site.register(NatureGreenSurfaceDocument, AssetAdmin) +admin.site.register(NatureTree, AssetAdmin) +admin.site.register(NatureShrub, AssetAdmin) +admin.site.register(NatureBench, AssetAdmin) +admin.site.register(NatureRWIZ, AssetAdmin) + + +class CustomNatureGeoAdmin(AssetGISAdmin): + gis_widget_kwargs = { + 'attrs': { + "default_lon" : 4.3517, # Bruxelles + "default_lat" : 50.8503, + "default_zoom" : 12 + }, + } + + +@admin.register(NatureRWIAsset) +class NatureRWIAssetAdmin(CustomNatureGeoAdmin): + list_display = ("code", "code_name", "structure_type", "geometry_type", "project_manager", "condition") + list_filter = ("structure_type", "geometry_type", "project_manager", "condition", "construction_status") + search_fields = ("code", "external_id", "administrator", "comment") + change_list_template = "admin/assets/naturerwiasset/change_list.html" + + def get_fields(self, request, obj=None): + fields = list(super().get_fields(request, obj)) + if obj and obj.geometry_type != 'point': + if 'has_filter' in fields: + fields.remove('has_filter') + return fields + + def get_urls(self): + from django.urls import path as urlpath + urls = super().get_urls() + custom_urls = [ + urlpath( + "import-rwi/", + self.admin_site.admin_view(self.import_rwi_view), + name="assets_naturerwiasset_import_rwi", + ), + ] + return custom_urls + urls + + def import_rwi_view(self, request): + import io + import os + import tempfile + from django.core.management import call_command + from django.contrib import messages + from django.shortcuts import render + + context = { + **self.admin_site.each_context(request), + "title": "Importation du GeoPackage RWI", + "opts": self.model._meta, + } + + if request.method == "POST": + dry_run = "dry_run" in request.POST + gpkg_file = request.FILES.get("gpkg_file") + + if not gpkg_file: + messages.error(request, "Aucun fichier GeoPackage sélectionné.") + return render(request, "admin/assets/naturerwiasset/import_rwi.html", context) + + if not gpkg_file.name.endswith(".gpkg"): + messages.error(request, "Seuls les fichiers .gpkg sont acceptés.") + return render(request, "admin/assets/naturerwiasset/import_rwi.html", context) + + # Save uploaded file to a temporary file + with tempfile.NamedTemporaryFile(suffix=".gpkg", delete=False) as tmp: + for chunk in gpkg_file.chunks(): + tmp.write(chunk) + tmp_path = tmp.name + + stdout_stream = io.StringIO() + stderr_stream = io.StringIO() + + import_error = None + try: + if dry_run: + call_command("import_rwi", tmp_path, "--dry-run", stdout=stdout_stream, stderr=stderr_stream) + else: + call_command("import_rwi", tmp_path, stdout=stdout_stream, stderr=stderr_stream) + except Exception as e: + import_error = str(e) + finally: + try: + os.unlink(tmp_path) + except OSError: + pass + + output_lines = (stdout_stream.getvalue() + stderr_stream.getvalue()).split("\n") + context["output_lines"] = output_lines + context["dry_run"] = dry_run + context["import_error"] = import_error + + if import_error: + messages.error(request, f"Erreur lors de l'importation : {import_error}") + elif dry_run: + messages.warning(request, "Simulation d'importation (dry-run) terminée – aucune donnée n'a été modifiée.") + else: + messages.success(request, "Importation du GeoPackage RWI terminée avec succès !") + + return render(request, "admin/assets/naturerwiasset/import_rwi.html", context) + + return render(request, "admin/assets/naturerwiasset/import_rwi.html", context) + + +admin.site.register(NatureRWIAssetDocument, AssetAdmin) + + +# ============================================================================ +# Green Surfaces Comments Admin +# ============================================================================ + +class GreenSurfacesCommentPhotoInline(admin.TabularInline): + model = GreenSurfacesCommentPhoto + extra = 1 + fields = ('photo', 'heading', 'order') + readonly_fields = ('created_at',) + + +@admin.register(GreenSurfacesComment) +class GreenSurfacesCommentAdmin(AssetAdmin): + list_display = ('id', 'comment_preview', 'created_by', 'created_at', 'address') + list_filter = ('created_at', 'created_by') + search_fields = ('comment', 'address', 'created_by__username') + readonly_fields = ('created_at', 'updated_at', 'geom') + inlines = [GreenSurfacesCommentPhotoInline] + + fieldsets = ( + ('Commentaire', { + 'fields': ('comment', 'created_by') + }), + ('Localisation', { + 'fields': ('latitude', 'longitude', 'geom', 'address') + }), + ('Métadonnées', { + 'fields': ('created_at', 'updated_at') + }), + ) + + def comment_preview(self, obj): + return obj.comment[:50] + '...' if len(obj.comment) > 50 else obj.comment + comment_preview.short_description = 'Commentaire' + + +@admin.register(GreenSurfacesCommentPhoto) +class GreenSurfacesCommentPhotoAdmin(AssetAdmin): + list_display = ('id', 'comment', 'heading', 'order', 'created_at') + list_filter = ('created_at',) + search_fields = ('comment__comment',) + readonly_fields = ('created_at',) + + + +# ============================================================================ +# Roads +# ============================================================================ +from .models import ( + RoadStreet, RoadStreetDocument, RoadwayModel, Roadway, + RoadSidewalkModel, RoadSidewalk, RoadFurnitureModel, RoadFurniture, + RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain +) + +admin.site.register(RoadStreet, AssetAdmin) +admin.site.register(RoadStreetDocument, AssetAdmin) +admin.site.register(RoadwayModel, AssetAdmin) +admin.site.register(Roadway, AssetAdmin) +admin.site.register(RoadSidewalkModel, AssetAdmin) +admin.site.register(RoadSidewalk, AssetAdmin) +admin.site.register(RoadFurnitureModel, AssetAdmin) +admin.site.register(RoadFurniture, AssetAdmin) +admin.site.register(RoadwayFunction, AssetAdmin) +admin.site.register(RoadConcreteBlockSet, AssetAdmin) +admin.site.register(RoadDrainModel, AssetAdmin) +admin.site.register(RoadDrain, AssetAdmin) + + +@admin.register(CleanLocation) +class CleanLocationAdmin(AssetAdmin): + search_fields = ("code", "name_fr", "name_nl") + list_display = ("code", "name_fr", "name_nl", "municipality") + list_filter = ("municipality",) + +admin.site.register(CleanLitterBinModel, AssetAdmin) +admin.site.register(CleanLitterBin, AssetAdmin) +admin.site.register(CleanGlassContainerModel, AssetAdmin) +admin.site.register(CleanGlassContainer, AssetAdmin) +admin.site.register(CleanLocationContract, AssetAdmin) + +from .models import CleanLitterBinPhoto, CleanLitterBinObservation + +admin.site.register(CleanLitterBinPhoto, AssetAdmin) +admin.site.register(CleanLitterBinObservation, AssetAdmin) \ No newline at end of file diff --git a/streetup/assets/apps.py b/streetup/assets/apps.py new file mode 100644 index 0000000..29af7f2 --- /dev/null +++ b/streetup/assets/apps.py @@ -0,0 +1,49 @@ +from django.apps import AppConfig + + +class AssetsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'assets' + + def ready(self): + """Import signals when app is ready""" + import assets.signals + + # Register private file guards + from common.private_files.registry import register + + # Guard for green surfaces comment photos + def guard_green_surfaces_comment_photo(obj, user): + """ + Allow access to comment photos for users with 'nature' thematic access. + """ + from assets.models.nature import GreenSurfacesCommentPhoto + from common.models import UserThematics + + if not isinstance(obj, GreenSurfacesCommentPhoto): + return False + + if not user.is_authenticated: + return False + + # Check if user has access to 'nature' thematic + user_thematics = UserThematics.objects.filter( + user_config__user=user, + thematic__code='nature' + ).exists() + + return user_thematics + + register('assets', 'GreenSurfacesCommentPhoto', 'photo', guard_green_surfaces_comment_photo) + + # Guard for traffic light controller log files + def guard_tl_controller_log(obj, user): + from common.models import UserThematics + if not user.is_authenticated: + return False + return UserThematics.objects.filter( + user_config__user=user, + thematic__code='trafficlights' + ).exists() + + register('assets', 'TrafficLightControllerLogFile', 'file', guard_tl_controller_log) diff --git a/streetup/assets/forms.py b/streetup/assets/forms.py new file mode 100644 index 0000000..e9ad790 --- /dev/null +++ b/streetup/assets/forms.py @@ -0,0 +1,273 @@ +from django import forms + +from assets.models import TrafficLightIntersectionDocument, PublicLightingStreetDocument, StructureDocument, ControlCenterDocument +from assets.models import SignStreetDocument, RoadStreetDocument, ITSLocationDocument +from assets.models import TrafficLightIntersection +from assets.models.its import ITSLocation +from assets.models.trafficlights import TL_INTERSECTION_STATUS_CHOICES, TL_VOLTAGE_CHOICES + + +class MultipleFileInput(forms.ClearableFileInput): + allow_multiple_selected = True + + +class MultipleFileField(forms.FileField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", MultipleFileInput()) + super().__init__(*args, **kwargs) + + def clean(self, data, initial=None): + single_file_clean = super().clean + if isinstance(data, (list, tuple)): + result = [single_file_clean(d, initial) for d in data] + else: + result = [single_file_clean(data, initial)] + return result + + +class IntersectionDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = TrafficLightIntersectionDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(IntersectionDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +class StreetDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = PublicLightingStreetDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(StreetDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +class StructureDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = StructureDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(StructureDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +class ControlCenterDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = ControlCenterDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(ControlCenterDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +class SignStreetDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = SignStreetDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(SignStreetDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +class RoadStreetDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = RoadStreetDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(RoadStreetDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +class ITSLocationDocumentForm(forms.ModelForm): + documents = MultipleFileField(required=True, label="Documents") + + class Meta: + model = ITSLocationDocument + fields = ['documents'] + + def __init__(self, *args, **kwargs): + super(ITSLocationDocumentForm, self).__init__(*args, **kwargs) + self.fields['documents'].widget.attrs.update({'class': 'form-control'}) + + +# --------------------------------------------------------------------------- +# Generic Location Forms (for create / edit location functionality) +# --------------------------------------------------------------------------- + +class TrafficLightIntersectionForm(forms.ModelForm): + """Form to create or edit a TrafficLightIntersection (localisation Feux).""" + + # Not a model field — computed from the linked controller. + # Presented as a 3-way choice (Yes / No / Unknown). + connected_to_control_center = forms.ChoiceField( + choices=[('', '—'), ('true', 'Oui'), ('false', 'Non')], + required=False, + label='Connexion à la centrale', + widget=forms.Select(attrs={'class': 'form-select'}), + ) + + class Meta: + model = TrafficLightIntersection + fields = [ + 'code', 'status', + 'name_fr', 'name_nl', + 'voltage_type', 'is_radar', + 'municipality', 'master', + 'current_index', 'begin_time', + 'lat', 'lon', + ] + widgets = { + 'code': forms.TextInput(attrs={'class': 'form-control'}), + 'name_fr': forms.TextInput(attrs={'class': 'form-control'}), + 'name_nl': forms.TextInput(attrs={'class': 'form-control'}), + 'status': forms.Select(attrs={'class': 'form-select'}), + 'voltage_type': forms.Select(attrs={'class': 'form-select'}), + 'is_radar': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'municipality': forms.Select(attrs={'class': 'form-select'}), + 'master': forms.Select(attrs={'class': 'form-select'}), + 'current_index': forms.TextInput(attrs={'class': 'form-control'}), + 'begin_time': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'), + 'lat': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}), + 'lon': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}), + } + labels = { + 'code': 'Code', + 'name_fr': 'Nom (FR)', + 'name_nl': 'Naam (NL)', + 'status': 'Statut', + 'voltage_type': 'Voltage', + 'is_radar': 'Est un radar ?', + 'municipality': 'Commune', + 'master': 'Carrefour principal', + 'current_index': 'Index', + 'begin_time': 'Date de mise en service', + 'lat': 'Latitude', + 'lon': 'Longitude', + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['municipality'].required = False + self.fields['master'].required = False + self.fields['current_index'].required = False + self.fields['begin_time'].required = False + self.fields['lat'].required = False + self.fields['lon'].required = False + self.fields['name_fr'].required = False + self.fields['name_nl'].required = False + # Exclude current instance from master choices to prevent self-referencing loops + if self.instance and self.instance.pk: + self.fields['master'].queryset = TrafficLightIntersection.objects.exclude(pk=self.instance.pk).order_by('code') + else: + self.fields['master'].queryset = TrafficLightIntersection.objects.all().order_by('code') + # Pre-populate connected_to_control_center from the computed property + if self.instance and self.instance.pk: + val = self.instance.is_connected_to_control_center + if val is True: + self.initial['connected_to_control_center'] = 'true' + elif val is False: + self.initial['connected_to_control_center'] = 'false' + else: + self.initial['connected_to_control_center'] = '' + # Enforce display order + from collections import OrderedDict + desired_order = [ + 'code', 'status', + 'name_fr', 'name_nl', + 'voltage_type', 'is_radar', 'connected_to_control_center', + 'municipality', 'master', + 'current_index', 'begin_time', + 'lat', 'lon', + ] + self.fields = OrderedDict( + (k, self.fields[k]) for k in desired_order if k in self.fields + ) + + def save(self, commit=True): + instance = super().save(commit=commit) + # Propagate connected_to_control_center to the latest active controller + raw = self.cleaned_data.get('connected_to_control_center', '') + if raw != '' and instance.pk: + controller = instance.controllers.filter(status='active').order_by('-installation_date').first() + if controller is not None: + controller.connected_to_control_center = (raw == 'true') + controller.save(update_fields=['connected_to_control_center']) + return instance + + +class ITSLocationForm(forms.ModelForm): + """Form to create or edit an ITSLocation.""" + + # Hidden field: WGS84 GeoJSON polygon drawn by the user on the map. + # Accepted values: a GeoJSON Polygon or MultiPolygon string, or empty. + geom_geojson = forms.CharField(required=False, widget=forms.HiddenInput()) + + class Meta: + model = ITSLocation + fields = [ + 'code', 'name_fr', 'name_nl', + 'is_main_location', 'parent_location', + ] + widgets = { + 'code': forms.TextInput(attrs={'class': 'form-control'}), + 'name_fr': forms.TextInput(attrs={'class': 'form-control'}), + 'name_nl': forms.TextInput(attrs={'class': 'form-control'}), + 'is_main_location': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'parent_location': forms.Select(attrs={'class': 'form-select'}), + } + labels = { + 'code': 'Code', + 'name_fr': 'Nom (FR)', + 'name_nl': 'Naam (NL)', + 'is_main_location': 'Localisation principale', + 'parent_location': 'Localisation parente', + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['name_fr'].required = False + self.fields['name_nl'].required = False + self.fields['parent_location'].required = False + if self.instance and self.instance.pk: + self.fields['parent_location'].queryset = ITSLocation.objects.exclude(pk=self.instance.pk).order_by('code') + # Pre-populate geom_geojson with the existing geometry in WGS84 + if self.instance.geom: + geom_wgs84 = self.instance.geom.transform(4326, clone=True) + self.initial['geom_geojson'] = geom_wgs84.geojson + else: + self.fields['parent_location'].queryset = ITSLocation.objects.all().order_by('code') + + +# Registry: maps location_type slug → form class + thematic code +# Used by the generic location_editing views. +LOCATION_FORM_REGISTRY = { + 'trafficlightintersection': { + 'form_class': TrafficLightIntersectionForm, + 'thematic_code': 'trafficlights', + 'label': 'Intersection', + }, + 'itslocation': { + 'form_class': ITSLocationForm, + 'thematic_code': 'its', + 'label': 'Localisation ITS', + }, +} \ No newline at end of file diff --git a/streetup/assets/locale/en/LC_MESSAGES/django.mo b/streetup/assets/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..abad66a Binary files /dev/null and b/streetup/assets/locale/en/LC_MESSAGES/django.mo differ diff --git a/streetup/assets/locale/en/LC_MESSAGES/django.po b/streetup/assets/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..0f0049e --- /dev/null +++ b/streetup/assets/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,5826 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-06 10:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: assets/models/clean.py:16 +msgid "Cemented to ground" +msgstr "Cemented to ground" + +#: assets/models/clean.py:17 +msgid "Bolted to ground" +msgstr "Bolted to ground" + +#: assets/models/clean.py:18 +msgid "Pole-mounted" +msgstr "Pole-mounted" + +#: assets/models/clean.py:19 +msgid "Wall-mounted" +msgstr "Wall-mounted" + +#: assets/models/clean.py:20 +msgid "Freestanding" +msgstr "Freestanding" + +#: assets/models/clean.py:21 +msgid "Other" +msgstr "Other" + +#: assets/models/clean.py:25 +msgid "Clear glass" +msgstr "Clear glass" + +#: assets/models/clean.py:26 +msgid "Coloured glass" +msgstr "Coloured glass" + +#: assets/models/clean.py:27 +msgid "Mixed" +msgstr "Mixed" + +#: assets/models/clean.py:82 assets/models/clean.py:118 +#: assets/models/clean.py:155 assets/models/clean.py:194 +msgid "Colour" +msgstr "Colour" + +#: assets/models/clean.py:83 assets/models/clean.py:156 +msgid "RAL code" +msgstr "RAL code" + +#: assets/models/clean.py:84 assets/models/clean.py:119 +#: assets/models/clean.py:157 assets/models/clean.py:195 +msgid "Material" +msgstr "Material" + +#: assets/models/clean.py:85 assets/models/clean.py:120 +msgid "Volume (litres)" +msgstr "Volume (litres)" + +#: assets/models/clean.py:86 assets/models/clean.py:121 +msgid "Cover" +msgstr "Cover" + +#: assets/models/clean.py:87 assets/models/clean.py:122 +msgid "Built-in ashtray" +msgstr "Built-in ashtray" + +#: assets/models/clean.py:90 +msgid "Litter bin model" +msgstr "Litter bin model" + +#: assets/models/clean.py:91 +msgid "Litter bin models" +msgstr "Litter bin models" + +#: assets/models/clean.py:104 assets/models/clean.py:177 +msgid "Location" +msgstr "Location" + +#: assets/models/clean.py:110 assets/models/clean.py:183 +#: assets/templates/assets/clean_index.html:28 +#: assets/templates/assets/clean_index.html:74 +msgid "Model" +msgstr "Model" + +#: assets/models/clean.py:116 +msgid "Fixation type" +msgstr "Fixation type" + +#: assets/models/clean.py:123 +msgid "Built-in extinguisher stub (éteignoir)" +msgstr "Built-in extinguisher stub (éteignoir)" + +#: assets/models/clean.py:126 assets/models/clean.py:198 +msgid "Emptying frequency" +msgstr "Emptying frequency" + +#: assets/models/clean.py:130 +msgid "Collection route (tournée)" +msgstr "Collection route (tournée)" + +#: assets/models/clean.py:134 +msgid "External ID (source system)" +msgstr "External ID (source system)" + +#: assets/models/clean.py:136 assets/models/clean.py:204 +msgid "Remarks" +msgstr "Remarks" + +#: assets/models/clean.py:139 assets/models/clean.py:238 +#: assets/models/clean.py:305 +msgid "Litter bin" +msgstr "Litter bin" + +#: assets/models/clean.py:140 +#: assets/templates/assets/clean_location_detail.html:72 +#: assets/templates/assets/clean_location_detail.html:110 +msgid "Litter bins" +msgstr "Litter bins" + +#: assets/models/clean.py:158 assets/models/clean.py:191 +msgid "Capacity (litres)" +msgstr "Capacity (litres)" + +#: assets/models/clean.py:159 assets/models/clean.py:192 +#: assets/templates/assets/clean_location_detail.html:96 +msgid "Underground" +msgstr "Underground" + +#: assets/models/clean.py:160 assets/models/clean.py:193 +msgid "Number of openings" +msgstr "Number of openings" + +#: assets/models/clean.py:163 +msgid "Glass container model" +msgstr "Glass container model" + +#: assets/models/clean.py:164 +msgid "Glass container models" +msgstr "Glass container models" + +#: assets/models/clean.py:189 +msgid "Accepted glass type" +msgstr "Accepted glass type" + +#: assets/models/clean.py:202 +msgid "Operator / manager" +msgstr "Operator / manager" + +#: assets/models/clean.py:207 +msgid "Glass container" +msgstr "Glass container" + +#: assets/models/clean.py:208 +#: assets/templates/assets/clean_location_detail.html:88 +#: assets/templates/assets/clean_location_detail.html:124 +msgid "Glass containers" +msgstr "Glass containers" + +#: assets/models/clean.py:243 +msgid "Photo" +msgstr "Photo" + +#: assets/models/clean.py:248 +msgid "Source URL (original)" +msgstr "Source URL (original)" + +#: assets/models/clean.py:250 +msgid "Order" +msgstr "Order" + +#: assets/models/clean.py:251 +msgid "Taken at" +msgstr "Taken at" + +#: assets/models/clean.py:252 +msgid "Caption" +msgstr "Caption" + +#: assets/models/clean.py:255 +msgid "Litter bin photo" +msgstr "Litter bin photo" + +#: assets/models/clean.py:256 +msgid "Litter bin photos" +msgstr "Litter bin photos" + +#: assets/models/clean.py:268 +#: assets/templates/assets/cleanlitterbin_detail.html:250 +#: assets/templates/assets/cleanlitterbin_detail.html:378 +msgid "Important – ponctuel" +msgstr "Important – one-off" + +#: assets/models/clean.py:269 +#: assets/templates/assets/cleanlitterbin_detail.html:251 +#: assets/templates/assets/cleanlitterbin_detail.html:378 +msgid "Important – continu" +msgstr "Important – continuous" + +#: assets/models/clean.py:270 assets/models/clean.py:277 +#: assets/templates/assets/cleanlitterbin_detail.html:252 +#: assets/templates/assets/cleanlitterbin_detail.html:262 +#: assets/templates/assets/cleanlitterbin_detail.html:378 +#: assets/templates/assets/cleanlitterbin_detail.html:379 +msgid "Faible" +msgstr "Low" + +#: assets/models/clean.py:271 assets/models/clean.py:295 +#: assets/models/roads.py:275 assets/models/roads.py:287 +#: assets/models/roads.py:297 assets/models/trafficlights.py:203 +#: assets/templates/assets/cleanlitterbin_detail.html:253 +#: assets/templates/assets/cleanlitterbin_detail.html:303 +#: assets/templates/assets/cleanlitterbin_detail.html:378 +#: assets/templates/assets/cleanlitterbin_detail.html:401 +#: assets/templates/assets/trafficlights_intersection_detail.html:1208 +msgid "Autre" +msgstr "Other" + +#: assets/models/clean.py:275 +#: assets/templates/assets/cleanlitterbin_detail.html:260 +#: assets/templates/assets/cleanlitterbin_detail.html:379 +msgid "Élevée" +msgstr "High" + +#: assets/models/clean.py:276 +#: assets/templates/assets/cleanlitterbin_detail.html:261 +#: assets/templates/assets/cleanlitterbin_detail.html:379 +msgid "Moyenne" +msgstr "Medium" + +#: assets/models/clean.py:281 +#: assets/templates/assets/cleanlitterbin_detail.html:165 +#: assets/templates/assets/cleanlitterbin_detail.html:175 +#: assets/templates/assets/cleanlitterbin_detail.html:269 +#: assets/templates/assets/cleanlitterbin_detail.html:278 +#: assets/templates/assets/cleanlitterbin_detail.html:380 +#: assets/templates/assets/cleanlitterbin_detail.html:381 +msgid "Normal" +msgstr "Normal" + +#: assets/models/clean.py:282 +#: assets/templates/assets/cleanlitterbin_detail.html:166 +#: assets/templates/assets/cleanlitterbin_detail.html:176 +#: assets/templates/assets/cleanlitterbin_detail.html:270 +#: assets/templates/assets/cleanlitterbin_detail.html:279 +#: assets/templates/assets/cleanlitterbin_detail.html:380 +#: assets/templates/assets/cleanlitterbin_detail.html:381 +msgid "Difficile" +msgstr "Difficult" + +#: assets/models/clean.py:283 +#: assets/templates/assets/cleanlitterbin_detail.html:167 +#: assets/templates/assets/cleanlitterbin_detail.html:177 +#: assets/templates/assets/cleanlitterbin_detail.html:271 +#: assets/templates/assets/cleanlitterbin_detail.html:280 +#: assets/templates/assets/cleanlitterbin_detail.html:380 +#: assets/templates/assets/cleanlitterbin_detail.html:381 +msgid "Impossible" +msgstr "Impossible" + +#: assets/models/clean.py:287 +#: assets/templates/assets/cleanlitterbin_detail.html:295 +#: assets/templates/assets/cleanlitterbin_detail.html:393 +msgid "Arrêt Bus/Tram" +msgstr "Bus/Tram stop" + +#: assets/models/clean.py:288 +#: assets/templates/assets/cleanlitterbin_detail.html:296 +#: assets/templates/assets/cleanlitterbin_detail.html:394 +msgid "Parc" +msgstr "Park" + +#: assets/models/clean.py:289 +#: assets/templates/assets/cleanlitterbin_detail.html:297 +#: assets/templates/assets/cleanlitterbin_detail.html:395 +msgid "École" +msgstr "School" + +#: assets/models/clean.py:290 +#: assets/templates/assets/cleanlitterbin_detail.html:298 +#: assets/templates/assets/cleanlitterbin_detail.html:396 +msgid "Commerce" +msgstr "Shop / Commerce" + +#: assets/models/clean.py:291 +#: assets/templates/assets/cleanlitterbin_detail.html:299 +#: assets/templates/assets/cleanlitterbin_detail.html:397 +#: assets/views/artworks.py:420 assets/views/artworks.py:424 +msgid "Monument" +msgstr "Monument" + +#: assets/models/clean.py:292 +#: assets/templates/assets/cleanlitterbin_detail.html:300 +#: assets/templates/assets/cleanlitterbin_detail.html:398 +msgid "Passage piéton" +msgstr "Pedestrian crossing" + +#: assets/models/clean.py:293 assets/models/roads.py:284 +#: assets/templates/assets/cleanlitterbin_detail.html:301 +#: assets/templates/assets/cleanlitterbin_detail.html:399 +msgid "Place" +msgstr "Square / Place" + +#: assets/models/clean.py:294 +#: assets/templates/assets/cleanlitterbin_detail.html:302 +#: assets/templates/assets/cleanlitterbin_detail.html:400 +#: assets/templates/assets/permissions/panel.html:230 +#: assets/templates/assets/permissions/panel.html:256 +#: assets/templates/assets/permissions/type_page.html:173 +#: assets/templates/assets/permissions/type_page.html:196 +#: assets/templates/assets/permissions/type_page.html:374 +#: assets/templates/assets/permissions/type_page.html:397 +#: assets/templates/assets/trafficlights_intersection_detail.html:68 +#: assets/templates/assets/trafficlights_intersection_detail.html:1285 +#: assets/templates/assets/unified_index.html:94 +#: assets/templates/assets/unified_index.html:132 +msgid "Aucun" +msgstr "None" + +#: assets/models/clean.py:307 +msgid "Observation date/time" +msgstr "Observation date/time" + +#: assets/models/clean.py:308 +msgid "Observer notes" +msgstr "Observer notes" + +#: assets/models/clean.py:311 +msgid "Slope too steep" +msgstr "Slope too steep" + +#: assets/models/clean.py:312 +msgid "Unstable ground" +msgstr "Unstable ground" + +#: assets/models/clean.py:313 +msgid "Reflective surface" +msgstr "Reflective surface" + +#: assets/models/clean.py:314 +msgid "Slippery surface" +msgstr "Slippery surface" + +#: assets/models/clean.py:315 +msgid "Cracks in ground" +msgstr "Cracks in ground" + +#: assets/models/clean.py:316 +msgid "Ground obstacle" +msgstr "Ground obstacle" + +#: assets/models/clean.py:317 +msgid "Tactile paving (dalles podotactiles)" +msgstr "Tactile paving (dalles podotactiles)" + +#: assets/models/clean.py:318 +msgid "Transversal slope (%)" +msgstr "Transversal slope (%)" + +#: assets/models/clean.py:319 +msgid "Longitudinal slope (%)" +msgstr "Longitudinal slope (%)" + +#: assets/models/clean.py:320 +msgid "Ground remarks" +msgstr "Ground remarks" + +#: assets/models/clean.py:327 +msgid "Neighbourhood type" +msgstr "Neighbourhood type" + +#: assets/models/clean.py:333 +msgid "Pedestrian flow density" +msgstr "Pedestrian flow density" + +#: assets/models/clean.py:338 +msgid "Points of interest" +msgstr "Points of interest" + +#: assets/models/clean.py:340 +msgid "Clear 2 m approach" +msgstr "Clear 2 m approach" + +#: assets/models/clean.py:341 +msgid "Road to cross" +msgstr "Road to cross" + +#: assets/models/clean.py:342 +msgid "Isolated bin" +msgstr "Isolated bin" + +#: assets/models/clean.py:343 +msgid "Obstructs road traffic" +msgstr "Obstructs road traffic" + +#: assets/models/clean.py:344 +msgid "Obstructs pedestrian flow" +msgstr "Obstructs pedestrian flow" + +#: assets/models/clean.py:345 +msgid "Logical path (itinéraire logique)" +msgstr "Logical path (itinéraire logique)" + +#: assets/models/clean.py:346 +msgid "Visual contrast" +msgstr "Visual contrast" + +#: assets/models/clean.py:347 +msgid "Sorting indicators present" +msgstr "Sorting indicators present" + +#: assets/models/clean.py:352 +msgid "Agent access – vehicle" +msgstr "Agent access – vehicle" + +#: assets/models/clean.py:358 +msgid "Agent access – on foot" +msgstr "Agent access – on foot" + +#: assets/models/clean.py:362 +msgid "Other point of interest" +msgstr "Other point of interest" + +#: assets/models/clean.py:367 +msgid "Placement relevance codes" +msgstr "Placement relevance codes" + +#: assets/models/clean.py:371 +msgid "Obstruction codes" +msgstr "Obstruction codes" + +#: assets/models/clean.py:373 +msgid "Point of interest remarks" +msgstr "Point of interest remarks" + +#: assets/models/clean.py:374 +msgid "Accessibility remarks" +msgstr "Accessibility remarks" + +#: assets/models/clean.py:377 +msgid "Litter bin observation" +msgstr "Litter bin observation" + +#: assets/models/clean.py:378 +msgid "Litter bin observations" +msgstr "Litter bin observations" + +#: assets/models/core.py:42 +msgid "Catégorie parente" +msgstr "Parent category" + +#: assets/models/core.py:43 +msgid "" +"Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne " +"peut pas avoir d'enfants." +msgstr "" + +#: assets/models/core.py:52 +#, fuzzy +#| msgid "Modèle non trouvé" +msgid "Modèles Django autorisés" +msgstr "Model not found" + +#: assets/models/core.py:53 +#, fuzzy +#| msgid "Type d'asset non supporté pour cette thématique/catégorie" +msgid "Les types de modèles d'assets (Django) autorisés pour cette catégorie." +msgstr "Asset type not supported for this theme/category" + +#: assets/models/core.py:104 +msgid "À approuver" +msgstr "To be approved" + +#: assets/models/core.py:105 +msgid "Planifié" +msgstr "Planned" + +#: assets/models/core.py:106 assets/models/trafficlights.py:18 +#: assets/templates/assets/detail_generic.html:1308 assets/views/artworks.py:78 +msgid "Actif" +msgstr "Active" + +#: assets/models/core.py:107 assets/templates/assets/detail_generic.html:1309 +msgid "Endommagé" +msgstr "Damaged" + +#: assets/models/core.py:108 assets/templates/assets/detail_generic.html:1310 +msgid "A remplacer" +msgstr "To be replaced" + +#: assets/models/core.py:109 assets/models/trafficlights.py:21 +msgid "Mis hors service volontairement" +msgstr "Decommissioned voluntarily" + +#: assets/models/core.py:110 assets/templates/assets/detail_generic.html:1311 +msgid "Enlevé" +msgstr "Removed" + +#: assets/models/core.py:111 assets/models/trafficlights.py:23 +#: assets/views/core.py:454 +msgid "Archivé" +msgstr "Archived" + +#: assets/models/core.py:115 +msgid "Bon état" +msgstr "Good condition" + +#: assets/models/core.py:116 +msgid "Moyen" +msgstr "Fair" + +#: assets/models/core.py:117 +msgid "Dégradé" +msgstr "Degraded" + +#: assets/models/core.py:118 +msgid "Critique" +msgstr "Critical" + +#: assets/models/core.py:119 +msgid "Inconnu" +msgstr "Unknown" + +#: assets/models/core.py:123 +msgid "À valider" +msgstr "To validate" + +#: assets/models/core.py:124 +msgid "Validé" +msgstr "Validated" + +#: assets/models/core.py:125 +msgid "À compléter" +msgstr "To complete" + +#: assets/models/core.py:126 +#: assets/templates/assets/document_version_history_list.html:41 +msgid "Rejeté" +msgstr "Rejected" + +#: assets/models/history.py:186 +msgid "Type d'asset" +msgstr "Asset type" + +#: assets/models/history.py:189 +msgid "ID de l'asset" +msgstr "Asset ID" + +#: assets/models/history.py:200 +#: assets/templates/assets/components/asset_history.html:38 +#: assets/templates/assets/permissions/panel.html:67 +#: assets/templates/assets/permissions/panel.html:109 +#: assets/templates/assets/permissions/panel.html:192 +#: assets/templates/assets/permissions/type_page.html:69 +#: assets/templates/assets/permissions/type_page.html:141 +#: assets/templates/assets/permissions/type_page.html:235 +#: assets/templates/assets/permissions/type_page.html:334 +msgid "Utilisateur" +msgstr "User" + +#: assets/models/history.py:205 +msgid "Date de modification" +msgstr "Modification date" + +#: assets/models/history.py:211 +msgid "Champ modifié" +msgstr "Modified field" + +#: assets/models/history.py:216 +#: assets/templates/assets/components/asset_history.html:40 +msgid "Ancienne valeur" +msgstr "Old value" + +#: assets/models/history.py:221 +#: assets/templates/assets/components/asset_history.html:41 +msgid "Nouvelle valeur" +msgstr "New value" + +#: assets/models/history.py:229 +msgid "Création" +msgstr "Creation" + +#: assets/models/history.py:230 +msgid "Modification" +msgstr "Modification" + +#: assets/models/history.py:231 +#: assets/templates/assets/cleanlitterbin_detail.html:47 +#: assets/templates/assets/cleanlitterbin_detail.html:343 +msgid "Suppression" +msgstr "Deletion" + +#: assets/models/history.py:232 +#: assets/templates/assets/components/asset_history.html:337 +msgid "Changement de statut" +msgstr "Status change" + +#: assets/models/history.py:234 +msgid "Type d'action" +msgstr "Action type" + +#: assets/models/history.py:240 +msgid "Historique d'asset" +msgstr "Asset history" + +#: assets/models/history.py:241 +msgid "Historiques d'assets" +msgstr "Assets history" + +#: assets/models/history.py:266 +msgid "(vide)" +msgstr "(empty)" + +#: assets/models/history.py:325 assets/templates/assets/clean_index.html:65 +#: assets/templates/assets/clean_index.html:71 +#: assets/templates/assets/clean_location_detail.html:158 +#: assets/templates/assets/components/add_asset_modal.html:70 +#: assets/templates/assets/components/asset_model_card.html:17 +#: assets/templates/assets/controlcenters_location_detail.html:242 +#: assets/templates/assets/detail_generic.html:209 +#: assets/templates/assets/detail_generic.html:576 +#: assets/templates/assets/its_location_detail.html:166 +#: assets/templates/assets/nature_locations_detail.html:72 +#: assets/templates/assets/naturerwiz_detail.html:241 +#: assets/templates/assets/publiclighting_street_detail.html:301 +#: assets/templates/assets/roads_street_detail.html:316 +#: assets/templates/assets/sign_street_detail.html:293 +#: assets/templates/assets/structures_location_detail.html:351 +#: assets/templates/assets/trafficlights_import_dxf.html:1480 +#: assets/templates/assets/trafficlights_intersection_detail.html:921 +#: assets/templates/assets/unified_index.html:310 assets/views/export.py:95 +msgid "Code" +msgstr "Code" + +#: assets/models/history.py:326 +msgid "Nom (FR)" +msgstr "Name (FR)" + +#: assets/models/history.py:327 +msgid "Nom (NL)" +msgstr "Name (NL)" + +#: assets/models/history.py:328 assets/templates/assets/artworks_index.html:8 +#: assets/templates/assets/artworks_index.html:36 +#: assets/templates/assets/controlcenters_location_detail.html:245 +#: assets/templates/assets/detail_generic.html:248 +#: assets/templates/assets/detail_generic.html:577 +#: assets/templates/assets/its_location_detail.html:169 +#: assets/templates/assets/nature_locations_detail.html:75 +#: assets/templates/assets/naturerwiz_detail.html:244 +#: assets/templates/assets/publiclighting_street_detail.html:304 +#: assets/templates/assets/roads_street_detail.html:319 +#: assets/templates/assets/sign_street_detail.html:296 +#: assets/templates/assets/structures_location_detail.html:354 +#: assets/templates/assets/trafficlights_import_cross_plan.html:181 +#: assets/templates/assets/trafficlights_import_cross_plan.html:262 +#: assets/templates/assets/trafficlights_import_cross_plan.html:344 +#: assets/templates/assets/trafficlights_import_cross_plan.html:433 +#: assets/templates/assets/trafficlights_index.html:8 +#: assets/templates/assets/trafficlights_intersection_detail.html:38 +#: assets/templates/assets/trafficlights_intersection_detail.html:924 +#: assets/templates/assets/trafficlights_intersection_detail.html:1159 +#: assets/templates/assets/trafficlights_intersection_detail.html:1250 +#: assets/templates/assets/unified_index.html:195 assets/views/export.py:99 +msgid "Statut" +msgstr "Status" + +#: assets/models/history.py:329 assets/views/export.py:103 +msgid "Date d'installation" +msgstr "Installation date" + +#: assets/models/history.py:330 +msgid "Date de désinstallation" +msgstr "Uninstallation date" + +#: assets/models/history.py:331 assets/templates/assets/detail_generic.html:747 +#: assets/templates/assets/detail_generic.html:748 assets/views/export.py:105 +msgid "Numéro de série" +msgstr "Serial number" + +#: assets/models/history.py:332 assets/templates/assets/detail_generic.html:363 +msgid "Durée de garantie" +msgstr "Warranty duration" + +#: assets/models/history.py:333 assets/templates/assets/detail_generic.html:392 +msgid "Dernière inspection" +msgstr "Last inspection" + +#: assets/models/history.py:334 +msgid "Dernier entretien préventif" +msgstr "Last preventive maintenance" + +#: assets/models/history.py:335 +msgid "Dernier entretien correctif" +msgstr "Last corrective maintenance" + +#: assets/models/history.py:336 +msgid "Dernier relevé" +msgstr "Last survey" + +#: assets/models/history.py:337 assets/templates/assets/artworks_index.html:72 +#: assets/templates/assets/components/add_asset_modal.html:34 +#: assets/templates/assets/components/asset_model_card.html:32 +#: assets/templates/assets/components/asset_model_card.html:74 +#: assets/templates/assets/controlcenters_index.html:25 +#: assets/templates/assets/controlcenters_index.html:68 +#: assets/templates/assets/detail_generic.html:306 +#: assets/templates/assets/its_index.html:36 +#: assets/templates/assets/its_index.html:88 +#: assets/templates/assets/nature_index.html:39 +#: assets/templates/assets/nature_index.html:82 +#: assets/templates/assets/publiclighting_index.html:29 +#: assets/templates/assets/publiclighting_index.html:73 +#: assets/templates/assets/roads_index.html:29 +#: assets/templates/assets/roads_index.html:85 +#: assets/templates/assets/sign_index.html:29 +#: assets/templates/assets/sign_index.html:85 +#: assets/templates/assets/structures_index.html:37 +#: assets/templates/assets/structures_index.html:82 +#: assets/templates/assets/trafficlights_index.html:81 +#: assets/templates/assets/trafficlights_index.html:126 +#: assets/templates/assets/trafficlights_intersection_detail.html:968 +#: assets/templates/assets/unified_index.html:115 +#: assets/templates/assets/unified_index.html:291 +#: assets/templates/assets/water_index.html:40 +#: assets/templates/assets/water_index.html:80 assets/views/export.py:97 +msgid "Catégorie" +msgstr "Category" + +#: assets/models/history.py:338 +#: assets/templates/assets/components/asset_model_card.html:26 +#: assets/templates/assets/detail_generic.html:350 +#: assets/templates/assets/detail_generic.html:752 +#: assets/templates/assets/detail_generic.html:753 assets/views/export.py:107 +msgid "Marque" +msgstr "Brand" + +#: assets/models/history.py:339 +#: assets/templates/assets/components/add_asset_modal.html:57 +#: assets/templates/assets/components/asset_model_card.html:6 +#: assets/templates/assets/controlcenters_index.html:35 +#: assets/templates/assets/controlcenters_index.html:69 +#: assets/templates/assets/its_index.html:46 +#: assets/templates/assets/its_index.html:89 +#: assets/templates/assets/nature_index.html:49 +#: assets/templates/assets/nature_index.html:83 +#: assets/templates/assets/publiclighting_index.html:39 +#: assets/templates/assets/publiclighting_index.html:74 +#: assets/templates/assets/roads_index.html:39 +#: assets/templates/assets/roads_index.html:86 +#: assets/templates/assets/sign_index.html:39 +#: assets/templates/assets/sign_index.html:86 +#: assets/templates/assets/structures_index.html:47 +#: assets/templates/assets/structures_index.html:83 +#: assets/templates/assets/trafficlights_import_dxf.html:1484 +#: assets/templates/assets/trafficlights_index.html:91 +#: assets/templates/assets/trafficlights_index.html:127 +#: assets/templates/assets/trafficlights_intersection_detail.html:617 +#: assets/templates/assets/trafficlights_intersection_detail.html:985 +#: assets/templates/assets/unified_index.html:129 +#: assets/templates/assets/unified_index.html:297 assets/views/export.py:98 +msgid "Modèle" +msgstr "Model" + +#: assets/models/history.py:340 +#: assets/templates/assets/components/add_asset_modal.html:101 +#: assets/templates/assets/unified_index.html:260 +#: assets/templates/assets/unified_index.html:303 +msgid "Localisation" +msgstr "Location" + +#: assets/models/history.py:341 +#: assets/templates/assets/structures_location_detail.html:16 +msgid "Structure" +msgstr "Structure" + +#: assets/models/history.py:342 assets/templates/assets/detail_generic.html:670 +#: assets/views/export.py:109 +msgid "Longitude" +msgstr "Longitude" + +#: assets/models/history.py:343 assets/templates/assets/detail_generic.html:681 +#: assets/views/export.py:110 +msgid "Latitude" +msgstr "Latitude" + +#: assets/models/history.py:344 +msgid "Géométrie" +msgstr "Geometry" + +#: assets/models/history.py:345 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: assets/models/nature.py:81 +msgid "Tulipes" +msgstr "Tulips" + +#: assets/models/nature.py:82 +msgid "Jacynthes" +msgstr "Hyacinths" + +#: assets/models/nature.py:83 +msgid "Jonquilles" +msgstr "Daffodils" + +#: assets/models/nature.py:84 +msgid "Narcisses" +msgstr "Narcissi" + +#: assets/models/nature.py:85 +msgid "Crocus" +msgstr "Crocus" + +#: assets/models/nature.py:86 +msgid "Perce-neige" +msgstr "Snowdrops" + +#: assets/models/nature.py:87 assets/views/core.py:475 +msgid "Autres" +msgstr "Others" + +#: assets/models/nature.py:106 +msgid "Types de bulbes" +msgstr "Bulb types" + +#: assets/models/nature.py:298 +msgid "Zone de gestion intégrée des eaux pluviales (ZIEP)" +msgstr "Integrated stormwater management zone" + +#: assets/models/nature.py:299 +msgid "Zones de gestion intégrée des eaux pluviales (ZIEP)" +msgstr "Integrated stormwater management zones" + +#: assets/models/nature.py:307 assets/models/nature.py:334 +msgid "Stormwater inlet (Avaloir)" +msgstr "Stormwater inlet" + +#: assets/models/nature.py:308 assets/models/nature.py:335 +msgid "Sequenced border (Bordure séquencée)" +msgstr "Sequenced border" + +#: assets/models/nature.py:309 assets/models/nature.py:336 +msgid "Injection port (Bouche d'injection)" +msgstr "Injection port" + +#: assets/models/nature.py:310 assets/models/nature.py:337 +msgid "Non-return valve (Clapet anti-retour)" +msgstr "Non-return valve" + +#: assets/models/nature.py:311 assets/models/nature.py:338 +msgid "Sedimentation (Empierrement sédimentation)" +msgstr "Sedimentation" + +#: assets/models/nature.py:312 assets/models/nature.py:339 +msgid "Kerb cell" +msgstr "" + +#: assets/models/nature.py:313 assets/models/nature.py:340 +msgid "Infiltration well (Puits d'infiltration)" +msgstr "Infiltration well" + +#: assets/models/nature.py:314 assets/models/nature.py:341 +msgid "Overflow (Surverse)" +msgstr "Overflow" + +#: assets/models/nature.py:315 assets/models/nature.py:342 +msgid "Isolation valve (Vanne d'isolation)" +msgstr "Isolation valve" + +#: assets/models/nature.py:316 assets/models/nature.py:343 +msgid "Control valve (Vanne de régulation)" +msgstr "Control valve" + +#: assets/models/nature.py:317 assets/models/nature.py:345 +msgid "Sidewalk gargoyle (Gargouille de trottoir)" +msgstr "Sidewalk gargoyle" + +#: assets/models/nature.py:318 assets/models/nature.py:346 +msgid "Trickle of water (Filet d'eau)" +msgstr "Trickle of water" + +#: assets/models/nature.py:319 assets/models/nature.py:347 +msgid "Rail drain (Avaloir de rail)" +msgstr "Rail drain" + +#: assets/models/nature.py:320 assets/models/nature.py:348 +msgid "Leveled edge (Bordure arasée)" +msgstr "Leveled edge" + +#: assets/models/nature.py:321 assets/models/nature.py:349 +msgid "Infiltration channel (Caniveau infiltrant)" +msgstr "Infiltration channel" + +#: assets/models/nature.py:322 assets/models/nature.py:350 +#, fuzzy +#| msgid "Road Drain" +msgid "Drain" +msgstr "Road Drain" + +#: assets/models/nature.py:323 assets/models/nature.py:351 +msgid "Overflowing (Trop plein)" +msgstr "Overflowing" + +#: assets/models/nature.py:324 assets/models/nature.py:352 +msgid "Separate sewer system (Réseau égout séparatif)" +msgstr "Separate sewer system" + +#: assets/models/nature.py:325 +#, fuzzy +#| msgid "Espace vert creux" +msgid "Hollow green space (Espace vert creux)" +msgstr "Depressed green space" + +#: assets/models/nature.py:326 +msgid "Decentralized underground structure (Ouvrage enterré décentralisé)" +msgstr "Decentralized underground structure" + +#: assets/models/nature.py:327 +msgid "Work on an impermeable surface (Ouvrage sur surface imperméable)" +msgstr "Work on an impermeable surface" + +#: assets/models/nature.py:328 +msgid "Alternative technique (Technique alternative)" +msgstr "Alternative technique" + +#: assets/models/nature.py:329 +msgid "Supplementary work (Ouvrage annexe)" +msgstr "Supplementary work" + +#: assets/models/nature.py:354 +#, fuzzy +#| msgid "Bassin sec/d'infiltration" +msgid "Dry/infiltration basin (Bassin sec/infiltration)" +msgstr "Dry/infiltration basin" + +#: assets/models/nature.py:355 +#, fuzzy +#| msgid "Bassin en eau" +msgid "Wet basin (Bassin en eau)" +msgstr "Wet basin / pond" + +#: assets/models/nature.py:356 +#, fuzzy +#| msgid "Fossé" +msgid "Ditch (Fossé)" +msgstr "Ditch" + +#: assets/models/nature.py:357 +#, fuzzy +#| msgid "Jardin de pluie" +msgid "Rain garden (Jardin de pluie)" +msgstr "Rain garden" + +#: assets/models/nature.py:358 +msgid "Swale (Noue)" +msgstr "Swale" + +#: assets/models/nature.py:359 +msgid "Draining structure (Massif drainant)" +msgstr "Draining structure" + +#: assets/models/nature.py:360 +msgid "Draining trench (Tranchée drainante)" +msgstr "Draining trench" + +#: assets/models/nature.py:361 +msgid "Retention roof (Toiture stockante)" +msgstr "Retention roof" + +#: assets/models/nature.py:362 +msgid "Green roof (Toiture verte)" +msgstr "Green roof" + +#: assets/models/nature.py:363 +msgid "Stormwater basin (Bassin d'orage)" +msgstr "Stormwater basin" + +#: assets/models/nature.py:364 +msgid "SAUL-empty (SAUL-vide)" +msgstr "SAUL-empty" + +#: assets/models/nature.py:365 +msgid "SAUL-earth (SAUL-terre)" +msgstr "SAUL-earth" + +#: assets/models/nature.py:366 +msgid "Cistern (Citerne)" +msgstr "Cistern" + +#: assets/models/nature.py:367 +msgid "Green space on slab (Espace vert sur dalle)" +msgstr "Green space on slab" + +#: assets/models/nature.py:368 +msgid "Filter strip (Bande filtrante)" +msgstr "Filter strip" + +#: assets/models/nature.py:369 +msgid "Porous concrete collector (Collecteur en béton poreux)" +msgstr "Porous concrete collector" + +#: assets/models/nature.py:384 +msgid "Design (Étude)" +msgstr "Design" + +#: assets/models/nature.py:385 +msgid "Construction (Travaux)" +msgstr "Construction" + +#: assets/models/nature.py:386 +msgid "Provisional acceptance (Réception provisoire)" +msgstr "Provisional acceptance" + +#: assets/models/nature.py:387 +msgid "Final acceptance (Réception définitive)" +msgstr "Final acceptance" + +#: assets/models/nature.py:391 +msgid "Good (Bon)" +msgstr "Good" + +#: assets/models/nature.py:392 +msgid "Inspection to schedule (Examen à programmer)" +msgstr "Inspection to schedule" + +#: assets/models/nature.py:393 +msgid "Curative action needed (Intervention curative nécessaire)" +msgstr "Curative action needed" + +#: assets/models/nature.py:394 +msgid "Total rehabilitation needed (Réfection totale nécessaire)" +msgstr "Total rehabilitation needed" + +#: assets/models/nature.py:395 +msgid "Maintenance works to perform (Travaux entretien à réaliser)" +msgstr "Maintenance works to perform" + +#: assets/models/nature.py:396 +msgid "Maintenance works (Travaux maintenance)" +msgstr "Maintenance works" + +#: assets/models/nature.py:397 +msgid "Maintenance works to schedule (Travaux maintenance à programmer)" +msgstr "Maintenance works to schedule" + +#: assets/models/nature.py:401 +msgid "Connected (Connecté)" +msgstr "Connected" + +#: assets/models/nature.py:402 +msgid "Not connected (Non connecté)" +msgstr "Not connected" + +#: assets/models/nature.py:406 +#, fuzzy +#| msgid "Végétal" +msgid "Vegetal (Végétal)" +msgstr "Vegetal" + +#: assets/models/nature.py:407 +#, fuzzy +#| msgid "Structure enherbée" +msgid "Grassed structure (Structure enherbée)" +msgstr "Grassed structure" + +#: assets/models/nature.py:408 +#, fuzzy +#| msgid "Matériau non agrégé" +msgid "Unbound material (Matériau non agrégé)" +msgstr "Unaggregated material" + +#: assets/models/nature.py:409 +#, fuzzy +#| msgid "Structure à remplissage minéral" +msgid "Mineral-filled structure (Structure à remplissage minéral)" +msgstr "Mineral-filled structure" + +#: assets/models/nature.py:410 +#, fuzzy +#| msgid "Revêtement surélevé" +msgid "Raised coating (Revêtement surélevé)" +msgstr "Raised pavement" + +#: assets/models/nature.py:411 +#, fuzzy +#| msgid "Modulaire poreux" +msgid "Porous modular (Modulaire poreux)" +msgstr "Porous modular" + +#: assets/models/nature.py:412 +msgid "Porous continuous (Continu poreux)" +msgstr "Porous continuous" + +#: assets/models/nature.py:413 +msgid "Impermeable (Imperméable)" +msgstr "Impermeable" + +#: assets/models/nature.py:417 +msgid "Open soil (Pleine terre)" +msgstr "Open soil" + +#: assets/models/nature.py:418 +#, fuzzy +#| msgid "Mélange terre-pierre" +msgid "Soil-stone mixture (Mélange terre-pierre)" +msgstr "Soil-stone mixture" + +#: assets/models/nature.py:419 +msgid "Grassed wide-joint pavers (Pavés à joints larges enherbés)" +msgstr "Grassed wide-joint pavers" + +#: assets/models/nature.py:420 +msgid "Concrete grass grids (Dalles gazon en béton)" +msgstr "Concrete grass grids" + +#: assets/models/nature.py:421 +msgid "Vegetated cellular tiles (Dalles alvéolaires végétalisées)" +msgstr "Vegetated cellular tiles" + +#: assets/models/nature.py:422 +msgid "Woodchips/mulch (Mulch végétal)" +msgstr "Woodchips/mulch" + +#: assets/models/nature.py:423 +msgid "Limestone gravel (Graviers calcaires)" +msgstr "Limestone gravel" + +#: assets/models/nature.py:424 +msgid "Non-limestone gravel (Graviers non calcaires)" +msgstr "Non-limestone gravel" + +#: assets/models/nature.py:425 +msgid "Dirt ground (Terre battue)" +msgstr "Dirt ground" + +#: assets/models/nature.py:426 +msgid "Gravel wide-joint pavers (Pavés à joints larges engravillonnés)" +msgstr "Gravel wide-joint pavers" + +#: assets/models/nature.py:427 +msgid "Gravel cellular tiles (Dalles alvéolaires en gravier)" +msgstr "Gravel cellular tiles" + +#: assets/models/nature.py:428 +msgid "HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)" +msgstr "HDPE tiles filled with pavers" + +#: assets/models/nature.py:429 +msgid "Grates (Grilles)" +msgstr "Grates" + +#: assets/models/nature.py:430 +msgid "Wooden decking (Platelages bois)" +msgstr "Wooden decking" + +#: assets/models/nature.py:431 +msgid "Porous concrete pavers (Pavé en béton poreux)" +msgstr "Porous concrete pavers" + +#: assets/models/nature.py:432 +msgid "Porous concrete slab (Dalle en béton poreux)" +msgstr "Porous concrete slab" + +#: assets/models/nature.py:433 +msgid "Porous asphalt (Enrobés bitumineux poreux)" +msgstr "Porous asphalt" + +#: assets/models/nature.py:434 +msgid "Porous concrete (Bétons poreux)" +msgstr "Porous concrete" + +#: assets/models/nature.py:435 +msgid "Resin-bound gravel (Graviers liés à la résine)" +msgstr "Resin-bound gravel" + +#: assets/models/nature.py:436 +msgid "Polymer-bound crushed gravel (Graviers concassés liés aux polymères)" +msgstr "Polymer-bound crushed gravel" + +#: assets/models/nature.py:437 +msgid "EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)" +msgstr "EPDM and SBR rubber coatings" + +#: assets/models/nature.py:438 +msgid "" +"Non-porous cement concrete coatings (Revêtements en béton de ciment non " +"perméables)" +msgstr "" + +#: assets/models/nature.py:439 +msgid "Non-porous asphalt coatings (Revêtements bitumineux non perméables)" +msgstr "Non-porous asphalt coatings" + +#: assets/models/nature.py:440 +msgid "Non-porous tiling (Dallages non perméables)" +msgstr "Non-porous tiling" + +#: assets/models/nature.py:441 +msgid "Non-porous paving (Pavages non perméables)" +msgstr "Non-porous paving" + +#: assets/models/nature.py:442 +msgid "Other non-porous coatings (Autres revêtements non perméables)" +msgstr "Other non-porous coatings" + +#: assets/models/nature.py:447 +msgid "Point" +msgstr "" + +#: assets/models/nature.py:448 +msgid "Line" +msgstr "" + +#: assets/models/nature.py:449 +msgid "Polygon" +msgstr "" + +#: assets/models/nature.py:456 +msgid "External ID" +msgstr "" + +#: assets/models/nature.py:457 +#, fuzzy +#| msgid "Code, nom…" +msgid "Code name" +msgstr "Code, name..." + +#: assets/models/nature.py:458 +#, fuzzy +#| msgid "Structure" +msgid "Structure type" +msgstr "Structure" + +#: assets/models/nature.py:459 +msgid "Geometry type" +msgstr "" + +#: assets/models/nature.py:460 +#, fuzzy +#| msgid "Operator / manager" +msgid "Project manager" +msgstr "Operator / manager" + +#: assets/models/nature.py:462 +#, fuzzy +#| msgid "Obstruction codes" +msgid "Construction status" +msgstr "Obstruction codes" + +#: assets/models/nature.py:463 +msgid "Commissioning date" +msgstr "" + +#: assets/models/nature.py:464 +#, fuzzy +#| msgid "Modification" +msgid "Condition" +msgstr "Modification" + +#: assets/models/nature.py:465 +msgid "Administrator" +msgstr "" + +#: assets/models/nature.py:466 +msgid "Monitoring" +msgstr "" + +#: assets/models/nature.py:467 +#, fuzzy +#| msgid "Commune" +msgid "Comment" +msgstr "Municipality" + +#: assets/models/nature.py:468 +#, fuzzy +#| msgid "Filter" +msgid "Has filter" +msgstr "Filter" + +#: assets/models/nature.py:470 +#, fuzzy +#| msgid "Section" +msgid "Sewer connection" +msgstr "Section" + +#: assets/models/nature.py:471 +msgid "Separate network connection" +msgstr "" + +#: assets/models/nature.py:472 +msgid "Hydro network connection" +msgstr "" + +#: assets/models/nature.py:474 +msgid "Surface area (m²)" +msgstr "Surface area" + +#: assets/models/nature.py:475 +msgid "Length (m)" +msgstr "Length" + +#: assets/models/nature.py:477 +#, fuzzy +#| msgid "Category" +msgid "Coating category" +msgstr "Category" + +#: assets/models/nature.py:478 +#, fuzzy +#| msgid "Fixation type" +msgid "Coating type" +msgstr "Fixation type" + +#: assets/models/nature.py:485 assets/models/nature.py:493 +#, fuzzy +#| msgid "Asset" +msgid "RWI Asset" +msgstr "Asset" + +#: assets/models/nature.py:486 +#, fuzzy +#| msgid "Assets" +msgid "RWI Assets" +msgstr "Assets" + +#: assets/models/nature.py:494 +msgid "File" +msgstr "" + +#: assets/models/nature.py:498 +#, fuzzy +#| msgid "Assets uniquement" +msgid "RWI Asset Document" +msgstr "Assets only" + +#: assets/models/nature.py:499 +#, fuzzy +#| msgid "Documents" +msgid "RWI Asset Documents" +msgstr "Documents" + +#: assets/models/publiclighting.py:85 +msgid "Public lighting pole" +msgstr "" + +#: assets/models/publiclighting.py:86 +msgid "Public lighting poles" +msgstr "" + +#: assets/models/publiclighting.py:108 +msgid "Luminaire" +msgstr "Streetlight" + +#: assets/models/publiclighting.py:109 +msgid "Luminaires" +msgstr "Streetlights" + +#: assets/models/roads.py:224 +msgid "Protection Asset" +msgstr "Protection Asset" + +#: assets/models/roads.py:225 +msgid "Antiterroriste" +msgstr "Anti-terrorist" + +#: assets/models/roads.py:226 +msgid "Neutralisation stationnement" +msgstr "Parking neutralization" + +#: assets/models/roads.py:227 +msgid "Sécurité routière" +msgstr "Road safety" + +#: assets/models/roads.py:228 +msgid "Espace public" +msgstr "Public space" + +#: assets/models/roads.py:229 +msgid "Chantier" +msgstr "Works" + +#: assets/models/roads.py:230 +msgid "Non identifié" +msgstr "Unidentified" + +#: assets/models/roads.py:234 +msgid "Maintenir" +msgstr "Maintain" + +#: assets/models/roads.py:235 +msgid "Enlever" +msgstr "Remove" + +#: assets/models/roads.py:236 assets/templates/assets/detail_generic.html:137 +#: assets/templates/assets/detail_generic.html:138 +#: assets/templates/assets/detail_generic.html:181 +#: assets/templates/assets/detail_generic.html:182 +#: assets/templates/assets/detail_generic.html:759 +msgid "Remplacer" +msgstr "Replace" + +#: assets/models/roads.py:258 +msgid "Road Concrete Block Set" +msgstr "Road Concrete Block Set" + +#: assets/models/roads.py:264 +msgid "Béton" +msgstr "Concrete" + +#: assets/models/roads.py:265 +msgid "Asphalte" +msgstr "Asphalt" + +#: assets/models/roads.py:266 +msgid "Pierre" +msgstr "Stone" + +#: assets/models/roads.py:267 +msgid "Dalles en béton" +msgstr "Concrete slabs" + +#: assets/models/roads.py:268 +msgid "Pavés en béton" +msgstr "Concrete pavers" + +#: assets/models/roads.py:269 +msgid "Dalles en pierre" +msgstr "Stone slabs" + +#: assets/models/roads.py:270 +msgid "Pavés en pierre" +msgstr "Stone pavers" + +#: assets/models/roads.py:271 +msgid "Béton/Gazon" +msgstr "Concrete/Grass" + +#: assets/models/roads.py:272 +msgid "Gravier" +msgstr "Gravel" + +#: assets/models/roads.py:273 +msgid "Gazon" +msgstr "Grass" + +#: assets/models/roads.py:274 +msgid "Terre" +msgstr "Soil" + +#: assets/models/roads.py:276 assets/models/roads.py:288 +msgid "N/A" +msgstr "N/A" + +#: assets/models/roads.py:280 +msgid "Filet d'eau" +msgstr "Gutter" + +#: assets/models/roads.py:281 +msgid "Trottoir" +msgstr "Sidewalk" + +#: assets/models/roads.py:282 +msgid "Berme" +msgstr "Berm" + +#: assets/models/roads.py:283 +msgid "Trottoir façade vers chaussée" +msgstr "Sidewalk façade to road" + +#: assets/models/roads.py:285 +msgid "Le long du trottoir" +msgstr "Along the sidewalk" + +#: assets/models/roads.py:286 +msgid "Milieu chaussée" +msgstr "Middle of road" + +#: assets/models/roads.py:292 +msgid "Obstrué" +msgstr "Obstructed" + +#: assets/models/roads.py:293 +msgid "Coupe-odeur manquant" +msgstr "Siphon missing" + +#: assets/models/roads.py:294 +msgid "Grille cassée" +msgstr "Broken grate" + +#: assets/models/roads.py:295 +msgid "Grille manquante" +msgstr "Missing grate" + +#: assets/models/roads.py:296 +msgid "Mauvais écoulement" +msgstr "Poor drainage" + +#: assets/models/roads.py:330 +msgid "Road Drain" +msgstr "Road Drain" + +#: assets/models/sign.py:173 +msgid "Panneau de signalisation" +msgstr "Road sign" + +#: assets/models/sign.py:174 +msgid "Panneaux de signalisation" +msgstr "Road signs" + +#: assets/models/trafficlights.py:19 +msgid "En préparation" +msgstr "In preparation" + +#: assets/models/trafficlights.py:20 +msgid "En cours de réparation" +msgstr "Under repair" + +#: assets/models/trafficlights.py:22 +msgid "Désactivé" +msgstr "Disabled" + +#: assets/models/trafficlights.py:73 +msgid "Est un radar ?" +msgstr "" + +#: assets/models/trafficlights.py:202 +msgid "Plan de croix" +msgstr "Cross plan" + +#: assets/models/trafficlights.py:204 +msgid "Fichier de programmation" +msgstr "Programming file" + +#: assets/models/trafficlights.py:205 +msgid "Plan de raccordement" +msgstr "Connection plan" + +#: assets/models/trafficlights.py:209 +msgid "Programmation associée" +msgstr "Associated programming" + +#: assets/models/trafficlights.py:235 assets/models/trafficlights.py:246 +#: assets/models/trafficlights.py:502 +msgid "Couleur" +msgstr "Color" + +#: assets/models/trafficlights.py:236 assets/models/trafficlights.py:247 +#: assets/models/trafficlights.py:503 +msgid "RAL" +msgstr "RAL" + +#: assets/models/trafficlights.py:237 +msgid "Hauteur (m)" +msgstr "Height (m)" + +#: assets/models/trafficlights.py:244 +msgid "Longueur de l'arc (m)" +msgstr "Arc length (m)" + +#: assets/models/trafficlights.py:245 +msgid "Extension" +msgstr "Extension" + +#: assets/models/trafficlights.py:262 +#, fuzzy +#| msgid "Traffic Light Radar Model" +msgid "Traffic light pole" +msgstr "Traffic Light Radar Model" + +#: assets/models/trafficlights.py:263 +#, fuzzy +#| msgid "Traffic Light Radar Models" +msgid "Traffic light poles" +msgstr "Traffic Light Radar Models" + +#: assets/models/trafficlights.py:268 +msgid "Nombre de brins" +msgstr "Number of strands" + +#: assets/models/trafficlights.py:277 +msgid "Câble" +msgstr "Cable" + +#: assets/models/trafficlights.py:278 +#: assets/templates/assets/publiclighting_street_detail.html:269 +#: assets/templates/assets/trafficlights_import_cross_plan.html:154 +#: assets/templates/assets/trafficlights_import_cross_plan.html:539 +#: assets/templates/assets/trafficlights_intersection_detail.html:542 +msgid "Câbles" +msgstr "Cables" + +#: assets/models/trafficlights.py:283 +msgid "Tension (V)" +msgstr "Voltage (V)" + +#: assets/models/trafficlights.py:292 +msgid "Lanterne" +msgstr "Lantern" + +#: assets/models/trafficlights.py:293 +#: assets/templates/assets/trafficlights_import_cross_plan.html:144 +msgid "Lanternes" +msgstr "Lanterns" + +#: assets/models/trafficlights.py:304 +msgid "Connecté à la centrale de contrôle" +msgstr "Connected to control center" + +#: assets/models/trafficlights.py:306 assets/models/trafficlights.py:483 +#: assets/templates/assets/trafficlights_intersection_detail.html:625 +#: assets/templates/assets/trafficlights_intersection_detail.html:949 +msgid "Plan" +msgstr "Plan" + +#: assets/models/trafficlights.py:307 +#: assets/templates/assets/sign_index.html:87 +#: assets/templates/assets/structures_index.html:76 +#: assets/templates/assets/trafficlights_index.html:120 +#: assets/templates/assets/trafficlights_intersection_detail.html:621 +#: assets/templates/assets/trafficlights_intersection_detail.html:1204 +#: assets/views/export.py:93 +msgid "Type" +msgstr "Type" + +#: assets/models/trafficlights.py:308 +#: assets/templates/assets/trafficlights_intersection_detail.html:629 +msgid "Puissance" +msgstr "Power" + +#: assets/models/trafficlights.py:309 +msgid "Version du firmware" +msgstr "Firmware version" + +#: assets/models/trafficlights.py:310 +msgid "RMS" +msgstr "RMS" + +#: assets/models/trafficlights.py:311 +msgid "URL" +msgstr "URL" + +#: assets/models/trafficlights.py:314 +#: assets/templates/assets/trafficlights_intersection_detail.html:401 +msgid "Contrôleur" +msgstr "Controller" + +#: assets/models/trafficlights.py:315 +msgid "Contrôleurs" +msgstr "Controllers" + +#: assets/models/trafficlights.py:321 assets/models/trafficlights.py:348 +#: assets/models/trafficlights.py:364 assets/models/trafficlights.py:380 +msgid "Hardware" +msgstr "Hardware" + +#: assets/models/trafficlights.py:322 assets/models/trafficlights.py:363 +#: assets/models/trafficlights.py:379 +msgid "Nom du matériel" +msgstr "Hardware name" + +#: assets/models/trafficlights.py:323 +msgid "Description du matériel" +msgstr "Hardware description" + +#: assets/models/trafficlights.py:326 +msgid "Hardware contrôleur" +msgstr "Controller hardware" + +#: assets/models/trafficlights.py:327 +msgid "Hardwares contrôleur" +msgstr "Controller hardwares" + +#: assets/models/trafficlights.py:334 +msgid "Date du log" +msgstr "Log date" + +#: assets/models/trafficlights.py:337 +msgid "Fichier de log du contrôleur" +msgstr "Controller log file" + +#: assets/models/trafficlights.py:338 +msgid "Fichiers de log du contrôleur" +msgstr "Controller log files" + +#: assets/models/trafficlights.py:347 +msgid "Nom du détecteur" +msgstr "Detector name" + +#: assets/models/trafficlights.py:353 +msgid "Phases associées" +msgstr "Associated phases" + +#: assets/models/trafficlights.py:354 assets/models/trafficlights.py:367 +#: assets/models/trafficlights.py:383 +msgid "Mentionné sur le plan" +msgstr "Mentioned on the plan" + +#: assets/models/trafficlights.py:357 +msgid "Détecteur" +msgstr "Detector" + +#: assets/models/trafficlights.py:358 +#: assets/templates/assets/trafficlights_intersection_detail.html:692 +msgid "Détecteurs" +msgstr "Detectors" + +#: assets/models/trafficlights.py:373 +msgid "Network Hardware" +msgstr "Network Hardware" + +#: assets/models/trafficlights.py:374 +msgid "Network Hardwares" +msgstr "Network Hardwares" + +#: assets/models/trafficlights.py:389 +msgid "Voltage Hardware" +msgstr "Voltage Hardware" + +#: assets/models/trafficlights.py:390 +msgid "Voltage Hardwares" +msgstr "Voltage Hardwares" + +#: assets/models/trafficlights.py:396 +msgid "Largeur (cm)" +msgstr "Width (cm)" + +#: assets/models/trafficlights.py:397 +msgid "Hauteur (cm)" +msgstr "Height (cm)" + +#: assets/models/trafficlights.py:398 +msgid "Profondeur (cm)" +msgstr "Depth (cm)" + +#: assets/models/trafficlights.py:399 +msgid "Indice de protection IP" +msgstr "IP protection index" + +#: assets/models/trafficlights.py:424 assets/models/trafficlights.py:454 +msgid "Code EAN" +msgstr "Code EAN" + +#: assets/models/trafficlights.py:425 +msgid "Alimentation électrique" +msgstr "Power supply" + +#: assets/models/trafficlights.py:426 +msgid "Numéro de compteur" +msgstr "Meter number" + +#: assets/models/trafficlights.py:427 +msgid "Publicité" +msgstr "Advertising" + +#: assets/models/trafficlights.py:428 +msgid "Type de clé" +msgstr "Key type" + +#: assets/models/trafficlights.py:429 +msgid "Type d'armoire" +msgstr "Cabinet type" + +#: assets/models/trafficlights.py:430 +msgid "Géré par" +msgstr "Managed by" + +#: assets/models/trafficlights.py:431 +msgid "Présence Compteur" +msgstr "Meter presence" + +#: assets/models/trafficlights.py:432 +msgid "Interrupteur différentiel" +msgstr "Differential switch" + +#: assets/models/trafficlights.py:433 +msgid "Calibre du disjoncteur différentiel" +msgstr "Differential switch rating" + +#: assets/models/trafficlights.py:434 +msgid "Disjoncteur différentiel" +msgstr "Differential circuit breaker" + +#: assets/models/trafficlights.py:435 +msgid "Calibre du disjoncteur" +msgstr "Circuit breaker rating" + +#: assets/models/trafficlights.py:436 +msgid "Calibre différentiel" +msgstr "Differential rating" + +#: assets/models/trafficlights.py:437 +msgid "Départ vers" +msgstr "Departure to" + +#: assets/models/trafficlights.py:438 +msgid "Conformité visuelle" +msgstr "Visual compliance" + +#: assets/models/trafficlights.py:439 assets/views/locations.py:33 +msgid "Propreté" +msgstr "Cleanliness" + +#: assets/models/trafficlights.py:442 +msgid "Armoire" +msgstr "Cabinet" + +#: assets/models/trafficlights.py:443 +msgid "Armoires" +msgstr "Cabinets" + +#: assets/models/trafficlights.py:452 +msgid "Description du contenu" +msgstr "Content description" + +#: assets/models/trafficlights.py:453 +msgid "Numéro de référence" +msgstr "Reference number" + +#: assets/models/trafficlights.py:457 +msgid "Contenu d'armoire" +msgstr "Cabinet content" + +#: assets/models/trafficlights.py:458 +msgid "Contenus d'armoires" +msgstr "Cabinet contents" + +#: assets/models/trafficlights.py:473 +msgid "Accessoire" +msgstr "Accessory" + +#: assets/models/trafficlights.py:474 +msgid "Accessoires" +msgstr "Accessories" + +#: assets/models/trafficlights.py:481 +#: assets/templates/assets/trafficlights_intersection_detail.html:633 +msgid "Date de mise en service" +msgstr "Commissioning date" + +#: assets/models/trafficlights.py:482 +msgid "Date de mise hors service" +msgstr "Decommissioning date" + +#: assets/models/trafficlights.py:484 +#: assets/templates/assets/trafficlights_intersection_detail.html:50 +msgid "Index" +msgstr "Index" + +#: assets/models/trafficlights.py:485 +msgid "Informations" +msgstr "Information" + +#: assets/models/trafficlights.py:518 +msgid "Radar Pole" +msgstr "Radar Pole" + +#: assets/models/trafficlights.py:519 +msgid "Radar Poles" +msgstr "Radar Poles" + +#: assets/models/trafficlights.py:524 +msgid "Traffic Light Radar Model" +msgstr "Traffic Light Radar Model" + +#: assets/models/trafficlights.py:525 +msgid "Traffic Light Radar Models" +msgstr "Traffic Light Radar Models" + +#: assets/models/trafficlights.py:536 +msgid "Radar" +msgstr "Radar" + +#: assets/models/trafficlights.py:537 +msgid "Radars" +msgstr "Radars" + +#: assets/permissions.py:502 +msgid "Missing asset information" +msgstr "Missing asset information" + +#: assets/permissions.py:509 +msgid "Invalid asset type" +msgstr "Invalid asset type" + +#: assets/permissions.py:512 +msgid "You do not have permission to edit this asset" +msgstr "You do not have permission to edit this asset" + +#: assets/permissions.py:549 +msgid "Invalid thematic" +msgstr "Invalid thematic" + +#: assets/permissions.py:553 assets/views/history.py:44 +msgid "You do not have permission to edit assets for this thematic" +msgstr "You do not have permission to edit assets for this thematic" + +#: assets/templates/assets/artworks_index.html:18 +#: assets/templates/assets/artworks_index.html:46 +#: assets/templates/assets/clean_index.html:8 +#: assets/templates/assets/controlcenters_index.html:16 +#: assets/templates/assets/controlcenters_index.html:45 +#: assets/templates/assets/its_index.html:16 +#: assets/templates/assets/its_index.html:56 +#: assets/templates/assets/nature_index.html:28 +#: assets/templates/assets/nature_index.html:59 +#: assets/templates/assets/publiclighting_index.html:18 +#: assets/templates/assets/publiclighting_index.html:49 +#: assets/templates/assets/roads_index.html:18 +#: assets/templates/assets/roads_index.html:61 +#: assets/templates/assets/sign_index.html:18 +#: assets/templates/assets/sign_index.html:61 +#: assets/templates/assets/structures_index.html:26 +#: assets/templates/assets/structures_index.html:57 +#: assets/templates/assets/trafficlights_index.html:61 +#: assets/templates/assets/trafficlights_index.html:101 +#: assets/templates/assets/unified_index.html:229 +#: assets/templates/assets/water_index.html:22 +#: assets/templates/assets/water_index.html:50 +msgid "Filtrer" +msgstr "Filter" + +#: assets/templates/assets/artworks_index.html:19 +#: assets/templates/assets/artworks_index.html:47 +#: assets/templates/assets/clean_index.html:9 +#: assets/templates/assets/controlcenters_index.html:17 +#: assets/templates/assets/controlcenters_index.html:46 +#: assets/templates/assets/its_index.html:17 +#: assets/templates/assets/its_index.html:57 +#: assets/templates/assets/nature_index.html:29 +#: assets/templates/assets/nature_index.html:60 +#: assets/templates/assets/publiclighting_index.html:19 +#: assets/templates/assets/publiclighting_index.html:50 +#: assets/templates/assets/roads_index.html:19 +#: assets/templates/assets/roads_index.html:62 +#: assets/templates/assets/sign_index.html:19 +#: assets/templates/assets/sign_index.html:62 +#: assets/templates/assets/structures_index.html:27 +#: assets/templates/assets/structures_index.html:58 +#: assets/templates/assets/trafficlights_index.html:62 +#: assets/templates/assets/trafficlights_index.html:102 +#: assets/templates/assets/unified_index.html:227 +#: assets/templates/assets/water_index.html:23 +#: assets/templates/assets/water_index.html:51 +msgid "Réinitialiser" +msgstr "Reset" + +#: assets/templates/assets/artworks_index.html:22 +#: assets/templates/assets/artworks_index.html:64 +#: assets/templates/assets/artworks_street_detail.html:15 +#: assets/templates/assets/publiclighting_index.html:8 +#: assets/templates/assets/publiclighting_index.html:67 +#: assets/templates/assets/roads_index.html:8 +#: assets/templates/assets/roads_index.html:79 +#: assets/templates/assets/sign_index.html:8 +#: assets/templates/assets/sign_index.html:79 +#: assets/templates/assets/trafficlights_intersection_detail.html:79 +#: assets/views/core.py:537 +msgid "Commune" +msgstr "Municipality" + +#: assets/templates/assets/artworks_index.html:63 +#: assets/templates/assets/artworks_index.html:71 +#: assets/templates/assets/components/add_asset_modal.html:82 +#: assets/templates/assets/components/add_asset_modal.html:91 +#: assets/templates/assets/components/asset_model_card.html:21 +#: assets/templates/assets/controlcenters_index.html:62 +#: assets/templates/assets/controlcenters_index.html:67 +#: assets/templates/assets/detail_generic.html:222 +#: assets/templates/assets/detail_generic.html:235 +#: assets/templates/assets/its_index.html:82 +#: assets/templates/assets/its_index.html:87 +#: assets/templates/assets/nature_index.html:76 +#: assets/templates/assets/publiclighting_index.html:66 +#: assets/templates/assets/publiclighting_index.html:72 +#: assets/templates/assets/roads_index.html:78 +#: assets/templates/assets/roads_index.html:84 +#: assets/templates/assets/sign_index.html:78 +#: assets/templates/assets/sign_index.html:84 +#: assets/templates/assets/structures_index.html:75 +#: assets/templates/assets/structures_index.html:81 +#: assets/templates/assets/trafficlights_import_cross_plan.html:180 +#: assets/templates/assets/trafficlights_import_cross_plan.html:260 +#: assets/templates/assets/trafficlights_import_cross_plan.html:342 +#: assets/templates/assets/trafficlights_import_cross_plan.html:432 +#: assets/templates/assets/trafficlights_index.html:119 +#: assets/templates/assets/trafficlights_index.html:125 +#: assets/templates/assets/unified_index.html:315 +#: assets/templates/assets/unified_index.html:319 +#: assets/templates/assets/water_index.html:75 assets/views/export.py:96 +msgid "Nom" +msgstr "Name" + +#: assets/templates/assets/artworks_index.html:65 +#: assets/templates/assets/artworks_street_detail.html:19 +msgid "Code voirie" +msgstr "Road code" + +#: assets/templates/assets/artworks_index.html:66 +#: assets/templates/assets/artworks_street_detail.html:23 +msgid "Section" +msgstr "Section" + +#: assets/templates/assets/artworks_index.html:73 +msgid "Sous-type" +msgstr "Subtype" + +#: assets/templates/assets/artworks_street_detail.html:7 +msgid "Retour à la vue Artworks" +msgstr "Back to Artworks view" + +#: assets/templates/assets/artworks_street_detail.html:27 +msgid "Niveau" +msgstr "Level" + +#: assets/templates/assets/artworks_street_detail.html:38 +msgid "Monuments" +msgstr "Monuments" + +#: assets/templates/assets/artworks_street_detail.html:51 +#: assets/templates/assets/artworks_street_detail.html:78 +#: assets/templates/assets/controlcenters_location_detail.html:251 +#: assets/templates/assets/its_location_detail.html:175 +#: assets/templates/assets/nature_locations_detail.html:81 +#: assets/templates/assets/naturerwiz_detail.html:250 +#: assets/templates/assets/publiclighting_street_detail.html:310 +#: assets/templates/assets/roads_street_detail.html:325 +#: assets/templates/assets/sign_street_detail.html:302 +#: assets/templates/assets/structures_location_detail.html:117 +#: assets/templates/assets/structures_location_detail.html:360 +#: assets/templates/assets/trafficlights_intersection_detail.html:930 +msgid "Détail" +msgstr "Detail" + +#: assets/templates/assets/artworks_street_detail.html:57 +msgid "Aucun monument actif n'est lié à cette localisation." +msgstr "No active monument is linked to this location." + +#: assets/templates/assets/artworks_street_detail.html:65 +msgid "Fontaines" +msgstr "Fountains" + +#: assets/templates/assets/artworks_street_detail.html:84 +msgid "Aucune fontaine active n'est liée à cette localisation." +msgstr "No active fountain is linked to this location." + +#: assets/templates/assets/base_assets_index.html:22 +msgid "Thématique :" +msgstr "Theme:" + +#: assets/templates/assets/base_assets_index.html:33 +msgid "Sélectionnez une thématique" +msgstr "Select a theme" + +#: assets/templates/assets/base_assets_index.html:56 +#: assets/templates/assets/base_assets_index.html:69 +msgid "Filtre" +msgstr "Filter" + +#: assets/templates/assets/base_assets_index.html:81 +msgid "Sélection de la vue" +msgstr "View selection" + +#: assets/templates/assets/base_assets_index.html:82 +#: assets/templates/assets/unified_index.html:425 +msgid "Vue tableau uniquement" +msgstr "Table view only" + +#: assets/templates/assets/base_assets_index.html:85 +#: assets/templates/assets/unified_index.html:428 +msgid "Vue partagée" +msgstr "Split view" + +#: assets/templates/assets/base_assets_index.html:88 +#: assets/templates/assets/unified_index.html:431 +msgid "Vue carte uniquement" +msgstr "Map view only" + +#: assets/templates/assets/base_assets_index.html:102 +msgid "Code, nom…" +msgstr "Code, name..." + +#: assets/templates/assets/base_assets_index.html:107 +msgid "Effacer la recherche" +msgstr "Clear search" + +#: assets/templates/assets/base_assets_index.html:113 +msgid "Mode" +msgstr "Mode" + +#: assets/templates/assets/base_assets_index.html:115 +msgid "Tous les mots doivent correspondre" +msgstr "All words must match" + +#: assets/templates/assets/base_assets_index.html:117 +msgid "Au moins un mot doit correspondre" +msgstr "At least one word must match" + +#: assets/templates/assets/base_assets_index.html:123 +msgid "Rechercher" +msgstr "Search" + +#: assets/templates/assets/base_assets_index.html:151 +#: assets/templates/assets/base_assets_index.html:188 +#: assets/templates/assets/unified_index.html:475 +msgid "Tout sélectionner" +msgstr "Select all" + +#: assets/templates/assets/base_assets_index.html:152 +#: assets/templates/assets/base_assets_index.html:189 +#: assets/templates/assets/unified_index.html:476 +msgid "Tout désélectionner" +msgstr "Deselect all" + +#: assets/templates/assets/base_assets_index.html:154 +#: assets/templates/assets/base_assets_index.html:191 +#: assets/templates/assets/unified_index.html:378 +#: assets/templates/assets/unified_index.html:478 +msgid "Valider la sélection" +msgstr "Validate selection" + +#: assets/templates/assets/base_assets_index.html:157 +#, python-format +msgid "" +"\n" +" %(counter)s localisation sélectionnée\n" +" " +msgid_plural "" +"\n" +" %(counter)s localisations sélectionnées\n" +" " +msgstr[0] "" +"\n" +" %(counter)s location selected\n" +" " +msgstr[1] "" +"\n" +" %(counter)s locations selected\n" +" " + +#: assets/templates/assets/base_assets_index.html:171 +#: assets/templates/assets/base_assets_index.html:225 +#: assets/templates/assets/unified_index.html:520 +msgid "Loading..." +msgstr "Loading..." + +#: assets/templates/assets/base_assets_index.html:194 +#, python-format +msgid "" +"\n" +" %(counter)s asset sélectionné\n" +" " +msgid_plural "" +"\n" +" %(counter)s assets sélectionnés\n" +" " +msgstr[0] "" +"\n" +" %(counter)s asset selected\n" +" " +msgstr[1] "" +"\n" +" %(counter)s assets selected\n" +" " + +#: assets/templates/assets/base_assets_index.html:205 +#: assets/templates/assets/unified_index.html:484 +msgid "Action" +msgstr "Action" + +#: assets/templates/assets/base_assets_index.html:211 +#: assets/templates/assets/base_assets_index.html:321 +#: assets/templates/assets/unified_index.html:490 +#: assets/templates/assets/unified_index.html:603 +msgid "Exporter" +msgstr "Export" + +#: assets/templates/assets/base_assets_index.html:261 +msgid "Carte" +msgstr "Map" + +#: assets/templates/assets/base_assets_index.html:269 +msgid "Filtrer automatiquement sur la zone visible" +msgstr "Automatically filter on the visible area" + +#: assets/templates/assets/base_assets_index.html:280 +#: assets/templates/assets/unified_index.html:566 +msgid "Totaux des assets" +msgstr "Asset totals" + +#: assets/templates/assets/base_assets_index.html:282 +#: assets/templates/assets/base_assets_index.html:305 +#: assets/templates/assets/components/add_asset_modal.html:19 +#: assets/templates/assets/components/asset_documents.html:37 +#: assets/templates/assets/components/asset_location_link.html:50 +#: assets/templates/assets/components/asset_model_card.html:62 +#: assets/templates/assets/components/location_edit_modals.html:24 +#: assets/templates/assets/components/location_edit_modals.html:56 +#: assets/templates/assets/components/location_edit_modals.html:82 +#: assets/templates/assets/controlcenters_location_detail.html:112 +#: assets/templates/assets/controlcenters_location_detail.html:145 +#: assets/templates/assets/controlcenters_location_detail.html:170 +#: assets/templates/assets/its_location_detail.html:55 +#: assets/templates/assets/its_location_detail.html:88 +#: assets/templates/assets/naturerwiz_detail.html:273 +#: assets/templates/assets/naturerwiz_detail.html:306 +#: assets/templates/assets/publiclighting_street_detail.html:161 +#: assets/templates/assets/publiclighting_street_detail.html:194 +#: assets/templates/assets/publiclighting_street_detail.html:219 +#: assets/templates/assets/roads_street_detail.html:160 +#: assets/templates/assets/roads_street_detail.html:193 +#: assets/templates/assets/roads_street_detail.html:218 +#: assets/templates/assets/sign_street_detail.html:160 +#: assets/templates/assets/sign_street_detail.html:193 +#: assets/templates/assets/sign_street_detail.html:218 +#: assets/templates/assets/structures_location_detail.html:190 +#: assets/templates/assets/structures_location_detail.html:232 +#: assets/templates/assets/structures_location_detail.html:257 +#: assets/templates/assets/trafficlights_import_cross_plan.html:733 +#: assets/templates/assets/trafficlights_intersection_detail.html:248 +#: assets/templates/assets/trafficlights_intersection_detail.html:281 +#: assets/templates/assets/trafficlights_intersection_detail.html:306 +#: assets/templates/assets/trafficlights_intersection_detail.html:1098 +#: assets/templates/assets/trafficlights_intersection_detail.html:1146 +#: assets/templates/assets/trafficlights_intersection_detail.html:1222 +#: assets/templates/assets/unified_index.html:84 +#: assets/templates/assets/unified_index.html:241 +#: assets/templates/assets/unified_index.html:283 +#: assets/templates/assets/unified_index.html:343 +#: assets/templates/assets/unified_index.html:568 +#: assets/templates/assets/unified_index.html:587 +msgid "Fermer" +msgstr "Close" + +#: assets/templates/assets/base_assets_index.html:293 +#: assets/templates/assets/unified_index.html:575 +msgid "Totaux" +msgstr "Totals" + +#: assets/templates/assets/base_assets_index.html:304 +#: assets/templates/assets/unified_index.html:586 +msgid "Exporter les assets sélectionnés" +msgstr "Export selected assets" + +#: assets/templates/assets/base_assets_index.html:308 +#: assets/templates/assets/unified_index.html:590 +msgid "Choisissez le format d'export :" +msgstr "Choose export format:" + +#: assets/templates/assets/base_assets_index.html:320 +#: assets/templates/assets/components/add_asset_modal.html:138 +#: assets/templates/assets/components/asset_documents.html:87 +#: assets/templates/assets/components/asset_location_link.html:75 +#: assets/templates/assets/components/asset_model_card.html:98 +#: assets/templates/assets/components/location_edit_modals.html:38 +#: assets/templates/assets/components/location_edit_modals.html:63 +#: assets/templates/assets/components/location_edit_modals.html:96 +#: assets/templates/assets/controlcenters_location_detail.html:156 +#: assets/templates/assets/detail_generic.html:148 +#: assets/templates/assets/detail_generic.html:192 +#: assets/templates/assets/detail_generic.html:757 +#: assets/templates/assets/its_location_detail.html:99 +#: assets/templates/assets/permissions/access_edit.html:121 +#: assets/templates/assets/permissions/type_access_edit.html:126 +#: assets/templates/assets/publiclighting_street_detail.html:205 +#: assets/templates/assets/roads_street_detail.html:204 +#: assets/templates/assets/sign_street_detail.html:204 +#: assets/templates/assets/structures_location_detail.html:243 +#: assets/templates/assets/trafficlights_import_dxf.html:167 +#: assets/templates/assets/trafficlights_import_dxf.html:1490 +#: assets/templates/assets/trafficlights_intersection_detail.html:292 +#: assets/templates/assets/trafficlights_intersection_detail.html:992 +#: assets/templates/assets/unified_index.html:228 +#: assets/templates/assets/unified_index.html:327 +#: assets/templates/assets/unified_index.html:359 +#: assets/templates/assets/unified_index.html:602 +#: assets/templates/assets/unified_index.html:631 +#: assets/templates/assets/unified_index.html:661 +msgid "Annuler" +msgstr "Cancel" + +#: assets/templates/assets/clean_index.html:18 +#: assets/templates/assets/clean_index.html:73 +msgid "Category" +msgstr "Category" + +#: assets/templates/assets/clean_index.html:38 +#: assets/templates/assets/clean_location_detail.html:161 +msgid "Status" +msgstr "Status" + +#: assets/templates/assets/clean_index.html:40 +msgid "To be approved" +msgstr "To be approved" + +#: assets/templates/assets/clean_index.html:41 +#: assets/templates/assets/clean_location_detail.html:53 +msgid "Active" +msgstr "Active" + +#: assets/templates/assets/clean_index.html:42 +msgid "Damaged" +msgstr "Damaged" + +#: assets/templates/assets/clean_index.html:43 +msgid "To replace" +msgstr "To replace" + +#: assets/templates/assets/clean_index.html:44 +msgid "Removed" +msgstr "Removed" + +#: assets/templates/assets/clean_index.html:48 +msgid "Filter" +msgstr "Filter" + +#: assets/templates/assets/clean_index.html:49 +msgid "Reset" +msgstr "Reset" + +#: assets/templates/assets/clean_index.html:66 +#: assets/templates/assets/clean_index.html:72 +msgid "Name" +msgstr "Name" + +#: assets/templates/assets/clean_location_detail.html:19 +msgid "Municipality:" +msgstr "Municipality:" + +#: assets/templates/assets/clean_location_detail.html:22 +msgid "Road code:" +msgstr "Road code:" + +#: assets/templates/assets/clean_location_detail.html:25 +#: assets/templates/assets/roads_street_detail.html:20 +msgid "Section:" +msgstr "Section:" + +#: assets/templates/assets/clean_location_detail.html:28 +msgid "Street level:" +msgstr "Street level:" + +#: assets/templates/assets/clean_location_detail.html:35 +#: assets/templates/assets/controlcenters_location_detail.html:34 +#: assets/templates/assets/its_location_detail.html:30 +#: assets/templates/assets/nature_locations_detail.html:20 +#: assets/templates/assets/naturerwiz_detail.html:20 +#: assets/templates/assets/publiclighting_street_detail.html:18 +#: assets/templates/assets/roads_street_detail.html:27 +#: assets/templates/assets/sign_street_detail.html:18 +#: assets/templates/assets/structures_location_detail.html:35 +#: assets/templates/assets/trafficlights_intersection_detail.html:82 +msgid "Street View" +msgstr "Street View" + +#: assets/templates/assets/clean_location_detail.html:36 +#: assets/templates/assets/controlcenters_location_detail.html:35 +#: assets/templates/assets/its_location_detail.html:31 +#: assets/templates/assets/nature_locations_detail.html:21 +#: assets/templates/assets/naturerwiz_detail.html:21 +#: assets/templates/assets/publiclighting_street_detail.html:19 +#: assets/templates/assets/roads_street_detail.html:28 +#: assets/templates/assets/sign_street_detail.html:19 +#: assets/templates/assets/structures_location_detail.html:36 +#: assets/templates/assets/trafficlights_intersection_detail.html:83 +msgid "View in Google Street View" +msgstr "View in Google Street View" + +#: assets/templates/assets/clean_location_detail.html:45 +#: assets/templates/assets/controlcenters_location_detail.html:186 +#: assets/templates/assets/its_location_detail.html:110 +#: assets/templates/assets/naturerwiz_detail.html:180 +#: assets/templates/assets/publiclighting_street_detail.html:233 +#: assets/templates/assets/roads_street_detail.html:232 +#: assets/templates/assets/sign_street_detail.html:232 +#: assets/templates/assets/structures_location_detail.html:273 +#: assets/templates/assets/trafficlights_intersection_detail.html:324 +#: assets/templates/assets/unified_index.html:448 assets/views/artworks.py:95 +#: assets/views/clean.py:260 assets/views/controlcenters.py:108 +msgid "Assets" +msgstr "Assets" + +#: assets/templates/assets/clean_location_detail.html:60 +msgid "All" +msgstr "All" + +#: assets/templates/assets/clean_location_detail.html:82 +#: assets/templates/assets/clean_location_detail.html:119 +msgid "No litter bins" +msgstr "No litter bins" + +#: assets/templates/assets/clean_location_detail.html:99 +#: assets/templates/assets/clean_location_detail.html:133 +msgid "No glass containers" +msgstr "No glass containers" + +#: assets/templates/assets/clean_location_detail.html:147 +#: assets/templates/assets/controlcenters_location_detail.html:231 +#: assets/templates/assets/its_location_detail.html:155 +#: assets/templates/assets/nature_locations_detail.html:61 +#: assets/templates/assets/naturerwiz_detail.html:230 +#: assets/templates/assets/publiclighting_street_detail.html:290 +#: assets/templates/assets/roads_street_detail.html:305 +#: assets/templates/assets/sign_street_detail.html:282 +#: assets/templates/assets/structures_location_detail.html:340 +#: assets/templates/assets/trafficlights_intersection_detail.html:910 +msgid "Interventions" +msgstr "Interventions" + +#: assets/templates/assets/clean_location_detail.html:159 +msgid "Title" +msgstr "Title" + +#: assets/templates/assets/clean_location_detail.html:160 +msgid "Start" +msgstr "Start" + +#: assets/templates/assets/clean_location_detail.html:168 +msgid "Detail" +msgstr "Detail" + +#: assets/templates/assets/clean_location_detail.html:179 +msgid "No interventions" +msgstr "No interventions" + +#: assets/templates/assets/cleanlitterbin_detail.html:18 +msgid "Observations" +msgstr "Observations" + +#: assets/templates/assets/cleanlitterbin_detail.html:23 +#: assets/templates/assets/components/asset_documents.html:14 +#: assets/templates/assets/controlcenters_location_detail.html:46 +#: assets/templates/assets/controlcenters_location_detail.html:129 +#: assets/templates/assets/its_location_detail.html:41 +#: assets/templates/assets/its_location_detail.html:72 +#: assets/templates/assets/naturerwiz_detail.html:82 +#: assets/templates/assets/naturerwiz_detail.html:290 +#: assets/templates/assets/publiclighting_street_detail.html:29 +#: assets/templates/assets/publiclighting_street_detail.html:178 +#: assets/templates/assets/roads_street_detail.html:38 +#: assets/templates/assets/roads_street_detail.html:177 +#: assets/templates/assets/sign_street_detail.html:29 +#: assets/templates/assets/sign_street_detail.html:177 +#: assets/templates/assets/structures_location_detail.html:47 +#: assets/templates/assets/structures_location_detail.html:207 +#: assets/templates/assets/trafficlights_import_dxf.html:1492 +#: assets/templates/assets/trafficlights_intersection_detail.html:157 +#: assets/templates/assets/trafficlights_intersection_detail.html:265 +#: assets/templates/assets/trafficlights_intersection_detail.html:329 +#: assets/templates/assets/trafficlights_intersection_detail.html:1214 +msgid "Ajouter" +msgstr "Add" + +#: assets/templates/assets/cleanlitterbin_detail.html:31 +msgid "Aucune observation enregistrée." +msgstr "No observation recorded." + +#: assets/templates/assets/cleanlitterbin_detail.html:42 +msgid "Date inconnue" +msgstr "Unknown date" + +#: assets/templates/assets/cleanlitterbin_detail.html:65 +#: assets/templates/assets/cleanlitterbin_detail.html:221 +#: assets/templates/assets/cleanlitterbin_detail.html:362 +msgid "Sol" +msgstr "Ground" + +#: assets/templates/assets/cleanlitterbin_detail.html:70 +#: assets/templates/assets/cleanlitterbin_detail.html:223 +#: assets/templates/assets/cleanlitterbin_detail.html:364 +msgid "Pente trop forte" +msgstr "Slope too steep" + +#: assets/templates/assets/cleanlitterbin_detail.html:74 +#: assets/templates/assets/cleanlitterbin_detail.html:224 +#: assets/templates/assets/cleanlitterbin_detail.html:365 +msgid "Sol instable" +msgstr "Unstable ground" + +#: assets/templates/assets/cleanlitterbin_detail.html:78 +#: assets/templates/assets/cleanlitterbin_detail.html:225 +#: assets/templates/assets/cleanlitterbin_detail.html:366 +msgid "Surface réfléchissante" +msgstr "Reflective surface" + +#: assets/templates/assets/cleanlitterbin_detail.html:82 +#: assets/templates/assets/cleanlitterbin_detail.html:226 +#: assets/templates/assets/cleanlitterbin_detail.html:367 +msgid "Surface glissante" +msgstr "Slippery surface" + +#: assets/templates/assets/cleanlitterbin_detail.html:86 +#: assets/templates/assets/cleanlitterbin_detail.html:227 +#: assets/templates/assets/cleanlitterbin_detail.html:368 +msgid "Fissures" +msgstr "Cracks" + +#: assets/templates/assets/cleanlitterbin_detail.html:90 +#: assets/templates/assets/cleanlitterbin_detail.html:228 +#: assets/templates/assets/cleanlitterbin_detail.html:369 +msgid "Entrave" +msgstr "Obstruction / Obstacle" + +#: assets/templates/assets/cleanlitterbin_detail.html:94 +#: assets/templates/assets/cleanlitterbin_detail.html:229 +#: assets/templates/assets/cleanlitterbin_detail.html:370 +msgid "Dalles podotactiles" +msgstr "Tactile paving" + +#: assets/templates/assets/cleanlitterbin_detail.html:99 +msgid "Pente transversale" +msgstr "Transversal slope" + +#: assets/templates/assets/cleanlitterbin_detail.html:103 +msgid "Pente longitudinale" +msgstr "Longitudinal slope" + +#: assets/templates/assets/cleanlitterbin_detail.html:108 +#: assets/templates/assets/cleanlitterbin_detail.html:194 +msgid "Remarque" +msgstr "Remark" + +#: assets/templates/assets/cleanlitterbin_detail.html:118 +#: assets/templates/assets/cleanlitterbin_detail.html:244 +#: assets/templates/assets/cleanlitterbin_detail.html:376 +msgid "Accessibilité" +msgstr "Accessibility" + +#: assets/templates/assets/cleanlitterbin_detail.html:122 +#: assets/templates/assets/cleanlitterbin_detail.html:247 +#: assets/templates/assets/cleanlitterbin_detail.html:378 +msgid "Type de quartier" +msgstr "Neighbourhood type" + +#: assets/templates/assets/cleanlitterbin_detail.html:126 +#: assets/templates/assets/cleanlitterbin_detail.html:257 +#: assets/templates/assets/cleanlitterbin_detail.html:379 +msgid "Densité flux piéton" +msgstr "Pedestrian flow density" + +#: assets/templates/assets/cleanlitterbin_detail.html:130 +#: assets/templates/assets/cleanlitterbin_detail.html:283 +#: assets/templates/assets/cleanlitterbin_detail.html:382 +msgid "Approche 2 m dégagée" +msgstr "Clear 2 m approach" + +#: assets/templates/assets/cleanlitterbin_detail.html:134 +#: assets/templates/assets/cleanlitterbin_detail.html:284 +#: assets/templates/assets/cleanlitterbin_detail.html:383 +msgid "Voirie à traverser" +msgstr "Road to cross" + +#: assets/templates/assets/cleanlitterbin_detail.html:138 +#: assets/templates/assets/cleanlitterbin_detail.html:285 +#: assets/templates/assets/cleanlitterbin_detail.html:384 +msgid "Poubelle isolée" +msgstr "Isolated bin" + +#: assets/templates/assets/cleanlitterbin_detail.html:142 +#: assets/templates/assets/cleanlitterbin_detail.html:286 +#: assets/templates/assets/cleanlitterbin_detail.html:385 +msgid "Gêne flux routier" +msgstr "Obstructs road traffic" + +#: assets/templates/assets/cleanlitterbin_detail.html:146 +#: assets/templates/assets/cleanlitterbin_detail.html:287 +#: assets/templates/assets/cleanlitterbin_detail.html:386 +msgid "Gêne flux piéton" +msgstr "Obstructs pedestrian flow" + +#: assets/templates/assets/cleanlitterbin_detail.html:150 +#: assets/templates/assets/cleanlitterbin_detail.html:288 +#: assets/templates/assets/cleanlitterbin_detail.html:387 +msgid "Itinéraire logique" +msgstr "Logical path" + +#: assets/templates/assets/cleanlitterbin_detail.html:154 +#: assets/templates/assets/cleanlitterbin_detail.html:289 +#: assets/templates/assets/cleanlitterbin_detail.html:388 +msgid "Contraste visuel" +msgstr "Visual contrast" + +#: assets/templates/assets/cleanlitterbin_detail.html:158 +#: assets/templates/assets/cleanlitterbin_detail.html:290 +#: assets/templates/assets/cleanlitterbin_detail.html:389 +msgid "Indicateurs de tri" +msgstr "Sorting indicators" + +#: assets/templates/assets/cleanlitterbin_detail.html:162 +#: assets/templates/assets/cleanlitterbin_detail.html:266 +#: assets/templates/assets/cleanlitterbin_detail.html:380 +msgid "Accès agent (véhicule)" +msgstr "Agent access (vehicle)" + +#: assets/templates/assets/cleanlitterbin_detail.html:172 +#: assets/templates/assets/cleanlitterbin_detail.html:275 +#: assets/templates/assets/cleanlitterbin_detail.html:381 +msgid "Accès agent (pied)" +msgstr "Agent access (on foot)" + +#: assets/templates/assets/cleanlitterbin_detail.html:183 +#: assets/templates/assets/cleanlitterbin_detail.html:292 +#: assets/templates/assets/cleanlitterbin_detail.html:391 +msgid "Points d'intérêt" +msgstr "Points of interest" + +#: assets/templates/assets/cleanlitterbin_detail.html:208 +#: assets/templates/assets/cleanlitterbin_detail.html:350 +msgid "Général" +msgstr "General" + +#: assets/templates/assets/cleanlitterbin_detail.html:211 +#: assets/templates/assets/cleanlitterbin_detail.html:353 +msgid "Date / heure" +msgstr "Date / time" + +#: assets/templates/assets/cleanlitterbin_detail.html:216 +#: assets/templates/assets/cleanlitterbin_detail.html:357 +#: assets/views/export.py:108 +msgid "Note" +msgstr "Note" + +#: assets/templates/assets/cleanlitterbin_detail.html:231 +#: assets/templates/assets/cleanlitterbin_detail.html:371 +#, python-format +msgid "Pente transversale (%%)" +msgstr "Transversal slope (%%)" + +#: assets/templates/assets/cleanlitterbin_detail.html:235 +#: assets/templates/assets/cleanlitterbin_detail.html:372 +#, python-format +msgid "Pente longitudinale (%%)" +msgstr "Longitudinal slope (%%)" + +#: assets/templates/assets/cleanlitterbin_detail.html:239 +#: assets/templates/assets/cleanlitterbin_detail.html:373 +msgid "Remarque sol" +msgstr "Ground remarks" + +#: assets/templates/assets/cleanlitterbin_detail.html:308 +#: assets/templates/assets/cleanlitterbin_detail.html:404 +msgid "Autre point d'intérêt" +msgstr "Other point of interest" + +#: assets/templates/assets/cleanlitterbin_detail.html:312 +#: assets/templates/assets/cleanlitterbin_detail.html:405 +msgid "Remarque accessibilité" +msgstr "Accessibility remarks" + +#: assets/templates/assets/cleanlitterbin_detail.html:319 +#: assets/templates/assets/cleanlitterbin_detail.html:409 +msgid "Supprimer cette observation" +msgstr "Delete this observation" + +#: assets/templates/assets/components/add_asset_modal.html:17 +msgid "Ajouter un nouvel équipement" +msgstr "Add a new asset" + +#: assets/templates/assets/components/add_asset_modal.html:37 +#: assets/templates/assets/unified_index.html:293 +msgid "Sélectionner une catégorie..." +msgstr "Select a category..." + +#: assets/templates/assets/components/add_asset_modal.html:47 +#: assets/templates/assets/trafficlights_intersection_detail.html:977 +#, fuzzy +#| msgid "Type d'élément" +msgid "Type d'équipement" +msgstr "Item type" + +#: assets/templates/assets/components/add_asset_modal.html:50 +#: assets/templates/assets/trafficlights_intersection_detail.html:980 +#, fuzzy +#| msgid "Sélectionner un type" +msgid "Sélectionner un type..." +msgstr "Select a type" + +#: assets/templates/assets/components/add_asset_modal.html:60 +#: assets/templates/assets/trafficlights_intersection_detail.html:987 +#: assets/templates/assets/unified_index.html:299 +msgid "Sélectionner d'abord une catégorie..." +msgstr "Select a category first..." + +#: assets/templates/assets/components/add_asset_modal.html:63 +msgid "Le modèle détermine les caractéristiques de l'équipement." +msgstr "The model determines the characteristics of the asset." + +#: assets/templates/assets/components/add_asset_modal.html:73 +msgid "Ex: STR-001" +msgstr "Ex: STR-001" + +#: assets/templates/assets/components/add_asset_modal.html:75 +msgid "Code unique pour identifier l'équipement." +msgstr "Unique code to identify the asset." + +#: assets/templates/assets/components/add_asset_modal.html:85 +msgid "Nom en français (optionnel)" +msgstr "Name in French (optional)" + +#: assets/templates/assets/components/add_asset_modal.html:94 +msgid "Naam in het Nederlands (optioneel)" +msgstr "Name in Dutch (optional)" + +#: assets/templates/assets/components/add_asset_modal.html:104 +msgid "Aucune localisation (optionnel)" +msgstr "No location (optional)" + +#: assets/templates/assets/components/add_asset_modal.html:110 +msgid "Associez l'équipement à une localisation spécifique." +msgstr "Associate the asset with a specific location." + +#: assets/templates/assets/components/add_asset_modal.html:119 +#: assets/templates/assets/structures_index.html:74 +msgid "Ouvrage" +msgstr "Structure" + +#: assets/templates/assets/components/add_asset_modal.html:122 +msgid "Aucun ouvrage (optionnel)" +msgstr "No structure (optional)" + +#: assets/templates/assets/components/add_asset_modal.html:132 +msgid "L'équipement sera créé avec le statut 'À approuver'." +msgstr "The asset will be created with the status 'To be approved'." + +#: assets/templates/assets/components/add_asset_modal.html:141 +msgid "Créer l'équipement" +msgstr "Create asset" + +#: assets/templates/assets/components/asset_children_tree.html:25 +#: assets/templates/assets/detail_generic.html:790 +#: assets/templates/assets/detail_generic.html:800 +#: assets/templates/assets/detail_generic.html:1388 +msgid "Lier un asset existant" +msgstr "Link an existing asset" + +#: assets/templates/assets/components/asset_children_tree.html:36 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Changer le parent" +msgstr "Change model" + +#: assets/templates/assets/components/asset_children_tree.html:82 +#: assets/templates/assets/detail_generic.html:1355 +msgid "Délier cet asset" +msgstr "Unlink this asset" + +#: assets/templates/assets/components/asset_documents.html:6 +msgid "Documents associés" +msgstr "Associated documents" + +#: assets/templates/assets/components/asset_documents.html:34 +#: assets/templates/assets/controlcenters_location_detail.html:111 +#: assets/templates/assets/its_location_detail.html:54 +#: assets/templates/assets/naturerwiz_detail.html:272 +#: assets/templates/assets/publiclighting_street_detail.html:160 +#: assets/templates/assets/roads_street_detail.html:159 +#: assets/templates/assets/sign_street_detail.html:159 +#: assets/templates/assets/structures_location_detail.html:189 +#: assets/templates/assets/trafficlights_intersection_detail.html:247 +msgid "Ajouter des documents" +msgstr "Add documents" + +#: assets/templates/assets/components/asset_documents.html:48 +msgid "Sélectionnez un ou plusieurs fichiers pour continuer." +msgstr "Select one or more files to continue." + +#: assets/templates/assets/components/asset_documents.html:50 +msgid "Choisir des fichiers" +msgstr "Choose files" + +#: assets/templates/assets/components/asset_documents.html:59 +msgid "Changer les fichiers" +msgstr "Change files" + +#: assets/templates/assets/components/asset_documents.html:66 +msgid "Tag" +msgstr "Tag" + +#: assets/templates/assets/components/asset_documents.html:78 +msgid "Aucun tag disponible pour cette thématique." +msgstr "No tags available for this topic." + +#: assets/templates/assets/components/asset_documents.html:89 +msgid "Uploader" +msgstr "Upload" + +#: assets/templates/assets/components/asset_history.html:8 +msgid "Historique des modifications" +msgstr "Modification history" + +#: assets/templates/assets/components/asset_history.html:12 +msgid "Tableau" +msgstr "Table" + +#: assets/templates/assets/components/asset_history.html:17 +msgid "Ligne du temps" +msgstr "Timeline" + +#: assets/templates/assets/components/asset_history.html:28 +#: assets/templates/assets/detail_generic.html:785 +#: assets/templates/assets/trafficlights_import_dxf.html:262 +#: assets/templates/assets/trafficlights_import_dxf.html:1042 +#: assets/templates/assets/unified_index.html:305 +msgid "Chargement..." +msgstr "Loading..." + +#: assets/templates/assets/components/asset_history.html:37 +msgid "Date" +msgstr "Date" + +#: assets/templates/assets/components/asset_history.html:39 +msgid "Champ" +msgstr "Field" + +#: assets/templates/assets/components/asset_history.html:50 +#: assets/templates/assets/components/asset_history.html:67 +msgid "Aucune modification enregistrée pour cet asset." +msgstr "No modifications recorded for this asset." + +#: assets/templates/assets/components/asset_history.html:211 +msgid "Authentification requise" +msgstr "Authentication required" + +#: assets/templates/assets/components/asset_history.html:213 +msgid "Vous n'avez pas accès à cet historique" +msgstr "You do not have access to this history" + +#: assets/templates/assets/components/asset_history.html:214 +#: assets/templates/assets/components/asset_history.html:237 +#: assets/templates/assets/components/asset_history.html:242 +msgid "Erreur lors du chargement de l'historique" +msgstr "Error loading history" + +#: assets/templates/assets/components/asset_history.html:331 +msgid "Asset créé" +msgstr "Asset created" + +#: assets/templates/assets/components/asset_location_link.html:8 +msgid "Rue associée" +msgstr "Associated street" + +#: assets/templates/assets/components/asset_location_link.html:10 +msgid "Carrefour associé" +msgstr "Associated intersection" + +#: assets/templates/assets/components/asset_location_link.html:12 +msgid "Localisation associée" +msgstr "Associated location" + +#: assets/templates/assets/components/asset_location_link.html:17 +#: assets/templates/assets/components/asset_model_card.html:9 +msgid "Changer" +msgstr "Change" + +#: assets/templates/assets/components/asset_location_link.html:26 +msgid "Voir le détail de la localisation" +msgstr "View location details" + +#: assets/templates/assets/components/asset_location_link.html:35 +msgid "Aucune localisation associée" +msgstr "No associated location" + +#: assets/templates/assets/components/asset_location_link.html:48 +msgid "Changer la localisation" +msgstr "Change location" + +#: assets/templates/assets/components/asset_location_link.html:55 +msgid "Localisation actuelle :" +msgstr "Current location:" + +#: assets/templates/assets/components/asset_location_link.html:59 +msgid "Recherche" +msgstr "Search" + +#: assets/templates/assets/components/asset_location_link.html:62 +msgid "Code ou nom de localisation…" +msgstr "Location code or name..." + +#: assets/templates/assets/components/asset_location_link.html:67 +msgid "Nouvelle localisation" +msgstr "New location" + +#: assets/templates/assets/components/asset_location_link.html:70 +msgid "Sélectionner une localisation..." +msgstr "Select a location..." + +#: assets/templates/assets/components/asset_location_link.html:77 +#: assets/templates/assets/components/asset_model_card.html:101 +msgid "Appliquer" +msgstr "Apply" + +#: assets/templates/assets/components/asset_model_card.html:38 +msgid "Description" +msgstr "Description" + +#: assets/templates/assets/components/asset_model_card.html:60 +msgid "Changer le modèle" +msgstr "Change model" + +#: assets/templates/assets/components/asset_model_card.html:69 +msgid "Modèle actuel :" +msgstr "Current model:" + +#: assets/templates/assets/components/asset_model_card.html:77 +msgid "Toutes les catégories" +msgstr "All categories" + +#: assets/templates/assets/components/asset_model_card.html:86 +msgid "Nouveau modèle" +msgstr "New model" + +#: assets/templates/assets/components/asset_model_card.html:89 +msgid "Sélectionner un modèle..." +msgstr "Select a model..." + +#: assets/templates/assets/components/location_edit_modals.html:22 +#, python-format +msgid "Modifier %(label)s" +msgstr "Modify %(label)s" + +#: assets/templates/assets/components/location_edit_modals.html:40 +#: assets/templates/assets/permissions/access_edit.html:117 +#: assets/templates/assets/permissions/panel.html:278 +#: assets/templates/assets/permissions/type_access_edit.html:122 +#: assets/templates/assets/permissions/type_page.html:215 +#: assets/templates/assets/permissions/type_page.html:419 +#: assets/templates/assets/unified_index.html:632 +#: assets/templates/assets/unified_index.html:662 +msgid "Enregistrer" +msgstr "Save" + +#: assets/templates/assets/components/location_edit_modals.html:54 +#: assets/templates/assets/controlcenters_location_detail.html:144 +#: assets/templates/assets/its_location_detail.html:87 +#: assets/templates/assets/publiclighting_street_detail.html:193 +#: assets/templates/assets/roads_street_detail.html:192 +#: assets/templates/assets/sign_street_detail.html:192 +#: assets/templates/assets/structures_location_detail.html:231 +#: assets/templates/assets/trafficlights_intersection_detail.html:280 +msgid "Confirmer la suppression" +msgstr "Confirm deletion" + +#: assets/templates/assets/components/location_edit_modals.html:59 +#, python-format +msgid "Êtes-vous sûr de vouloir supprimer cette localisation (%(label)s) ?" +msgstr "Are you sure you want to delete this location (%(label)s)?" + +#: assets/templates/assets/components/location_edit_modals.html:60 +msgid "" +"Cette action est irréversible. Tous les assets associés seront également " +"affectés." +msgstr "" + +#: assets/templates/assets/components/location_edit_modals.html:65 +#: assets/templates/assets/controlcenters_location_detail.html:95 +#: assets/templates/assets/controlcenters_location_detail.html:157 +#: assets/templates/assets/document_version_history_list.html:59 +#: assets/templates/assets/its_location_detail.html:100 +#: assets/templates/assets/permissions/panel.html:70 +#: assets/templates/assets/permissions/panel.html:112 +#: assets/templates/assets/permissions/panel.html:215 +#: assets/templates/assets/permissions/type_page.html:72 +#: assets/templates/assets/permissions/type_page.html:161 +#: assets/templates/assets/permissions/type_page.html:239 +#: assets/templates/assets/permissions/type_page.html:364 +#: assets/templates/assets/publiclighting_street_detail.html:88 +#: assets/templates/assets/publiclighting_street_detail.html:143 +#: assets/templates/assets/publiclighting_street_detail.html:206 +#: assets/templates/assets/roads_street_detail.html:92 +#: assets/templates/assets/roads_street_detail.html:142 +#: assets/templates/assets/roads_street_detail.html:205 +#: assets/templates/assets/sign_street_detail.html:87 +#: assets/templates/assets/sign_street_detail.html:142 +#: assets/templates/assets/sign_street_detail.html:205 +#: assets/templates/assets/structures_location_detail.html:244 +#: assets/templates/assets/trafficlights_import_dxf.html:635 +#: assets/templates/assets/trafficlights_intersection_detail.html:293 +msgid "Supprimer" +msgstr "Delete" + +# , python-format +#: assets/templates/assets/components/location_edit_modals.html:80 +#, python-format +msgid "Créer une nouvelle localisation (%(label)s)" +msgstr "Create a new location (%(label)s)" + +#: assets/templates/assets/components/location_edit_modals.html:98 +#: assets/templates/assets/trafficlights_intersection_detail.html:994 +msgid "Créer" +msgstr "Create" + +#: assets/templates/assets/components/location_edit_modals.html:179 +msgid "Cliquez pour fermer le polygone" +msgstr "Click to close the polygon" + +#: assets/templates/assets/components/location_edit_modals.html:207 +msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." +msgstr "Polygon saved. Click \"Draw\" to redraw." + +#: assets/templates/assets/components/location_edit_modals.html:286 +msgid "Sommets" +msgstr "Vertices" + +#: assets/templates/assets/components/location_edit_modals.html:286 +msgid "Cliquez sur le premier sommet ou « Terminer » pour clore." +msgstr "Click on the first vertex or \"Finish\" to close." + +#: assets/templates/assets/components/location_edit_modals.html:301 +msgid "Cliquez sur la carte pour ajouter des sommets." +msgstr "Click on the map to add vertices." + +#: assets/templates/assets/components/location_edit_modals.html:321 +msgid "Polygone effacé. Cliquez sur « Dessiner » pour en tracer un nouveau." +msgstr "Polygon cleared. Click \"Draw\" to draw a new one." + +#: assets/templates/assets/components/location_edit_modals.html:436 +#: assets/templates/assets/components/location_edit_modals.html:515 +#: assets/templates/assets/detail_generic.html:1189 +msgid "Erreur lors du chargement du formulaire." +msgstr "Error while loading the form." + +#: assets/templates/assets/components/location_edit_modals.html:460 +msgid "Erreur lors de la sauvegarde." +msgstr "Error while saving." + +#: assets/templates/assets/components/location_edit_modals.html:463 +#: assets/templates/assets/components/location_edit_modals.html:544 +msgid "Erreur réseau." +msgstr "Network error." + +#: assets/templates/assets/components/location_edit_modals.html:541 +msgid "Erreur lors de la création." +msgstr "Error during creation." + +#: assets/templates/assets/components/map_location_picker.html:23 +msgid "Position géographique" +msgstr "Geographical position" + +#: assets/templates/assets/components/map_location_picker.html:24 +#: assets/templates/assets/components/map_polygon_picker.html:26 +msgid "facultatif" +msgstr "optional" + +#: assets/templates/assets/components/map_location_picker.html:29 +msgid "Utiliser ma position" +msgstr "Use my position" + +#: assets/templates/assets/components/map_location_picker.html:34 +msgid "Effacer le marqueur" +msgstr "Clear marker" + +#: assets/templates/assets/components/map_location_picker.html:45 +msgid "" +"Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour " +"l'ajuster." +msgstr "" + +#: assets/templates/assets/components/map_polygon_picker.html:25 +msgid "Géométrie (polygone)" +msgstr "Geometry (polygon)" + +#: assets/templates/assets/components/map_polygon_picker.html:31 +msgid "Dessiner un polygone" +msgstr "Draw a polygon" + +#: assets/templates/assets/components/map_polygon_picker.html:32 +msgid "Dessiner" +msgstr "Draw" + +#: assets/templates/assets/components/map_polygon_picker.html:36 +msgid "Terminer le polygone" +msgstr "Finish polygon" + +#: assets/templates/assets/components/map_polygon_picker.html:37 +msgid "Terminer" +msgstr "Finish" + +#: assets/templates/assets/components/map_polygon_picker.html:41 +msgid "Effacer le polygone" +msgstr "Clear polygon" + +#: assets/templates/assets/components/map_polygon_picker.html:51 +msgid "" +"Cliquez sur « Dessiner » pour commencer à tracer un polygone sur la carte." +msgstr "" + +#: assets/templates/assets/controlcenters_index.html:8 +#: assets/templates/assets/its_index.html:8 +#: assets/templates/assets/structures_index.html:18 +msgid "Type de Localisation" +msgstr "Location type" + +#: assets/templates/assets/controlcenters_index.html:10 +#: assets/templates/assets/its_index.html:10 +#: assets/templates/assets/structures_index.html:20 +#: assets/templates/assets/trafficlights_import_dxf.html:148 +#: assets/templates/assets/trafficlights_import_dxf.html:255 +#: assets/templates/assets/unified_index.html:117 +msgid "Toutes" +msgstr "All" + +#: assets/templates/assets/controlcenters_index.html:11 +#: assets/templates/assets/its_index.html:11 +#: assets/templates/assets/structures_index.html:21 +msgid "Principale" +msgstr "Main" + +#: assets/templates/assets/controlcenters_index.html:12 +#: assets/templates/assets/its_index.html:12 +#: assets/templates/assets/structures_index.html:22 +msgid "Secondaire" +msgstr "Secondary" + +#: assets/templates/assets/controlcenters_index.html:61 +msgid "Centre de contrôle" +msgstr "Control center" + +#: assets/templates/assets/controlcenters_location_detail.html:15 +msgid "Centre de contrôle:" +msgstr "Control center:" + +#: assets/templates/assets/controlcenters_location_detail.html:18 +#: assets/templates/assets/its_location_detail.html:19 +msgid "Localisation principale:" +msgstr "Main location:" + +#: assets/templates/assets/controlcenters_location_detail.html:26 +msgid "Parent:" +msgstr "Parent:" + +#: assets/templates/assets/controlcenters_location_detail.html:45 +#: assets/templates/assets/its_location_detail.html:40 +#: assets/templates/assets/nature_locations_detail.html:37 +#: assets/templates/assets/naturerwiz_detail.html:80 +#: assets/templates/assets/publiclighting_street_detail.html:28 +#: assets/templates/assets/roads_street_detail.html:37 +#: assets/templates/assets/sign_street_detail.html:28 +#: assets/templates/assets/structures_location_detail.html:46 +#: assets/templates/assets/trafficlights_intersection_detail.html:156 +msgid "Documents" +msgstr "Documents" + +#: assets/templates/assets/controlcenters_location_detail.html:86 +#: assets/templates/assets/detail_generic.html:132 +#: assets/templates/assets/detail_generic.html:133 +#: assets/templates/assets/detail_generic.html:176 +#: assets/templates/assets/detail_generic.html:177 +#: assets/templates/assets/publiclighting_street_detail.html:78 +#: assets/templates/assets/publiclighting_street_detail.html:133 +#: assets/templates/assets/sign_street_detail.html:78 +#: assets/templates/assets/sign_street_detail.html:132 +#: assets/templates/assets/trafficlights_import_cross_plan.html:211 +#: assets/templates/assets/trafficlights_import_cross_plan.html:293 +#: assets/templates/assets/trafficlights_import_cross_plan.html:375 +#: assets/templates/assets/trafficlights_import_cross_plan.html:463 +msgid "Archiver" +msgstr "Archive" + +#: assets/templates/assets/controlcenters_location_detail.html:119 +#: assets/templates/assets/its_location_detail.html:62 +#: assets/templates/assets/naturerwiz_detail.html:280 +#: assets/templates/assets/publiclighting_street_detail.html:168 +#: assets/templates/assets/roads_street_detail.html:167 +#: assets/templates/assets/sign_street_detail.html:167 +#: assets/templates/assets/structures_location_detail.html:197 +#: assets/templates/assets/trafficlights_intersection_detail.html:255 +msgid "Sélectionner des fichiers" +msgstr "Select files" + +#: assets/templates/assets/controlcenters_location_detail.html:149 +#: assets/templates/assets/its_location_detail.html:92 +#: assets/templates/assets/publiclighting_street_detail.html:198 +#: assets/templates/assets/roads_street_detail.html:197 +#: assets/templates/assets/sign_street_detail.html:197 +#: assets/templates/assets/structures_location_detail.html:236 +#: assets/templates/assets/trafficlights_intersection_detail.html:285 +msgid "Voulez-vous vraiment supprimer ce document ?" +msgstr "Are you sure you want to delete this document?" + +#: assets/templates/assets/controlcenters_location_detail.html:150 +#: assets/templates/assets/its_location_detail.html:93 +#: assets/templates/assets/publiclighting_street_detail.html:199 +#: assets/templates/assets/roads_street_detail.html:198 +#: assets/templates/assets/sign_street_detail.html:198 +#: assets/templates/assets/structures_location_detail.html:237 +#: assets/templates/assets/trafficlights_intersection_detail.html:286 +msgid "Nom du fichier" +msgstr "File name" + +#: assets/templates/assets/controlcenters_location_detail.html:169 +#: assets/templates/assets/naturerwiz_detail.html:305 +#: assets/templates/assets/publiclighting_street_detail.html:218 +#: assets/templates/assets/roads_street_detail.html:217 +#: assets/templates/assets/sign_street_detail.html:217 +#: assets/templates/assets/structures_location_detail.html:256 +#: assets/templates/assets/trafficlights_intersection_detail.html:305 +msgid "Historique des documents" +msgstr "Document history" + +#: assets/templates/assets/controlcenters_location_detail.html:195 +#: assets/templates/assets/its_location_detail.html:119 +#: assets/templates/assets/naturerwiz_detail.html:195 +#: assets/templates/assets/publiclighting_street_detail.html:238 +#: assets/templates/assets/roads_street_detail.html:237 +#: assets/templates/assets/sign_street_detail.html:237 +#: assets/templates/assets/structures_location_detail.html:287 +msgid "Arborescence" +msgstr "Tree structure" + +#: assets/templates/assets/controlcenters_location_detail.html:202 +#: assets/templates/assets/its_location_detail.html:126 +#: assets/templates/assets/publiclighting_street_detail.html:241 +#: assets/templates/assets/roads_street_detail.html:240 +#: assets/templates/assets/sign_street_detail.html:240 +#: assets/templates/assets/structures_location_detail.html:294 +msgid "Archives" +msgstr "Archives" + +#: assets/templates/assets/controlcenters_location_detail.html:212 +#: assets/templates/assets/controlcenters_location_tree.html:21 +#: assets/templates/assets/its_location_detail.html:136 +#: assets/templates/assets/its_location_tree.html:21 +#: assets/templates/assets/structures_location_tree.html:21 +msgid "sous-localisations" +msgstr "sub-locations" + +#: assets/templates/assets/controlcenters_location_detail.html:215 +#: assets/templates/assets/its_location_detail.html:139 +#: assets/templates/assets/its_location_tree.html:24 +#: assets/templates/assets/naturerwiz_detail.html:206 +#: assets/templates/assets/naturerwiz_location_tree.html:35 +msgid "assets" +msgstr "assets" + +#: assets/templates/assets/controlcenters_location_detail.html:243 +#: assets/templates/assets/its_location_detail.html:167 +#: assets/templates/assets/nature_locations_detail.html:73 +#: assets/templates/assets/naturerwiz_detail.html:242 +#: assets/templates/assets/publiclighting_street_detail.html:302 +#: assets/templates/assets/roads_street_detail.html:317 +#: assets/templates/assets/sign_street_detail.html:294 +#: assets/templates/assets/structures_location_detail.html:352 +#: assets/templates/assets/trafficlights_intersection_detail.html:922 +msgid "Titre" +msgstr "Title" + +#: assets/templates/assets/controlcenters_location_detail.html:244 +#: assets/templates/assets/its_location_detail.html:168 +#: assets/templates/assets/nature_locations_detail.html:74 +#: assets/templates/assets/naturerwiz_detail.html:243 +#: assets/templates/assets/publiclighting_street_detail.html:303 +#: assets/templates/assets/roads_street_detail.html:318 +#: assets/templates/assets/sign_street_detail.html:295 +#: assets/templates/assets/structures_location_detail.html:353 +#: assets/templates/assets/trafficlights_intersection_detail.html:923 +msgid "Début" +msgstr "Start" + +#: assets/templates/assets/detail_generic.html:73 +#: assets/templates/assets/detail_generic.html:814 +#: assets/templates/assets/detail_generic.html:831 +msgid "Retour" +msgstr "Back" + +#: assets/templates/assets/detail_generic.html:85 +msgid "Détails" +msgstr "Details" + +#: assets/templates/assets/detail_generic.html:87 +msgid "StreetUp" +msgstr "StreetUp" + +#: assets/templates/assets/detail_generic.html:116 +#: assets/templates/assets/detail_generic.html:158 +#: assets/templates/assets/unified_index.html:250 +msgid "Asset" +msgstr "Asset" + +#: assets/templates/assets/detail_generic.html:142 +#: assets/templates/assets/detail_generic.html:186 +msgid "Éditer" +msgstr "Edit" + +#: assets/templates/assets/detail_generic.html:145 +#: assets/templates/assets/detail_generic.html:189 +msgid "Sauvegarder" +msgstr "Save" + +#: assets/templates/assets/detail_generic.html:202 +msgid "Informations générales" +msgstr "General information" + +#: assets/templates/assets/detail_generic.html:275 +#: assets/templates/assets/unified_index.html:212 +msgid "Statut de validation" +msgstr "Validation status" + +#: assets/templates/assets/detail_generic.html:324 +#: assets/templates/assets/detail_generic.html:578 +msgid "Installé le" +msgstr "Installed on" + +#: assets/templates/assets/detail_generic.html:337 +msgid "N° de série" +msgstr "Serial number" + +#: assets/templates/assets/detail_generic.html:368 +msgid "mois" +msgstr "months" + +#: assets/templates/assets/detail_generic.html:373 +#, python-format +msgid "%(n)s mois restants" +msgstr "%(n)s months remaining" + +#: assets/templates/assets/detail_generic.html:375 +msgid "Expire ce mois-ci" +msgstr "Expires this month" + +#: assets/templates/assets/detail_generic.html:377 +#, python-format +msgid "Expirée (%(n)s mois)" +msgstr "Expired (%(n)s months)" + +#: assets/templates/assets/detail_generic.html:415 +msgid "Attributs spécifiques" +msgstr "Specific attributes" + +#: assets/templates/assets/detail_generic.html:430 +#: assets/templates/assets/trafficlights_index.html:35 +#: assets/templatetags/asset_tags.py:122 assets/views/core.py:562 +#: assets/views/core.py:588 assets/views/trafficlights.py:183 +msgid "Oui" +msgstr "Yes" + +#: assets/templates/assets/detail_generic.html:431 +#: assets/templates/assets/trafficlights_index.html:39 +#: assets/templatetags/asset_tags.py:122 assets/views/core.py:563 +#: assets/views/core.py:589 assets/views/trafficlights.py:183 +msgid "Non" +msgstr "No" + +#: assets/templates/assets/detail_generic.html:472 +msgid "Permissions spécifiques" +msgstr "Specific permissions" + +#: assets/templates/assets/detail_generic.html:477 +msgid "Voir tous les accès pour ce type" +msgstr "View all access for this type" + +#: assets/templates/assets/detail_generic.html:479 +msgid "Vue par type" +msgstr "View by type" + +#: assets/templates/assets/detail_generic.html:490 +msgid "Chargement des permissions…" +msgstr "Loading permissions..." + +#: assets/templates/assets/detail_generic.html:508 +msgid "Erreur lors du chargement." +msgstr "Error while loading." + +#: assets/templates/assets/detail_generic.html:520 +msgid "Assets liés" +msgstr "Linked assets" + +#: assets/templates/assets/detail_generic.html:526 +msgid "Lier ou créer un asset" +msgstr "Link or create an asset" + +#: assets/templates/assets/detail_generic.html:527 +msgid "Lier / Créer" +msgstr "Link / Create" + +#: assets/templates/assets/detail_generic.html:533 +msgid "Aucun asset lié." +msgstr "No linked asset." + +#: assets/templates/assets/detail_generic.html:545 +msgid "Historique des remplacements" +msgstr "Replacement history" + +#: assets/templates/assets/detail_generic.html:579 +msgid "Désinstallé le" +msgstr "Uninstalled on" + +#: assets/templates/assets/detail_generic.html:580 +msgid "Durée" +msgstr "Duration" + +#: assets/templates/assets/detail_generic.html:616 +msgid "En service" +msgstr "In service" + +#: assets/templates/assets/detail_generic.html:642 +msgid "Géolocalisation" +msgstr "Geolocation" + +#: assets/templates/assets/detail_generic.html:649 +msgid "Voir sur Street View" +msgstr "View in Street View" + +#: assets/templates/assets/detail_generic.html:655 +msgid "Voir sur Mapillary" +msgstr "View in Mapillary" + +#: assets/templates/assets/detail_generic.html:726 +msgid "Remplacer l'asset" +msgstr "Replace the asset" + +#: assets/templates/assets/detail_generic.html:731 +msgid "L'asset actuel sera archivé et un nouvel asset sera créé." +msgstr "The current asset will be archived and a new asset will be created." + +#: assets/templates/assets/detail_generic.html:732 +msgid "Les noms (FR/NL) de l'ancien asset seront automatiquement copiés." +msgstr "The names (FR/NL) of the old asset will be automatically copied." + +#: assets/templates/assets/detail_generic.html:736 +msgid "Modèle du nouvel asset" +msgstr "Model of the new asset" + +#: assets/templates/assets/detail_generic.html:738 +msgid "Garder le même modèle" +msgstr "Keep the same model" + +#: assets/templates/assets/detail_generic.html:747 +#: assets/templates/assets/detail_generic.html:752 +msgid "optionnel" +msgstr "optional" + +#: assets/templates/assets/detail_generic.html:775 +msgid "Lier / Créer un asset" +msgstr "Link / Create an asset" + +#: assets/templates/assets/detail_generic.html:783 +msgid "Type d'asset à lier" +msgstr "Type of asset to link" + +#: assets/templates/assets/detail_generic.html:793 +#: assets/templates/assets/detail_generic.html:824 +msgid "Créer un nouvel asset" +msgstr "Create a new asset" + +#: assets/templates/assets/detail_generic.html:802 +#: assets/templates/assets/detail_generic.html:1078 +msgid "Rechercher l'asset à lier" +msgstr "Search for asset to link" + +#: assets/templates/assets/detail_generic.html:804 +msgid "Code ou nom..." +msgstr "Code or name..." + +#: assets/templates/assets/detail_generic.html:817 +msgid "Lier" +msgstr "Link" + +#: assets/templates/assets/detail_generic.html:825 +msgid "Un nouvel asset sera créé et lié automatiquement à cet asset parent." +msgstr "" +"A new asset will be created and automatically linked to this parent asset." + +#: assets/templates/assets/detail_generic.html:834 +msgid "Créer et lier" +msgstr "Create and link" + +#: assets/templates/assets/detail_generic.html:940 +#, fuzzy +#| msgid "Assets uniquement" +msgid "Assets enfants" +msgstr "Assets only" + +#: assets/templates/assets/detail_generic.html:957 +#, fuzzy +#| msgid "Assets uniquement" +msgid "Assets parents" +msgstr "Assets only" + +#: assets/templates/assets/detail_generic.html:1046 +#: assets/templates/assets/detail_generic.html:1077 +#, fuzzy +#| msgid "Rechercher l'asset à lier" +msgid "Rechercher le parent à lier" +msgstr "Search for asset to link" + +#: assets/templates/assets/detail_generic.html:1178 +msgid "Chargement du formulaire…" +msgstr "Loading form..." + +#: assets/templates/assets/detail_generic.html:1196 +msgid "Aucun champ supplémentaire requis." +msgstr "No additional fields required." + +#: assets/templates/assets/detail_generic.html:1221 +msgid "— Sélectionner —" +msgstr "— Select —" + +#: assets/templates/assets/detail_generic.html:1221 +msgid "— Aucun —" +msgstr "— None —" + +#: assets/templates/assets/detail_generic.html:1497 +msgid "Champs obligatoires manquants" +msgstr "Missing required fields" + +#: assets/templates/assets/detail_generic.html:1580 +msgid "Délier cet asset de son parent ?" +msgstr "Unlink this asset from its parent?" + +#: assets/templates/assets/document_history_list.html:19 +#: assets/templates/assets/document_version_history_list.html:27 +msgid "Ajouté le" +msgstr "Added on" + +#: assets/templates/assets/document_history_list.html:21 +#: assets/templates/assets/document_version_history_list.html:29 +msgid "par" +msgstr "by" + +#: assets/templates/assets/document_history_list.html:42 +msgid "Aucun document trouvé." +msgstr "No document found." + +#: assets/templates/assets/document_version_history_list.html:37 +msgid "Approuvé" +msgstr "Approved" + +#: assets/templates/assets/document_version_history_list.html:39 +msgid "En attente" +msgstr "Pending" + +#: assets/templates/assets/document_version_history_list.html:43 +msgid "Brouillon" +msgstr "Draft" + +#: assets/templates/assets/document_version_history_list.html:50 +msgid "Télécharger" +msgstr "Download" + +#: assets/templates/assets/document_version_history_list.html:66 +msgid "Aucune version trouvée." +msgstr "No version found." + +#: assets/templates/assets/its_index.html:27 +msgid "Créer une nouvelle localisation ITS" +msgstr "Create new ITS location" + +#: assets/templates/assets/its_index.html:66 +msgid "Créer un nouvel asset ITS" +msgstr "Create new ITS asset" + +#: assets/templates/assets/its_location_detail.html:22 +msgid "Localisation parente:" +msgstr "Parent location:" + +#: assets/templates/assets/nature_index.html:8 +#: assets/templates/assets/water_index.html:7 assets/views/core.py:607 +#: assets/views/core.py:627 +msgid "Type de localisation" +msgstr "Location type" + +#: assets/templates/assets/nature_index.html:13 +#: assets/templates/assets/permissions/panel.html:147 +#: assets/templates/assets/permissions/panel.html:228 +#: assets/templates/assets/permissions/panel.html:254 +#: assets/templates/assets/permissions/type_page.html:98 +#: assets/templates/assets/permissions/type_page.html:171 +#: assets/templates/assets/permissions/type_page.html:194 +#: assets/templates/assets/permissions/type_page.html:282 +#: assets/templates/assets/permissions/type_page.html:372 +#: assets/templates/assets/permissions/type_page.html:395 +#: assets/templates/assets/trafficlights_index.html:43 +#: assets/templates/assets/trafficlights_index.html:51 +#: assets/templates/assets/unified_index.html:93 +#: assets/templates/assets/unified_index.html:131 +#: assets/templates/assets/water_index.html:12 assets/views/core.py:610 +#: assets/views/core.py:630 +msgid "Tous" +msgstr "All" + +#: assets/templates/assets/nature_index.html:18 assets/views/core.py:611 +#: assets/views/nature.py:104 assets/views/publiclighting.py:93 +#: assets/views/roads.py:99 assets/views/sign.py:105 +msgid "Voiries" +msgstr "Roads" + +#: assets/templates/assets/nature_index.html:24 assets/views/core.py:616 +msgid "Ring et autoroutes" +msgstr "Ring and highways" + +#: assets/templates/assets/nature_index.html:77 +msgid "Ring/Autoroute" +msgstr "Ring/Highway" + +#: assets/templates/assets/nature_index.html:84 +#: assets/templates/assets/water_index.html:81 +msgid "Superficie" +msgstr "Area" + +#: assets/templates/assets/nature_locations_detail.html:14 +msgid "Zone:" +msgstr "Zone:" + +#: assets/templates/assets/nature_locations_detail.html:15 +msgid "Autoroute:" +msgstr "Highway:" + +#: assets/templates/assets/nature_locations_detail.html:28 +msgid "Nombre d'arbres:" +msgstr "Number of trees:" + +#: assets/templates/assets/nature_locations_detail.html:29 +msgid "Superficie massif arboré:" +msgstr "Wooded area:" + +#: assets/templates/assets/nature_locations_detail.html:30 +msgid "Superficie pré fleuri:" +msgstr "Flower meadow area:" + +#: assets/templates/assets/naturerwiz_detail.html:116 +msgid "Documents de la zone" +msgstr "Zone documents" + +#: assets/templates/assets/naturerwiz_detail.html:124 +msgid "Documents des assets" +msgstr "Asset documents" + +#: assets/templates/assets/naturerwiz_detail.html:135 +#: assets/templates/assets/structures_location_detail.html:54 +#: assets/templates/assets/trafficlights_intersection_detail.html:165 +msgid "Répertoire principal" +msgstr "Main directory" + +#: assets/templates/assets/naturerwiz_detail.html:143 +#, fuzzy +#| msgid "Aucun document trouvé." +msgid "Aucun document de zone configuré." +msgstr "No document found." + +#: assets/templates/assets/naturerwiz_detail.html:169 +#, fuzzy +#| msgid "Aucun contrat associé." +msgid "Aucun document d'asset associé." +msgstr "No associated contract." + +#: assets/templates/assets/naturerwiz_detail.html:211 +#: assets/templates/assets/structures_location_detail.html:321 +msgid "Masquer les non-actifs" +msgstr "Hide inactive" + +#: assets/templates/assets/naturerwiz_detail.html:213 +#: assets/templates/assets/structures_location_detail.html:323 +msgid "Afficher les non-actifs" +msgstr "Show inactive" + +#: assets/templates/assets/naturerwiz_detail.html:220 +#, fuzzy +#| msgid "Aucun asset lié." +msgid "Aucun asset associé." +msgstr "No linked asset." + +#: assets/templates/assets/permissions/access_edit.html:10 +#: assets/templates/assets/permissions/type_access_edit.html:10 +#: assets/templates/assets/permissions/type_page.html:10 +msgid "Accueil" +msgstr "Home" + +#: assets/templates/assets/permissions/access_edit.html:11 +msgid "Modifier un accès" +msgstr "Modify access" + +#: assets/templates/assets/permissions/access_edit.html:18 +msgid "Modifier l'accès" +msgstr "Modify access" + +#: assets/templates/assets/permissions/access_edit.html:29 +msgid "Type :" +msgstr "Type:" + +#: assets/templates/assets/permissions/access_edit.html:31 +msgid "Objet #" +msgstr "Object #" + +#: assets/templates/assets/permissions/access_edit.html:37 +#: assets/templates/assets/permissions/panel.html:204 +#: assets/templates/assets/permissions/type_access_edit.html:42 +#: assets/templates/assets/permissions/type_page.html:150 +#: assets/templates/assets/permissions/type_page.html:353 +msgid "Droits" +msgstr "Rights / Permissions" + +#: assets/templates/assets/permissions/access_edit.html:41 +#: assets/templates/assets/permissions/type_access_edit.html:46 +msgid "Peut visualiser" +msgstr "Can view" + +#: assets/templates/assets/permissions/access_edit.html:46 +#: assets/templates/assets/permissions/type_access_edit.html:51 +msgid "Peut modifier" +msgstr "Can modify" + +#: assets/templates/assets/permissions/access_edit.html:51 +#: assets/templates/assets/permissions/type_access_edit.html:56 +msgid "Peut supprimer" +msgstr "Can delete" + +#: assets/templates/assets/permissions/access_edit.html:61 +#: assets/templates/assets/permissions/panel.html:113 +#: assets/templates/assets/permissions/panel.html:224 +#: assets/templates/assets/permissions/type_access_edit.html:66 +#: assets/templates/assets/permissions/type_page.html:73 +#: assets/templates/assets/permissions/type_page.html:169 +#: assets/templates/assets/permissions/type_page.html:240 +#: assets/templates/assets/permissions/type_page.html:393 +msgid "Champs visibles" +msgstr "Visible fields" + +#: assets/templates/assets/permissions/access_edit.html:62 +#: assets/templates/assets/permissions/type_access_edit.html:67 +msgid "vide = tous les champs" +msgstr "empty = all fields" + +#: assets/templates/assets/permissions/access_edit.html:65 +#: assets/templates/assets/permissions/access_edit.html:93 +#: assets/templates/assets/permissions/type_access_edit.html:70 +#: assets/templates/assets/permissions/type_access_edit.html:98 +msgid "Tout cocher" +msgstr "Check all" + +#: assets/templates/assets/permissions/access_edit.html:67 +#: assets/templates/assets/permissions/access_edit.html:95 +#: assets/templates/assets/permissions/type_access_edit.html:72 +#: assets/templates/assets/permissions/type_access_edit.html:100 +msgid "Tout décocher" +msgstr "Uncheck all" + +#: assets/templates/assets/permissions/access_edit.html:80 +#: assets/templates/assets/permissions/access_edit.html:108 +#: assets/templates/assets/permissions/panel.html:242 +#: assets/templates/assets/permissions/panel.html:268 +#: assets/templates/assets/permissions/type_access_edit.html:85 +#: assets/templates/assets/permissions/type_access_edit.html:113 +#: assets/templates/assets/permissions/type_page.html:185 +#: assets/templates/assets/permissions/type_page.html:208 +#: assets/templates/assets/permissions/type_page.html:386 +#: assets/templates/assets/permissions/type_page.html:409 +msgid "Aucun champ disponible" +msgstr "No fields available" + +#: assets/templates/assets/permissions/access_edit.html:89 +#: assets/templates/assets/permissions/panel.html:114 +#: assets/templates/assets/permissions/panel.html:250 +#: assets/templates/assets/permissions/type_access_edit.html:94 +#: assets/templates/assets/permissions/type_page.html:74 +#: assets/templates/assets/permissions/type_page.html:192 +#: assets/templates/assets/permissions/type_page.html:241 +#: assets/templates/assets/permissions/type_page.html:370 +msgid "Champs modifiables" +msgstr "Editable fields" + +#: assets/templates/assets/permissions/access_edit.html:90 +#: assets/templates/assets/permissions/type_access_edit.html:95 +msgid "vide = droits par rôle" +msgstr "empty = rights by role" + +#: assets/templates/assets/permissions/panel.html:29 +msgid "Type d'objet :" +msgstr "Object type:" + +#: assets/templates/assets/permissions/panel.html:38 +msgid "Droits explicites requis (droits thématiques insuffisants)" +msgstr "Explicit rights required (insufficient thematic rights)" + +#: assets/templates/assets/permissions/panel.html:43 +msgid "Accès restreint" +msgstr "Restricted access" + +#: assets/templates/assets/permissions/panel.html:55 +msgid "Accès au niveau du type (tous les objets)" +msgstr "Access at type level (all objects)" + +#: assets/templates/assets/permissions/panel.html:58 +msgid "Gérer" +msgstr "Manage" + +#: assets/templates/assets/permissions/panel.html:62 +msgid "" +"Les utilisateurs ci-dessous ont un accès défini pour TOUS les objets de ce " +"type. Ces droits s'appliquent à cet objet sauf si une ligne d'accès " +"spécifique ci-dessous les écrase." +msgstr "" + +#: assets/templates/assets/permissions/panel.html:68 +#: assets/templates/assets/permissions/panel.html:110 +#: assets/templates/assets/permissions/panel.html:207 +#: assets/templates/assets/permissions/type_page.html:70 +#: assets/templates/assets/permissions/type_page.html:153 +#: assets/templates/assets/permissions/type_page.html:237 +#: assets/templates/assets/permissions/type_page.html:356 +msgid "Voir" +msgstr "View" + +#: assets/templates/assets/permissions/panel.html:69 +#: assets/templates/assets/permissions/panel.html:111 +#: assets/templates/assets/permissions/panel.html:211 +#: assets/templates/assets/permissions/type_page.html:71 +#: assets/templates/assets/permissions/type_page.html:157 +#: assets/templates/assets/permissions/type_page.html:238 +#: assets/templates/assets/permissions/type_page.html:360 +msgid "Modifier" +msgstr "Edit" + +#: assets/templates/assets/permissions/panel.html:71 +msgid "Champs" +msgstr "Fields" + +#: assets/templates/assets/permissions/panel.html:94 +#: assets/templates/assets/permissions/panel.html:154 +#: assets/templates/assets/permissions/type_page.html:105 +#: assets/templates/assets/permissions/type_page.html:289 +msgid "Par rôle" +msgstr "By role" + +#: assets/templates/assets/permissions/panel.html:165 +#: assets/templates/assets/permissions/type_page.html:300 +msgid "Supprimer cet accès ?" +msgstr "Delete this access?" + +#: assets/templates/assets/permissions/panel.html:177 +msgid "Aucun accès spécifique défini pour cet objet." +msgstr "No specific access defined for this object." + +#: assets/templates/assets/permissions/panel.html:183 +msgid "Ajouter / mettre à jour un accès" +msgstr "Add / update access" + +#: assets/templates/assets/permissions/panel.html:194 +#: assets/templates/assets/permissions/type_page.html:143 +#: assets/templates/assets/permissions/type_page.html:336 +msgid "choisir" +msgstr "choose" + +#: assets/templates/assets/permissions/panel.html:225 +#: assets/templates/assets/permissions/type_page.html:169 +#: assets/templates/assets/permissions/type_page.html:393 +msgid "vide = tous" +msgstr "empty = all" + +#: assets/templates/assets/permissions/panel.html:251 +#: assets/templates/assets/permissions/type_page.html:192 +#: assets/templates/assets/permissions/type_page.html:370 +msgid "vide = rôle" +msgstr "empty = role" + +#: assets/templates/assets/permissions/type_access_edit.html:13 +#: assets/templates/assets/permissions/type_page.html:12 +msgid "Permissions" +msgstr "Permissions" + +#: assets/templates/assets/permissions/type_access_edit.html:16 +#: assets/templates/assets/permissions/type_access_edit.html:23 +msgid "Modifier l'accès de type" +msgstr "Modify type-level access" + +#: assets/templates/assets/permissions/type_access_edit.html:34 +msgid "Type d'asset :" +msgstr "Asset type:" + +#: assets/templates/assets/permissions/type_access_edit.html:36 +msgid "s'applique à tous les objets de ce type" +msgstr "applies to all objects of this type" + +#: assets/templates/assets/permissions/type_page.html:26 +msgid "Configuration du type" +msgstr "Type configuration" + +#: assets/templates/assets/permissions/type_page.html:38 +msgid "Droits explicites requis" +msgstr "Explicit rights required" + +#: assets/templates/assets/permissions/type_page.html:42 +msgid "" +"Si activé, les droits thématiques seuls ne suffisent pas pour accéder aux " +"objets de ce type. Chaque utilisateur doit avoir une ligne d'accès explicite " +"ci-dessous." +msgstr "" + +#: assets/templates/assets/permissions/type_page.html:47 +msgid "" +"Accès restreint actif — les droits thématiques ne sont pas pris en compte " +"pour ce type." +msgstr "" + +#: assets/templates/assets/permissions/type_page.html:59 +msgid "Accès utilisateur (tous les objets de ce type)" +msgstr "User access (all objects of this type)" + +#: assets/templates/assets/permissions/type_page.html:116 +msgid "Supprimer cet accès de type ?" +msgstr "Delete this type access?" + +#: assets/templates/assets/permissions/type_page.html:130 +msgid "" +"Aucun accès de type défini. Les droits thématiques s'appliquent (sauf " +"restriction ci-dessus)." +msgstr "" + +#: assets/templates/assets/permissions/type_page.html:226 +msgid "Accès par utilisateur" +msgstr "Access by user" + +#: assets/templates/assets/permissions/type_page.html:236 +msgid "Objet" +msgstr "Object" + +#: assets/templates/assets/permissions/type_page.html:315 +msgid "Aucun accès spécifique défini pour ce type d'objet." +msgstr "No specific access defined for this object type." + +#: assets/templates/assets/permissions/type_page.html:324 +msgid "Ajouter un accès sur un objet spécifique" +msgstr "Add access to a specific object" + +#: assets/templates/assets/permissions/type_page.html:346 +msgid "ID objet" +msgstr "Object ID" + +#: assets/templates/assets/permissions/type_page.html:349 +msgid "ID de l'instance précise" +msgstr "ID of precise instance" + +#: assets/templates/assets/publiclighting_street_detail.html:15 +#: assets/templates/assets/roads_street_detail.html:15 +#: assets/templates/assets/sign_street_detail.html:15 +msgid "Commune:" +msgstr "Municipality:" + +#: assets/templates/assets/publiclighting_street_detail.html:74 +#: assets/templates/assets/roads_street_detail.html:83 +#: assets/templates/assets/sign_street_detail.html:74 +#: assets/templates/assets/structures_location_detail.html:111 +#: assets/templates/assets/structures_location_detail.html:171 +#: assets/templates/assets/trafficlights_intersection_detail.html:217 +msgid "Historique" +msgstr "History" + +#: assets/templates/assets/publiclighting_street_detail.html:250 +#: assets/templates/assets/sign_street_detail.html:249 +#: assets/templates/assets/trafficlights_import_cross_plan.html:139 +#: assets/templates/assets/trafficlights_import_cross_plan.html:534 +#: assets/templates/assets/trafficlights_intersection_detail.html:425 +msgid "Poteaux" +msgstr "Poles" + +#: assets/templates/assets/publiclighting_street_detail.html:267 +msgid "Alimentation:" +msgstr "Power supply:" + +#: assets/templates/assets/roads_index.html:49 +msgid "Qualité globale" +msgstr "Overall quality" + +#: assets/templates/assets/roads_index.html:87 +#: assets/templates/assets/roads_street_detail.html:255 +#: assets/templates/assets/roads_street_detail.html:270 +msgid "Qualité" +msgstr "Quality" + +#: assets/templates/assets/roads_street_detail.html:17 +msgid "Code voirie:" +msgstr "Road code:" + +#: assets/templates/assets/roads_street_detail.html:23 +msgid "Niveau:" +msgstr "Level:" + +#: assets/templates/assets/roads_street_detail.html:249 +msgid "Chaussées" +msgstr "Roadways" + +#: assets/templates/assets/roads_street_detail.html:259 +msgid "Aucune chaussée" +msgstr "No roadway" + +#: assets/templates/assets/roads_street_detail.html:264 +msgid "Trottoirs" +msgstr "Sidewalks" + +#: assets/templates/assets/roads_street_detail.html:274 +msgid "Aucun trottoir" +msgstr "No sidewalk" + +#: assets/templates/assets/roads_street_detail.html:279 +msgid "Mobilier urbain" +msgstr "Street furniture" + +#: assets/templates/assets/roads_street_detail.html:287 +msgid "Aucun mobilier urbain" +msgstr "No street furniture" + +#: assets/templates/assets/roads_street_detail.html:296 +msgid "Aucun asset archivé" +msgstr "No archived asset" + +#: assets/templates/assets/roads_street_detail.html:336 +msgid "Aucune intervention" +msgstr "No intervention" + +#: assets/templates/assets/sign_index.html:49 assets/views/core.py:649 +msgid "Type de panneau" +msgstr "Sign type" + +#: assets/templates/assets/sign_index.html:88 +msgid "Texte" +msgstr "Text" + +#: assets/templates/assets/structures_index.html:8 assets/views/core.py:597 +msgid "Type d'ouvrage" +msgstr "Structure type" + +#: assets/templates/assets/structures_location_detail.html:19 +#: assets/views/core.py:585 +msgid "Localisation principale" +msgstr "Main location" + +#: assets/templates/assets/structures_location_detail.html:27 +msgid "Parent" +msgstr "Parent" + +#: assets/templates/assets/structures_location_detail.html:275 +msgid "Créer un nouvel équipement" +msgstr "Create a new asset" + +#: assets/templates/assets/structures_location_detail.html:276 +#: assets/templates/assets/trafficlights_import_cross_plan.html:196 +#: assets/templates/assets/trafficlights_import_cross_plan.html:278 +#: assets/templates/assets/trafficlights_import_cross_plan.html:360 +#: assets/templates/assets/trafficlights_import_cross_plan.html:448 +msgid "Nouveau" +msgstr "New" + +#: assets/templates/assets/structures_location_detail.html:305 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(c)s sous-localisation\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(c)s sous-localisations\n" +#| " " +msgid "" +"\n" +" %(c)s sous-localisation\n" +" " +msgid_plural "" +"\n" +" %(c)s sous-localisations\n" +" " +msgstr[0] "" +"\n" +" %(c)s sub-location\n" +" " +msgstr[1] "" +"\n" +" %(c)s sub-locations\n" +" " + +#: assets/templates/assets/structures_location_detail.html:312 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(a)s asset\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(a)s assets\n" +#| " " +msgid "" +"\n" +" %(a)s asset\n" +" " +msgid_plural "" +"\n" +" %(a)s assets\n" +" " +msgstr[0] "" +"\n" +" %(a)s asset\n" +" " +msgstr[1] "" +"\n" +" %(a)s assets\n" +" " + +#: assets/templates/assets/trafficlights_import_cross_plan.html:18 +#: assets/templates/assets/trafficlights_import_dxf.html:17 +#, fuzzy +#| msgid "Thématique" +msgid "Thématiques" +msgstr "Topic" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:20 +#, fuzzy +#| msgid "Plan de croix" +msgid "Importation Plan de croix" +msgstr "Cross plan" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:26 +msgid "Importation d'un plan de croix (Excel)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:29 +#: assets/templates/assets/trafficlights_import_dxf.html:28 +#: assets/templates/assets/trafficlights_import_dxf.html:323 +#, fuzzy +#| msgid "Statut des carrefours" +msgid "Retour au carrefour" +msgstr "Intersections status" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:42 +msgid "Téléverser le plan de croix (.xlsx)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:59 +msgid "Fichier Excel du plan de croix" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:63 +msgid "" +"Téléversez un ou plusieurs classeurs Excel de plans de croix (contenant " +"l'onglet 'kruisjesplan' ou 'Plan croix')." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:69 +#, fuzzy +#| msgid "Filtrer les assets" +msgid "Analyser et comparer les assets" +msgstr "Filter assets" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:110 +msgid "Aperçu de la comparaison des assets" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:113 +#, fuzzy +#| msgid "Fichier" +msgid "Fichiers :" +msgstr "File" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:115 +#, fuzzy +#| msgid "Fichier" +msgid "Fichier :" +msgstr "File" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:117 +#, fuzzy +#| msgid "Carrefour associé" +msgid "Carrefour :" +msgstr "Associated intersection" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:122 +msgid "Tout remplacer (Archiver l'existant + Recréer)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:125 +msgid "Appliquer les décisions" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:149 +#, fuzzy +#| msgid "Détecteurs" +msgid "Détecteurs & BP" +msgstr "Detectors" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:168 +msgid "" +"Vous pouvez géolocaliser les poteaux en les plaçant sur le plan de la carte." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:171 +msgid "Positionner les poteaux sur la carte" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:179 +#: assets/templates/assets/trafficlights_import_cross_plan.html:259 +#: assets/templates/assets/trafficlights_import_cross_plan.html:341 +#: assets/templates/assets/trafficlights_import_cross_plan.html:431 +#, fuzzy +#| msgid "Code EAN" +msgid "Code Asset" +msgstr "Code EAN" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:182 +#: assets/templates/assets/trafficlights_import_cross_plan.html:263 +#: assets/templates/assets/trafficlights_import_cross_plan.html:345 +#: assets/templates/assets/trafficlights_import_cross_plan.html:434 +msgid "Modèle Déduit (Excel)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:183 +#: assets/templates/assets/trafficlights_import_cross_plan.html:264 +#: assets/templates/assets/trafficlights_import_cross_plan.html:346 +#: assets/templates/assets/trafficlights_import_cross_plan.html:435 +#, fuzzy +#| msgid "Modèle actuel :" +msgid "Modèle Actuel (DB)" +msgstr "Current model:" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:184 +#: assets/templates/assets/trafficlights_import_cross_plan.html:265 +#: assets/templates/assets/trafficlights_import_cross_plan.html:347 +#: assets/templates/assets/trafficlights_import_cross_plan.html:436 +msgid "Décision" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:185 +#: assets/templates/assets/trafficlights_import_cross_plan.html:266 +#: assets/templates/assets/trafficlights_import_cross_plan.html:348 +#: assets/templates/assets/trafficlights_import_cross_plan.html:437 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Forcer le Modèle" +msgstr "Change model" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:198 +#: assets/templates/assets/trafficlights_import_cross_plan.html:280 +#: assets/templates/assets/trafficlights_import_cross_plan.html:362 +#: assets/templates/assets/trafficlights_import_cross_plan.html:450 +#, fuzzy +#| msgid "Modifier" +msgid "Modifié" +msgstr "Edit" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:200 +#: assets/templates/assets/trafficlights_import_cross_plan.html:282 +#: assets/templates/assets/trafficlights_import_cross_plan.html:364 +#: assets/templates/assets/trafficlights_import_cross_plan.html:452 +msgid "Identique" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:202 +#: assets/templates/assets/trafficlights_import_cross_plan.html:284 +#: assets/templates/assets/trafficlights_import_cross_plan.html:366 +#: assets/templates/assets/trafficlights_import_cross_plan.html:454 +#, fuzzy +#| msgid "Assets uniquement" +msgid "DB uniquement" +msgstr "Assets only" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:210 +#: assets/templates/assets/trafficlights_import_cross_plan.html:292 +#: assets/templates/assets/trafficlights_import_cross_plan.html:374 +#: assets/templates/assets/trafficlights_import_cross_plan.html:462 +msgid "Conserver en DB" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:216 +#: assets/templates/assets/trafficlights_import_cross_plan.html:298 +#: assets/templates/assets/trafficlights_import_cross_plan.html:380 +#: assets/templates/assets/trafficlights_import_cross_plan.html:468 +#: assets/templates/assets/unified_index.html:329 +msgid "Créer l'asset" +msgstr "Create the asset" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:217 +#: assets/templates/assets/trafficlights_import_cross_plan.html:299 +#: assets/templates/assets/trafficlights_import_cross_plan.html:381 +#: assets/templates/assets/trafficlights_import_cross_plan.html:469 +msgid "Ignorer" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:219 +#: assets/templates/assets/trafficlights_import_cross_plan.html:301 +#: assets/templates/assets/trafficlights_import_cross_plan.html:383 +#: assets/templates/assets/trafficlights_import_cross_plan.html:471 +#, fuzzy +#| msgid "Créer l'asset" +msgid "Mettre à jour l'asset" +msgstr "Create the asset" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:220 +#: assets/templates/assets/trafficlights_import_cross_plan.html:224 +#: assets/templates/assets/trafficlights_import_cross_plan.html:302 +#: assets/templates/assets/trafficlights_import_cross_plan.html:306 +#: assets/templates/assets/trafficlights_import_cross_plan.html:384 +#: assets/templates/assets/trafficlights_import_cross_plan.html:388 +#: assets/templates/assets/trafficlights_import_cross_plan.html:472 +#: assets/templates/assets/trafficlights_import_cross_plan.html:476 +msgid "Remplacer (Archiver + Créer)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:221 +#: assets/templates/assets/trafficlights_import_cross_plan.html:303 +#: assets/templates/assets/trafficlights_import_cross_plan.html:385 +#: assets/templates/assets/trafficlights_import_cross_plan.html:473 +#, fuzzy +#| msgid "Inexistant" +msgid "Conserver existant" +msgstr "Nonexistent" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:223 +#: assets/templates/assets/trafficlights_import_cross_plan.html:305 +#: assets/templates/assets/trafficlights_import_cross_plan.html:387 +#: assets/templates/assets/trafficlights_import_cross_plan.html:475 +msgid "Ne rien faire (Identique)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:232 +#: assets/templates/assets/trafficlights_import_cross_plan.html:314 +#: assets/templates/assets/trafficlights_import_cross_plan.html:396 +#: assets/templates/assets/trafficlights_import_cross_plan.html:484 +msgid "Laisser par défaut" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:244 +#, fuzzy +#| msgid "Aucun poteau" +msgid "Aucun poteau détecté." +msgstr "No pole" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:261 +#: assets/templates/assets/trafficlights_import_cross_plan.html:343 +msgid "Poteau" +msgstr "Pole" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:326 +#, fuzzy +#| msgid "Aucun détecteur" +msgid "Aucune lanterne détectée." +msgstr "No detector" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:408 +#, fuzzy +#| msgid "Aucun détecteur" +msgid "Aucun détecteur détecté." +msgstr "No detector" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:420 +msgid "" +"Vous pouvez tracer précisément les câbles d'interconnexion le long des " +"tranchées de la carte." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:423 +msgid "Tracer les câbles sur la carte" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:496 +#, fuzzy +#| msgid "Aucun câble" +msgid "Aucun câble détecté." +msgstr "No cable" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:521 +msgid "Retour au tableau comparatif" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:524 +msgid "Poteaux positionnés :" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:554 +msgid "Aide au placement" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:557 +msgid "" +"Cliquez sur 'Placer sur la carte' pour un poteau, puis cliquez sur la carte " +"pour le positionner." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:567 +#, fuzzy +#| msgid "A remplacer" +msgid "À placer" +msgstr "To be replaced" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:576 +msgid "Placer sur la carte" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:591 +msgid "Tracé des câbles" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:594 +msgid "" +"Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer " +"la ligne du câble." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:608 +#, fuzzy +#| msgid "Éditer" +msgid "Tracer / Éditer" +msgstr "Edit" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:628 +#: assets/templates/assets/trafficlights_import_cross_plan.html:1250 +#: assets/templates/assets/trafficlights_import_cross_plan.html:1304 +msgid "Mode: Lecture" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:630 +#: assets/templates/assets/unified_index.html:399 +msgid "Effacer" +msgstr "Clear" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:646 +#: assets/templates/assets/trafficlights_intersection_detail.html:1008 +msgid "Plans géoréférencés" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:654 +#: assets/templates/assets/trafficlights_intersection_detail.html:1016 +msgid "Plan image" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:659 +#: assets/templates/assets/trafficlights_intersection_detail.html:1021 +#, fuzzy +#| msgid "Plan" +msgid "Plan CAO" +msgstr "Plan" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:674 +#: assets/templates/assets/trafficlights_intersection_detail.html:1036 +msgid "Aucun plan d'image géoréférencé disponible" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:675 +#: assets/templates/assets/trafficlights_intersection_detail.html:1037 +msgid "" +"Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la " +"fiche." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:686 +#: assets/templates/assets/trafficlights_intersection_detail.html:1048 +#, fuzzy +#| msgid "Aucun tag disponible pour cette thématique." +msgid "Aucun fichier DXF disponible pour ce carrefour" +msgstr "No tags available for this topic." + +#: assets/templates/assets/trafficlights_import_cross_plan.html:687 +#: assets/templates/assets/trafficlights_intersection_detail.html:1049 +msgid "Veuillez d'abord en importer un." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:692 +#: assets/templates/assets/trafficlights_intersection_detail.html:1057 +#, fuzzy +#| msgid "Sélection de la vue" +msgid "Sélectionner le plan DXF" +msgstr "View selection" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:700 +#: assets/templates/assets/trafficlights_intersection_detail.html:1065 +#, fuzzy +#| msgid "Sélectionner un type" +msgid "-- Sélectionner un plan CAO (DXF) --" +msgstr "Select a type" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:711 +#: assets/templates/assets/trafficlights_intersection_detail.html:1076 +msgid "Couches vectorielles CAO" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:713 +#: assets/templates/assets/trafficlights_intersection_detail.html:1078 +msgid "Décocher tout" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:718 +#: assets/templates/assets/trafficlights_intersection_detail.html:1083 +msgid "Lecture des couches du plan..." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:731 +#: assets/templates/assets/trafficlights_intersection_detail.html:1096 +#, fuzzy +#| msgid "Désactivé" +msgid "Désactiver tout" +msgstr "Disabled" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:790 +msgid "" +"Voulez-vous vraiment archiver TOUS les assets actifs existants de ce " +"carrefour et recréer les nouveaux à partir du fichier Excel ? Cette action " +"est irréversible." +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:945 +msgid "Rechercher une adresse…" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:1146 +#, fuzzy +#| msgid "Position géographique" +msgid "Positionné" +msgstr "Geographical position" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:1234 +msgid "Mode: Placement de" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:1275 +msgid "Mode: Tracé" +msgstr "" + +#: assets/templates/assets/trafficlights_import_cross_plan.html:1989 +#, fuzzy +#| msgid "Commentaire enregistré avec succès" +msgid "Couche vectorielle chargée avec succès !" +msgstr "Comment saved successfully" + +#: assets/templates/assets/trafficlights_import_dxf.html:19 +msgid "Importation DXF" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:25 +msgid "Importation d'un plan DXF — Poteaux" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:41 +msgid "Choisir un plan DXF" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:58 +#, fuzzy +#| msgid "Sélectionner une catégorie" +msgid "Sélectionner un fichier existant (déjà importé)" +msgstr "Select a category" + +#: assets/templates/assets/trafficlights_import_dxf.html:61 +#, fuzzy +#| msgid "Sélectionner des fichiers" +msgid "Sélectionner un fichier" +msgstr "Select files" + +#: assets/templates/assets/trafficlights_import_dxf.html:67 +msgid "" +"Sélectionnez un fichier DXF parmi les fichiers déjà disponibles sur le " +"serveur." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:73 +#: assets/templates/assets/unified_index.html:412 +msgid "OU" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:80 +msgid "Téléverser un nouveau fichier DXF" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:87 +#, fuzzy +#| msgid "Changer les fichiers" +msgid "Analyser le fichier" +msgstr "Change files" + +#: assets/templates/assets/trafficlights_import_dxf.html:107 +msgid "Option d'importation du plan DXF" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:112 +msgid "" +"Le fichier DXF contient des données géométriques (vecteurs) structurées en " +"couches. Vous pouvez choisir de toutes les importer, d'en sélectionner une " +"partie pour accélérer le chargement, ou de n'afficher que l'image de fond." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:120 +msgid "Importer toutes les couches vectorielles" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:122 +msgid "" +"Affiche l'intégralité du plan vectoriel (peut être plus lent à charger)." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:127 +msgid "Sélectionner les couches à importer (Recommandé)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:129 +msgid "" +"Permet de choisir uniquement les couches utiles (ex: signalisation, poteaux) " +"pour un chargement instantané." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:134 +msgid "Ne pas importer de couches vectorielles (image uniquement)" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:136 +msgid "Charge uniquement l'image de fond du plan. Très rapide." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:145 +msgid "Rechercher une couche..." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:149 +#: assets/templates/assets/trafficlights_import_dxf.html:256 +#: assets/templates/assets/unified_index.html:118 +msgid "Aucune" +msgstr "None" + +#: assets/templates/assets/trafficlights_import_dxf.html:156 +msgid "Lecture des couches du fichier..." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:171 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Charger le plan" +msgstr "Change model" + +#: assets/templates/assets/trafficlights_import_dxf.html:206 +#, fuzzy +#| msgid "Configuration du type" +msgid "Configuration" +msgstr "Type configuration" + +#: assets/templates/assets/trafficlights_import_dxf.html:218 +msgid "Couches CAO" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:223 +#, fuzzy +#| msgid "Totaux des assets" +msgid "Nouveaux assets" +msgstr "Asset totals" + +#: assets/templates/assets/trafficlights_import_dxf.html:236 +msgid "Opacité du plan image" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:245 +msgid "Gérer / Charger d'autres couches CAO" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:252 +#, fuzzy +#| msgid "Rechercher" +msgid "Rechercher..." +msgstr "Search" + +#: assets/templates/assets/trafficlights_import_dxf.html:264 +#: assets/templates/assets/trafficlights_import_dxf.html:1044 +msgid "Traitement et conversion du plan..." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:266 +msgid "" +"Cette opération (lecture, vectorisation et optimisation) peut prendre " +"environ 1 minute pour les plans complexes lors du premier chargement." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:278 +#, fuzzy +#| msgid "Cliquez sur la carte pour ajouter des sommets." +msgid "Cliquez sur la carte pour ajouter manuellement un nouveau poteau." +msgstr "Click on the map to add vertices." + +#: assets/templates/assets/trafficlights_import_dxf.html:286 +#, fuzzy +#| msgid "Aucun template trouvé pour ce type d’asset." +msgid "Aucun poteau ajouté pour le moment." +msgstr "No template found for this asset type." + +#: assets/templates/assets/trafficlights_import_dxf.html:299 +msgid "Plan interactif" +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:302 +#, fuzzy +#| msgid "Inexistant" +msgid "Existants" +msgstr "Nonexistent" + +#: assets/templates/assets/trafficlights_import_dxf.html:303 +#, fuzzy +#| msgid "Nouveau" +msgid "Nouveaux" +msgstr "New" + +#: assets/templates/assets/trafficlights_import_dxf.html:319 +msgid "" +"Les poteaux placés sur la carte sont persistés à chaque ajout/suppression." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:326 +#, fuzzy +#| msgid "Valider la sélection" +msgid "Valider l'importation" +msgstr "Validate selection" + +#: assets/templates/assets/trafficlights_import_dxf.html:395 +#, fuzzy +#| msgid "Erreur lors de la sauvegarde." +msgid "Erreur lors de la lecture des couches." +msgstr "Error while saving." + +#: assets/templates/assets/trafficlights_import_dxf.html:1136 +msgid "Aucune couche vectorielle chargée." +msgstr "" + +#: assets/templates/assets/trafficlights_import_dxf.html:1477 +#, fuzzy +#| msgid "Ajouter un contrat" +msgid "Ajouter un poteau" +msgstr "Add a contract" + +#: assets/templates/assets/trafficlights_index.html:18 assets/views/core.py:553 +msgid "Voltage" +msgstr "Voltage" + +#: assets/templates/assets/trafficlights_index.html:28 assets/views/core.py:568 +msgid "Date de mise en service (à partir de)" +msgstr "Commissioning date (from)" + +#: assets/templates/assets/trafficlights_index.html:31 assets/views/core.py:559 +msgid "Connexion à la centrale" +msgstr "Connection to central" + +#: assets/templates/assets/trafficlights_index.html:49 assets/views/core.py:575 +msgid "Prestataire maintenance contrôleurs" +msgstr "Controller maintenance provider" + +#: assets/templates/assets/trafficlights_index.html:72 +msgid "Créer une nouvelle intersection" +msgstr "Create a new intersection" + +#: assets/templates/assets/trafficlights_intersection_detail.html:29 +msgid "Modifier cette intersection" +msgstr "Modify this intersection" + +#: assets/templates/assets/trafficlights_intersection_detail.html:39 +msgid "Tension (Voltage)" +msgstr "Voltage" + +#: assets/templates/assets/trafficlights_intersection_detail.html:41 +msgid "Connexion centrale" +msgstr "Central connection" + +#: assets/templates/assets/trafficlights_intersection_detail.html:53 +msgid "Carrefour principal" +msgstr "Main intersection" + +#: assets/templates/assets/trafficlights_intersection_detail.html:58 +msgid "Contrats" +msgstr "Contracts" + +#: assets/templates/assets/trafficlights_intersection_detail.html:72 +msgid "Modifier les contrats" +msgstr "Modify contracts" + +#: assets/templates/assets/trafficlights_intersection_detail.html:94 +msgid "Assets importés par DXF en attente de validation" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:97 +#, python-format +msgid "" +"\n" +" Il y a %(count)s poteau importé depuis " +"un plan DXF qui est en attente de validation pour ce carrefour.\n" +" " +msgid_plural "" +"\n" +" Il y a %(count)s poteaux importés " +"depuis un plan DXF qui sont en attente de validation pour ce carrefour.\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:108 +#, fuzzy +#| msgid "Date d'installation" +msgid "Date d'installation :" +msgstr "Installation date" + +#: assets/templates/assets/trafficlights_intersection_detail.html:119 +msgid "Option A : Remplacer par code et créer les nouveaux" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:127 +msgid "" +"Êtes-vous sûr de vouloir archiver TOUS les poteaux existants de ce carrefour " +"et d\\'activer uniquement les nouveaux poteaux importés ?" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:128 +msgid "Option B : Remplacement complet (Archiver tout le reste)" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:133 +msgid "Réimporter / Corriger" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:328 +#: assets/templates/assets/trafficlights_intersection_detail.html:962 +#: assets/templates/assets/unified_index.html:453 +msgid "Ajouter un asset" +msgstr "Add an asset" + +#: assets/templates/assets/trafficlights_intersection_detail.html:333 +#: assets/templates/assets/trafficlights_intersection_detail.html:334 +msgid "Importer plan DXF" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:336 +#: assets/templates/assets/trafficlights_intersection_detail.html:337 +#, fuzzy +#| msgid "Plan de croix" +msgid "Importer plan de croix" +msgstr "Cross plan" + +#: assets/templates/assets/trafficlights_intersection_detail.html:341 +msgid "Archiver tous les assets" +msgstr "Archive all assets" + +#: assets/templates/assets/trafficlights_intersection_detail.html:342 +msgid "Tout archiver" +msgstr "Archive all" + +#: assets/templates/assets/trafficlights_intersection_detail.html:392 +msgid "Installation extérieure" +msgstr "Outdoor installation" + +#: assets/templates/assets/trafficlights_intersection_detail.html:409 +msgid "Programmation" +msgstr "Programming" + +#: assets/templates/assets/trafficlights_intersection_detail.html:429 +msgid "Tout déplier / replier" +msgstr "Expand / collapse all" + +#: assets/templates/assets/trafficlights_intersection_detail.html:464 +#, fuzzy +#| msgid "Afficher les lanternes et accessoires" +msgid "Afficher les lanternes, accessoires et radars" +msgstr "Show lanterns and accessories" + +#: assets/templates/assets/trafficlights_intersection_detail.html:504 +msgid "Aucun poteau" +msgstr "No pole" + +#: assets/templates/assets/trafficlights_intersection_detail.html:517 +msgid "Armoires électriques" +msgstr "Electrical cabinets" + +#: assets/templates/assets/trafficlights_intersection_detail.html:533 +msgid "Aucune armoire électrique" +msgstr "No electrical cabinet" + +#: assets/templates/assets/trafficlights_intersection_detail.html:549 +msgid "Couleur du câble sur la carte" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:561 +msgid "Aucun câble" +msgstr "No cable" + +#: assets/templates/assets/trafficlights_intersection_detail.html:570 +msgid "Radars préventifs" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:586 +#, fuzzy +#| msgid "Aucun hardware réseau" +msgid "Aucun radar préventif" +msgstr "No network hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:605 +msgid "Contrôleur de trafic" +msgstr "Traffic controller" + +#: assets/templates/assets/trafficlights_intersection_detail.html:652 +msgid "Hardwares du contrôleur" +msgstr "Controller hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:676 +msgid "Aucun hardware" +msgstr "No hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:716 +msgid "Aucun détecteur" +msgstr "No detector" + +#: assets/templates/assets/trafficlights_intersection_detail.html:732 +msgid "Hardware réseau" +msgstr "Network hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:756 +msgid "Aucun hardware réseau" +msgstr "No network hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:772 +msgid "Hardware Tension" +msgstr "Voltage hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:796 +msgid "Aucun hardware tension" +msgstr "No voltage hardware" + +#: assets/templates/assets/trafficlights_intersection_detail.html:809 +msgid "Aucun contrôleur" +msgstr "No controller" + +#: assets/templates/assets/trafficlights_intersection_detail.html:825 +msgid "Programmations" +msgstr "Programmings" + +#: assets/templates/assets/trafficlights_intersection_detail.html:852 +msgid "Aucune programmation" +msgstr "No programming" + +#: assets/templates/assets/trafficlights_intersection_detail.html:870 +msgid "Fichiers de log" +msgstr "Log files" + +#: assets/templates/assets/trafficlights_intersection_detail.html:890 +msgid "Aucun fichier de log" +msgstr "No log file" + +#: assets/templates/assets/trafficlights_intersection_detail.html:970 +msgid "Sélectionner une catégorie" +msgstr "Select a category" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1051 +msgid "Importer un plan DXF" +msgstr "" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1144 +msgid "Gérer les contrats" +msgstr "Manage contracts" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1150 +msgid "Contrats associés" +msgstr "Associated contracts" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1156 +#: assets/templates/assets/trafficlights_intersection_detail.html:1247 +msgid "Numéro de contrat" +msgstr "Contract number" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1157 +#: assets/templates/assets/trafficlights_intersection_detail.html:1248 +msgid "Entreprise" +msgstr "Company" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1158 +#: assets/templates/assets/trafficlights_intersection_detail.html:1249 +msgid "Type de maintenance" +msgstr "Maintenance type" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1174 +#: assets/templates/assets/trafficlights_intersection_detail.html:1325 +msgid "Retirer ce contrat" +msgstr "Remove this contract" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1183 +msgid "Aucun contrat associé." +msgstr "No associated contract." + +#: assets/templates/assets/trafficlights_intersection_detail.html:1190 +msgid "Ajouter un contrat" +msgstr "Add a contract" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1195 +msgid "Contrat" +msgstr "Contract" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1197 +msgid "Sélectionner" +msgstr "Select" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1206 +msgid "Infrastructure" +msgstr "Infrastructure" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1207 +msgid "Contrôleur de traffic" +msgstr "Traffic controller" + +#: assets/templates/assets/trafficlights_intersection_detail.html:1332 +msgid "Erreur lors de l’ajout." +msgstr "Error while adding." + +#: assets/templates/assets/unified_index.html:83 +msgid "Filtrer les assets" +msgstr "Filter assets" + +#: assets/templates/assets/unified_index.html:91 +#: assets/templates/assets/unified_index.html:251 +#: assets/templates/assets/unified_index.html:261 +msgid "Thématique" +msgstr "Topic" + +#: assets/templates/assets/unified_index.html:122 +#: assets/templates/assets/unified_index.html:147 +msgid "Sélectionnez d'abord une ou plusieurs thématiques." +msgstr "Select one or more topics first." + +#: assets/templates/assets/unified_index.html:137 +msgid "Sélectionnez d'abord une ou plusieurs catégories." +msgstr "Select one or more categories first." + +#: assets/templates/assets/unified_index.html:144 +msgid "Filtres thématiques" +msgstr "Thematic filters" + +#: assets/templates/assets/unified_index.html:154 +msgid "Type d'élément" +msgstr "Item type" + +#: assets/templates/assets/unified_index.html:161 +msgid "Assets et localisations" +msgstr "Assets and locations" + +#: assets/templates/assets/unified_index.html:167 +msgid "Localisations uniquement" +msgstr "Locations only" + +#: assets/templates/assets/unified_index.html:173 +msgid "Assets uniquement" +msgstr "Assets only" + +#: assets/templates/assets/unified_index.html:181 +#, fuzzy +#| msgid "Description du matériel" +msgid "Options de géométrie" +msgstr "Hardware description" + +#: assets/templates/assets/unified_index.html:186 +msgid "Inclure les assets sans géométrie" +msgstr "" + +#: assets/templates/assets/unified_index.html:240 +msgid "Créer un élément" +msgstr "Create an item" + +#: assets/templates/assets/unified_index.html:245 +msgid "Choisissez d'abord le type d'élément, puis la thématique autorisée." +msgstr "Choose the item type first, then the authorized topic." + +#: assets/templates/assets/unified_index.html:254 +#: assets/templates/assets/unified_index.html:282 +msgid "Créer un asset" +msgstr "Create an asset" + +#: assets/templates/assets/unified_index.html:264 +#: assets/templates/assets/unified_index.html:342 +msgid "Créer une localisation" +msgstr "Create a location" + +#: assets/templates/assets/unified_index.html:270 +msgid "Aucune thématique éditable n'est disponible pour la création." +msgstr "No editable topic is available for creation." + +#: assets/templates/assets/unified_index.html:323 +msgid "L'asset sera créé avec le statut 'À approuver'." +msgstr "The asset will be created with status 'To approve'." + +#: assets/templates/assets/unified_index.html:361 +msgid "Créer la localisation" +msgstr "Create the location" + +#: assets/templates/assets/unified_index.html:373 +msgid "Mode sélection intervention préventive" +msgstr "Preventive intervention selection mode" + +#: assets/templates/assets/unified_index.html:394 +msgid "Code, nom, type…" +msgstr "Code, name, type..." + +#: assets/templates/assets/unified_index.html:408 +#, fuzzy +#| msgid "Tous les mots doivent correspondre" +msgid "Tous les mots recherchés doivent être présents" +msgstr "All words must match" + +#: assets/templates/assets/unified_index.html:408 +msgid "ET" +msgstr "" + +#: assets/templates/assets/unified_index.html:412 +#, fuzzy +#| msgid "Au moins un mot doit correspondre" +msgid "Au moins un des mots recherchés doit être présent" +msgstr "At least one word must match" + +#: assets/templates/assets/unified_index.html:460 +#: assets/templates/assets/unified_index.html:461 +msgid "Filtres" +msgstr "Filters" + +#: assets/templates/assets/unified_index.html:480 +msgid "sélectionné(s)" +msgstr "selected" + +#: assets/templates/assets/unified_index.html:497 +#: assets/templates/assets/unified_index.html:616 +#, fuzzy +#| msgid "Modifier les contrats" +msgid "Modifier le statut en masse" +msgstr "Modify contracts" + +#: assets/templates/assets/unified_index.html:505 +#: assets/templates/assets/unified_index.html:645 +msgid "Modifier le statut de validation en masse" +msgstr "Bulk modify validation status" + +#: assets/templates/assets/unified_index.html:527 +msgid "" +"Sélectionnez une ou plusieurs thématiques dans les filtres, saisissez une " +"recherche ou zoomez sur la carte." +msgstr "" + +#: assets/templates/assets/unified_index.html:560 +msgid "Choisissez une catégorie ou zoomez sur la carte pour voir les assets." +msgstr "Choose a category or zoom in on the map to see assets." + +#: assets/templates/assets/unified_index.html:621 +#, fuzzy +#| msgid "Nouveau" +msgid "Nouveau statut" +msgstr "New" + +#: assets/templates/assets/unified_index.html:650 +msgid "Nouveau statut de validation" +msgstr "New validation status" + +#: assets/templates/assets/water_index.html:17 assets/views/core.py:631 +#: assets/views/locations.py:29 +msgid "GIEP" +msgstr "GIEP" + +#: assets/templates/assets/water_index.html:31 +#, fuzzy +#| msgid "Créer une nouvelle localisation ITS" +msgid "Créer une nouvelle localisation GIEP" +msgstr "Create new ITS location" + +#: assets/templates/assets/water_index.html:59 +#, fuzzy +#| msgid "Créer un nouvel asset ITS" +msgid "Créer un nouvel ouvrage GIEP" +msgstr "Create new ITS asset" + +#: assets/views/artworks.py:79 +msgid "Inactif" +msgstr "Inactive" + +#: assets/views/artworks.py:80 +msgid "À supprimer" +msgstr "To be deleted" + +#: assets/views/artworks.py:94 assets/views/controlcenters.py:107 +#: assets/views/its.py:110 assets/views/structures.py:111 +msgid "Localisations" +msgstr "Locations" + +#: assets/views/artworks.py:268 +#, python-format +msgid "Type d'asset inconnu: %(asset_model)s" +msgstr "Unknown asset type: %(asset_model)s" + +#: assets/views/artworks.py:328 assets/views/trafficlights.py:1360 +msgid "Coordonnées invalides" +msgstr "Invalid coordinates" + +#: assets/views/artworks.py:343 assets/views/its.py:853 +msgid "Aucune localisation trouvée" +msgstr "No location found" + +#: assets/views/artworks.py:436 assets/views/artworks.py:440 +msgid "Fontaine" +msgstr "Fountain" + +#: assets/views/artworks.py:817 +msgid "Aucun équipement trouvé" +msgstr "No equipment found" + +#: assets/views/asset_editing.py:49 assets/views/asset_editing.py:132 +#: assets/views/asset_editing.py:1833 assets/views/asset_editing.py:1972 +#: assets/views/asset_editing.py:2026 assets/views/asset_editing.py:2084 +#: assets/views/asset_editing.py:2160 +msgid "Permission refusée" +msgstr "Permission denied" + +#: assets/views/asset_editing.py:55 +msgid "Aucun fichier fourni" +msgstr "No file provided" + +#: assets/views/asset_editing.py:145 +msgid "Document non trouvé" +msgstr "Document not found" + +#: assets/views/asset_editing.py:170 assets/views/asset_editing.py:1987 +#: assets/views/asset_editing.py:2041 assets/views/asset_editing.py:2113 +#: assets/views/asset_editing.py:2194 +msgid "Type d'asset inconnu" +msgstr "Unknown asset type" + +#: assets/views/asset_editing.py:289 +#, python-format +msgid "Champ inconnu: %(field)s" +msgstr "Unknown field: %(field)s" + +#: assets/views/asset_editing.py:303 assets/views/asset_editing.py:459 +#, python-format +msgid "Format de date invalide pour %(field)s" +msgstr "Invalid date format for %(field)s" + +#: assets/views/asset_editing.py:318 +msgid "Modèle non trouvé" +msgstr "Model not found" + +#: assets/views/asset_editing.py:329 assets/views/asset_editing.py:1521 +msgid "Catégorie non trouvée" +msgstr "Category not found" + +#: assets/views/asset_editing.py:346 +msgid "Localisation non trouvée" +msgstr "Location not found" + +#: assets/views/asset_editing.py:355 +#, python-format +msgid "Statut invalide: %(status)s" +msgstr "Invalid status: %(status)s" + +#: assets/views/asset_editing.py:362 +#, python-format +msgid "Statut de validation invalide: %(status)s" +msgstr "Invalid validation status: %(status)s" + +#: assets/views/asset_editing.py:376 assets/views/asset_editing.py:413 +#: assets/views/asset_editing.py:431 assets/views/asset_editing.py:440 +#, python-format +msgid "Valeur numérique invalide pour %(field)s" +msgstr "Invalid numeric value for %(field)s" + +#: assets/views/asset_editing.py:403 +#, python-format +msgid "Géométrie invalide: %(error)s" +msgstr "Invalid geometry: %(error)s" + +#: assets/views/asset_editing.py:485 assets/views/asset_editing.py:555 +#: assets/views/asset_editing.py:590 assets/views/clean.py:512 +msgid "Vous n'avez pas la permission de modifier cet asset" +msgstr "You do not have permission to edit this asset" + +#: assets/views/asset_editing.py:495 assets/views/asset_editing.py:605 +#: assets/views/asset_editing.py:756 assets/views/asset_editing.py:1449 +#: assets/views/asset_editing.py:1977 assets/views/asset_editing.py:2031 +#: assets/views/asset_editing.py:2089 assets/views/asset_editing.py:2182 +#: assets/views/asset_editing.py:2302 assets/views/clean.py:518 +msgid "Format JSON invalide" +msgstr "Invalid JSON format" + +#: assets/views/asset_editing.py:504 +#, python-format +msgid "Champ non modifiable: %(field)s" +msgstr "Field not editable: %(field)s" + +#: assets/views/asset_editing.py:534 +msgid "Aucun champ mis à jour" +msgstr "No fields updated" + +#: assets/views/asset_editing.py:541 +#, python-format +msgid "%(count)s champ(s) mis à jour" +msgstr "%(count)s field(s) updated" + +#: assets/views/asset_editing.py:562 +msgid "Vous n'avez pas la permission d'archiver cet asset" +msgstr "You do not have permission to archive this asset" + +#: assets/views/asset_editing.py:572 +msgid "Asset archivé avec succès" +msgstr "Asset archived successfully" + +#: assets/views/asset_editing.py:597 +msgid "Vous n'avez pas la permission de remplacer cet asset" +msgstr "You do not have permission to replace this asset" + +#: assets/views/asset_editing.py:682 +msgid "Asset remplacé avec succès" +msgstr "Asset replaced successfully" + +#: assets/views/asset_editing.py:741 assets/views/asset_editing.py:933 +#: assets/views/asset_editing.py:1505 +msgid "Thématique non trouvée" +msgstr "Theme not found" + +#: assets/views/asset_editing.py:748 assets/views/asset_editing.py:2175 +msgid "Vous n'avez pas la permission de créer des assets" +msgstr "You do not have permission to create assets" + +#: assets/views/asset_editing.py:802 +msgid "Type d'asset requis" +msgstr "Asset type required" + +#: assets/views/asset_editing.py:810 +#, python-format +msgid "Type d'asset invalide: %(type)s" +msgstr "Unknown asset type: %(type)s" + +#: assets/views/asset_editing.py:821 assets/views/asset_editing.py:951 +msgid "Type de localisation invalide" +msgstr "Invalid location type" + +#: assets/views/asset_editing.py:844 +msgid "Impossible de lier l'asset à cette localisation" +msgstr "Cannot link the asset to this location" + +#: assets/views/asset_editing.py:859 +msgid "Le modèle d'asset est requis pour ce type d'asset" +msgstr "Asset model is required for this asset type" + +#: assets/views/asset_editing.py:864 +msgid "Modèle d'asset non trouvé" +msgstr "Asset model not found" + +#: assets/views/asset_editing.py:897 +msgid "Asset créé avec succès" +msgstr "Asset created successfully" + +#: assets/views/asset_editing.py:940 +msgid "Vous n'avez pas la permission d'archiver en masse" +msgstr "You do not have permission for bulk archiving" + +#: assets/views/asset_editing.py:958 +msgid "Cette localisation ne supporte pas la gestion des assets" +msgstr "This location does not support asset management" + +#: assets/views/asset_editing.py:973 +#, python-format +msgid "%(count)s asset(s) archivé(s)" +msgstr "%(count)s asset(s) archived" + +#: assets/views/asset_editing.py:1477 +msgid "Format d'ID invalide" +msgstr "Invalid ID format" + +#: assets/views/asset_editing.py:1484 +msgid "Thématique requise" +msgstr "Theme required" + +#: assets/views/asset_editing.py:1490 +msgid "Catégorie requise" +msgstr "Category required" + +#: assets/views/asset_editing.py:1496 +msgid "Code requis" +msgstr "Code required" + +#: assets/views/asset_editing.py:1512 +msgid "Vous n'avez pas la permission de créer des équipements" +msgstr "You do not have permission to create equipment" + +#: assets/views/asset_editing.py:1533 +msgid "Type d'asset non supporté pour cette thématique/catégorie" +msgstr "Asset type not supported for this theme/category" + +#: assets/views/asset_editing.py:1540 +msgid "Un équipement avec ce code existe déjà" +msgstr "An equipment with this code already exists" + +#: assets/views/asset_editing.py:1550 +msgid "Modèle d'équipement non trouvé" +msgstr "Equipment model not found" + +#: assets/views/asset_editing.py:1555 +msgid "Modèle requis" +msgstr "Model required" + +#: assets/views/asset_editing.py:1624 +msgid "Une localisation est requise pour créer cet asset." +msgstr "A location is required to create this asset." + +#: assets/views/asset_editing.py:1634 +msgid "Localisation introuvable" +msgstr "Location not found" + +#: assets/views/asset_editing.py:1680 +msgid "Équipement créé avec succès" +msgstr "Equipment created successfully" + +#: assets/views/asset_editing.py:1682 +#, python-format +msgid " • %(count)d opération(s) ajoutée(s)" +msgstr " • %(count)d operation(s) added" + +#: assets/views/asset_editing.py:1995 assets/views/asset_editing.py:2100 +#: assets/views/asset_editing.py:2103 assets/views/asset_editing.py:2117 +#: assets/views/asset_editing.py:2200 +msgid "Champ de liaison invalide" +msgstr "Invalid linking field" + +#: assets/views/asset_editing.py:1997 assets/views/asset_editing.py:2061 +#: assets/views/asset_editing.py:2105 assets/views/asset_editing.py:2202 +msgid "Champ de liaison inconnu" +msgstr "Unknown linking field" + +#: assets/views/asset_editing.py:2007 +msgid "Asset lié avec succès" +msgstr "Asset linked successfully" + +#: assets/views/asset_editing.py:2049 +msgid "Cet asset n'est pas lié à ce parent" +msgstr "This asset is not linked to this parent" + +#: assets/views/asset_editing.py:2057 assets/views/asset_editing.py:2140 +msgid "Ce champ est obligatoire et ne peut pas être vidé" +msgstr "This field is required and cannot be emptied" + +#: assets/views/asset_editing.py:2068 +msgid "Asset délié avec succès" +msgstr "Asset unlinked successfully" + +#: assets/views/asset_editing.py:2129 +#, fuzzy +#| msgid "Asset lié avec succès" +msgid "Parent lié avec succès" +msgstr "Asset linked successfully" + +#: assets/views/asset_editing.py:2148 +#, fuzzy +#| msgid "Asset délié avec succès" +msgid "Parent délié avec succès" +msgstr "Asset unlinked successfully" + +#: assets/views/asset_editing.py:2267 +#, python-format +msgid "Le champ « %(field)s » est obligatoire." +msgstr "The field \"%(field)s\" is required." + +#: assets/views/asset_editing.py:2269 +msgid "Impossible de créer l'asset : un champ obligatoire est manquant." +msgstr "Unable to create asset: a required field is missing." + +#: assets/views/asset_editing.py:2275 +msgid "Asset créé et lié avec succès" +msgstr "Asset created and linked successfully" + +#: assets/views/asset_editing.py:2309 +#, fuzzy +#| msgid "Champ non modifiable: %(field)s" +msgid "Champ non modifiable en masse" +msgstr "Field not editable: %(field)s" + +#: assets/views/asset_editing.py:2355 +#, python-format +msgid "%(updated)s équipement(s) mis à jour, %(skipped)s ignoré(s)." +msgstr "" + +#: assets/views/asset_permissions.py:47 +msgid "Accès refusé — rôle admin requis." +msgstr "Access denied — admin role required." + +#: assets/views/asset_permissions.py:100 +msgid "Utilisateur requis." +msgstr "User required." + +#: assets/views/asset_permissions.py:106 +msgid "Utilisateur introuvable." +msgstr "User not found." + +#: assets/views/clean.py:259 +msgid "Locations" +msgstr "Locations" + +#: assets/views/clean.py:475 +#, python-format +msgid "Unknown model: %(asset_model)s" +msgstr "Unknown model: %(asset_model)s" + +#: assets/views/clean.py:632 +msgid "No location found nearby" +msgstr "No location found nearby" + +#: assets/views/controlcenters.py:396 +#, python-format +msgid "Trop d'objets à afficher (> %(max)d)" +msgstr "Too many objects to display (> %(max)d)" + +#: assets/views/controlcenters.py:410 +#, python-format +msgid "Trop d'objets à afficher (> %(max)d). Limitez votre sélection." +msgstr "Too many objects to display (> %(max)d). Limit your selection." + +#: assets/views/controlcenters.py:528 +#, python-format +msgid "Modèle '%(model)s' inconnu." +msgstr "Unknown model '%(model)s'." + +#: assets/views/controlcenters.py:541 assets/views/its.py:544 +#: assets/views/nature.py:237 assets/views/publiclighting.py:278 +#: assets/views/roads.py:541 assets/views/sign.py:794 +#: assets/views/structures.py:666 assets/views/trafficlights.py:1240 +msgid "Aucun template trouvé pour ce type d’asset." +msgstr "No template found for this asset type." + +#: assets/views/core.py:186 assets/views/nature.py:224 +#: assets/views/publiclighting.py:265 assets/views/roads.py:528 +#: assets/views/sign.py:781 assets/views/structures.py:653 +#: assets/views/trafficlights.py:1227 assets/views/water.py:523 +#, python-format +msgid "Modèle %(asset_model)s inconnu." +msgstr "Unknown model %(asset_model)s." + +#: assets/views/core.py:467 +msgid "Signaux de danger" +msgstr "Danger signals" + +#: assets/views/core.py:468 +msgid "Signaux de priorité" +msgstr "Priority signals" + +#: assets/views/core.py:469 +msgid "Signaux d'interdiction" +msgstr "Prohibition signs" + +#: assets/views/core.py:470 +msgid "Signaux d'obligation" +msgstr "Obligation signals" + +#: assets/views/core.py:471 +msgid "Arrêt / Stationnement" +msgstr "Stopping / Parking" + +#: assets/views/core.py:472 +msgid "Signaux d'indication" +msgstr "Indication signals" + +#: assets/views/core.py:473 +msgid "Signaux de zone" +msgstr "Zone signals" + +#: assets/views/core.py:474 +msgid "Signaux additionnels" +msgstr "Additional signals" + +#: assets/views/core.py:547 +msgid "Statut des carrefours" +msgstr "Intersections status" + +#: assets/views/core.py:640 +msgid "Niveau de qualité" +msgstr "Quality level" + +#: assets/views/export.py:94 +msgid "ID" +msgstr "ID" + +#: assets/views/export.py:100 +msgid "Obsolescence" +msgstr "Obsolescence" + +#: assets/views/export.py:101 +msgid "Code localisation" +msgstr "Location code" + +#: assets/views/export.py:102 +msgid "Nom localisation" +msgstr "Location name" + +#: assets/views/export.py:104 +msgid "Date de déinstallation" +msgstr "Deinstallation date" + +#: assets/views/export.py:106 +msgid "Garantie (mois)" +msgstr "Warranty (months)" + +#: assets/views/export.py:133 +msgid "Paramètre 'ids' invalide." +msgstr "Invalid parameter 'ids'." + +#: assets/views/export.py:136 +msgid "Aucun asset sélectionné." +msgstr "No asset selected." + +#: assets/views/export.py:168 +msgid "Export XLSX indisponible (dépendance manquante)." +msgstr "XLSX export unavailable (missing dependency)." + +#: assets/views/history.py:27 assets/views/history.py:89 +#: assets/views/history.py:112 assets/views/history.py:318 +msgid "Authentication required" +msgstr "Authentication required" + +#: assets/views/history.py:35 +msgid "Cannot determine asset's thematic" +msgstr "Cannot determine asset's thematic" + +#: assets/views/history.py:50 +msgid "You do not have permission to view assets for this thematic" +msgstr "You do not have permission to view assets for this thematic" + +#: assets/views/history.py:148 assets/views/history.py:149 +#: assets/views/history.py:280 assets/views/history.py:281 +msgid "Système" +msgstr "System" + +#: assets/views/history.py:196 +msgid "You do not have access to any thematics" +msgstr "You do not have access to any thematics" + +#: assets/views/history.py:224 +msgid "You do not have permission to view this thematic" +msgstr "You do not have permission to view this thematic" + +#: assets/views/history.py:241 +msgid "Thematic not found" +msgstr "Thematic not found" + +#: assets/views/history.py:326 +msgid "Paramètre 'date' manquant" +msgstr "Missing 'date' parameter" + +#: assets/views/history.py:342 +msgid "Format de date invalide. Utilisez ISO 8601." +msgstr "Invalid date format. Use ISO 8601." + +#: assets/views/history.py:426 +msgid "Inexistant" +msgstr "Nonexistent" + +#: assets/views/its.py:404 assets/views/its.py:415 assets/views/nature.py:463 +#: assets/views/nature.py:543 assets/views/publiclighting.py:454 +#: assets/views/publiclighting.py:475 assets/views/publiclighting.py:496 +#: assets/views/publiclighting.py:506 assets/views/roads.py:412 +#: assets/views/roads.py:416 assets/views/roads.py:420 +#: assets/views/roads.py:424 assets/views/roads.py:428 +#: assets/views/roads.py:441 assets/views/roads.py:450 +#: assets/views/roads.py:459 assets/views/roads.py:468 +#: assets/views/roads.py:477 assets/views/sign.py:676 assets/views/sign.py:680 +#: assets/views/sign.py:721 assets/views/sign.py:730 +#: assets/views/structures.py:439 assets/views/structures.py:450 +#: assets/views/trafficlights.py:1193 assets/views/trafficlights.py:1207 +#: assets/views/water.py:223 +msgid "Trop d'objets à afficher" +msgstr "Too many objects to display" + +#: assets/views/its.py:415 assets/views/nature.py:543 +#: assets/views/publiclighting.py:506 assets/views/roads.py:441 +#: assets/views/roads.py:450 assets/views/roads.py:459 +#: assets/views/roads.py:468 assets/views/roads.py:477 assets/views/sign.py:721 +#: assets/views/sign.py:730 assets/views/structures.py:450 +#: assets/views/trafficlights.py:1207 +msgid "Limitez votre sélection." +msgstr "Limit your selection." + +#: assets/views/location_editing.py:46 +msgid "Authentification requise." +msgstr "Authentication required." + +#: assets/views/location_editing.py:48 assets/views/location_editing.py:59 +msgid "Permission refusée." +msgstr "Permission denied." + +#: assets/views/locations.py:25 assets/views/water.py:86 +msgid "Ouvrages" +msgstr "Structures" + +#: assets/views/locations.py:26 +msgid "Feux" +msgstr "Traffic lights" + +#: assets/views/locations.py:27 +msgid "Éclairage" +msgstr "Lighting" + +#: assets/views/locations.py:28 +msgid "Nature" +msgstr "Nature" + +#: assets/views/locations.py:30 +msgid "Centres de contrôle" +msgstr "Control centers" + +#: assets/views/locations.py:31 +msgid "Signalisation" +msgstr "Signage" + +#: assets/views/locations.py:32 +msgid "Voirie" +msgstr "Road" + +#: assets/views/nature.py:766 +msgid "Aucune section trouvée" +msgstr "No section found" + +#: assets/views/nature.py:797 assets/views/nature.py:838 +msgid "Aucun asset sélectionné" +msgstr "No asset selected" + +#: assets/views/nature.py:911 +msgid "Aucune modification fournie" +msgstr "No modification provided" + +#: assets/views/nature.py:1252 +msgid "Commentaire enregistré avec succès" +msgstr "Comment saved successfully" + +#: assets/views/nature.py:1280 +msgid "Commentaire supprimé avec succès" +msgstr "Comment deleted successfully" + +#: assets/views/nature.py:1331 +msgid "Photos ajoutées avec succès" +msgstr "Photos added successfully" + +#: assets/views/publiclighting.py:549 assets/views/roads.py:655 +#: assets/views/sign.py:908 +msgid "Aucune rue trouvée" +msgstr "No street found" + +#: assets/views/structures.py:1181 +msgid "Aucune localisation structure trouvée" +msgstr "No structure location found" + +#: assets/views/trafficlights.py:202 +msgid "Intersections" +msgstr "Intersections" + +#: assets/views/trafficlights.py:602 +msgid "Contrat requis." +msgstr "Contract required." + +#: assets/views/trafficlights.py:1116 +msgid "Date de mise en service invalide." +msgstr "Invalid commissioning date." + +#: assets/views/trafficlights.py:1323 assets/views/trafficlights.py:1351 +msgid "Intersection introuvable" +msgstr "Intersection not found" + +#: assets/views/trafficlights.py:1384 +msgid "Aucune intersection trouvée" +msgstr "No intersection found" + +#: assets/views/trafficlights.py:1849 +msgid "Veuillez sélectionner un fichier DXF ou en téléverser un." +msgstr "" + +#: assets/views/trafficlights.py:1867 +msgid "Le fichier sélectionné n'existe pas." +msgstr "" + +#: assets/views/trafficlights.py:3401 +msgid "Veuillez téléverser un ou plusieurs fichiers Excel de plan de croix." +msgstr "" + +#: assets/views/trafficlights.py:3577 +msgid "Absent du fichier Excel (Sera conservé ou archivé)" +msgstr "" + +#: assets/views/trafficlights.py:3622 assets/views/trafficlights.py:3684 +#: assets/views/trafficlights.py:3746 +#, fuzzy +#| msgid "Aucun fichier de log" +msgid "Absent du fichier Excel" +msgstr "No log file" + +#: assets/views/water.py:85 +msgid "Zones GIEP" +msgstr "" + +#: assets/views/water.py:572 +#, fuzzy +#| msgid "Aucune localisation trouvée" +msgid "Aucune localisation GIEP trouvée à proximité" +msgstr "No location found" + +#~ msgid "Noue" +#~ msgstr "Bioswale / swale" + +#~ msgid "Continu poreux" +#~ msgstr "Porous continuous" + +#~ msgid "Impermeable" +#~ msgstr "Impermeable" + +#~ msgid "Type d'EVC" +#~ msgstr "Type of EVC" + +#~ msgid "Type de revêtement" +#~ msgstr "Pavement type" + +#~ msgid "Espaces verts creux" +#~ msgstr "Depressed green spaces" + +#~ msgid "Armoire électrique" +#~ msgstr "Electrical cabinet" + +#~ msgid "Sélectionner un modèle" +#~ msgstr "Select a model" + +#~ msgid "Hauteur" +#~ msgstr "Height" + +#~ msgid "Alimentation" +#~ msgstr "Power supply" diff --git a/streetup/assets/locale/en/LC_MESSAGES/djangojs.mo b/streetup/assets/locale/en/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..214e901 Binary files /dev/null and b/streetup/assets/locale/en/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/assets/locale/en/LC_MESSAGES/djangojs.po b/streetup/assets/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..be14088 --- /dev/null +++ b/streetup/assets/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,579 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: assets/static/assets/asset_add_modal.js:52 +#: assets/static/assets/asset_location_edit.js:59 +#: assets/static/assets/unified_index.js:115 +#: assets/static/assets/unified_index.js:168 +#: assets/static/assets/unified_index.js:224 +msgid "Chargement..." +msgstr "Loading..." + +#: assets/static/assets/asset_add_modal.js:56 +#: assets/static/assets/asset_add_modal.js:426 +#: assets/static/assets/unified_index.js:114 +#: assets/static/assets/unified_index.js:220 +#: assets/static/assets/unified_index.js:262 +msgid "Sélectionner d'abord une catégorie..." +msgstr "Select a category first..." + +#: assets/static/assets/asset_add_modal.js:65 +#: assets/static/assets/unified_index.js:230 +msgid "Sélectionner un modèle..." +msgstr "Select a model..." + +#: assets/static/assets/asset_add_modal.js:77 +#: assets/static/assets/unified_index.js:243 +msgid "Aucun modèle disponible" +msgstr "No model available" + +#: assets/static/assets/asset_add_modal.js:82 +#: assets/static/assets/asset_location_edit.js:102 +#: assets/static/assets/unified_index.js:134 +#: assets/static/assets/unified_index.js:205 +#: assets/static/assets/unified_index.js:247 +msgid "Erreur de chargement" +msgstr "Loading error" + +#: assets/static/assets/asset_add_modal.js:90 +#: assets/static/assets/unified_index.js:848 +#: assets/static/assets/unified_index.js:935 +msgid "Création..." +msgstr "Creating..." + +#: assets/static/assets/asset_add_modal.js:146 +#: assets/static/assets/asset_add_modal.js:350 +msgid "Erreur lors de la création" +msgstr "Error during creation" + +#: assets/static/assets/asset_add_modal.js:156 +#: assets/static/assets/asset_add_modal.js:359 +msgid "Erreur de connexion" +msgstr "Connection error" + +#: assets/static/assets/asset_add_modal.js:221 +msgid "Sélection de template d'opérations" +msgstr "Operation template selection" + +#: assets/static/assets/asset_add_modal.js:223 +msgid "Fermer" +msgstr "Close" + +#: assets/static/assets/asset_add_modal.js:226 +msgid "Choisissez le(s) template(s) d'opérations à appliquer à cet équipement." +msgstr "Choose the operation template(s) to apply to this asset." + +#: assets/static/assets/asset_add_modal.js:228 +msgid "Choisir le(s) template(s) d'opérations :" +msgstr "Choose operation template(s):" + +#: assets/static/assets/asset_add_modal.js:242 +msgid "Tout sélectionner" +msgstr "Select all" + +#: assets/static/assets/asset_add_modal.js:245 +msgid "Tout désélectionner" +msgstr "Deselect all" + +#: assets/static/assets/asset_add_modal.js:253 +msgid "Ignorer et continuer" +msgstr "Ignore and continue" + +#: assets/static/assets/asset_add_modal.js:256 +msgid "Créer les opérations" +msgstr "Create operations" + +#: assets/static/assets/asset_add_modal.js:317 +msgid "Création en cours..." +msgstr "Creating..." + +#: assets/static/assets/asset_location_edit.js:37 +msgid "Saisissez au moins 2 caractères pour rechercher." +msgstr "Enter at least 2 characters to search." + +#: assets/static/assets/asset_location_edit.js:76 +#: assets/static/assets/unified_index.js:177 +msgid "Sélectionner une localisation..." +msgstr "Select location..." + +#: assets/static/assets/asset_location_edit.js:87 +msgid "Résultats limités. Affinez la recherche pour trouver plus facilement." +msgstr "Limited results. Refine your search to find more easily." + +#: assets/static/assets/asset_location_edit.js:92 +#: assets/static/assets/unified_index.js:196 +msgid "Aucune localisation disponible" +msgstr "No location available" + +#: assets/static/assets/asset_location_edit.js:94 +msgid "Aucun résultat pour cette recherche." +msgstr "No results for this search." + +#: assets/static/assets/asset_location_edit.js:104 +msgid "Erreur lors du chargement des localisations" +msgstr "Error during loading locations" + +#: assets/static/assets/asset_location_edit.js:117 +msgid "Veuillez sélectionner une localisation" +msgstr "Please select a location" + +#: assets/static/assets/asset_location_edit.js:124 +msgid "Mise à jour..." +msgstr "Updating..." + +#: assets/static/assets/asset_location_edit.js:149 +msgid "Localisation mise à jour" +msgstr "Location updated" + +#: assets/static/assets/asset_location_edit.js:154 +#: assets/static/assets/asset_location_edit.js:161 +msgid "Erreur lors de la mise à jour" +msgstr "Error during updating" + +#: assets/static/assets/assets_index.js:568 +#: assets/static/assets/assets_index.js:592 +#: assets/static/assets/unified_index.js:499 +msgid "Code" +msgstr "Code" + +#: assets/static/assets/assets_index.js:594 +#: assets/static/assets/green-surfaces-comments.js:953 +#: assets/static/assets/unified_index.js:510 +msgid "Statut" +msgstr "Status" + +#: assets/static/assets/assets_index.js:777 +msgid "%(count)s localisation sélectionnée" +msgid_plural "%(count)s localisations sélectionnées" +msgstr[0] "%(count)s location selected" +msgstr[1] "%(count)s locations selected" + +#: assets/static/assets/assets_index.js:789 +msgid "%(count)s asset sélectionné" +msgid_plural "%(count)s assets sélectionnés" +msgstr[0] "%(count)s asset selected" +msgstr[1] "%(count)s assets selected" + +#: assets/static/assets/assets_index.js:880 +msgid "Sélectionner l'asset" +msgstr "Select asset" + +#: assets/static/assets/assets_index.js:886 +msgid "Désélectionner l'asset" +msgstr "Deselect asset" + +#: assets/static/assets/assets_index.js:997 +#: assets/static/assets/assets_index.js:1068 +msgid "Erreur lors de la récupération des données." +msgstr "Error while retrieving data." + +#: assets/static/assets/assets_index.js:1100 +msgid "" +"Sélectionnez des localisations ou lancez une recherche pour afficher les " +"assets." +msgstr "" + +#: assets/static/assets/assets_index.js:1102 +msgid "Aucun asset ne correspond aux critères de recherche." +msgstr "No asset matches the search criteria." + +#: assets/static/assets/assets_index.js:1309 +#: assets/static/assets/unified_index.js:2571 +#: assets/static/assets/unified_index.js:3330 +#: assets/static/assets/unified_index.js:3398 +msgid "Sans catégorie" +msgstr "Uncategorized" + +#: assets/static/assets/assets_index.js:1330 +#: assets/static/assets/unified_index.js:3424 +msgid "%(count)s élément pris en compte" +msgid_plural "%(count)s éléments pris en compte" +msgstr[0] "%(count)s item included" +msgstr[1] "%(count)s items included" + +#: assets/static/assets/assets_index.js:1338 +#: assets/static/assets/unified_index.js:3429 +msgid "(sélection)" +msgstr "(selection)" + +#: assets/static/assets/assets_index.js:1339 +#: assets/static/assets/unified_index.js:3429 +msgid "(table)" +msgstr "(table)" + +#: assets/static/assets/assets_index.js:1350 +#: assets/static/assets/unified_index.js:3434 +msgid "Aucune donnée disponible" +msgstr "No data available" + +#: assets/static/assets/assets_index.js:1383 +#: assets/static/assets/unified_index.js:3465 +msgid "au total" +msgstr "in total" + +#: assets/static/assets/assets_index.js:1389 +#: assets/static/assets/unified_index.js:509 +#: assets/static/assets/unified_index.js:3471 +msgid "Modèle" +msgstr "Model" + +#: assets/static/assets/assets_index.js:1390 +#: assets/static/assets/unified_index.js:3472 +msgid "Nombre" +msgstr "Number" + +#: assets/static/assets/assets_index.js:1391 +#: assets/static/assets/unified_index.js:3473 +msgid "Longueur (m)" +msgstr "Length (m)" + +#: assets/static/assets/assets_index.js:1392 +#: assets/static/assets/unified_index.js:3474 +msgid "Surface (m²)" +msgstr "Area (m²)" + +#: assets/static/assets/assets_index.js:1396 +#: assets/static/assets/unified_index.js:3477 +msgid "Aucun modèle" +msgstr "No model" + +#: assets/static/assets/assets_index.js:1400 +#: assets/static/assets/unified_index.js:3480 +msgid "Total catégorie" +msgstr "Category total" + +#: assets/static/assets/green-surfaces-comments.js:473 +#: assets/static/assets/green-surfaces-comments.js:902 +msgid "Nouveau commentaire" +msgstr "New comment" + +#: assets/static/assets/green-surfaces-comments.js:480 +msgid "Textes rapides" +msgstr "Quick texts" + +#: assets/static/assets/green-surfaces-comments.js:482 +#: assets/static/assets/green-surfaces-comments.js:483 +#: assets/static/assets/green-surfaces-comments.js:956 +msgid "Mémo" +msgstr "Memo" + +#: assets/static/assets/green-surfaces-comments.js:485 +#: assets/static/assets/green-surfaces-comments.js:486 +msgid "Point d'attention" +msgstr "Point of attention" + +#: assets/static/assets/green-surfaces-comments.js:488 +#: assets/static/assets/green-surfaces-comments.js:489 +#: assets/static/assets/green-surfaces-comments.js:962 +msgid "À faire" +msgstr "To do" + +#: assets/static/assets/green-surfaces-comments.js:491 +#: assets/static/assets/green-surfaces-comments.js:492 +#: assets/static/assets/green-surfaces-comments.js:967 +msgid "OK" +msgstr "OK" + +#: assets/static/assets/green-surfaces-comments.js:494 +#: assets/static/assets/green-surfaces-comments.js:495 +#: assets/static/assets/green-surfaces-comments.js:970 +msgid "Problème" +msgstr "Problem" + +#: assets/static/assets/green-surfaces-comments.js:501 +#: assets/static/assets/green-surfaces-comments.js:977 +msgid "Commentaire" +msgstr "Comment" + +#: assets/static/assets/green-surfaces-comments.js:502 +msgid "Votre commentaire..." +msgstr "Your comment..." + +#: assets/static/assets/green-surfaces-comments.js:506 +#: assets/static/assets/green-surfaces-comments.js:990 +msgid "Photos" +msgstr "Photos" + +#: assets/static/assets/green-surfaces-comments.js:509 +msgid "Prendre des photos (mode continu)" +msgstr "Take photos (continuous mode)" + +#: assets/static/assets/green-surfaces-comments.js:512 +msgid "Sélectionner des fichiers" +msgstr "Select files" + +#: assets/static/assets/green-surfaces-comments.js:521 +msgid "Position" +msgstr "Position" + +#: assets/static/assets/green-surfaces-comments.js:525 +msgid "Position actuelle sera utilisée" +msgstr "Current position will be used" + +#: assets/static/assets/green-surfaces-comments.js:528 +#: assets/static/assets/green-surfaces-comments.js:985 +msgid "Repositionner" +msgstr "Reposition" + +#: assets/static/assets/green-surfaces-comments.js:536 +#: assets/static/assets/green-surfaces-comments.js:1008 +msgid "Annuler" +msgstr "Cancel" + +#: assets/static/assets/green-surfaces-comments.js:538 +#: assets/static/assets/green-surfaces-comments.js:1005 +msgid "Enregistrer" +msgstr "Save" + +#: assets/static/assets/green-surfaces-comments.js:570 +msgid "Caméra" +msgstr "Camera" + +#: assets/static/assets/green-surfaces-comments.js:585 +#: assets/static/assets/green-surfaces-comments.js:1127 +msgid "Terminer" +msgstr "Finish" + +#: assets/static/assets/green-surfaces-comments.js:588 +#: assets/static/assets/green-surfaces-comments.js:1124 +msgid "Capturer" +msgstr "Capture" + +#: assets/static/assets/green-surfaces-comments.js:959 +msgid "Attention" +msgstr "Warning" + +#: assets/static/assets/green-surfaces-comments.js:977 +msgid "(optionnel)" +msgstr "(optional)" + +#: assets/static/assets/green-surfaces-comments.js:978 +msgid "Entrez un commentaire optionnel..." +msgstr "Enter an optional comment..." + +#: assets/static/assets/green-surfaces-comments.js:982 +msgid "Localisation" +msgstr "Location" + +#: assets/static/assets/green-surfaces-comments.js:983 +msgid "Calcul en cours..." +msgstr "Calculating..." + +#: assets/static/assets/green-surfaces-comments.js:993 +msgid "Appareil photo" +msgstr "Camera" + +#: assets/static/assets/green-surfaces-comments.js:996 +msgid "Sélectionner fichiers" +msgstr "Select files" + +#: assets/static/assets/unified_index.js:121 +#: assets/static/assets/unified_index.js:258 +msgid "Sélectionner une catégorie..." +msgstr "Select a category..." + +#: assets/static/assets/unified_index.js:130 +msgid "Aucune catégorie disponible" +msgstr "No category available" + +#: assets/static/assets/unified_index.js:170 +msgid "Sélectionnez la localisation liée à ce nouvel asset." +msgstr "Select the location linked to this new asset." + +#: assets/static/assets/unified_index.js:200 +msgid "Aucune localisation disponible pour créer cet asset." +msgstr "No location available to create this asset." + +#: assets/static/assets/unified_index.js:209 +msgid "Impossible de charger les localisations pour cette thématique." +msgstr "Unable to load locations for this topic." + +#: assets/static/assets/unified_index.js:294 +msgid "Créer un asset" +msgstr "Create an asset" + +#: assets/static/assets/unified_index.js:308 +msgid "Le code est généré automatiquement." +msgstr "The code is generated automatically." + +#: assets/static/assets/unified_index.js:444 +#: assets/static/assets/unified_index.js:449 +msgid "Erreur lors du chargement du formulaire." +msgstr "Error while loading the form." + +#: assets/static/assets/unified_index.js:466 +msgid "Créer une localisation" +msgstr "Create a location" + +#: assets/static/assets/unified_index.js:467 +#: assets/static/assets/unified_index.js:506 +msgid "Type" +msgstr "Type" + +#: assets/static/assets/unified_index.js:490 +msgid "Zoom" +msgstr "Zoom" + +#: assets/static/assets/unified_index.js:505 +msgid "Nom" +msgstr "Name" + +#: assets/static/assets/unified_index.js:507 +msgid "Thématique" +msgstr "Topic" + +#: assets/static/assets/unified_index.js:508 +msgid "Catégorie" +msgstr "Category" + +#: assets/static/assets/unified_index.js:783 +#: assets/static/assets/unified_index.js:784 +msgid "Sélectionner une thématique..." +msgstr "Select a topic..." + +#: assets/static/assets/unified_index.js:783 +#: assets/static/assets/unified_index.js:784 +msgid "Aucune thématique disponible" +msgstr "No topic available" + +#: assets/static/assets/unified_index.js:791 +msgid "Vous n'avez pas de droits de création" +msgstr "You do not have creation rights" + +#: assets/static/assets/unified_index.js:809 +msgid "Sélectionnez une thématique pour créer un asset." +msgstr "Select a topic to create an asset." + +#: assets/static/assets/unified_index.js:821 +msgid "Sélectionnez une thématique pour créer une localisation." +msgstr "Select a topic to create a location." + +#: assets/static/assets/unified_index.js:840 +msgid "Sélectionnez une localisation avant de créer cet asset." +msgstr "Select a location before creating this asset." + +#: assets/static/assets/unified_index.js:871 +msgid "Type d'asset introuvable pour ce modèle." +msgstr "Asset type not found for this model." + +#: assets/static/assets/unified_index.js:901 +msgid "Asset créé." +msgstr "Asset created." + +#: assets/static/assets/unified_index.js:912 +#: assets/static/assets/unified_index.js:959 +msgid "Erreur lors de la création." +msgstr "Error during creation." + +#: assets/static/assets/unified_index.js:916 +#: assets/static/assets/unified_index.js:963 +msgid "Erreur de connexion." +msgstr "Connection error." + +#: assets/static/assets/unified_index.js:952 +msgid "Localisation créée." +msgstr "Location created." + +#: assets/static/assets/unified_index.js:1359 +msgid "Veuillez sélectionner au moins un asset." +msgstr "Please select at least one asset." + +#: assets/static/assets/unified_index.js:1423 +msgid "" +"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre " +"pour charger le tableau et la carte." +msgstr "" + +#: assets/static/assets/unified_index.js:1671 +#: assets/static/assets/unified_index.js:1702 +msgid "Voir dans le tableau" +msgstr "View in table" + +#: assets/static/assets/unified_index.js:1711 +msgid "autre(s) asset(s)" +msgstr "other asset(s)" + +#: assets/static/assets/unified_index.js:1715 +msgid "autre(s) localisation(s)" +msgstr "other location(s)" + +#: assets/static/assets/unified_index.js:1720 +msgid "Assets trouvés" +msgstr "Assets found" + +#: assets/static/assets/unified_index.js:1728 +msgid "Localisations trouvées" +msgstr "Locations found" + +#: assets/static/assets/unified_index.js:2209 +msgid "Vue initiale (Bruxelles)" +msgstr "Initial view (Brussels)" + +#: assets/static/assets/unified_index.js:2349 +msgid "sélectionné(s)" +msgstr "selected" + +#: assets/static/assets/unified_index.js:2889 +msgid "Aucun type sélectionné" +msgstr "No type selected" + +#: assets/static/assets/unified_index.js:2893 +msgid "Aucun type disponible." +msgstr "No type available." + +#: assets/static/assets/unified_index.js:3034 +#: assets/static/assets/unified_index.js:3204 +#: assets/static/assets/unified_index.js:3262 +msgid "Sélectionnez d'abord une ou plusieurs thématiques." +msgstr "Select one or more topics first." + +#: assets/static/assets/unified_index.js:3104 +msgid "Tous" +msgstr "All" + +#: assets/static/assets/unified_index.js:3269 +msgid "Sélectionnez d'abord une ou plusieurs catégories." +msgstr "Select one or more categories first." + +#: assets/static/assets/unified_index.js:3285 +msgid "Chargement des modèles..." +msgstr "Loading models..." + +#: assets/static/assets/unified_index.js:3296 +msgid "Aucun modèle disponible pour les catégories sélectionnées." +msgstr "No model available for the selected categories." + +#: assets/static/assets/unified_index.js:3307 +msgid "Afficher les modèles" +msgstr "Show models" + +#: assets/static/assets/unified_index.js:3308 +msgid "Replier les modèles" +msgstr "Collapse models" + +#: assets/static/assets/unified_index.js:3313 +msgid "" +"Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options." +msgstr "" + +#~ msgid "Erreur lors de la liaison" +#~ msgstr "Error during linking" + +#~ msgid "Erreur lors de la désélection" +#~ msgstr "Error during deselection" diff --git a/streetup/assets/locale/fr/LC_MESSAGES/django.mo b/streetup/assets/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..d7f48f0 Binary files /dev/null and b/streetup/assets/locale/fr/LC_MESSAGES/django.mo differ diff --git a/streetup/assets/locale/fr/LC_MESSAGES/django.po b/streetup/assets/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..104e1d1 --- /dev/null +++ b/streetup/assets/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,5754 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:26+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: models/clean.py:16 +msgid "Cemented to ground" +msgstr "Cemented to ground" + +#: models/clean.py:17 +msgid "Bolted to ground" +msgstr "Bolted to ground" + +#: models/clean.py:18 +msgid "Pole-mounted" +msgstr "Pole-mounted" + +#: models/clean.py:19 +msgid "Wall-mounted" +msgstr "Wall-mounted" + +#: models/clean.py:20 +msgid "Freestanding" +msgstr "Freestanding" + +#: models/clean.py:21 +msgid "Other" +msgstr "Other" + +#: models/clean.py:25 +msgid "Clear glass" +msgstr "Clear glass" + +#: models/clean.py:26 +msgid "Coloured glass" +msgstr "Coloured glass" + +#: models/clean.py:27 +msgid "Mixed" +msgstr "Mixed" + +#: models/clean.py:82 models/clean.py:118 models/clean.py:155 +#: models/clean.py:194 +msgid "Colour" +msgstr "Colour" + +#: models/clean.py:83 models/clean.py:156 +msgid "RAL code" +msgstr "RAL code" + +#: models/clean.py:84 models/clean.py:119 models/clean.py:157 +#: models/clean.py:195 +msgid "Material" +msgstr "Material" + +#: models/clean.py:85 models/clean.py:120 +msgid "Volume (litres)" +msgstr "Volume (litres)" + +#: models/clean.py:86 models/clean.py:121 +msgid "Cover" +msgstr "Cover" + +#: models/clean.py:87 models/clean.py:122 +msgid "Built-in ashtray" +msgstr "Built-in ashtray" + +#: models/clean.py:90 +msgid "Litter bin model" +msgstr "Litter bin model" + +#: models/clean.py:91 +msgid "Litter bin models" +msgstr "Litter bin models" + +#: models/clean.py:104 models/clean.py:177 +msgid "Location" +msgstr "Location" + +#: models/clean.py:110 models/clean.py:183 templates/assets/clean_index.html:28 +#: templates/assets/clean_index.html:74 +msgid "Model" +msgstr "Model" + +#: models/clean.py:116 +msgid "Fixation type" +msgstr "Fixation type" + +#: models/clean.py:123 +msgid "Built-in extinguisher stub (éteignoir)" +msgstr "Built-in extinguisher stub (éteignoir)" + +#: models/clean.py:126 models/clean.py:198 +msgid "Emptying frequency" +msgstr "Emptying frequency" + +#: models/clean.py:130 +msgid "Collection route (tournée)" +msgstr "Collection route (tournée)" + +#: models/clean.py:134 +msgid "External ID (source system)" +msgstr "External ID (source system)" + +#: models/clean.py:136 models/clean.py:204 +msgid "Remarks" +msgstr "Remarks" + +#: models/clean.py:139 models/clean.py:238 models/clean.py:305 +msgid "Litter bin" +msgstr "Litter bin" + +#: models/clean.py:140 templates/assets/clean_location_detail.html:72 +#: templates/assets/clean_location_detail.html:110 +msgid "Litter bins" +msgstr "Litter bins" + +#: models/clean.py:158 models/clean.py:191 +msgid "Capacity (litres)" +msgstr "Capacity (litres)" + +#: models/clean.py:159 models/clean.py:192 +#: templates/assets/clean_location_detail.html:96 +msgid "Underground" +msgstr "Underground" + +#: models/clean.py:160 models/clean.py:193 +msgid "Number of openings" +msgstr "Number of openings" + +#: models/clean.py:163 +msgid "Glass container model" +msgstr "Glass container model" + +#: models/clean.py:164 +msgid "Glass container models" +msgstr "Glass container models" + +#: models/clean.py:189 +msgid "Accepted glass type" +msgstr "Accepted glass type" + +#: models/clean.py:202 +msgid "Operator / manager" +msgstr "Operator / manager" + +#: models/clean.py:207 +msgid "Glass container" +msgstr "Glass container" + +#: models/clean.py:208 templates/assets/clean_location_detail.html:88 +#: templates/assets/clean_location_detail.html:124 +msgid "Glass containers" +msgstr "Glass containers" + +#: models/clean.py:243 +msgid "Photo" +msgstr "Photo" + +#: models/clean.py:248 +msgid "Source URL (original)" +msgstr "Source URL (original)" + +#: models/clean.py:250 +msgid "Order" +msgstr "Order" + +#: models/clean.py:251 +msgid "Taken at" +msgstr "Taken at" + +#: models/clean.py:252 +msgid "Caption" +msgstr "Caption" + +#: models/clean.py:255 +msgid "Litter bin photo" +msgstr "Litter bin photo" + +#: models/clean.py:256 +msgid "Litter bin photos" +msgstr "Litter bin photos" + +#: models/clean.py:268 templates/assets/cleanlitterbin_detail.html:250 +#: templates/assets/cleanlitterbin_detail.html:378 +msgid "Important – ponctuel" +msgstr "Important – ponctuel" + +#: models/clean.py:269 templates/assets/cleanlitterbin_detail.html:251 +#: templates/assets/cleanlitterbin_detail.html:378 +msgid "Important – continu" +msgstr "Important – continu" + +#: models/clean.py:270 models/clean.py:277 +#: templates/assets/cleanlitterbin_detail.html:252 +#: templates/assets/cleanlitterbin_detail.html:262 +#: templates/assets/cleanlitterbin_detail.html:378 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Faible" +msgstr "Faible" + +#: models/clean.py:271 models/clean.py:295 models/roads.py:275 +#: models/roads.py:287 models/roads.py:297 models/trafficlights.py:203 +#: templates/assets/cleanlitterbin_detail.html:253 +#: templates/assets/cleanlitterbin_detail.html:303 +#: templates/assets/cleanlitterbin_detail.html:378 +#: templates/assets/cleanlitterbin_detail.html:401 +#: templates/assets/trafficlights_intersection_detail.html:1220 +msgid "Autre" +msgstr "Autre" + +#: models/clean.py:275 templates/assets/cleanlitterbin_detail.html:260 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Élevée" +msgstr "Élevée" + +#: models/clean.py:276 templates/assets/cleanlitterbin_detail.html:261 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Moyenne" +msgstr "Moyenne" + +#: models/clean.py:281 templates/assets/cleanlitterbin_detail.html:165 +#: templates/assets/cleanlitterbin_detail.html:175 +#: templates/assets/cleanlitterbin_detail.html:269 +#: templates/assets/cleanlitterbin_detail.html:278 +#: templates/assets/cleanlitterbin_detail.html:380 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Normal" +msgstr "Normal" + +#: models/clean.py:282 templates/assets/cleanlitterbin_detail.html:166 +#: templates/assets/cleanlitterbin_detail.html:176 +#: templates/assets/cleanlitterbin_detail.html:270 +#: templates/assets/cleanlitterbin_detail.html:279 +#: templates/assets/cleanlitterbin_detail.html:380 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Difficile" +msgstr "Difficile" + +#: models/clean.py:283 templates/assets/cleanlitterbin_detail.html:167 +#: templates/assets/cleanlitterbin_detail.html:177 +#: templates/assets/cleanlitterbin_detail.html:271 +#: templates/assets/cleanlitterbin_detail.html:280 +#: templates/assets/cleanlitterbin_detail.html:380 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Impossible" +msgstr "Impossible" + +#: models/clean.py:287 templates/assets/cleanlitterbin_detail.html:295 +#: templates/assets/cleanlitterbin_detail.html:393 +msgid "Arrêt Bus/Tram" +msgstr "Arrêt Bus/Tram" + +#: models/clean.py:288 templates/assets/cleanlitterbin_detail.html:296 +#: templates/assets/cleanlitterbin_detail.html:394 +msgid "Parc" +msgstr "Parc" + +#: models/clean.py:289 templates/assets/cleanlitterbin_detail.html:297 +#: templates/assets/cleanlitterbin_detail.html:395 +msgid "École" +msgstr "École" + +#: models/clean.py:290 templates/assets/cleanlitterbin_detail.html:298 +#: templates/assets/cleanlitterbin_detail.html:396 +msgid "Commerce" +msgstr "Commerce" + +#: models/clean.py:291 templates/assets/cleanlitterbin_detail.html:299 +#: templates/assets/cleanlitterbin_detail.html:397 views/artworks.py:420 +#: views/artworks.py:424 +msgid "Monument" +msgstr "Monument" + +#: models/clean.py:292 templates/assets/cleanlitterbin_detail.html:300 +#: templates/assets/cleanlitterbin_detail.html:398 +msgid "Passage piéton" +msgstr "Passage piéton" + +#: models/clean.py:293 models/roads.py:284 +#: templates/assets/cleanlitterbin_detail.html:301 +#: templates/assets/cleanlitterbin_detail.html:399 +msgid "Place" +msgstr "Place" + +#: models/clean.py:294 templates/assets/cleanlitterbin_detail.html:302 +#: templates/assets/cleanlitterbin_detail.html:400 +#: templates/assets/permissions/panel.html:230 +#: templates/assets/permissions/panel.html:256 +#: templates/assets/permissions/type_page.html:173 +#: templates/assets/permissions/type_page.html:196 +#: templates/assets/permissions/type_page.html:374 +#: templates/assets/permissions/type_page.html:397 +#: templates/assets/trafficlights_intersection_detail.html:68 +#: templates/assets/trafficlights_intersection_detail.html:1297 +#: templates/assets/unified_index.html:94 +#: templates/assets/unified_index.html:132 +msgid "Aucun" +msgstr "Aucun" + +#: models/clean.py:307 +msgid "Observation date/time" +msgstr "Observation date/time" + +#: models/clean.py:308 +msgid "Observer notes" +msgstr "Observer notes" + +#: models/clean.py:311 +msgid "Slope too steep" +msgstr "Slope too steep" + +#: models/clean.py:312 +msgid "Unstable ground" +msgstr "Unstable ground" + +#: models/clean.py:313 +msgid "Reflective surface" +msgstr "Reflective surface" + +#: models/clean.py:314 +msgid "Slippery surface" +msgstr "Slippery surface" + +#: models/clean.py:315 +msgid "Cracks in ground" +msgstr "Cracks in ground" + +#: models/clean.py:316 +msgid "Ground obstacle" +msgstr "Ground obstacle" + +#: models/clean.py:317 +msgid "Tactile paving (dalles podotactiles)" +msgstr "Tactile paving (dalles podotactiles)" + +#: models/clean.py:318 +msgid "Transversal slope (%)" +msgstr "Transversal slope (%)" + +#: models/clean.py:319 +msgid "Longitudinal slope (%)" +msgstr "Longitudinal slope (%)" + +#: models/clean.py:320 +msgid "Ground remarks" +msgstr "Ground remarks" + +#: models/clean.py:327 +msgid "Neighbourhood type" +msgstr "Neighbourhood type" + +#: models/clean.py:333 +msgid "Pedestrian flow density" +msgstr "Pedestrian flow density" + +#: models/clean.py:338 +msgid "Points of interest" +msgstr "Points of interest" + +#: models/clean.py:340 +msgid "Clear 2 m approach" +msgstr "Clear 2 m approach" + +#: models/clean.py:341 +msgid "Road to cross" +msgstr "Road to cross" + +#: models/clean.py:342 +msgid "Isolated bin" +msgstr "Isolated bin" + +#: models/clean.py:343 +msgid "Obstructs road traffic" +msgstr "Obstructs road traffic" + +#: models/clean.py:344 +msgid "Obstructs pedestrian flow" +msgstr "Obstructs pedestrian flow" + +#: models/clean.py:345 +msgid "Logical path (itinéraire logique)" +msgstr "Logical path (itinéraire logique)" + +#: models/clean.py:346 +msgid "Visual contrast" +msgstr "Visual contrast" + +#: models/clean.py:347 +msgid "Sorting indicators present" +msgstr "Sorting indicators present" + +#: models/clean.py:352 +msgid "Agent access – vehicle" +msgstr "Agent access – vehicle" + +#: models/clean.py:358 +msgid "Agent access – on foot" +msgstr "Agent access – on foot" + +#: models/clean.py:362 +msgid "Other point of interest" +msgstr "Other point of interest" + +#: models/clean.py:367 +msgid "Placement relevance codes" +msgstr "Placement relevance codes" + +#: models/clean.py:371 +msgid "Obstruction codes" +msgstr "Obstruction codes" + +#: models/clean.py:373 +msgid "Point of interest remarks" +msgstr "Point of interest remarks" + +#: models/clean.py:374 +msgid "Accessibility remarks" +msgstr "Accessibility remarks" + +#: models/clean.py:377 +msgid "Litter bin observation" +msgstr "Litter bin observation" + +#: models/clean.py:378 +msgid "Litter bin observations" +msgstr "Litter bin observations" + +#: models/core.py:42 +msgid "Catégorie parente" +msgstr "Catégorie parente" + +#: models/core.py:43 +msgid "" +"Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne " +"peut pas avoir d'enfants." +msgstr "" +"Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne " +"peut pas avoir d'enfants." + +#: models/core.py:52 +#, fuzzy +#| msgid "Modèle non trouvé" +msgid "Modèles Django autorisés" +msgstr "Modèle non trouvé" + +#: models/core.py:53 +#, fuzzy +#| msgid "Type d'asset non supporté pour cette thématique/catégorie" +msgid "Les types de modèles d'assets (Django) autorisés pour cette catégorie." +msgstr "Type d'asset non supporté pour cette thématique/catégorie" + +#: models/core.py:104 +msgid "À approuver" +msgstr "À approuver" + +#: models/core.py:105 +msgid "Planifié" +msgstr "Planifié" + +#: models/core.py:106 models/trafficlights.py:18 +#: templates/assets/detail_generic.html:1310 views/artworks.py:78 +msgid "Actif" +msgstr "Actif" + +#: models/core.py:107 templates/assets/detail_generic.html:1311 +msgid "Endommagé" +msgstr "Endommagé" + +#: models/core.py:108 templates/assets/detail_generic.html:1312 +msgid "A remplacer" +msgstr "A remplacer" + +#: models/core.py:109 models/trafficlights.py:21 +msgid "Mis hors service volontairement" +msgstr "Mis hors service volontairement" + +#: models/core.py:110 templates/assets/detail_generic.html:1313 +msgid "Enlevé" +msgstr "Enlevé" + +#: models/core.py:111 models/trafficlights.py:23 views/core.py:514 +msgid "Archivé" +msgstr "Archivé" + +#: models/core.py:115 +msgid "Bon état" +msgstr "Bon état" + +#: models/core.py:116 +msgid "Moyen" +msgstr "Moyen" + +#: models/core.py:117 +msgid "Dégradé" +msgstr "Dégradé" + +#: models/core.py:118 +msgid "Critique" +msgstr "Critique" + +#: models/core.py:119 +msgid "Inconnu" +msgstr "Inconnu" + +#: models/core.py:123 +msgid "À valider" +msgstr "À valider" + +#: models/core.py:124 +msgid "Validé" +msgstr "Validé" + +#: models/core.py:125 +msgid "À compléter" +msgstr "À compléter" + +#: models/core.py:126 templates/assets/document_version_history_list.html:41 +msgid "Rejeté" +msgstr "Rejeté" + +#: models/history.py:186 +msgid "Type d'asset" +msgstr "Type d'asset" + +#: models/history.py:189 +msgid "ID de l'asset" +msgstr "ID de l'asset" + +#: models/history.py:200 templates/assets/components/asset_history.html:38 +#: templates/assets/permissions/panel.html:67 +#: templates/assets/permissions/panel.html:109 +#: templates/assets/permissions/panel.html:192 +#: templates/assets/permissions/type_page.html:69 +#: templates/assets/permissions/type_page.html:141 +#: templates/assets/permissions/type_page.html:235 +#: templates/assets/permissions/type_page.html:334 +msgid "Utilisateur" +msgstr "Utilisateur" + +#: models/history.py:205 +msgid "Date de modification" +msgstr "Date de modification" + +#: models/history.py:211 +msgid "Champ modifié" +msgstr "Champ modifié" + +#: models/history.py:216 templates/assets/components/asset_history.html:40 +msgid "Ancienne valeur" +msgstr "Ancienne valeur" + +#: models/history.py:221 templates/assets/components/asset_history.html:41 +msgid "Nouvelle valeur" +msgstr "Nouvelle valeur" + +#: models/history.py:229 +msgid "Création" +msgstr "Création" + +#: models/history.py:230 +msgid "Modification" +msgstr "Modification" + +#: models/history.py:231 templates/assets/cleanlitterbin_detail.html:47 +#: templates/assets/cleanlitterbin_detail.html:343 +msgid "Suppression" +msgstr "Suppression" + +#: models/history.py:232 templates/assets/components/asset_history.html:337 +msgid "Changement de statut" +msgstr "Changement de statut" + +#: models/history.py:234 +msgid "Type d'action" +msgstr "Type d'action" + +#: models/history.py:240 +msgid "Historique d'asset" +msgstr "Historique d'asset" + +#: models/history.py:241 +msgid "Historiques d'assets" +msgstr "Historiques d'assets" + +#: models/history.py:266 +msgid "(vide)" +msgstr "(vide)" + +#: models/history.py:325 templates/assets/clean_index.html:65 +#: templates/assets/clean_index.html:71 +#: templates/assets/clean_location_detail.html:158 +#: templates/assets/components/add_asset_modal.html:70 +#: templates/assets/components/asset_model_card.html:17 +#: templates/assets/controlcenters_location_detail.html:242 +#: templates/assets/detail_generic.html:211 +#: templates/assets/detail_generic.html:578 +#: templates/assets/its_location_detail.html:166 +#: templates/assets/nature_locations_detail.html:72 +#: templates/assets/naturerwiz_detail.html:241 +#: templates/assets/publiclighting_street_detail.html:301 +#: templates/assets/roads_street_detail.html:316 +#: templates/assets/sign_street_detail.html:293 +#: templates/assets/structures_location_detail.html:351 +#: templates/assets/trafficlights_intersection_detail.html:932 +#: templates/assets/unified_index.html:310 views/export.py:95 +msgid "Code" +msgstr "Code" + +#: models/history.py:326 +msgid "Nom (FR)" +msgstr "Nom (FR)" + +#: models/history.py:327 +msgid "Nom (NL)" +msgstr "Nom (NL)" + +#: models/history.py:328 templates/assets/artworks_index.html:8 +#: templates/assets/artworks_index.html:36 +#: templates/assets/controlcenters_location_detail.html:245 +#: templates/assets/detail_generic.html:250 +#: templates/assets/detail_generic.html:579 +#: templates/assets/its_location_detail.html:169 +#: templates/assets/nature_locations_detail.html:75 +#: templates/assets/naturerwiz_detail.html:244 +#: templates/assets/publiclighting_street_detail.html:304 +#: templates/assets/roads_street_detail.html:319 +#: templates/assets/sign_street_detail.html:296 +#: templates/assets/structures_location_detail.html:354 +#: templates/assets/trafficlights_import_cross_plan.html:242 +#: templates/assets/trafficlights_import_cross_plan.html:325 +#: templates/assets/trafficlights_import_cross_plan.html:409 +#: templates/assets/trafficlights_import_cross_plan.html:500 +#: templates/assets/trafficlights_index.html:8 +#: templates/assets/trafficlights_intersection_detail.html:38 +#: templates/assets/trafficlights_intersection_detail.html:935 +#: templates/assets/trafficlights_intersection_detail.html:1171 +#: templates/assets/trafficlights_intersection_detail.html:1262 +#: templates/assets/unified_index.html:195 views/export.py:99 +msgid "Statut" +msgstr "Statut" + +#: models/history.py:329 views/export.py:103 +msgid "Date d'installation" +msgstr "Date d'installation" + +#: models/history.py:330 +msgid "Date de désinstallation" +msgstr "Date de désinstallation" + +#: models/history.py:331 templates/assets/detail_generic.html:749 +#: templates/assets/detail_generic.html:750 views/export.py:105 +msgid "Numéro de série" +msgstr "Numéro de série" + +#: models/history.py:332 templates/assets/detail_generic.html:365 +msgid "Durée de garantie" +msgstr "Durée de garantie" + +#: models/history.py:333 templates/assets/detail_generic.html:394 +msgid "Dernière inspection" +msgstr "Dernière inspection" + +#: models/history.py:334 +msgid "Dernier entretien préventif" +msgstr "Dernier entretien préventif" + +#: models/history.py:335 +msgid "Dernier entretien correctif" +msgstr "Dernier entretien correctif" + +#: models/history.py:336 +msgid "Dernier relevé" +msgstr "Dernier relevé" + +#: models/history.py:337 templates/assets/artworks_index.html:72 +#: templates/assets/components/add_asset_modal.html:34 +#: templates/assets/components/asset_model_card.html:32 +#: templates/assets/components/asset_model_card.html:74 +#: templates/assets/controlcenters_index.html:25 +#: templates/assets/controlcenters_index.html:68 +#: templates/assets/detail_generic.html:308 templates/assets/its_index.html:36 +#: templates/assets/its_index.html:88 templates/assets/nature_index.html:39 +#: templates/assets/nature_index.html:82 +#: templates/assets/publiclighting_index.html:29 +#: templates/assets/publiclighting_index.html:73 +#: templates/assets/roads_index.html:29 templates/assets/roads_index.html:85 +#: templates/assets/sign_index.html:29 templates/assets/sign_index.html:85 +#: templates/assets/structures_index.html:37 +#: templates/assets/structures_index.html:82 +#: templates/assets/trafficlights_index.html:81 +#: templates/assets/trafficlights_index.html:126 +#: templates/assets/trafficlights_intersection_detail.html:979 +#: templates/assets/unified_index.html:115 +#: templates/assets/unified_index.html:291 templates/assets/water_index.html:40 +#: templates/assets/water_index.html:80 views/export.py:97 +msgid "Catégorie" +msgstr "Catégorie" + +#: models/history.py:338 templates/assets/components/asset_model_card.html:26 +#: templates/assets/detail_generic.html:352 +#: templates/assets/detail_generic.html:754 +#: templates/assets/detail_generic.html:755 views/export.py:107 +msgid "Marque" +msgstr "Marque" + +#: models/history.py:339 templates/assets/components/add_asset_modal.html:57 +#: templates/assets/components/asset_model_card.html:6 +#: templates/assets/controlcenters_index.html:35 +#: templates/assets/controlcenters_index.html:69 +#: templates/assets/its_index.html:46 templates/assets/its_index.html:89 +#: templates/assets/nature_index.html:49 templates/assets/nature_index.html:83 +#: templates/assets/publiclighting_index.html:39 +#: templates/assets/publiclighting_index.html:74 +#: templates/assets/roads_index.html:39 templates/assets/roads_index.html:86 +#: templates/assets/sign_index.html:39 templates/assets/sign_index.html:86 +#: templates/assets/structures_index.html:47 +#: templates/assets/structures_index.html:83 +#: templates/assets/trafficlights_index.html:91 +#: templates/assets/trafficlights_index.html:127 +#: templates/assets/trafficlights_intersection_detail.html:628 +#: templates/assets/trafficlights_intersection_detail.html:996 +#: templates/assets/unified_index.html:129 +#: templates/assets/unified_index.html:297 views/export.py:98 +msgid "Modèle" +msgstr "Modèle" + +#: models/history.py:340 templates/assets/components/add_asset_modal.html:101 +#: templates/assets/unified_index.html:260 +#: templates/assets/unified_index.html:303 +msgid "Localisation" +msgstr "Localisation" + +#: models/history.py:341 templates/assets/structures_location_detail.html:16 +msgid "Structure" +msgstr "Structure" + +#: models/history.py:342 templates/assets/detail_generic.html:672 +#: views/export.py:109 +msgid "Longitude" +msgstr "Longitude" + +#: models/history.py:343 templates/assets/detail_generic.html:683 +#: views/export.py:110 +msgid "Latitude" +msgstr "Latitude" + +#: models/history.py:344 +msgid "Géométrie" +msgstr "Géométrie" + +#: models/history.py:345 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: models/nature.py:81 +msgid "Tulipes" +msgstr "Tulipes" + +#: models/nature.py:82 +msgid "Jacynthes" +msgstr "Jacynthes" + +#: models/nature.py:83 +msgid "Jonquilles" +msgstr "Jonquilles" + +#: models/nature.py:84 +msgid "Narcisses" +msgstr "Narcisses" + +#: models/nature.py:85 +msgid "Crocus" +msgstr "Crocus" + +#: models/nature.py:86 +msgid "Perce-neige" +msgstr "Perce-neige" + +#: models/nature.py:87 views/core.py:535 +msgid "Autres" +msgstr "Autres" + +#: models/nature.py:106 +msgid "Types de bulbes" +msgstr "Types de bulbes" + +#: models/nature.py:298 +msgid "Zone de gestion intégrée des eaux pluviales (ZIEP)" +msgstr "ZIEP" + +#: models/nature.py:299 +msgid "Zones de gestion intégrée des eaux pluviales (ZIEP)" +msgstr "ZIEP" + +#: models/nature.py:307 models/nature.py:334 +msgid "Stormwater inlet (Avaloir)" +msgstr "Avaloir" + +#: models/nature.py:308 models/nature.py:335 +msgid "Sequenced border (Bordure séquencée)" +msgstr "Bordure séquencée" + +#: models/nature.py:309 models/nature.py:336 +msgid "Injection port (Bouche d'injection)" +msgstr "Bouche d'injection" + +#: models/nature.py:310 models/nature.py:337 +msgid "Non-return valve (Clapet anti-retour)" +msgstr "Clapet anti-retour" + +#: models/nature.py:311 models/nature.py:338 +msgid "Sedimentation (Empierrement sédimentation)" +msgstr "Empierrement sédimentation" + +#: models/nature.py:312 models/nature.py:339 +msgid "Kerb cell" +msgstr "" + +#: models/nature.py:313 models/nature.py:340 +msgid "Infiltration well (Puits d'infiltration)" +msgstr "Puits d'infiltration" + +#: models/nature.py:314 models/nature.py:341 +msgid "Overflow (Surverse)" +msgstr "Surverse" + +#: models/nature.py:315 models/nature.py:342 +msgid "Isolation valve (Vanne d'isolation)" +msgstr "Vanne d'isolation" + +#: models/nature.py:316 models/nature.py:343 +msgid "Control valve (Vanne de régulation)" +msgstr "Vanne de régulation" + +#: models/nature.py:317 models/nature.py:345 +msgid "Sidewalk gargoyle (Gargouille de trottoir)" +msgstr "Gargouille de trottoir" + +#: models/nature.py:318 models/nature.py:346 +msgid "Trickle of water (Filet d'eau)" +msgstr "Filet d'eau" + +#: models/nature.py:319 models/nature.py:347 +msgid "Rail drain (Avaloir de rail)" +msgstr "Avaloir de rail" + +#: models/nature.py:320 models/nature.py:348 +msgid "Leveled edge (Bordure arasée)" +msgstr "Bordure arasée" + +#: models/nature.py:321 models/nature.py:349 +msgid "Infiltration channel (Caniveau infiltrant)" +msgstr "Caniveau infiltrant" + +#: models/nature.py:322 models/nature.py:350 +#, fuzzy +#| msgid "Road Drain" +msgid "Drain" +msgstr "Road Drain" + +#: models/nature.py:323 models/nature.py:351 +msgid "Overflowing (Trop plein)" +msgstr "Trop plein" + +#: models/nature.py:324 models/nature.py:352 +msgid "Separate sewer system (Réseau égout séparatif)" +msgstr "Réseau égout séparatif" + +#: models/nature.py:325 +#, fuzzy +#| msgid "Espace vert creux" +msgid "Hollow green space (Espace vert creux)" +msgstr "Espace vert creux" + +#: models/nature.py:326 +msgid "Decentralized underground structure (Ouvrage enterré décentralisé)" +msgstr "Ouvrage enterré décentralisé" + +#: models/nature.py:327 +msgid "Work on an impermeable surface (Ouvrage sur surface imperméable)" +msgstr "Ouvrage sur surface imperméable" + +#: models/nature.py:328 +msgid "Alternative technique (Technique alternative)" +msgstr "Technique alternative" + +#: models/nature.py:329 +msgid "Supplementary work (Ouvrage annexe)" +msgstr "Ouvrage annexe" + +#: models/nature.py:354 +#, fuzzy +#| msgid "Bassin sec/d'infiltration" +msgid "Dry/infiltration basin (Bassin sec/infiltration)" +msgstr "Bassin sec/d'infiltration" + +#: models/nature.py:355 +#, fuzzy +#| msgid "Bassin en eau" +msgid "Wet basin (Bassin en eau)" +msgstr "Bassin en eau" + +#: models/nature.py:356 +#, fuzzy +#| msgid "Fossé" +msgid "Ditch (Fossé)" +msgstr "Fossé" + +#: models/nature.py:357 +#, fuzzy +#| msgid "Jardin de pluie" +msgid "Rain garden (Jardin de pluie)" +msgstr "Jardin de pluie" + +#: models/nature.py:358 +msgid "Swale (Noue)" +msgstr "Noue" + +#: models/nature.py:359 +msgid "Draining structure (Massif drainant)" +msgstr "Massif drainant" + +#: models/nature.py:360 +msgid "Draining trench (Tranchée drainante)" +msgstr "Tranchée drainante" + +#: models/nature.py:361 +msgid "Retention roof (Toiture stockante)" +msgstr "Toiture stockante" + +#: models/nature.py:362 +msgid "Green roof (Toiture verte)" +msgstr "Toiture verte" + +#: models/nature.py:363 +msgid "Stormwater basin (Bassin d'orage)" +msgstr "Bassin d'orage" + +#: models/nature.py:364 +msgid "SAUL-empty (SAUL-vide)" +msgstr "SAUL-vide" + +#: models/nature.py:365 +msgid "SAUL-earth (SAUL-terre)" +msgstr "SAUL-terre" + +#: models/nature.py:366 +msgid "Cistern (Citerne)" +msgstr "Citerne" + +#: models/nature.py:367 +msgid "Green space on slab (Espace vert sur dalle)" +msgstr "Espace vert sur dalle" + +#: models/nature.py:368 +msgid "Filter strip (Bande filtrante)" +msgstr "Bande filtrante" + +#: models/nature.py:369 +msgid "Porous concrete collector (Collecteur en béton poreux)" +msgstr "Collecteur en béton poreux" + +#: models/nature.py:384 +msgid "Design (Étude)" +msgstr "Étude" + +#: models/nature.py:385 +#, fuzzy +#| msgid "Construction status" +msgid "Construction (Travaux)" +msgstr "Statut des travaux" + +#: models/nature.py:386 +msgid "Provisional acceptance (Réception provisoire)" +msgstr "Réception provisoire" + +#: models/nature.py:387 +msgid "Final acceptance (Réception définitive)" +msgstr "Réception définitive" + +#: models/nature.py:391 +msgid "Good (Bon)" +msgstr "Bon" + +#: models/nature.py:392 +msgid "Inspection to schedule (Examen à programmer)" +msgstr "Examen à programmer" + +#: models/nature.py:393 +msgid "Curative action needed (Intervention curative nécessaire)" +msgstr "Intervention curative nécessaire" + +#: models/nature.py:394 +msgid "Total rehabilitation needed (Réfection totale nécessaire)" +msgstr "Réfection totale nécessaire" + +#: models/nature.py:395 +msgid "Maintenance works to perform (Travaux entretien à réaliser)" +msgstr "Travaux entretien à réaliser" + +#: models/nature.py:396 +msgid "Maintenance works (Travaux maintenance)" +msgstr "Travaux maintenance" + +#: models/nature.py:397 +msgid "Maintenance works to schedule (Travaux maintenance à programmer)" +msgstr "Travaux maintenance à programmer" + +#: models/nature.py:401 +msgid "Connected (Connecté)" +msgstr "Connecté" + +#: models/nature.py:402 +msgid "Not connected (Non connecté)" +msgstr "Non connecté" + +#: models/nature.py:406 +#, fuzzy +#| msgid "Végétal" +msgid "Vegetal (Végétal)" +msgstr "Végétal" + +#: models/nature.py:407 +#, fuzzy +#| msgid "Structure enherbée" +msgid "Grassed structure (Structure enherbée)" +msgstr "Structure enherbée" + +#: models/nature.py:408 +#, fuzzy +#| msgid "Matériau non agrégé" +msgid "Unbound material (Matériau non agrégé)" +msgstr "Matériau non agrégé" + +#: models/nature.py:409 +#, fuzzy +#| msgid "Structure à remplissage minéral" +msgid "Mineral-filled structure (Structure à remplissage minéral)" +msgstr "Structure à remplissage minéral" + +#: models/nature.py:410 +#, fuzzy +#| msgid "Revêtement surélevé" +msgid "Raised coating (Revêtement surélevé)" +msgstr "Revêtement surélevé" + +#: models/nature.py:411 +#, fuzzy +#| msgid "Modulaire poreux" +msgid "Porous modular (Modulaire poreux)" +msgstr "Modulaire poreux" + +#: models/nature.py:412 +msgid "Porous continuous (Continu poreux)" +msgstr "Continu poreux" + +#: models/nature.py:413 +msgid "Impermeable (Imperméable)" +msgstr "Imperméable" + +#: models/nature.py:417 +msgid "Open soil (Pleine terre)" +msgstr "Pleine terre" + +#: models/nature.py:418 +#, fuzzy +#| msgid "Mélange terre-pierre" +msgid "Soil-stone mixture (Mélange terre-pierre)" +msgstr "Mélange terre-pierre" + +#: models/nature.py:419 +msgid "Grassed wide-joint pavers (Pavés à joints larges enherbés)" +msgstr "Pavés à joints larges enherbés" + +#: models/nature.py:420 +msgid "Concrete grass grids (Dalles gazon en béton)" +msgstr "Dalles gazon en béton" + +#: models/nature.py:421 +msgid "Vegetated cellular tiles (Dalles alvéolaires végétalisées)" +msgstr "Dalles alvéolaires végétalisées" + +#: models/nature.py:422 +msgid "Woodchips/mulch (Mulch végétal)" +msgstr "Mulch végétal" + +#: models/nature.py:423 +msgid "Limestone gravel (Graviers calcaires)" +msgstr "Graviers calcaires" + +#: models/nature.py:424 +msgid "Non-limestone gravel (Graviers non calcaires)" +msgstr "Graviers non calcaires" + +#: models/nature.py:425 +msgid "Dirt ground (Terre battue)" +msgstr "Terre battue" + +#: models/nature.py:426 +msgid "Gravel wide-joint pavers (Pavés à joints larges engravillonnés)" +msgstr "Pavés à joints larges engravillonnés" + +#: models/nature.py:427 +msgid "Gravel cellular tiles (Dalles alvéolaires en gravier)" +msgstr "Dalles alvéolaires en gravier" + +#: models/nature.py:428 +msgid "HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)" +msgstr "Dalles en PEHD remplies de pavé" + +#: models/nature.py:429 +msgid "Grates (Grilles)" +msgstr "Grilles" + +#: models/nature.py:430 +msgid "Wooden decking (Platelages bois)" +msgstr "Platelages bois" + +#: models/nature.py:431 +msgid "Porous concrete pavers (Pavé en béton poreux)" +msgstr "Pavé en béton poreux" + +#: models/nature.py:432 +msgid "Porous concrete slab (Dalle en béton poreux)" +msgstr "Dalle en béton poreux" + +#: models/nature.py:433 +msgid "Porous asphalt (Enrobés bitumineux poreux)" +msgstr "Enrobés bitumineux poreux" + +#: models/nature.py:434 +msgid "Porous concrete (Bétons poreux)" +msgstr "Bétons poreux" + +#: models/nature.py:435 +msgid "Resin-bound gravel (Graviers liés à la résine)" +msgstr "Graviers liés à la résine" + +#: models/nature.py:436 +msgid "Polymer-bound crushed gravel (Graviers concassés liés aux polymères)" +msgstr "Graviers concassés liés aux polymères" + +#: models/nature.py:437 +msgid "EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)" +msgstr "Revêtements EPDM et ESBR" + +#: models/nature.py:438 +msgid "" +"Non-porous cement concrete coatings (Revêtements en béton de ciment non " +"perméables)" +msgstr "" + +#: models/nature.py:439 +msgid "Non-porous asphalt coatings (Revêtements bitumineux non perméables)" +msgstr "Revêtements bitumineux non perméables" + +#: models/nature.py:440 +msgid "Non-porous tiling (Dallages non perméables)" +msgstr "Dallages non perméables" + +#: models/nature.py:441 +msgid "Non-porous paving (Pavages non perméables)" +msgstr "Pavages non perméables" + +#: models/nature.py:442 +msgid "Other non-porous coatings (Autres revêtements non perméables)" +msgstr "Autres revêtements non perméables" + +#: models/nature.py:447 +msgid "Point" +msgstr "" + +#: models/nature.py:448 +msgid "Line" +msgstr "" + +#: models/nature.py:449 +msgid "Polygon" +msgstr "" + +#: models/nature.py:456 +msgid "External ID" +msgstr "Identifiant externe" + +#: models/nature.py:457 +msgid "Code name" +msgstr "Code" + +#: models/nature.py:458 +msgid "Structure type" +msgstr "Type d'ouvrage" + +#: models/nature.py:459 +msgid "Geometry type" +msgstr "Type de géométrie" + +#: models/nature.py:460 +msgid "Project manager" +msgstr "Chef de projet" + +#: models/nature.py:462 +msgid "Construction status" +msgstr "Statut des travaux" + +#: models/nature.py:463 +msgid "Commissioning date" +msgstr "Date de mise en service" + +#: models/nature.py:464 +msgid "Condition" +msgstr "État" + +#: models/nature.py:465 +msgid "Administrator" +msgstr "Administrateur" + +#: models/nature.py:466 +msgid "Monitoring" +msgstr "Suivi" + +#: models/nature.py:467 +msgid "Comment" +msgstr "Commentaire" + +#: models/nature.py:468 +msgid "Has filter" +msgstr "Présence d'un filtre" + +#: models/nature.py:470 +msgid "Sewer connection" +msgstr "Connexion à l'égout" + +#: models/nature.py:471 +msgid "Separate network connection" +msgstr "Connexion réseau séparatif" + +#: models/nature.py:472 +msgid "Hydro network connection" +msgstr "Connexion réseau hydrographique" + +#: models/nature.py:474 +msgid "Surface area (m²)" +msgstr "Surface (m²)" + +#: models/nature.py:475 +msgid "Length (m)" +msgstr "Longueur (m)" + +#: models/nature.py:477 +msgid "Coating category" +msgstr "Catégorie de revêtement" + +#: models/nature.py:478 +msgid "Coating type" +msgstr "Type de revêtement" + +#: models/nature.py:485 models/nature.py:493 +#, fuzzy +#| msgid "Asset" +msgid "RWI Asset" +msgstr "Asset" + +#: models/nature.py:486 +#, fuzzy +#| msgid "Assets" +msgid "RWI Assets" +msgstr "Assets" + +#: models/nature.py:494 +msgid "File" +msgstr "" + +#: models/nature.py:498 +#, fuzzy +#| msgid "Assets uniquement" +msgid "RWI Asset Document" +msgstr "Assets uniquement" + +#: models/nature.py:499 +#, fuzzy +#| msgid "Documents" +msgid "RWI Asset Documents" +msgstr "Documents" + +#: models/publiclighting.py:85 +msgid "Public lighting pole" +msgstr "" + +#: models/publiclighting.py:86 +msgid "Public lighting poles" +msgstr "" + +#: models/publiclighting.py:108 +msgid "Luminaire" +msgstr "Luminaire" + +#: models/publiclighting.py:109 +msgid "Luminaires" +msgstr "Luminaires" + +#: models/roads.py:224 +msgid "Protection Asset" +msgstr "Protection Asset" + +#: models/roads.py:225 +msgid "Antiterroriste" +msgstr "Antiterroriste" + +#: models/roads.py:226 +msgid "Neutralisation stationnement" +msgstr "Neutralisation stationnement" + +#: models/roads.py:227 +msgid "Sécurité routière" +msgstr "Sécurité routière" + +#: models/roads.py:228 +msgid "Espace public" +msgstr "Espace public" + +#: models/roads.py:229 +msgid "Chantier" +msgstr "Chantier" + +#: models/roads.py:230 +msgid "Non identifié" +msgstr "Non identifié" + +#: models/roads.py:234 +msgid "Maintenir" +msgstr "Maintenir" + +#: models/roads.py:235 +msgid "Enlever" +msgstr "Enlever" + +#: models/roads.py:236 templates/assets/detail_generic.html:139 +#: templates/assets/detail_generic.html:140 +#: templates/assets/detail_generic.html:183 +#: templates/assets/detail_generic.html:184 +#: templates/assets/detail_generic.html:761 +msgid "Remplacer" +msgstr "Remplacer" + +#: models/roads.py:258 +msgid "Road Concrete Block Set" +msgstr "Road Concrete Block Set" + +#: models/roads.py:264 +msgid "Béton" +msgstr "Béton" + +#: models/roads.py:265 +msgid "Asphalte" +msgstr "Asphalte" + +#: models/roads.py:266 +msgid "Pierre" +msgstr "Pierre" + +#: models/roads.py:267 +msgid "Dalles en béton" +msgstr "Dalles en béton" + +#: models/roads.py:268 +msgid "Pavés en béton" +msgstr "Pavés en béton" + +#: models/roads.py:269 +msgid "Dalles en pierre" +msgstr "Dalles en pierre" + +#: models/roads.py:270 +msgid "Pavés en pierre" +msgstr "Pavés en pierre" + +#: models/roads.py:271 +msgid "Béton/Gazon" +msgstr "Béton/Gazon" + +#: models/roads.py:272 +msgid "Gravier" +msgstr "Gravier" + +#: models/roads.py:273 +msgid "Gazon" +msgstr "Gazon" + +#: models/roads.py:274 +msgid "Terre" +msgstr "Terre" + +#: models/roads.py:276 models/roads.py:288 +msgid "N/A" +msgstr "N/A" + +#: models/roads.py:280 +msgid "Filet d'eau" +msgstr "Filet d'eau" + +#: models/roads.py:281 +msgid "Trottoir" +msgstr "Trottoir" + +#: models/roads.py:282 +msgid "Berme" +msgstr "Berme" + +#: models/roads.py:283 +msgid "Trottoir façade vers chaussée" +msgstr "Trottoir façade vers chaussée" + +#: models/roads.py:285 +msgid "Le long du trottoir" +msgstr "Le long du trottoir" + +#: models/roads.py:286 +msgid "Milieu chaussée" +msgstr "Milieu chaussée" + +#: models/roads.py:292 +msgid "Obstrué" +msgstr "Obstrué" + +#: models/roads.py:293 +msgid "Coupe-odeur manquant" +msgstr "Coupe-odeur manquant" + +#: models/roads.py:294 +msgid "Grille cassée" +msgstr "Grille cassée" + +#: models/roads.py:295 +msgid "Grille manquante" +msgstr "Grille manquante" + +#: models/roads.py:296 +msgid "Mauvais écoulement" +msgstr "Mauvais écoulement" + +#: models/roads.py:330 +msgid "Road Drain" +msgstr "Road Drain" + +#: models/sign.py:173 +msgid "Panneau de signalisation" +msgstr "Panneau de signalisation" + +#: models/sign.py:174 +msgid "Panneaux de signalisation" +msgstr "Panneaux de signalisation" + +#: models/trafficlights.py:19 +msgid "En préparation" +msgstr "En préparation" + +#: models/trafficlights.py:20 +msgid "En cours de réparation" +msgstr "En cours de réparation" + +#: models/trafficlights.py:22 +msgid "Désactivé" +msgstr "Désactivé" + +#: models/trafficlights.py:73 +msgid "Est un radar ?" +msgstr "" + +#: models/trafficlights.py:202 +msgid "Plan de croix" +msgstr "Plan de croix" + +#: models/trafficlights.py:204 +msgid "Fichier de programmation" +msgstr "Fichier de programmation" + +#: models/trafficlights.py:205 +msgid "Plan de raccordement" +msgstr "Plan de raccordement" + +#: models/trafficlights.py:211 +msgid "Programmation associée" +msgstr "Programmation associée" + +#: models/trafficlights.py:262 models/trafficlights.py:273 +#: models/trafficlights.py:533 +msgid "Couleur" +msgstr "Couleur" + +#: models/trafficlights.py:263 models/trafficlights.py:274 +#: models/trafficlights.py:534 +msgid "RAL" +msgstr "RAL" + +#: models/trafficlights.py:264 +msgid "Hauteur (m)" +msgstr "Hauteur (m)" + +#: models/trafficlights.py:271 +msgid "Longueur de l'arc (m)" +msgstr "Longueur de l'arc (m)" + +#: models/trafficlights.py:272 +msgid "Extension" +msgstr "Extension" + +#: models/trafficlights.py:293 +#, fuzzy +#| msgid "Traffic Light Radar Model" +msgid "Traffic light pole" +msgstr "Modèle de radar" + +#: models/trafficlights.py:294 +#, fuzzy +#| msgid "Traffic Light Radar Models" +msgid "Traffic light poles" +msgstr "Modèles de radar" + +#: models/trafficlights.py:299 +msgid "Nombre de brins" +msgstr "Nombre de brins" + +#: models/trafficlights.py:308 +msgid "Câble" +msgstr "Câble" + +#: models/trafficlights.py:309 +#: templates/assets/publiclighting_street_detail.html:269 +#: templates/assets/trafficlights_import_cross_plan.html:215 +#: templates/assets/trafficlights_import_cross_plan.html:608 +#: templates/assets/trafficlights_intersection_detail.html:553 +msgid "Câbles" +msgstr "Câbles" + +#: models/trafficlights.py:314 +msgid "Tension (V)" +msgstr "Tension (V)" + +#: models/trafficlights.py:323 +msgid "Lanterne" +msgstr "Lanterne" + +#: models/trafficlights.py:324 +#: templates/assets/trafficlights_import_cross_plan.html:205 +msgid "Lanternes" +msgstr "Lanternes" + +#: models/trafficlights.py:335 +msgid "Connecté à la centrale de contrôle" +msgstr "Connecté à la centrale de contrôle" + +#: models/trafficlights.py:337 models/trafficlights.py:514 +#: templates/assets/trafficlights_intersection_detail.html:636 +#: templates/assets/trafficlights_intersection_detail.html:960 +msgid "Plan" +msgstr "Plan" + +#: models/trafficlights.py:338 templates/assets/sign_index.html:87 +#: templates/assets/structures_index.html:76 +#: templates/assets/trafficlights_index.html:120 +#: templates/assets/trafficlights_intersection_detail.html:632 +#: templates/assets/trafficlights_intersection_detail.html:1216 +#: views/export.py:93 +msgid "Type" +msgstr "Type" + +#: models/trafficlights.py:339 +#: templates/assets/trafficlights_intersection_detail.html:640 +msgid "Puissance" +msgstr "Puissance" + +#: models/trafficlights.py:340 +msgid "Version du firmware" +msgstr "Version du firmware" + +#: models/trafficlights.py:341 +msgid "RMS" +msgstr "RMS" + +#: models/trafficlights.py:342 +msgid "URL" +msgstr "URL" + +#: models/trafficlights.py:345 +#: templates/assets/trafficlights_intersection_detail.html:401 +msgid "Contrôleur" +msgstr "Contrôleur" + +#: models/trafficlights.py:346 +msgid "Contrôleurs" +msgstr "Contrôleurs" + +#: models/trafficlights.py:352 models/trafficlights.py:379 +#: models/trafficlights.py:395 models/trafficlights.py:411 +msgid "Hardware" +msgstr "Hardware" + +#: models/trafficlights.py:353 models/trafficlights.py:394 +#: models/trafficlights.py:410 +msgid "Nom du matériel" +msgstr "Nom du matériel" + +#: models/trafficlights.py:354 +msgid "Description du matériel" +msgstr "Description du matériel" + +#: models/trafficlights.py:357 +msgid "Hardware contrôleur" +msgstr "Hardware contrôleur" + +#: models/trafficlights.py:358 +msgid "Hardwares contrôleur" +msgstr "Hardwares contrôleur" + +#: models/trafficlights.py:365 +msgid "Date du log" +msgstr "Date du log" + +#: models/trafficlights.py:368 +msgid "Fichier de log du contrôleur" +msgstr "Fichier de log du contrôleur" + +#: models/trafficlights.py:369 +msgid "Fichiers de log du contrôleur" +msgstr "Fichiers de log du contrôleur" + +#: models/trafficlights.py:378 +msgid "Nom du détecteur" +msgstr "Nom du détecteur" + +#: models/trafficlights.py:384 +msgid "Phases associées" +msgstr "Phases associées" + +#: models/trafficlights.py:385 models/trafficlights.py:398 +#: models/trafficlights.py:414 +msgid "Mentionné sur le plan" +msgstr "Mentionné sur le plan" + +#: models/trafficlights.py:388 +msgid "Détecteur" +msgstr "Détecteur" + +#: models/trafficlights.py:389 +#: templates/assets/trafficlights_intersection_detail.html:703 +msgid "Détecteurs" +msgstr "Détecteurs" + +#: models/trafficlights.py:404 +msgid "Network Hardware" +msgstr "Network Hardware" + +#: models/trafficlights.py:405 +msgid "Network Hardwares" +msgstr "Network Hardwares" + +#: models/trafficlights.py:420 +msgid "Voltage Hardware" +msgstr "Voltage Hardware" + +#: models/trafficlights.py:421 +msgid "Voltage Hardwares" +msgstr "Voltage Hardwares" + +#: models/trafficlights.py:427 +msgid "Largeur (cm)" +msgstr "Largeur (cm)" + +#: models/trafficlights.py:428 +msgid "Hauteur (cm)" +msgstr "Hauteur (cm)" + +#: models/trafficlights.py:429 +msgid "Profondeur (cm)" +msgstr "Profondeur (cm)" + +#: models/trafficlights.py:430 +msgid "Indice de protection IP" +msgstr "Indice de protection IP" + +#: models/trafficlights.py:455 models/trafficlights.py:485 +msgid "Code EAN" +msgstr "Code EAN" + +#: models/trafficlights.py:456 +msgid "Alimentation électrique" +msgstr "Alimentation électrique" + +#: models/trafficlights.py:457 +msgid "Numéro de compteur" +msgstr "Numéro de compteur" + +#: models/trafficlights.py:458 +msgid "Publicité" +msgstr "Publicité" + +#: models/trafficlights.py:459 +msgid "Type de clé" +msgstr "Type de clé" + +#: models/trafficlights.py:460 +msgid "Type d'armoire" +msgstr "Type d'armoire" + +#: models/trafficlights.py:461 +msgid "Géré par" +msgstr "Géré par" + +#: models/trafficlights.py:462 +msgid "Présence Compteur" +msgstr "Présence Compteur" + +#: models/trafficlights.py:463 +msgid "Interrupteur différentiel" +msgstr "Interrupteur différentiel" + +#: models/trafficlights.py:464 +msgid "Calibre du disjoncteur différentiel" +msgstr "Calibre du disjoncteur différentiel" + +#: models/trafficlights.py:465 +msgid "Disjoncteur différentiel" +msgstr "Disjoncteur différentiel" + +#: models/trafficlights.py:466 +msgid "Calibre du disjoncteur" +msgstr "Calibre du disjoncteur" + +#: models/trafficlights.py:467 +msgid "Calibre différentiel" +msgstr "Calibre différentiel" + +#: models/trafficlights.py:468 +msgid "Départ vers" +msgstr "Départ vers" + +#: models/trafficlights.py:469 +msgid "Conformité visuelle" +msgstr "Conformité visuelle" + +#: models/trafficlights.py:470 views/locations.py:33 +msgid "Propreté" +msgstr "Propreté" + +#: models/trafficlights.py:473 +msgid "Armoire" +msgstr "Armoire" + +#: models/trafficlights.py:474 +msgid "Armoires" +msgstr "Armoires" + +#: models/trafficlights.py:483 +msgid "Description du contenu" +msgstr "Description du contenu" + +#: models/trafficlights.py:484 +msgid "Numéro de référence" +msgstr "Numéro de référence" + +#: models/trafficlights.py:488 +msgid "Contenu d'armoire" +msgstr "Contenu d'armoire" + +#: models/trafficlights.py:489 +msgid "Contenus d'armoires" +msgstr "Contenus d'armoires" + +#: models/trafficlights.py:504 +msgid "Accessoire" +msgstr "Accessoire" + +#: models/trafficlights.py:505 +msgid "Accessoires" +msgstr "Accessoires" + +#: models/trafficlights.py:512 +#: templates/assets/trafficlights_intersection_detail.html:644 +msgid "Date de mise en service" +msgstr "Date de mise en service" + +#: models/trafficlights.py:513 +msgid "Date de mise hors service" +msgstr "Date de mise hors service" + +#: models/trafficlights.py:515 +#: templates/assets/trafficlights_intersection_detail.html:50 +msgid "Index" +msgstr "Index" + +#: models/trafficlights.py:516 +msgid "Informations" +msgstr "Informations" + +#: models/trafficlights.py:549 +msgid "Radar Pole" +msgstr "Poteau radar" + +#: models/trafficlights.py:550 +msgid "Radar Poles" +msgstr "Poteaux radar" + +#: models/trafficlights.py:555 +msgid "Traffic Light Radar Model" +msgstr "Modèle de radar" + +#: models/trafficlights.py:556 +msgid "Traffic Light Radar Models" +msgstr "Modèles de radar" + +#: models/trafficlights.py:564 +msgid "Radar Pole (Support)" +msgstr "Poteau radar (Support)" + +#: models/trafficlights.py:565 +msgid "Public Lighting Pole (Support)" +msgstr "Poteau d'éclairage public (Support)" + +#: models/trafficlights.py:566 +msgid "Traffic Light Pole (Support)" +msgstr "Poteau tricolore (Support)" + +#: models/trafficlights.py:569 +msgid "Public Lighting Pole (Power)" +msgstr "Poteau d'éclairage public (Alimentation)" + +#: models/trafficlights.py:570 +msgid "Traffic Light Pole (Power)" +msgstr "Poteau tricolore (Alimentation)" + +#: models/trafficlights.py:573 +msgid "Radar" +msgstr "Radar" + +#: models/trafficlights.py:574 +msgid "Radars" +msgstr "Radars" + +#: permissions.py:502 +msgid "Missing asset information" +msgstr "Missing asset information" + +#: permissions.py:509 +msgid "Invalid asset type" +msgstr "Invalid asset type" + +#: permissions.py:512 +msgid "You do not have permission to edit this asset" +msgstr "You do not have permission to edit this asset" + +#: permissions.py:549 +msgid "Invalid thematic" +msgstr "Invalid thematic" + +#: permissions.py:553 views/history.py:44 +msgid "You do not have permission to edit assets for this thematic" +msgstr "You do not have permission to edit assets for this thematic" + +#: templates/assets/artworks_index.html:18 +#: templates/assets/artworks_index.html:46 templates/assets/clean_index.html:8 +#: templates/assets/controlcenters_index.html:16 +#: templates/assets/controlcenters_index.html:45 +#: templates/assets/its_index.html:16 templates/assets/its_index.html:56 +#: templates/assets/nature_index.html:28 templates/assets/nature_index.html:59 +#: templates/assets/publiclighting_index.html:18 +#: templates/assets/publiclighting_index.html:49 +#: templates/assets/roads_index.html:18 templates/assets/roads_index.html:61 +#: templates/assets/sign_index.html:18 templates/assets/sign_index.html:61 +#: templates/assets/structures_index.html:26 +#: templates/assets/structures_index.html:57 +#: templates/assets/trafficlights_index.html:61 +#: templates/assets/trafficlights_index.html:101 +#: templates/assets/unified_index.html:229 templates/assets/water_index.html:22 +#: templates/assets/water_index.html:50 +msgid "Filtrer" +msgstr "Filtrer" + +#: templates/assets/artworks_index.html:19 +#: templates/assets/artworks_index.html:47 templates/assets/clean_index.html:9 +#: templates/assets/controlcenters_index.html:17 +#: templates/assets/controlcenters_index.html:46 +#: templates/assets/its_index.html:17 templates/assets/its_index.html:57 +#: templates/assets/nature_index.html:29 templates/assets/nature_index.html:60 +#: templates/assets/publiclighting_index.html:19 +#: templates/assets/publiclighting_index.html:50 +#: templates/assets/roads_index.html:19 templates/assets/roads_index.html:62 +#: templates/assets/sign_index.html:19 templates/assets/sign_index.html:62 +#: templates/assets/structures_index.html:27 +#: templates/assets/structures_index.html:58 +#: templates/assets/trafficlights_index.html:62 +#: templates/assets/trafficlights_index.html:102 +#: templates/assets/unified_index.html:227 templates/assets/water_index.html:23 +#: templates/assets/water_index.html:51 +msgid "Réinitialiser" +msgstr "Réinitialiser" + +#: templates/assets/artworks_index.html:22 +#: templates/assets/artworks_index.html:64 +#: templates/assets/artworks_street_detail.html:15 +#: templates/assets/publiclighting_index.html:8 +#: templates/assets/publiclighting_index.html:67 +#: templates/assets/roads_index.html:8 templates/assets/roads_index.html:79 +#: templates/assets/sign_index.html:8 templates/assets/sign_index.html:79 +#: templates/assets/trafficlights_intersection_detail.html:79 views/core.py:597 +msgid "Commune" +msgstr "Commune" + +#: templates/assets/artworks_index.html:63 +#: templates/assets/artworks_index.html:71 +#: templates/assets/components/add_asset_modal.html:82 +#: templates/assets/components/add_asset_modal.html:91 +#: templates/assets/components/asset_model_card.html:21 +#: templates/assets/controlcenters_index.html:62 +#: templates/assets/controlcenters_index.html:67 +#: templates/assets/detail_generic.html:224 +#: templates/assets/detail_generic.html:237 templates/assets/its_index.html:82 +#: templates/assets/its_index.html:87 templates/assets/nature_index.html:76 +#: templates/assets/publiclighting_index.html:66 +#: templates/assets/publiclighting_index.html:72 +#: templates/assets/roads_index.html:78 templates/assets/roads_index.html:84 +#: templates/assets/sign_index.html:78 templates/assets/sign_index.html:84 +#: templates/assets/structures_index.html:75 +#: templates/assets/structures_index.html:81 +#: templates/assets/trafficlights_import_cross_plan.html:241 +#: templates/assets/trafficlights_import_cross_plan.html:323 +#: templates/assets/trafficlights_import_cross_plan.html:407 +#: templates/assets/trafficlights_import_cross_plan.html:499 +#: templates/assets/trafficlights_index.html:119 +#: templates/assets/trafficlights_index.html:125 +#: templates/assets/unified_index.html:315 +#: templates/assets/unified_index.html:319 templates/assets/water_index.html:75 +#: views/export.py:96 +msgid "Nom" +msgstr "Nom" + +#: templates/assets/artworks_index.html:65 +#: templates/assets/artworks_street_detail.html:19 +msgid "Code voirie" +msgstr "Code voirie" + +#: templates/assets/artworks_index.html:66 +#: templates/assets/artworks_street_detail.html:23 +msgid "Section" +msgstr "Section" + +#: templates/assets/artworks_index.html:73 +msgid "Sous-type" +msgstr "Sous-type" + +#: templates/assets/artworks_street_detail.html:7 +msgid "Retour à la vue Artworks" +msgstr "Retour à la vue Artworks" + +#: templates/assets/artworks_street_detail.html:27 +msgid "Niveau" +msgstr "Niveau" + +#: templates/assets/artworks_street_detail.html:38 +msgid "Monuments" +msgstr "Monuments" + +#: templates/assets/artworks_street_detail.html:51 +#: templates/assets/artworks_street_detail.html:78 +#: templates/assets/controlcenters_location_detail.html:251 +#: templates/assets/its_location_detail.html:175 +#: templates/assets/nature_locations_detail.html:81 +#: templates/assets/naturerwiz_detail.html:250 +#: templates/assets/publiclighting_street_detail.html:310 +#: templates/assets/roads_street_detail.html:325 +#: templates/assets/sign_street_detail.html:302 +#: templates/assets/structures_location_detail.html:117 +#: templates/assets/structures_location_detail.html:360 +#: templates/assets/trafficlights_intersection_detail.html:941 +msgid "Détail" +msgstr "Détail" + +#: templates/assets/artworks_street_detail.html:57 +msgid "Aucun monument actif n'est lié à cette localisation." +msgstr "Aucun monument actif n'est lié à cette localisation." + +#: templates/assets/artworks_street_detail.html:65 +msgid "Fontaines" +msgstr "Fontaines" + +#: templates/assets/artworks_street_detail.html:84 +msgid "Aucune fontaine active n'est liée à cette localisation." +msgstr "Aucune fontaine active n'est liée à cette localisation." + +#: templates/assets/base_assets_index.html:22 +msgid "Thématique :" +msgstr "Thématique :" + +#: templates/assets/base_assets_index.html:33 +msgid "Sélectionnez une thématique" +msgstr "Sélectionnez une thématique" + +#: templates/assets/base_assets_index.html:56 +#: templates/assets/base_assets_index.html:69 +msgid "Filtre" +msgstr "Filtre" + +#: templates/assets/base_assets_index.html:81 +msgid "Sélection de la vue" +msgstr "Sélection de la vue" + +#: templates/assets/base_assets_index.html:82 +#: templates/assets/unified_index.html:425 +msgid "Vue tableau uniquement" +msgstr "Vue tableau uniquement" + +#: templates/assets/base_assets_index.html:85 +#: templates/assets/unified_index.html:428 +msgid "Vue partagée" +msgstr "Vue partagée" + +#: templates/assets/base_assets_index.html:88 +#: templates/assets/unified_index.html:431 +msgid "Vue carte uniquement" +msgstr "Vue carte uniquement" + +#: templates/assets/base_assets_index.html:102 +msgid "Code, nom…" +msgstr "Code, nom…" + +#: templates/assets/base_assets_index.html:107 +msgid "Effacer la recherche" +msgstr "Effacer la recherche" + +#: templates/assets/base_assets_index.html:113 +msgid "Mode" +msgstr "Mode" + +#: templates/assets/base_assets_index.html:115 +msgid "Tous les mots doivent correspondre" +msgstr "Tous les mots doivent correspondre" + +#: templates/assets/base_assets_index.html:117 +msgid "Au moins un mot doit correspondre" +msgstr "Au moins un mot doit correspondre" + +#: templates/assets/base_assets_index.html:123 +msgid "Rechercher" +msgstr "Rechercher" + +#: templates/assets/base_assets_index.html:151 +#: templates/assets/base_assets_index.html:188 +#: templates/assets/trafficlights_import_cross_plan.html:64 +#: templates/assets/unified_index.html:475 +msgid "Tout sélectionner" +msgstr "Tout sélectionner" + +#: templates/assets/base_assets_index.html:152 +#: templates/assets/base_assets_index.html:189 +#: templates/assets/unified_index.html:476 +msgid "Tout désélectionner" +msgstr "Tout désélectionner" + +#: templates/assets/base_assets_index.html:154 +#: templates/assets/base_assets_index.html:191 +#: templates/assets/unified_index.html:378 +#: templates/assets/unified_index.html:478 +msgid "Valider la sélection" +msgstr "Valider la sélection" + +#: templates/assets/base_assets_index.html:157 +#, python-format +msgid "" +"\n" +" %(counter)s localisation sélectionnée\n" +" " +msgid_plural "" +"\n" +" %(counter)s localisations sélectionnées\n" +" " +msgstr[0] "" +"\n" +" %(counter)s localisation sélectionnée\n" +" " +msgstr[1] "" +"\n" +" %(counter)s localisations sélectionnées\n" +" " + +#: templates/assets/base_assets_index.html:171 +#: templates/assets/base_assets_index.html:225 +#: templates/assets/unified_index.html:520 +msgid "Loading..." +msgstr "Loading..." + +#: templates/assets/base_assets_index.html:194 +#, python-format +msgid "" +"\n" +" %(counter)s asset sélectionné\n" +" " +msgid_plural "" +"\n" +" %(counter)s assets sélectionnés\n" +" " +msgstr[0] "" +"\n" +" %(counter)s asset sélectionné\n" +" " +msgstr[1] "" +"\n" +" %(counter)s assets sélectionnés\n" +" " + +#: templates/assets/base_assets_index.html:205 +#: templates/assets/unified_index.html:484 +msgid "Action" +msgstr "Action" + +#: templates/assets/base_assets_index.html:211 +#: templates/assets/base_assets_index.html:321 +#: templates/assets/unified_index.html:490 +#: templates/assets/unified_index.html:603 +msgid "Exporter" +msgstr "Exporter" + +#: templates/assets/base_assets_index.html:261 +msgid "Carte" +msgstr "Carte" + +#: templates/assets/base_assets_index.html:269 +msgid "Filtrer automatiquement sur la zone visible" +msgstr "Filtrer automatiquement sur la zone visible" + +#: templates/assets/base_assets_index.html:280 +#: templates/assets/unified_index.html:566 +msgid "Totaux des assets" +msgstr "Totaux des assets" + +#: templates/assets/base_assets_index.html:282 +#: templates/assets/base_assets_index.html:305 +#: templates/assets/components/add_asset_modal.html:19 +#: templates/assets/components/asset_documents.html:37 +#: templates/assets/components/asset_location_link.html:50 +#: templates/assets/components/asset_model_card.html:62 +#: templates/assets/components/location_edit_modals.html:24 +#: templates/assets/components/location_edit_modals.html:56 +#: templates/assets/components/location_edit_modals.html:82 +#: templates/assets/controlcenters_location_detail.html:112 +#: templates/assets/controlcenters_location_detail.html:145 +#: templates/assets/controlcenters_location_detail.html:170 +#: templates/assets/its_location_detail.html:55 +#: templates/assets/its_location_detail.html:88 +#: templates/assets/naturerwiz_detail.html:273 +#: templates/assets/naturerwiz_detail.html:306 +#: templates/assets/publiclighting_street_detail.html:161 +#: templates/assets/publiclighting_street_detail.html:194 +#: templates/assets/publiclighting_street_detail.html:219 +#: templates/assets/roads_street_detail.html:160 +#: templates/assets/roads_street_detail.html:193 +#: templates/assets/roads_street_detail.html:218 +#: templates/assets/sign_street_detail.html:160 +#: templates/assets/sign_street_detail.html:193 +#: templates/assets/sign_street_detail.html:218 +#: templates/assets/structures_location_detail.html:190 +#: templates/assets/structures_location_detail.html:232 +#: templates/assets/structures_location_detail.html:257 +#: templates/assets/trafficlights_import_cross_plan.html:802 +#: templates/assets/trafficlights_intersection_detail.html:248 +#: templates/assets/trafficlights_intersection_detail.html:281 +#: templates/assets/trafficlights_intersection_detail.html:306 +#: templates/assets/trafficlights_intersection_detail.html:1109 +#: templates/assets/trafficlights_intersection_detail.html:1158 +#: templates/assets/trafficlights_intersection_detail.html:1234 +#: templates/assets/unified_index.html:84 +#: templates/assets/unified_index.html:241 +#: templates/assets/unified_index.html:283 +#: templates/assets/unified_index.html:343 +#: templates/assets/unified_index.html:568 +#: templates/assets/unified_index.html:587 +msgid "Fermer" +msgstr "Fermer" + +#: templates/assets/base_assets_index.html:293 +#: templates/assets/unified_index.html:575 +msgid "Totaux" +msgstr "Totaux" + +#: templates/assets/base_assets_index.html:304 +#: templates/assets/unified_index.html:586 +msgid "Exporter les assets sélectionnés" +msgstr "Exporter les assets sélectionnés" + +#: templates/assets/base_assets_index.html:308 +#: templates/assets/unified_index.html:590 +msgid "Choisissez le format d'export :" +msgstr "Choisissez le format d'export :" + +#: templates/assets/base_assets_index.html:320 +#: templates/assets/components/add_asset_modal.html:138 +#: templates/assets/components/asset_documents.html:87 +#: templates/assets/components/asset_location_link.html:75 +#: templates/assets/components/asset_model_card.html:98 +#: templates/assets/components/location_edit_modals.html:38 +#: templates/assets/components/location_edit_modals.html:63 +#: templates/assets/components/location_edit_modals.html:96 +#: templates/assets/controlcenters_location_detail.html:156 +#: templates/assets/detail_generic.html:150 +#: templates/assets/detail_generic.html:194 +#: templates/assets/detail_generic.html:759 +#: templates/assets/its_location_detail.html:99 +#: templates/assets/permissions/access_edit.html:121 +#: templates/assets/permissions/type_access_edit.html:126 +#: templates/assets/publiclighting_street_detail.html:205 +#: templates/assets/roads_street_detail.html:204 +#: templates/assets/sign_street_detail.html:204 +#: templates/assets/structures_location_detail.html:243 +#: templates/assets/trafficlights_import_dxf.html:126 +#: templates/assets/trafficlights_intersection_detail.html:292 +#: templates/assets/trafficlights_intersection_detail.html:1003 +#: templates/assets/unified_index.html:228 +#: templates/assets/unified_index.html:327 +#: templates/assets/unified_index.html:359 +#: templates/assets/unified_index.html:602 +#: templates/assets/unified_index.html:631 +#: templates/assets/unified_index.html:661 +msgid "Annuler" +msgstr "Annuler" + +#: templates/assets/clean_index.html:18 templates/assets/clean_index.html:73 +msgid "Category" +msgstr "Category" + +#: templates/assets/clean_index.html:38 +#: templates/assets/clean_location_detail.html:161 +msgid "Status" +msgstr "Status" + +#: templates/assets/clean_index.html:40 +msgid "To be approved" +msgstr "To be approved" + +#: templates/assets/clean_index.html:41 +#: templates/assets/clean_location_detail.html:53 +msgid "Active" +msgstr "Active" + +#: templates/assets/clean_index.html:42 +msgid "Damaged" +msgstr "Damaged" + +#: templates/assets/clean_index.html:43 +msgid "To replace" +msgstr "To replace" + +#: templates/assets/clean_index.html:44 +msgid "Removed" +msgstr "Removed" + +#: templates/assets/clean_index.html:48 +msgid "Filter" +msgstr "Filter" + +#: templates/assets/clean_index.html:49 +msgid "Reset" +msgstr "Reset" + +#: templates/assets/clean_index.html:66 templates/assets/clean_index.html:72 +msgid "Name" +msgstr "Name" + +#: templates/assets/clean_location_detail.html:19 +msgid "Municipality:" +msgstr "Municipality:" + +#: templates/assets/clean_location_detail.html:22 +msgid "Road code:" +msgstr "Road code:" + +#: templates/assets/clean_location_detail.html:25 +#: templates/assets/roads_street_detail.html:20 +msgid "Section:" +msgstr "Section:" + +#: templates/assets/clean_location_detail.html:28 +msgid "Street level:" +msgstr "Street level:" + +#: templates/assets/clean_location_detail.html:35 +#: templates/assets/controlcenters_location_detail.html:34 +#: templates/assets/its_location_detail.html:30 +#: templates/assets/nature_locations_detail.html:20 +#: templates/assets/naturerwiz_detail.html:20 +#: templates/assets/publiclighting_street_detail.html:18 +#: templates/assets/roads_street_detail.html:27 +#: templates/assets/sign_street_detail.html:18 +#: templates/assets/structures_location_detail.html:35 +#: templates/assets/trafficlights_intersection_detail.html:82 +msgid "Street View" +msgstr "Street View" + +#: templates/assets/clean_location_detail.html:36 +#: templates/assets/controlcenters_location_detail.html:35 +#: templates/assets/its_location_detail.html:31 +#: templates/assets/nature_locations_detail.html:21 +#: templates/assets/naturerwiz_detail.html:21 +#: templates/assets/publiclighting_street_detail.html:19 +#: templates/assets/roads_street_detail.html:28 +#: templates/assets/sign_street_detail.html:19 +#: templates/assets/structures_location_detail.html:36 +#: templates/assets/trafficlights_intersection_detail.html:83 +msgid "View in Google Street View" +msgstr "View in Google Street View" + +#: templates/assets/clean_location_detail.html:45 +#: templates/assets/controlcenters_location_detail.html:186 +#: templates/assets/its_location_detail.html:110 +#: templates/assets/naturerwiz_detail.html:180 +#: templates/assets/publiclighting_street_detail.html:233 +#: templates/assets/roads_street_detail.html:232 +#: templates/assets/sign_street_detail.html:232 +#: templates/assets/structures_location_detail.html:273 +#: templates/assets/trafficlights_intersection_detail.html:324 +#: templates/assets/unified_index.html:448 views/artworks.py:95 +#: views/clean.py:260 views/controlcenters.py:108 +msgid "Assets" +msgstr "Assets" + +#: templates/assets/clean_location_detail.html:60 +msgid "All" +msgstr "All" + +#: templates/assets/clean_location_detail.html:82 +#: templates/assets/clean_location_detail.html:119 +msgid "No litter bins" +msgstr "No litter bins" + +#: templates/assets/clean_location_detail.html:99 +#: templates/assets/clean_location_detail.html:133 +msgid "No glass containers" +msgstr "No glass containers" + +#: templates/assets/clean_location_detail.html:147 +#: templates/assets/controlcenters_location_detail.html:231 +#: templates/assets/its_location_detail.html:155 +#: templates/assets/nature_locations_detail.html:61 +#: templates/assets/naturerwiz_detail.html:230 +#: templates/assets/publiclighting_street_detail.html:290 +#: templates/assets/roads_street_detail.html:305 +#: templates/assets/sign_street_detail.html:282 +#: templates/assets/structures_location_detail.html:340 +#: templates/assets/trafficlights_intersection_detail.html:921 +msgid "Interventions" +msgstr "Interventions" + +#: templates/assets/clean_location_detail.html:159 +msgid "Title" +msgstr "Title" + +#: templates/assets/clean_location_detail.html:160 +msgid "Start" +msgstr "Start" + +#: templates/assets/clean_location_detail.html:168 +msgid "Detail" +msgstr "Detail" + +#: templates/assets/clean_location_detail.html:179 +msgid "No interventions" +msgstr "No interventions" + +#: templates/assets/cleanlitterbin_detail.html:18 +msgid "Observations" +msgstr "Observations" + +#: templates/assets/cleanlitterbin_detail.html:23 +#: templates/assets/components/asset_documents.html:14 +#: templates/assets/controlcenters_location_detail.html:46 +#: templates/assets/controlcenters_location_detail.html:129 +#: templates/assets/its_location_detail.html:41 +#: templates/assets/its_location_detail.html:72 +#: templates/assets/naturerwiz_detail.html:82 +#: templates/assets/naturerwiz_detail.html:290 +#: templates/assets/publiclighting_street_detail.html:29 +#: templates/assets/publiclighting_street_detail.html:178 +#: templates/assets/roads_street_detail.html:38 +#: templates/assets/roads_street_detail.html:177 +#: templates/assets/sign_street_detail.html:29 +#: templates/assets/sign_street_detail.html:177 +#: templates/assets/structures_location_detail.html:47 +#: templates/assets/structures_location_detail.html:207 +#: templates/assets/trafficlights_intersection_detail.html:157 +#: templates/assets/trafficlights_intersection_detail.html:265 +#: templates/assets/trafficlights_intersection_detail.html:329 +#: templates/assets/trafficlights_intersection_detail.html:1226 +msgid "Ajouter" +msgstr "Ajouter" + +#: templates/assets/cleanlitterbin_detail.html:31 +msgid "Aucune observation enregistrée." +msgstr "Aucune observation enregistrée." + +#: templates/assets/cleanlitterbin_detail.html:42 +msgid "Date inconnue" +msgstr "Date inconnue" + +#: templates/assets/cleanlitterbin_detail.html:65 +#: templates/assets/cleanlitterbin_detail.html:221 +#: templates/assets/cleanlitterbin_detail.html:362 +msgid "Sol" +msgstr "Sol" + +#: templates/assets/cleanlitterbin_detail.html:70 +#: templates/assets/cleanlitterbin_detail.html:223 +#: templates/assets/cleanlitterbin_detail.html:364 +msgid "Pente trop forte" +msgstr "Pente trop forte" + +#: templates/assets/cleanlitterbin_detail.html:74 +#: templates/assets/cleanlitterbin_detail.html:224 +#: templates/assets/cleanlitterbin_detail.html:365 +msgid "Sol instable" +msgstr "Sol instable" + +#: templates/assets/cleanlitterbin_detail.html:78 +#: templates/assets/cleanlitterbin_detail.html:225 +#: templates/assets/cleanlitterbin_detail.html:366 +msgid "Surface réfléchissante" +msgstr "Surface réfléchissante" + +#: templates/assets/cleanlitterbin_detail.html:82 +#: templates/assets/cleanlitterbin_detail.html:226 +#: templates/assets/cleanlitterbin_detail.html:367 +msgid "Surface glissante" +msgstr "Surface glissante" + +#: templates/assets/cleanlitterbin_detail.html:86 +#: templates/assets/cleanlitterbin_detail.html:227 +#: templates/assets/cleanlitterbin_detail.html:368 +msgid "Fissures" +msgstr "Fissures" + +#: templates/assets/cleanlitterbin_detail.html:90 +#: templates/assets/cleanlitterbin_detail.html:228 +#: templates/assets/cleanlitterbin_detail.html:369 +msgid "Entrave" +msgstr "Entrave" + +#: templates/assets/cleanlitterbin_detail.html:94 +#: templates/assets/cleanlitterbin_detail.html:229 +#: templates/assets/cleanlitterbin_detail.html:370 +msgid "Dalles podotactiles" +msgstr "Dalles podotactiles" + +#: templates/assets/cleanlitterbin_detail.html:99 +msgid "Pente transversale" +msgstr "Pente transversale" + +#: templates/assets/cleanlitterbin_detail.html:103 +msgid "Pente longitudinale" +msgstr "Pente longitudinale" + +#: templates/assets/cleanlitterbin_detail.html:108 +#: templates/assets/cleanlitterbin_detail.html:194 +msgid "Remarque" +msgstr "Remarque" + +#: templates/assets/cleanlitterbin_detail.html:118 +#: templates/assets/cleanlitterbin_detail.html:244 +#: templates/assets/cleanlitterbin_detail.html:376 +msgid "Accessibilité" +msgstr "Accessibilité" + +#: templates/assets/cleanlitterbin_detail.html:122 +#: templates/assets/cleanlitterbin_detail.html:247 +#: templates/assets/cleanlitterbin_detail.html:378 +msgid "Type de quartier" +msgstr "Type de quartier" + +#: templates/assets/cleanlitterbin_detail.html:126 +#: templates/assets/cleanlitterbin_detail.html:257 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Densité flux piéton" +msgstr "Densité flux piéton" + +#: templates/assets/cleanlitterbin_detail.html:130 +#: templates/assets/cleanlitterbin_detail.html:283 +#: templates/assets/cleanlitterbin_detail.html:382 +msgid "Approche 2 m dégagée" +msgstr "Approche 2 m dégagée" + +#: templates/assets/cleanlitterbin_detail.html:134 +#: templates/assets/cleanlitterbin_detail.html:284 +#: templates/assets/cleanlitterbin_detail.html:383 +msgid "Voirie à traverser" +msgstr "Voirie à traverser" + +#: templates/assets/cleanlitterbin_detail.html:138 +#: templates/assets/cleanlitterbin_detail.html:285 +#: templates/assets/cleanlitterbin_detail.html:384 +msgid "Poubelle isolée" +msgstr "Poubelle isolée" + +#: templates/assets/cleanlitterbin_detail.html:142 +#: templates/assets/cleanlitterbin_detail.html:286 +#: templates/assets/cleanlitterbin_detail.html:385 +msgid "Gêne flux routier" +msgstr "Gêne flux routier" + +#: templates/assets/cleanlitterbin_detail.html:146 +#: templates/assets/cleanlitterbin_detail.html:287 +#: templates/assets/cleanlitterbin_detail.html:386 +msgid "Gêne flux piéton" +msgstr "Gêne flux piéton" + +#: templates/assets/cleanlitterbin_detail.html:150 +#: templates/assets/cleanlitterbin_detail.html:288 +#: templates/assets/cleanlitterbin_detail.html:387 +msgid "Itinéraire logique" +msgstr "Itinéraire logique" + +#: templates/assets/cleanlitterbin_detail.html:154 +#: templates/assets/cleanlitterbin_detail.html:289 +#: templates/assets/cleanlitterbin_detail.html:388 +msgid "Contraste visuel" +msgstr "Contraste visuel" + +#: templates/assets/cleanlitterbin_detail.html:158 +#: templates/assets/cleanlitterbin_detail.html:290 +#: templates/assets/cleanlitterbin_detail.html:389 +msgid "Indicateurs de tri" +msgstr "Indicateurs de tri" + +#: templates/assets/cleanlitterbin_detail.html:162 +#: templates/assets/cleanlitterbin_detail.html:266 +#: templates/assets/cleanlitterbin_detail.html:380 +msgid "Accès agent (véhicule)" +msgstr "Accès agent (véhicule)" + +#: templates/assets/cleanlitterbin_detail.html:172 +#: templates/assets/cleanlitterbin_detail.html:275 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Accès agent (pied)" +msgstr "Accès agent (pied)" + +#: templates/assets/cleanlitterbin_detail.html:183 +#: templates/assets/cleanlitterbin_detail.html:292 +#: templates/assets/cleanlitterbin_detail.html:391 +msgid "Points d'intérêt" +msgstr "Points d'intérêt" + +#: templates/assets/cleanlitterbin_detail.html:208 +#: templates/assets/cleanlitterbin_detail.html:350 +msgid "Général" +msgstr "Général" + +#: templates/assets/cleanlitterbin_detail.html:211 +#: templates/assets/cleanlitterbin_detail.html:353 +msgid "Date / heure" +msgstr "Date / heure" + +#: templates/assets/cleanlitterbin_detail.html:216 +#: templates/assets/cleanlitterbin_detail.html:357 views/export.py:108 +msgid "Note" +msgstr "Note" + +#: templates/assets/cleanlitterbin_detail.html:231 +#: templates/assets/cleanlitterbin_detail.html:371 +#, python-format +msgid "Pente transversale (%%)" +msgstr "Pente transversale (%%)" + +#: templates/assets/cleanlitterbin_detail.html:235 +#: templates/assets/cleanlitterbin_detail.html:372 +#, python-format +msgid "Pente longitudinale (%%)" +msgstr "Pente longitudinale (%%)" + +#: templates/assets/cleanlitterbin_detail.html:239 +#: templates/assets/cleanlitterbin_detail.html:373 +msgid "Remarque sol" +msgstr "Remarque sol" + +#: templates/assets/cleanlitterbin_detail.html:308 +#: templates/assets/cleanlitterbin_detail.html:404 +msgid "Autre point d'intérêt" +msgstr "Autre point d'intérêt" + +#: templates/assets/cleanlitterbin_detail.html:312 +#: templates/assets/cleanlitterbin_detail.html:405 +msgid "Remarque accessibilité" +msgstr "Remarque accessibilité" + +#: templates/assets/cleanlitterbin_detail.html:319 +#: templates/assets/cleanlitterbin_detail.html:409 +msgid "Supprimer cette observation" +msgstr "Supprimer cette observation" + +#: templates/assets/components/add_asset_modal.html:17 +msgid "Ajouter un nouvel équipement" +msgstr "Ajouter un nouvel équipement" + +#: templates/assets/components/add_asset_modal.html:37 +#: templates/assets/unified_index.html:293 +msgid "Sélectionner une catégorie..." +msgstr "Sélectionner une catégorie..." + +#: templates/assets/components/add_asset_modal.html:47 +#: templates/assets/trafficlights_intersection_detail.html:988 +#, fuzzy +#| msgid "Type d'élément" +msgid "Type d'équipement" +msgstr "Type d'élément" + +#: templates/assets/components/add_asset_modal.html:50 +#: templates/assets/trafficlights_intersection_detail.html:991 +#, fuzzy +#| msgid "Sélectionner un type" +msgid "Sélectionner un type..." +msgstr "Sélectionner un type" + +#: templates/assets/components/add_asset_modal.html:60 +#: templates/assets/trafficlights_intersection_detail.html:998 +#: templates/assets/unified_index.html:299 +msgid "Sélectionner d'abord une catégorie..." +msgstr "Sélectionner d'abord une catégorie..." + +#: templates/assets/components/add_asset_modal.html:63 +msgid "Le modèle détermine les caractéristiques de l'équipement." +msgstr "Le modèle détermine les caractéristiques de l'équipement." + +#: templates/assets/components/add_asset_modal.html:73 +msgid "Ex: STR-001" +msgstr "Ex: STR-001" + +#: templates/assets/components/add_asset_modal.html:75 +msgid "Code unique pour identifier l'équipement." +msgstr "Code unique pour identifier l'équipement." + +#: templates/assets/components/add_asset_modal.html:85 +msgid "Nom en français (optionnel)" +msgstr "Nom en français (optionnel)" + +#: templates/assets/components/add_asset_modal.html:94 +msgid "Naam in het Nederlands (optioneel)" +msgstr "Naam in het Nederlands (optioneel)" + +#: templates/assets/components/add_asset_modal.html:104 +msgid "Aucune localisation (optionnel)" +msgstr "Aucune localisation (optionnel)" + +#: templates/assets/components/add_asset_modal.html:110 +msgid "Associez l'équipement à une localisation spécifique." +msgstr "Associez l'équipement à une localisation spécifique." + +#: templates/assets/components/add_asset_modal.html:119 +#: templates/assets/structures_index.html:74 +msgid "Ouvrage" +msgstr "Ouvrage" + +#: templates/assets/components/add_asset_modal.html:122 +msgid "Aucun ouvrage (optionnel)" +msgstr "Aucun ouvrage (optionnel)" + +#: templates/assets/components/add_asset_modal.html:132 +msgid "L'équipement sera créé avec le statut 'À approuver'." +msgstr "L'équipement sera créé avec le statut 'À approuver'." + +#: templates/assets/components/add_asset_modal.html:141 +msgid "Créer l'équipement" +msgstr "Créer l'équipement" + +#: templates/assets/components/asset_children_tree.html:25 +#: templates/assets/detail_generic.html:792 +#: templates/assets/detail_generic.html:802 +#: templates/assets/detail_generic.html:1390 +msgid "Lier un asset existant" +msgstr "Lier un asset existant" + +#: templates/assets/components/asset_children_tree.html:36 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Changer le parent" +msgstr "Changer le modèle" + +#: templates/assets/components/asset_children_tree.html:82 +#: templates/assets/components/asset_children_tree.html:96 +#: templates/assets/detail_generic.html:1357 +msgid "Délier cet asset" +msgstr "Délier cet asset" + +#: templates/assets/components/asset_documents.html:6 +msgid "Documents associés" +msgstr "Documents associés" + +#: templates/assets/components/asset_documents.html:34 +#: templates/assets/controlcenters_location_detail.html:111 +#: templates/assets/its_location_detail.html:54 +#: templates/assets/naturerwiz_detail.html:272 +#: templates/assets/publiclighting_street_detail.html:160 +#: templates/assets/roads_street_detail.html:159 +#: templates/assets/sign_street_detail.html:159 +#: templates/assets/structures_location_detail.html:189 +#: templates/assets/trafficlights_intersection_detail.html:247 +msgid "Ajouter des documents" +msgstr "Ajouter des documents" + +#: templates/assets/components/asset_documents.html:48 +msgid "Sélectionnez un ou plusieurs fichiers pour continuer." +msgstr "Sélectionnez un ou plusieurs fichiers pour continuer." + +#: templates/assets/components/asset_documents.html:50 +msgid "Choisir des fichiers" +msgstr "Choisir des fichiers" + +#: templates/assets/components/asset_documents.html:59 +msgid "Changer les fichiers" +msgstr "Changer les fichiers" + +#: templates/assets/components/asset_documents.html:66 +msgid "Tag" +msgstr "Tag" + +#: templates/assets/components/asset_documents.html:78 +msgid "Aucun tag disponible pour cette thématique." +msgstr "Aucun tag disponible pour cette thématique." + +#: templates/assets/components/asset_documents.html:89 +msgid "Uploader" +msgstr "Uploader" + +#: templates/assets/components/asset_history.html:8 +msgid "Historique des modifications" +msgstr "Historique des modifications" + +#: templates/assets/components/asset_history.html:12 +msgid "Tableau" +msgstr "Tableau" + +#: templates/assets/components/asset_history.html:17 +msgid "Ligne du temps" +msgstr "Ligne du temps" + +#: templates/assets/components/asset_history.html:28 +#: templates/assets/detail_generic.html:787 +#: templates/assets/unified_index.html:305 +msgid "Chargement..." +msgstr "Chargement..." + +#: templates/assets/components/asset_history.html:37 +msgid "Date" +msgstr "Date" + +#: templates/assets/components/asset_history.html:39 +msgid "Champ" +msgstr "Champ" + +#: templates/assets/components/asset_history.html:50 +#: templates/assets/components/asset_history.html:67 +msgid "Aucune modification enregistrée pour cet asset." +msgstr "Aucune modification enregistrée pour cet asset." + +#: templates/assets/components/asset_history.html:211 +msgid "Authentification requise" +msgstr "Authentification requise" + +#: templates/assets/components/asset_history.html:213 +msgid "Vous n'avez pas accès à cet historique" +msgstr "Vous n'avez pas accès à cet historique" + +#: templates/assets/components/asset_history.html:214 +#: templates/assets/components/asset_history.html:237 +#: templates/assets/components/asset_history.html:242 +msgid "Erreur lors du chargement de l'historique" +msgstr "Erreur lors du chargement de l'historique" + +#: templates/assets/components/asset_history.html:331 +msgid "Asset créé" +msgstr "Asset créé" + +#: templates/assets/components/asset_location_link.html:8 +msgid "Rue associée" +msgstr "Rue associée" + +#: templates/assets/components/asset_location_link.html:10 +msgid "Carrefour associé" +msgstr "Carrefour associé" + +#: templates/assets/components/asset_location_link.html:12 +msgid "Localisation associée" +msgstr "Localisation associée" + +#: templates/assets/components/asset_location_link.html:17 +#: templates/assets/components/asset_model_card.html:9 +msgid "Changer" +msgstr "Changer" + +#: templates/assets/components/asset_location_link.html:26 +msgid "Voir le détail de la localisation" +msgstr "Voir le détail de la localisation" + +#: templates/assets/components/asset_location_link.html:35 +msgid "Aucune localisation associée" +msgstr "Aucune localisation associée" + +#: templates/assets/components/asset_location_link.html:48 +msgid "Changer la localisation" +msgstr "Changer la localisation" + +#: templates/assets/components/asset_location_link.html:55 +msgid "Localisation actuelle :" +msgstr "Localisation actuelle :" + +#: templates/assets/components/asset_location_link.html:59 +msgid "Recherche" +msgstr "Recherche" + +#: templates/assets/components/asset_location_link.html:62 +msgid "Code ou nom de localisation…" +msgstr "Code ou nom de localisation…" + +#: templates/assets/components/asset_location_link.html:67 +msgid "Nouvelle localisation" +msgstr "Nouvelle localisation" + +#: templates/assets/components/asset_location_link.html:70 +msgid "Sélectionner une localisation..." +msgstr "Sélectionner une localisation..." + +#: templates/assets/components/asset_location_link.html:77 +#: templates/assets/components/asset_model_card.html:101 +msgid "Appliquer" +msgstr "Appliquer" + +#: templates/assets/components/asset_model_card.html:38 +msgid "Description" +msgstr "Description" + +#: templates/assets/components/asset_model_card.html:60 +msgid "Changer le modèle" +msgstr "Changer le modèle" + +#: templates/assets/components/asset_model_card.html:69 +msgid "Modèle actuel :" +msgstr "Modèle actuel :" + +#: templates/assets/components/asset_model_card.html:77 +msgid "Toutes les catégories" +msgstr "Toutes les catégories" + +#: templates/assets/components/asset_model_card.html:86 +msgid "Nouveau modèle" +msgstr "Nouveau modèle" + +#: templates/assets/components/asset_model_card.html:89 +msgid "Sélectionner un modèle..." +msgstr "Sélectionner un modèle..." + +#: templates/assets/components/location_edit_modals.html:22 +#, python-format +msgid "Modifier %(label)s" +msgstr "Modifier %(label)s" + +#: templates/assets/components/location_edit_modals.html:40 +#: templates/assets/permissions/access_edit.html:117 +#: templates/assets/permissions/panel.html:278 +#: templates/assets/permissions/type_access_edit.html:122 +#: templates/assets/permissions/type_page.html:215 +#: templates/assets/permissions/type_page.html:419 +#: templates/assets/unified_index.html:632 +#: templates/assets/unified_index.html:662 +msgid "Enregistrer" +msgstr "Enregistrer" + +#: templates/assets/components/location_edit_modals.html:54 +#: templates/assets/controlcenters_location_detail.html:144 +#: templates/assets/its_location_detail.html:87 +#: templates/assets/publiclighting_street_detail.html:193 +#: templates/assets/roads_street_detail.html:192 +#: templates/assets/sign_street_detail.html:192 +#: templates/assets/structures_location_detail.html:231 +#: templates/assets/trafficlights_intersection_detail.html:280 +msgid "Confirmer la suppression" +msgstr "Confirmer la suppression" + +#: templates/assets/components/location_edit_modals.html:59 +#, python-format +msgid "Êtes-vous sûr de vouloir supprimer cette localisation (%(label)s) ?" +msgstr "Êtes-vous sûr de vouloir supprimer cette localisation (%(label)s) ?" + +#: templates/assets/components/location_edit_modals.html:60 +msgid "" +"Cette action est irréversible. Tous les assets associés seront également " +"affectés." +msgstr "" +"Cette action est irréversible. Tous les assets associés seront également " +"affectés." + +#: templates/assets/components/location_edit_modals.html:65 +#: templates/assets/controlcenters_location_detail.html:95 +#: templates/assets/controlcenters_location_detail.html:157 +#: templates/assets/document_version_history_list.html:59 +#: templates/assets/its_location_detail.html:100 +#: templates/assets/permissions/panel.html:70 +#: templates/assets/permissions/panel.html:112 +#: templates/assets/permissions/panel.html:215 +#: templates/assets/permissions/type_page.html:72 +#: templates/assets/permissions/type_page.html:161 +#: templates/assets/permissions/type_page.html:239 +#: templates/assets/permissions/type_page.html:364 +#: templates/assets/publiclighting_street_detail.html:88 +#: templates/assets/publiclighting_street_detail.html:143 +#: templates/assets/publiclighting_street_detail.html:206 +#: templates/assets/roads_street_detail.html:92 +#: templates/assets/roads_street_detail.html:142 +#: templates/assets/roads_street_detail.html:205 +#: templates/assets/sign_street_detail.html:87 +#: templates/assets/sign_street_detail.html:142 +#: templates/assets/sign_street_detail.html:205 +#: templates/assets/structures_location_detail.html:244 +#: templates/assets/trafficlights_intersection_detail.html:293 +msgid "Supprimer" +msgstr "Supprimer" + +# , python-format +#: templates/assets/components/location_edit_modals.html:80 +#, python-format +msgid "Créer une nouvelle localisation (%(label)s)" +msgstr "Créer une nouvelle localisation (%(label)s)" + +#: templates/assets/components/location_edit_modals.html:98 +#: templates/assets/trafficlights_intersection_detail.html:1005 +msgid "Créer" +msgstr "Créer" + +#: templates/assets/components/location_edit_modals.html:179 +msgid "Cliquez pour fermer le polygone" +msgstr "Cliquez pour fermer le polygone" + +#: templates/assets/components/location_edit_modals.html:207 +msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." +msgstr "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." + +#: templates/assets/components/location_edit_modals.html:286 +msgid "Sommets" +msgstr "Sommets" + +#: templates/assets/components/location_edit_modals.html:286 +msgid "Cliquez sur le premier sommet ou « Terminer » pour clore." +msgstr "Cliquez sur le premier sommet ou « Terminer » pour clore." + +#: templates/assets/components/location_edit_modals.html:301 +msgid "Cliquez sur la carte pour ajouter des sommets." +msgstr "Cliquez sur la carte pour ajouter des sommets." + +#: templates/assets/components/location_edit_modals.html:321 +msgid "Polygone effacé. Cliquez sur « Dessiner » pour en tracer un nouveau." +msgstr "Polygone effacé. Cliquez sur « Dessiner » pour en tracer un nouveau." + +#: templates/assets/components/location_edit_modals.html:436 +#: templates/assets/components/location_edit_modals.html:515 +#: templates/assets/detail_generic.html:1191 +msgid "Erreur lors du chargement du formulaire." +msgstr "Erreur lors du chargement du formulaire." + +#: templates/assets/components/location_edit_modals.html:460 +msgid "Erreur lors de la sauvegarde." +msgstr "Erreur lors de la sauvegarde." + +#: templates/assets/components/location_edit_modals.html:463 +#: templates/assets/components/location_edit_modals.html:544 +msgid "Erreur réseau." +msgstr "Erreur réseau." + +#: templates/assets/components/location_edit_modals.html:541 +msgid "Erreur lors de la création." +msgstr "Erreur lors de la création." + +#: templates/assets/components/map_location_picker.html:23 +msgid "Position géographique" +msgstr "Position géographique" + +#: templates/assets/components/map_location_picker.html:24 +#: templates/assets/components/map_polygon_picker.html:26 +msgid "facultatif" +msgstr "facultatif" + +#: templates/assets/components/map_location_picker.html:29 +msgid "Utiliser ma position" +msgstr "Utiliser ma position" + +#: templates/assets/components/map_location_picker.html:34 +msgid "Effacer le marqueur" +msgstr "Effacer le marqueur" + +#: templates/assets/components/map_location_picker.html:45 +msgid "" +"Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour " +"l'ajuster." +msgstr "" +"Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour " +"l'ajuster." + +#: templates/assets/components/map_polygon_picker.html:25 +msgid "Géométrie (polygone)" +msgstr "Géométrie (polygone)" + +#: templates/assets/components/map_polygon_picker.html:31 +msgid "Dessiner un polygone" +msgstr "Dessiner un polygone" + +#: templates/assets/components/map_polygon_picker.html:32 +msgid "Dessiner" +msgstr "Dessiner" + +#: templates/assets/components/map_polygon_picker.html:36 +msgid "Terminer le polygone" +msgstr "Terminer le polygone" + +#: templates/assets/components/map_polygon_picker.html:37 +msgid "Terminer" +msgstr "Terminer" + +#: templates/assets/components/map_polygon_picker.html:41 +msgid "Effacer le polygone" +msgstr "Effacer le polygone" + +#: templates/assets/components/map_polygon_picker.html:51 +msgid "" +"Cliquez sur « Dessiner » pour commencer à tracer un polygone sur la carte." +msgstr "" +"Cliquez sur « Dessiner » pour commencer à tracer un polygone sur la carte." + +#: templates/assets/controlcenters_index.html:8 +#: templates/assets/its_index.html:8 templates/assets/structures_index.html:18 +msgid "Type de Localisation" +msgstr "Type de Localisation" + +#: templates/assets/controlcenters_index.html:10 +#: templates/assets/its_index.html:10 templates/assets/structures_index.html:20 +#: templates/assets/unified_index.html:117 +msgid "Toutes" +msgstr "Toutes" + +#: templates/assets/controlcenters_index.html:11 +#: templates/assets/its_index.html:11 templates/assets/structures_index.html:21 +msgid "Principale" +msgstr "Principale" + +#: templates/assets/controlcenters_index.html:12 +#: templates/assets/its_index.html:12 templates/assets/structures_index.html:22 +msgid "Secondaire" +msgstr "Secondaire" + +#: templates/assets/controlcenters_index.html:61 +msgid "Centre de contrôle" +msgstr "Centre de contrôle" + +#: templates/assets/controlcenters_location_detail.html:15 +msgid "Centre de contrôle:" +msgstr "Centre de contrôle:" + +#: templates/assets/controlcenters_location_detail.html:18 +#: templates/assets/its_location_detail.html:19 +msgid "Localisation principale:" +msgstr "Localisation principale:" + +#: templates/assets/controlcenters_location_detail.html:26 +msgid "Parent:" +msgstr "Parent:" + +#: templates/assets/controlcenters_location_detail.html:45 +#: templates/assets/its_location_detail.html:40 +#: templates/assets/nature_locations_detail.html:37 +#: templates/assets/naturerwiz_detail.html:80 +#: templates/assets/publiclighting_street_detail.html:28 +#: templates/assets/roads_street_detail.html:37 +#: templates/assets/sign_street_detail.html:28 +#: templates/assets/structures_location_detail.html:46 +#: templates/assets/trafficlights_intersection_detail.html:156 +msgid "Documents" +msgstr "Documents" + +#: templates/assets/controlcenters_location_detail.html:86 +#: templates/assets/detail_generic.html:134 +#: templates/assets/detail_generic.html:135 +#: templates/assets/detail_generic.html:178 +#: templates/assets/detail_generic.html:179 +#: templates/assets/publiclighting_street_detail.html:78 +#: templates/assets/publiclighting_street_detail.html:133 +#: templates/assets/sign_street_detail.html:78 +#: templates/assets/sign_street_detail.html:132 +#: templates/assets/trafficlights_import_cross_plan.html:274 +#: templates/assets/trafficlights_import_cross_plan.html:358 +#: templates/assets/trafficlights_import_cross_plan.html:442 +#: templates/assets/trafficlights_import_cross_plan.html:532 +msgid "Archiver" +msgstr "Archiver" + +#: templates/assets/controlcenters_location_detail.html:119 +#: templates/assets/its_location_detail.html:62 +#: templates/assets/naturerwiz_detail.html:280 +#: templates/assets/publiclighting_street_detail.html:168 +#: templates/assets/roads_street_detail.html:167 +#: templates/assets/sign_street_detail.html:167 +#: templates/assets/structures_location_detail.html:197 +#: templates/assets/trafficlights_intersection_detail.html:255 +msgid "Sélectionner des fichiers" +msgstr "Sélectionner des fichiers" + +#: templates/assets/controlcenters_location_detail.html:149 +#: templates/assets/its_location_detail.html:92 +#: templates/assets/publiclighting_street_detail.html:198 +#: templates/assets/roads_street_detail.html:197 +#: templates/assets/sign_street_detail.html:197 +#: templates/assets/structures_location_detail.html:236 +#: templates/assets/trafficlights_intersection_detail.html:285 +msgid "Voulez-vous vraiment supprimer ce document ?" +msgstr "Voulez-vous vraiment supprimer ce document ?" + +#: templates/assets/controlcenters_location_detail.html:150 +#: templates/assets/its_location_detail.html:93 +#: templates/assets/publiclighting_street_detail.html:199 +#: templates/assets/roads_street_detail.html:198 +#: templates/assets/sign_street_detail.html:198 +#: templates/assets/structures_location_detail.html:237 +#: templates/assets/trafficlights_intersection_detail.html:286 +msgid "Nom du fichier" +msgstr "Nom du fichier" + +#: templates/assets/controlcenters_location_detail.html:169 +#: templates/assets/naturerwiz_detail.html:305 +#: templates/assets/publiclighting_street_detail.html:218 +#: templates/assets/roads_street_detail.html:217 +#: templates/assets/sign_street_detail.html:217 +#: templates/assets/structures_location_detail.html:256 +#: templates/assets/trafficlights_intersection_detail.html:305 +msgid "Historique des documents" +msgstr "Historique des documents" + +#: templates/assets/controlcenters_location_detail.html:195 +#: templates/assets/its_location_detail.html:119 +#: templates/assets/naturerwiz_detail.html:195 +#: templates/assets/publiclighting_street_detail.html:238 +#: templates/assets/roads_street_detail.html:237 +#: templates/assets/sign_street_detail.html:237 +#: templates/assets/structures_location_detail.html:287 +msgid "Arborescence" +msgstr "Arborescence" + +#: templates/assets/controlcenters_location_detail.html:202 +#: templates/assets/its_location_detail.html:126 +#: templates/assets/publiclighting_street_detail.html:241 +#: templates/assets/roads_street_detail.html:240 +#: templates/assets/sign_street_detail.html:240 +#: templates/assets/structures_location_detail.html:294 +msgid "Archives" +msgstr "Archives" + +#: templates/assets/controlcenters_location_detail.html:212 +#: templates/assets/controlcenters_location_tree.html:21 +#: templates/assets/its_location_detail.html:136 +#: templates/assets/its_location_tree.html:21 +#: templates/assets/structures_location_tree.html:21 +msgid "sous-localisations" +msgstr "sous-localisations" + +#: templates/assets/controlcenters_location_detail.html:215 +#: templates/assets/its_location_detail.html:139 +#: templates/assets/its_location_tree.html:24 +#: templates/assets/naturerwiz_detail.html:206 +#: templates/assets/naturerwiz_location_tree.html:35 +msgid "assets" +msgstr "assets" + +#: templates/assets/controlcenters_location_detail.html:243 +#: templates/assets/its_location_detail.html:167 +#: templates/assets/nature_locations_detail.html:73 +#: templates/assets/naturerwiz_detail.html:242 +#: templates/assets/publiclighting_street_detail.html:302 +#: templates/assets/roads_street_detail.html:317 +#: templates/assets/sign_street_detail.html:294 +#: templates/assets/structures_location_detail.html:352 +#: templates/assets/trafficlights_intersection_detail.html:933 +msgid "Titre" +msgstr "Titre" + +#: templates/assets/controlcenters_location_detail.html:244 +#: templates/assets/its_location_detail.html:168 +#: templates/assets/nature_locations_detail.html:74 +#: templates/assets/naturerwiz_detail.html:243 +#: templates/assets/publiclighting_street_detail.html:303 +#: templates/assets/roads_street_detail.html:318 +#: templates/assets/sign_street_detail.html:295 +#: templates/assets/structures_location_detail.html:353 +#: templates/assets/trafficlights_intersection_detail.html:934 +msgid "Début" +msgstr "Début" + +#: templates/assets/detail_generic.html:73 +#: templates/assets/detail_generic.html:816 +#: templates/assets/detail_generic.html:833 +msgid "Retour" +msgstr "Retour" + +#: templates/assets/detail_generic.html:85 +msgid "Détails" +msgstr "Détails" + +#: templates/assets/detail_generic.html:87 +msgid "StreetUp" +msgstr "StreetUp" + +#: templates/assets/detail_generic.html:118 +#: templates/assets/detail_generic.html:160 +#: templates/assets/unified_index.html:250 +msgid "Asset" +msgstr "Asset" + +#: templates/assets/detail_generic.html:144 +#: templates/assets/detail_generic.html:188 +msgid "Éditer" +msgstr "Éditer" + +#: templates/assets/detail_generic.html:147 +#: templates/assets/detail_generic.html:191 +msgid "Sauvegarder" +msgstr "Sauvegarder" + +#: templates/assets/detail_generic.html:204 +msgid "Informations générales" +msgstr "Informations générales" + +#: templates/assets/detail_generic.html:277 +#: templates/assets/unified_index.html:212 +msgid "Statut de validation" +msgstr "Statut de validation" + +#: templates/assets/detail_generic.html:326 +#: templates/assets/detail_generic.html:580 +msgid "Installé le" +msgstr "Installé le" + +#: templates/assets/detail_generic.html:339 +msgid "N° de série" +msgstr "N° de série" + +#: templates/assets/detail_generic.html:370 +msgid "mois" +msgstr "mois" + +#: templates/assets/detail_generic.html:375 +#, python-format +msgid "%(n)s mois restants" +msgstr "%(n)s mois restants" + +#: templates/assets/detail_generic.html:377 +msgid "Expire ce mois-ci" +msgstr "Expire ce mois-ci" + +#: templates/assets/detail_generic.html:379 +#, python-format +msgid "Expirée (%(n)s mois)" +msgstr "Expirée (%(n)s mois)" + +#: templates/assets/detail_generic.html:417 +msgid "Attributs spécifiques" +msgstr "Attributs spécifiques" + +#: templates/assets/detail_generic.html:432 +#: templates/assets/trafficlights_index.html:35 templatetags/asset_tags.py:129 +#: views/core.py:622 views/core.py:648 views/trafficlights.py:186 +msgid "Oui" +msgstr "Oui" + +#: templates/assets/detail_generic.html:433 +#: templates/assets/trafficlights_index.html:39 templatetags/asset_tags.py:129 +#: views/core.py:623 views/core.py:649 views/trafficlights.py:186 +msgid "Non" +msgstr "Non" + +#: templates/assets/detail_generic.html:474 +msgid "Permissions spécifiques" +msgstr "Permissions spécifiques" + +#: templates/assets/detail_generic.html:479 +msgid "Voir tous les accès pour ce type" +msgstr "Voir tous les accès pour ce type" + +#: templates/assets/detail_generic.html:481 +msgid "Vue par type" +msgstr "Vue par type" + +#: templates/assets/detail_generic.html:492 +msgid "Chargement des permissions…" +msgstr "Chargement des permissions…" + +#: templates/assets/detail_generic.html:510 +msgid "Erreur lors du chargement." +msgstr "Erreur lors du chargement." + +#: templates/assets/detail_generic.html:522 +msgid "Assets liés" +msgstr "Assets liés" + +#: templates/assets/detail_generic.html:528 +msgid "Lier ou créer un asset" +msgstr "Lier ou créer un asset" + +#: templates/assets/detail_generic.html:529 +msgid "Lier / Créer" +msgstr "Lier / Créer" + +#: templates/assets/detail_generic.html:535 +msgid "Aucun asset lié." +msgstr "Aucun asset lié." + +#: templates/assets/detail_generic.html:547 +msgid "Historique des remplacements" +msgstr "Historique des remplacements" + +#: templates/assets/detail_generic.html:581 +msgid "Désinstallé le" +msgstr "Désinstallé le" + +#: templates/assets/detail_generic.html:582 +msgid "Durée" +msgstr "Durée" + +#: templates/assets/detail_generic.html:618 +msgid "En service" +msgstr "En service" + +#: templates/assets/detail_generic.html:644 +msgid "Géolocalisation" +msgstr "Géolocalisation" + +#: templates/assets/detail_generic.html:651 +msgid "Voir sur Street View" +msgstr "Voir sur Street View" + +#: templates/assets/detail_generic.html:657 +msgid "Voir sur Mapillary" +msgstr "Voir sur Mapillary" + +#: templates/assets/detail_generic.html:728 +msgid "Remplacer l'asset" +msgstr "Remplacer l'asset" + +#: templates/assets/detail_generic.html:733 +msgid "L'asset actuel sera archivé et un nouvel asset sera créé." +msgstr "L'asset actuel sera archivé et un nouvel asset sera créé." + +#: templates/assets/detail_generic.html:734 +msgid "Les noms (FR/NL) de l'ancien asset seront automatiquement copiés." +msgstr "Les noms (FR/NL) de l'ancien asset seront automatiquement copiés." + +#: templates/assets/detail_generic.html:738 +msgid "Modèle du nouvel asset" +msgstr "Modèle du nouvel asset" + +#: templates/assets/detail_generic.html:740 +msgid "Garder le même modèle" +msgstr "Garder le même modèle" + +#: templates/assets/detail_generic.html:749 +#: templates/assets/detail_generic.html:754 +msgid "optionnel" +msgstr "optionnel" + +#: templates/assets/detail_generic.html:777 +msgid "Lier / Créer un asset" +msgstr "Lier / Créer un asset" + +#: templates/assets/detail_generic.html:785 +msgid "Type d'asset à lier" +msgstr "Type d'asset à lier" + +#: templates/assets/detail_generic.html:795 +#: templates/assets/detail_generic.html:826 +msgid "Créer un nouvel asset" +msgstr "Créer un nouvel asset" + +#: templates/assets/detail_generic.html:804 +#: templates/assets/detail_generic.html:1080 +msgid "Rechercher l'asset à lier" +msgstr "Rechercher l'asset à lier" + +#: templates/assets/detail_generic.html:806 +msgid "Code ou nom..." +msgstr "Code ou nom..." + +#: templates/assets/detail_generic.html:819 +msgid "Lier" +msgstr "Lier" + +#: templates/assets/detail_generic.html:827 +msgid "Un nouvel asset sera créé et lié automatiquement à cet asset parent." +msgstr "Un nouvel asset sera créé et lié automatiquement à cet asset parent." + +#: templates/assets/detail_generic.html:836 +msgid "Créer et lier" +msgstr "Créer et lier" + +#: templates/assets/detail_generic.html:942 +#, fuzzy +#| msgid "Assets uniquement" +msgid "Assets enfants" +msgstr "Assets uniquement" + +#: templates/assets/detail_generic.html:959 +#, fuzzy +#| msgid "Assets uniquement" +msgid "Assets parents" +msgstr "Assets uniquement" + +#: templates/assets/detail_generic.html:1048 +#: templates/assets/detail_generic.html:1079 +#, fuzzy +#| msgid "Rechercher l'asset à lier" +msgid "Rechercher le parent à lier" +msgstr "Rechercher l'asset à lier" + +#: templates/assets/detail_generic.html:1180 +msgid "Chargement du formulaire…" +msgstr "Chargement du formulaire…" + +#: templates/assets/detail_generic.html:1198 +msgid "Aucun champ supplémentaire requis." +msgstr "Aucun champ supplémentaire requis." + +#: templates/assets/detail_generic.html:1223 +msgid "— Sélectionner —" +msgstr "— Sélectionner —" + +#: templates/assets/detail_generic.html:1223 +msgid "— Aucun —" +msgstr "— Aucun —" + +#: templates/assets/detail_generic.html:1513 +msgid "Champs obligatoires manquants" +msgstr "Champs obligatoires manquants" + +#: templates/assets/detail_generic.html:1596 +#: templates/assets/detail_generic.html:1629 +msgid "Délier cet asset de son parent ?" +msgstr "Délier cet asset de son parent ?" + +#: templates/assets/document_history_list.html:19 +#: templates/assets/document_version_history_list.html:27 +msgid "Ajouté le" +msgstr "Ajouté le" + +#: templates/assets/document_history_list.html:21 +#: templates/assets/document_version_history_list.html:29 +msgid "par" +msgstr "par" + +#: templates/assets/document_history_list.html:42 +msgid "Aucun document trouvé." +msgstr "Aucun document trouvé." + +#: templates/assets/document_version_history_list.html:37 +msgid "Approuvé" +msgstr "Approuvé" + +#: templates/assets/document_version_history_list.html:39 +msgid "En attente" +msgstr "En attente" + +#: templates/assets/document_version_history_list.html:43 +msgid "Brouillon" +msgstr "Brouillon" + +#: templates/assets/document_version_history_list.html:50 +msgid "Télécharger" +msgstr "Télécharger" + +#: templates/assets/document_version_history_list.html:66 +msgid "Aucune version trouvée." +msgstr "Aucune version trouvée." + +#: templates/assets/its_index.html:27 +msgid "Créer une nouvelle localisation ITS" +msgstr "Créer une nouvelle localisation ITS" + +#: templates/assets/its_index.html:66 +msgid "Créer un nouvel asset ITS" +msgstr "Créer un nouvel asset ITS" + +#: templates/assets/its_location_detail.html:22 +msgid "Localisation parente:" +msgstr "Localisation parente:" + +#: templates/assets/nature_index.html:8 templates/assets/water_index.html:7 +#: views/core.py:667 views/core.py:687 +msgid "Type de localisation" +msgstr "Type de localisation" + +#: templates/assets/nature_index.html:13 +#: templates/assets/permissions/panel.html:147 +#: templates/assets/permissions/panel.html:228 +#: templates/assets/permissions/panel.html:254 +#: templates/assets/permissions/type_page.html:98 +#: templates/assets/permissions/type_page.html:171 +#: templates/assets/permissions/type_page.html:194 +#: templates/assets/permissions/type_page.html:282 +#: templates/assets/permissions/type_page.html:372 +#: templates/assets/permissions/type_page.html:395 +#: templates/assets/trafficlights_index.html:43 +#: templates/assets/trafficlights_index.html:51 +#: templates/assets/unified_index.html:93 +#: templates/assets/unified_index.html:131 templates/assets/water_index.html:12 +#: views/core.py:670 views/core.py:690 +msgid "Tous" +msgstr "Tous" + +#: templates/assets/nature_index.html:18 views/core.py:671 views/nature.py:104 +#: views/publiclighting.py:93 views/roads.py:99 views/sign.py:105 +msgid "Voiries" +msgstr "Voiries" + +#: templates/assets/nature_index.html:24 views/core.py:676 +msgid "Ring et autoroutes" +msgstr "Ring et autoroutes" + +#: templates/assets/nature_index.html:77 +msgid "Ring/Autoroute" +msgstr "Ring/Autoroute" + +#: templates/assets/nature_index.html:84 templates/assets/water_index.html:81 +msgid "Superficie" +msgstr "Superficie" + +#: templates/assets/nature_locations_detail.html:14 +msgid "Zone:" +msgstr "Zone:" + +#: templates/assets/nature_locations_detail.html:15 +msgid "Autoroute:" +msgstr "Autoroute:" + +#: templates/assets/nature_locations_detail.html:28 +msgid "Nombre d'arbres:" +msgstr "Nombre d'arbres:" + +#: templates/assets/nature_locations_detail.html:29 +msgid "Superficie massif arboré:" +msgstr "Superficie massif arboré:" + +#: templates/assets/nature_locations_detail.html:30 +msgid "Superficie pré fleuri:" +msgstr "Superficie pré fleuri:" + +#: templates/assets/naturerwiz_detail.html:116 +msgid "Documents de la zone" +msgstr "Documents de la zone" + +#: templates/assets/naturerwiz_detail.html:124 +msgid "Documents des assets" +msgstr "Documents des assets" + +#: templates/assets/naturerwiz_detail.html:135 +#: templates/assets/structures_location_detail.html:54 +#: templates/assets/trafficlights_intersection_detail.html:165 +msgid "Répertoire principal" +msgstr "Répertoire principal" + +#: templates/assets/naturerwiz_detail.html:143 +#, fuzzy +#| msgid "Aucun document trouvé." +msgid "Aucun document de zone configuré." +msgstr "Aucun document trouvé." + +#: templates/assets/naturerwiz_detail.html:169 +#, fuzzy +#| msgid "Aucun contrat associé." +msgid "Aucun document d'asset associé." +msgstr "Aucun contrat associé." + +#: templates/assets/naturerwiz_detail.html:211 +#: templates/assets/structures_location_detail.html:321 +msgid "Masquer les non-actifs" +msgstr "Masquer les non-actifs" + +#: templates/assets/naturerwiz_detail.html:213 +#: templates/assets/structures_location_detail.html:323 +msgid "Afficher les non-actifs" +msgstr "Afficher les non-actifs" + +#: templates/assets/naturerwiz_detail.html:220 +#, fuzzy +#| msgid "Aucun asset lié." +msgid "Aucun asset associé." +msgstr "Aucun asset lié." + +#: templates/assets/permissions/access_edit.html:10 +#: templates/assets/permissions/type_access_edit.html:10 +#: templates/assets/permissions/type_page.html:10 +msgid "Accueil" +msgstr "Accueil" + +#: templates/assets/permissions/access_edit.html:11 +msgid "Modifier un accès" +msgstr "Modifier un accès" + +#: templates/assets/permissions/access_edit.html:18 +msgid "Modifier l'accès" +msgstr "Modifier l'accès" + +#: templates/assets/permissions/access_edit.html:29 +msgid "Type :" +msgstr "Type :" + +#: templates/assets/permissions/access_edit.html:31 +msgid "Objet #" +msgstr "Objet #" + +#: templates/assets/permissions/access_edit.html:37 +#: templates/assets/permissions/panel.html:204 +#: templates/assets/permissions/type_access_edit.html:42 +#: templates/assets/permissions/type_page.html:150 +#: templates/assets/permissions/type_page.html:353 +msgid "Droits" +msgstr "Droits" + +#: templates/assets/permissions/access_edit.html:41 +#: templates/assets/permissions/type_access_edit.html:46 +msgid "Peut visualiser" +msgstr "Peut visualiser" + +#: templates/assets/permissions/access_edit.html:46 +#: templates/assets/permissions/type_access_edit.html:51 +msgid "Peut modifier" +msgstr "Peut modifier" + +#: templates/assets/permissions/access_edit.html:51 +#: templates/assets/permissions/type_access_edit.html:56 +msgid "Peut supprimer" +msgstr "Peut supprimer" + +#: templates/assets/permissions/access_edit.html:61 +#: templates/assets/permissions/panel.html:113 +#: templates/assets/permissions/panel.html:224 +#: templates/assets/permissions/type_access_edit.html:66 +#: templates/assets/permissions/type_page.html:73 +#: templates/assets/permissions/type_page.html:169 +#: templates/assets/permissions/type_page.html:240 +#: templates/assets/permissions/type_page.html:393 +msgid "Champs visibles" +msgstr "Champs visibles" + +#: templates/assets/permissions/access_edit.html:62 +#: templates/assets/permissions/type_access_edit.html:67 +msgid "vide = tous les champs" +msgstr "vide = tous les champs" + +#: templates/assets/permissions/access_edit.html:65 +#: templates/assets/permissions/access_edit.html:93 +#: templates/assets/permissions/type_access_edit.html:70 +#: templates/assets/permissions/type_access_edit.html:98 +#: templates/assets/trafficlights_import_dxf.html:117 +msgid "Tout cocher" +msgstr "Tout cocher" + +#: templates/assets/permissions/access_edit.html:67 +#: templates/assets/permissions/access_edit.html:95 +#: templates/assets/permissions/type_access_edit.html:72 +#: templates/assets/permissions/type_access_edit.html:100 +#: templates/assets/trafficlights_import_dxf.html:118 +msgid "Tout décocher" +msgstr "Tout décocher" + +#: templates/assets/permissions/access_edit.html:80 +#: templates/assets/permissions/access_edit.html:108 +#: templates/assets/permissions/panel.html:242 +#: templates/assets/permissions/panel.html:268 +#: templates/assets/permissions/type_access_edit.html:85 +#: templates/assets/permissions/type_access_edit.html:113 +#: templates/assets/permissions/type_page.html:185 +#: templates/assets/permissions/type_page.html:208 +#: templates/assets/permissions/type_page.html:386 +#: templates/assets/permissions/type_page.html:409 +msgid "Aucun champ disponible" +msgstr "Aucun champ disponible" + +#: templates/assets/permissions/access_edit.html:89 +#: templates/assets/permissions/panel.html:114 +#: templates/assets/permissions/panel.html:250 +#: templates/assets/permissions/type_access_edit.html:94 +#: templates/assets/permissions/type_page.html:74 +#: templates/assets/permissions/type_page.html:192 +#: templates/assets/permissions/type_page.html:241 +#: templates/assets/permissions/type_page.html:370 +msgid "Champs modifiables" +msgstr "Champs modifiables" + +#: templates/assets/permissions/access_edit.html:90 +#: templates/assets/permissions/type_access_edit.html:95 +msgid "vide = droits par rôle" +msgstr "vide = droits par rôle" + +#: templates/assets/permissions/panel.html:29 +msgid "Type d'objet :" +msgstr "Type d'objet :" + +#: templates/assets/permissions/panel.html:38 +msgid "Droits explicites requis (droits thématiques insuffisants)" +msgstr "Droits explicites requis (droits thématiques insuffisants)" + +#: templates/assets/permissions/panel.html:43 +msgid "Accès restreint" +msgstr "Accès restreint" + +#: templates/assets/permissions/panel.html:55 +msgid "Accès au niveau du type (tous les objets)" +msgstr "Accès au niveau du type (tous les objets)" + +#: templates/assets/permissions/panel.html:58 +msgid "Gérer" +msgstr "Gérer" + +#: templates/assets/permissions/panel.html:62 +msgid "" +"Les utilisateurs ci-dessous ont un accès défini pour TOUS les objets de ce " +"type. Ces droits s'appliquent à cet objet sauf si une ligne d'accès " +"spécifique ci-dessous les écrase." +msgstr "" +"Les utilisateurs ci-dessous ont un accès défini pour TOUS les objets de ce " +"type. Ces droits s'appliquent à cet objet sauf si une ligne d'accès " +"spécifique ci-dessous les écrase." + +#: templates/assets/permissions/panel.html:68 +#: templates/assets/permissions/panel.html:110 +#: templates/assets/permissions/panel.html:207 +#: templates/assets/permissions/type_page.html:70 +#: templates/assets/permissions/type_page.html:153 +#: templates/assets/permissions/type_page.html:237 +#: templates/assets/permissions/type_page.html:356 +msgid "Voir" +msgstr "Voir" + +#: templates/assets/permissions/panel.html:69 +#: templates/assets/permissions/panel.html:111 +#: templates/assets/permissions/panel.html:211 +#: templates/assets/permissions/type_page.html:71 +#: templates/assets/permissions/type_page.html:157 +#: templates/assets/permissions/type_page.html:238 +#: templates/assets/permissions/type_page.html:360 +msgid "Modifier" +msgstr "Modifier" + +#: templates/assets/permissions/panel.html:71 +msgid "Champs" +msgstr "Champs" + +#: templates/assets/permissions/panel.html:94 +#: templates/assets/permissions/panel.html:154 +#: templates/assets/permissions/type_page.html:105 +#: templates/assets/permissions/type_page.html:289 +msgid "Par rôle" +msgstr "Par rôle" + +#: templates/assets/permissions/panel.html:165 +#: templates/assets/permissions/type_page.html:300 +msgid "Supprimer cet accès ?" +msgstr "Supprimer cet accès ?" + +#: templates/assets/permissions/panel.html:177 +msgid "Aucun accès spécifique défini pour cet objet." +msgstr "Aucun accès spécifique défini pour cet objet." + +#: templates/assets/permissions/panel.html:183 +msgid "Ajouter / mettre à jour un accès" +msgstr "Ajouter / mettre à jour un accès" + +#: templates/assets/permissions/panel.html:194 +#: templates/assets/permissions/type_page.html:143 +#: templates/assets/permissions/type_page.html:336 +msgid "choisir" +msgstr "choisir" + +#: templates/assets/permissions/panel.html:225 +#: templates/assets/permissions/type_page.html:169 +#: templates/assets/permissions/type_page.html:393 +msgid "vide = tous" +msgstr "vide = tous" + +#: templates/assets/permissions/panel.html:251 +#: templates/assets/permissions/type_page.html:192 +#: templates/assets/permissions/type_page.html:370 +msgid "vide = rôle" +msgstr "vide = rôle" + +#: templates/assets/permissions/type_access_edit.html:13 +#: templates/assets/permissions/type_page.html:12 +msgid "Permissions" +msgstr "Permissions" + +#: templates/assets/permissions/type_access_edit.html:16 +#: templates/assets/permissions/type_access_edit.html:23 +msgid "Modifier l'accès de type" +msgstr "Modifier l'accès de type" + +#: templates/assets/permissions/type_access_edit.html:34 +msgid "Type d'asset :" +msgstr "Type d'asset :" + +#: templates/assets/permissions/type_access_edit.html:36 +msgid "s'applique à tous les objets de ce type" +msgstr "s'applique à tous les objets de ce type" + +#: templates/assets/permissions/type_page.html:26 +msgid "Configuration du type" +msgstr "Configuration du type" + +#: templates/assets/permissions/type_page.html:38 +msgid "Droits explicites requis" +msgstr "Droits explicites requis" + +#: templates/assets/permissions/type_page.html:42 +msgid "" +"Si activé, les droits thématiques seuls ne suffisent pas pour accéder aux " +"objets de ce type. Chaque utilisateur doit avoir une ligne d'accès explicite " +"ci-dessous." +msgstr "" +"Si activé, les droits thématiques seuls ne suffisent pas pour accéder aux " +"objets de ce type. Chaque utilisateur doit avoir une ligne d'accès explicite " +"ci-dessous." + +#: templates/assets/permissions/type_page.html:47 +msgid "" +"Accès restreint actif — les droits thématiques ne sont pas pris en compte " +"pour ce type." +msgstr "" +"Accès restreint actif — les droits thématiques ne sont pas pris en compte " +"pour ce type." + +#: templates/assets/permissions/type_page.html:59 +msgid "Accès utilisateur (tous les objets de ce type)" +msgstr "Accès utilisateur (tous les objets de ce type)" + +#: templates/assets/permissions/type_page.html:116 +msgid "Supprimer cet accès de type ?" +msgstr "Supprimer cet accès de type ?" + +#: templates/assets/permissions/type_page.html:130 +msgid "" +"Aucun accès de type défini. Les droits thématiques s'appliquent (sauf " +"restriction ci-dessus)." +msgstr "" +"Aucun accès de type défini. Les droits thématiques s'appliquent (sauf " +"restriction ci-dessus)." + +#: templates/assets/permissions/type_page.html:226 +msgid "Accès par utilisateur" +msgstr "Accès par utilisateur" + +#: templates/assets/permissions/type_page.html:236 +msgid "Objet" +msgstr "Objet" + +#: templates/assets/permissions/type_page.html:315 +msgid "Aucun accès spécifique défini pour ce type d'objet." +msgstr "Aucun accès spécifique défini pour ce type d'objet." + +#: templates/assets/permissions/type_page.html:324 +msgid "Ajouter un accès sur un objet spécifique" +msgstr "Ajouter un accès sur un objet spécifique" + +#: templates/assets/permissions/type_page.html:346 +msgid "ID objet" +msgstr "ID objet" + +#: templates/assets/permissions/type_page.html:349 +msgid "ID de l'instance précise" +msgstr "ID de l'instance précise" + +#: templates/assets/publiclighting_street_detail.html:15 +#: templates/assets/roads_street_detail.html:15 +#: templates/assets/sign_street_detail.html:15 +msgid "Commune:" +msgstr "Commune:" + +#: templates/assets/publiclighting_street_detail.html:74 +#: templates/assets/roads_street_detail.html:83 +#: templates/assets/sign_street_detail.html:74 +#: templates/assets/structures_location_detail.html:111 +#: templates/assets/structures_location_detail.html:171 +#: templates/assets/trafficlights_intersection_detail.html:217 +msgid "Historique" +msgstr "Historique" + +#: templates/assets/publiclighting_street_detail.html:250 +#: templates/assets/sign_street_detail.html:249 +#: templates/assets/trafficlights_import_cross_plan.html:200 +#: templates/assets/trafficlights_import_cross_plan.html:603 +#: templates/assets/trafficlights_intersection_detail.html:425 +msgid "Poteaux" +msgstr "Poteaux" + +#: templates/assets/publiclighting_street_detail.html:267 +msgid "Alimentation:" +msgstr "Alimentation:" + +#: templates/assets/roads_index.html:49 +msgid "Qualité globale" +msgstr "Qualité globale" + +#: templates/assets/roads_index.html:87 +#: templates/assets/roads_street_detail.html:255 +#: templates/assets/roads_street_detail.html:270 +msgid "Qualité" +msgstr "Qualité" + +#: templates/assets/roads_street_detail.html:17 +msgid "Code voirie:" +msgstr "Code voirie:" + +#: templates/assets/roads_street_detail.html:23 +msgid "Niveau:" +msgstr "Niveau:" + +#: templates/assets/roads_street_detail.html:249 +msgid "Chaussées" +msgstr "Chaussées" + +#: templates/assets/roads_street_detail.html:259 +msgid "Aucune chaussée" +msgstr "Aucune chaussée" + +#: templates/assets/roads_street_detail.html:264 +msgid "Trottoirs" +msgstr "Trottoirs" + +#: templates/assets/roads_street_detail.html:274 +msgid "Aucun trottoir" +msgstr "Aucun trottoir" + +#: templates/assets/roads_street_detail.html:279 +msgid "Mobilier urbain" +msgstr "Mobilier urbain" + +#: templates/assets/roads_street_detail.html:287 +msgid "Aucun mobilier urbain" +msgstr "Aucun mobilier urbain" + +#: templates/assets/roads_street_detail.html:296 +msgid "Aucun asset archivé" +msgstr "Aucun asset archivé" + +#: templates/assets/roads_street_detail.html:336 +msgid "Aucune intervention" +msgstr "Aucune intervention" + +#: templates/assets/sign_index.html:49 views/core.py:709 +msgid "Type de panneau" +msgstr "Type de panneau" + +#: templates/assets/sign_index.html:88 +msgid "Texte" +msgstr "Texte" + +#: templates/assets/structures_index.html:8 views/core.py:657 +msgid "Type d'ouvrage" +msgstr "Type d'ouvrage" + +#: templates/assets/structures_location_detail.html:19 views/core.py:645 +msgid "Localisation principale" +msgstr "Localisation principale" + +#: templates/assets/structures_location_detail.html:27 +msgid "Parent" +msgstr "Parent" + +#: templates/assets/structures_location_detail.html:275 +msgid "Créer un nouvel équipement" +msgstr "Créer un nouvel équipement" + +#: templates/assets/structures_location_detail.html:276 +#: templates/assets/trafficlights_import_cross_plan.html:259 +#: templates/assets/trafficlights_import_cross_plan.html:343 +#: templates/assets/trafficlights_import_cross_plan.html:427 +#: templates/assets/trafficlights_import_cross_plan.html:517 +msgid "Nouveau" +msgstr "Nouveau" + +#: templates/assets/structures_location_detail.html:305 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(c)s sous-localisation\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(c)s sous-localisations\n" +#| " " +msgid "" +"\n" +" %(c)s sous-localisation\n" +" " +msgid_plural "" +"\n" +" %(c)s sous-localisations\n" +" " +msgstr[0] "" +"\n" +" %(c)s sous-localisation\n" +" " +msgstr[1] "" +"\n" +" %(c)s sous-localisations\n" +" " + +#: templates/assets/structures_location_detail.html:312 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(a)s asset\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(a)s assets\n" +#| " " +msgid "" +"\n" +" %(a)s asset\n" +" " +msgid_plural "" +"\n" +" %(a)s assets\n" +" " +msgstr[0] "" +"\n" +" %(a)s asset\n" +" " +msgstr[1] "" +"\n" +" %(a)s assets\n" +" " + +#: templates/assets/trafficlights_import_cross_plan.html:18 +#: templates/assets/trafficlights_import_dxf.html:12 +#, fuzzy +#| msgid "Thématique" +msgid "Thématiques" +msgstr "Thématique" + +#: templates/assets/trafficlights_import_cross_plan.html:20 +#, fuzzy +#| msgid "Plan de croix" +msgid "Importation Plan de croix" +msgstr "Plan de croix" + +#: templates/assets/trafficlights_import_cross_plan.html:26 +msgid "Importation d'un plan de croix (Excel)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:29 +#: templates/assets/trafficlights_import_dxf.html:23 +#, fuzzy +#| msgid "Statut des carrefours" +msgid "Retour au carrefour" +msgstr "Statut des carrefours" + +#: templates/assets/trafficlights_import_cross_plan.html:42 +msgid "Sélectionner / Téléverser le plan de croix (.xlsx)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:60 +msgid "Utiliser des plans de croix déjà rattachés" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:80 +msgid "" +"Sélectionnez un ou plusieurs plans de croix déjà associés à ce carrefour, ou " +"téléversez-en de nouveaux ci-dessous." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:85 +#: templates/assets/trafficlights_import_dxf.html:68 +#: templates/assets/unified_index.html:412 +msgid "OU" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:92 +msgid "Fichier Excel du plan de croix" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:96 +msgid "" +"Téléversez un ou plusieurs classeurs Excel de plans de croix (contenant " +"l'onglet 'kruisjesplan' ou 'Plan croix')." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:102 +#, fuzzy +#| msgid "Filtrer les assets" +msgid "Analyser et comparer les assets" +msgstr "Filtrer les assets" + +#: templates/assets/trafficlights_import_cross_plan.html:132 +msgid "" +"Veuillez téléverser un fichier Excel ou sélectionner au moins un plan " +"existant." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:171 +msgid "Aperçu de la comparaison des assets" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:174 +#, fuzzy +#| msgid "Fichier" +msgid "Fichiers :" +msgstr "Fichier" + +#: templates/assets/trafficlights_import_cross_plan.html:176 +#, fuzzy +#| msgid "Fichier" +msgid "Fichier :" +msgstr "Fichier" + +#: templates/assets/trafficlights_import_cross_plan.html:178 +#, fuzzy +#| msgid "Carrefour associé" +msgid "Carrefour :" +msgstr "Carrefour associé" + +#: templates/assets/trafficlights_import_cross_plan.html:183 +msgid "Tout remplacer (Archiver l'existant + Recréer)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:186 +msgid "Appliquer les décisions" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:210 +#, fuzzy +#| msgid "Détecteurs" +msgid "Détecteurs & BP" +msgstr "Détecteurs" + +#: templates/assets/trafficlights_import_cross_plan.html:229 +msgid "" +"Vous pouvez géolocaliser les poteaux en les plaçant sur le plan de la carte." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:232 +msgid "Positionner les poteaux sur la carte" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:240 +#: templates/assets/trafficlights_import_cross_plan.html:322 +#: templates/assets/trafficlights_import_cross_plan.html:406 +#: templates/assets/trafficlights_import_cross_plan.html:498 +#, fuzzy +#| msgid "Code EAN" +msgid "Code Asset" +msgstr "Code EAN" + +#: templates/assets/trafficlights_import_cross_plan.html:243 +#: templates/assets/trafficlights_import_cross_plan.html:326 +#: templates/assets/trafficlights_import_cross_plan.html:410 +#: templates/assets/trafficlights_import_cross_plan.html:501 +msgid "Modèle Déduit (Excel)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:244 +#: templates/assets/trafficlights_import_cross_plan.html:327 +#: templates/assets/trafficlights_import_cross_plan.html:411 +#: templates/assets/trafficlights_import_cross_plan.html:502 +#, fuzzy +#| msgid "Modèle actuel :" +msgid "Modèle Actuel (DB)" +msgstr "Modèle actuel :" + +#: templates/assets/trafficlights_import_cross_plan.html:245 +#: templates/assets/trafficlights_import_cross_plan.html:328 +#: templates/assets/trafficlights_import_cross_plan.html:412 +#: templates/assets/trafficlights_import_cross_plan.html:503 +msgid "Décision" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:246 +#: templates/assets/trafficlights_import_cross_plan.html:329 +#: templates/assets/trafficlights_import_cross_plan.html:413 +#: templates/assets/trafficlights_import_cross_plan.html:504 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Forcer le Modèle" +msgstr "Changer le modèle" + +#: templates/assets/trafficlights_import_cross_plan.html:261 +#: templates/assets/trafficlights_import_cross_plan.html:345 +#: templates/assets/trafficlights_import_cross_plan.html:429 +#: templates/assets/trafficlights_import_cross_plan.html:519 +#, fuzzy +#| msgid "Modifier" +msgid "Modifié" +msgstr "Modifier" + +#: templates/assets/trafficlights_import_cross_plan.html:263 +#: templates/assets/trafficlights_import_cross_plan.html:347 +#: templates/assets/trafficlights_import_cross_plan.html:431 +#: templates/assets/trafficlights_import_cross_plan.html:521 +msgid "Identique" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:265 +#: templates/assets/trafficlights_import_cross_plan.html:349 +#: templates/assets/trafficlights_import_cross_plan.html:433 +#: templates/assets/trafficlights_import_cross_plan.html:523 +#, fuzzy +#| msgid "Assets uniquement" +msgid "DB uniquement" +msgstr "Assets uniquement" + +#: templates/assets/trafficlights_import_cross_plan.html:273 +#: templates/assets/trafficlights_import_cross_plan.html:357 +#: templates/assets/trafficlights_import_cross_plan.html:441 +#: templates/assets/trafficlights_import_cross_plan.html:531 +msgid "Conserver en DB" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:279 +#: templates/assets/trafficlights_import_cross_plan.html:363 +#: templates/assets/trafficlights_import_cross_plan.html:447 +#: templates/assets/trafficlights_import_cross_plan.html:537 +#: templates/assets/unified_index.html:329 +msgid "Créer l'asset" +msgstr "Créer l'asset" + +#: templates/assets/trafficlights_import_cross_plan.html:280 +#: templates/assets/trafficlights_import_cross_plan.html:364 +#: templates/assets/trafficlights_import_cross_plan.html:448 +#: templates/assets/trafficlights_import_cross_plan.html:538 +msgid "Ignorer" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:282 +#: templates/assets/trafficlights_import_cross_plan.html:366 +#: templates/assets/trafficlights_import_cross_plan.html:450 +#: templates/assets/trafficlights_import_cross_plan.html:540 +#, fuzzy +#| msgid "Créer l'asset" +msgid "Mettre à jour l'asset" +msgstr "Créer l'asset" + +#: templates/assets/trafficlights_import_cross_plan.html:283 +#: templates/assets/trafficlights_import_cross_plan.html:287 +#: templates/assets/trafficlights_import_cross_plan.html:367 +#: templates/assets/trafficlights_import_cross_plan.html:371 +#: templates/assets/trafficlights_import_cross_plan.html:451 +#: templates/assets/trafficlights_import_cross_plan.html:455 +#: templates/assets/trafficlights_import_cross_plan.html:541 +#: templates/assets/trafficlights_import_cross_plan.html:545 +msgid "Remplacer (Archiver + Créer)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:284 +#: templates/assets/trafficlights_import_cross_plan.html:368 +#: templates/assets/trafficlights_import_cross_plan.html:452 +#: templates/assets/trafficlights_import_cross_plan.html:542 +#, fuzzy +#| msgid "Inexistant" +msgid "Conserver existant" +msgstr "Inexistant" + +#: templates/assets/trafficlights_import_cross_plan.html:286 +#: templates/assets/trafficlights_import_cross_plan.html:370 +#: templates/assets/trafficlights_import_cross_plan.html:454 +#: templates/assets/trafficlights_import_cross_plan.html:544 +msgid "Ne rien faire (Identique)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:295 +#: templates/assets/trafficlights_import_cross_plan.html:379 +#: templates/assets/trafficlights_import_cross_plan.html:463 +#: templates/assets/trafficlights_import_cross_plan.html:553 +msgid "Laisser par défaut" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:307 +#, fuzzy +#| msgid "Aucun poteau" +msgid "Aucun poteau détecté." +msgstr "Aucun poteau" + +#: templates/assets/trafficlights_import_cross_plan.html:324 +#: templates/assets/trafficlights_import_cross_plan.html:408 +msgid "Poteau" +msgstr "Poteau" + +#: templates/assets/trafficlights_import_cross_plan.html:391 +#, fuzzy +#| msgid "Aucun détecteur" +msgid "Aucune lanterne détectée." +msgstr "Aucun détecteur" + +#: templates/assets/trafficlights_import_cross_plan.html:475 +#, fuzzy +#| msgid "Aucun détecteur" +msgid "Aucun détecteur détecté." +msgstr "Aucun détecteur" + +#: templates/assets/trafficlights_import_cross_plan.html:487 +msgid "" +"Vous pouvez tracer précisément les câbles d'interconnexion le long des " +"tranchées de la carte." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:490 +msgid "Tracer les câbles sur la carte" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:565 +#, fuzzy +#| msgid "Aucun câble" +msgid "Aucun câble détecté." +msgstr "Aucun câble" + +#: templates/assets/trafficlights_import_cross_plan.html:590 +msgid "Retour au tableau comparatif" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:593 +msgid "Poteaux positionnés :" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:623 +msgid "Aide au placement" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:626 +#, fuzzy +#| msgid "" +#| "Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour " +#| "l'ajuster." +msgid "" +"Cliquez sur 'Placer sur la carte' pour un poteau, puis cliquez sur la carte " +"pour le positionner." +msgstr "" +"Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour " +"l'ajuster." + +#: templates/assets/trafficlights_import_cross_plan.html:636 +#, fuzzy +#| msgid "A remplacer" +msgid "À placer" +msgstr "A remplacer" + +#: templates/assets/trafficlights_import_cross_plan.html:645 +msgid "Placer sur la carte" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:660 +msgid "Tracé des câbles" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:663 +msgid "" +"Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer " +"la ligne du câble." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:677 +#, fuzzy +#| msgid "Éditer" +msgid "Tracer / Éditer" +msgstr "Éditer" + +#: templates/assets/trafficlights_import_cross_plan.html:697 +#: templates/assets/trafficlights_import_cross_plan.html:1319 +#: templates/assets/trafficlights_import_cross_plan.html:1373 +msgid "Mode: Lecture" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:699 +#: templates/assets/unified_index.html:399 +msgid "Effacer" +msgstr "Effacer" + +#: templates/assets/trafficlights_import_cross_plan.html:715 +#: templates/assets/trafficlights_intersection_detail.html:1019 +msgid "Plans géoréférencés" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:723 +#: templates/assets/trafficlights_intersection_detail.html:1027 +msgid "Plan image" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:728 +#: templates/assets/trafficlights_intersection_detail.html:1032 +#, fuzzy +#| msgid "Plan" +msgid "Plan CAO" +msgstr "Plan" + +#: templates/assets/trafficlights_import_cross_plan.html:743 +#: templates/assets/trafficlights_intersection_detail.html:1047 +msgid "Aucun plan d'image géoréférencé disponible" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:744 +#: templates/assets/trafficlights_intersection_detail.html:1048 +msgid "" +"Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la " +"fiche." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:755 +#: templates/assets/trafficlights_intersection_detail.html:1059 +#, fuzzy +#| msgid "Aucun tag disponible pour cette thématique." +msgid "Aucun fichier DXF disponible pour ce carrefour" +msgstr "Aucun tag disponible pour cette thématique." + +#: templates/assets/trafficlights_import_cross_plan.html:756 +#: templates/assets/trafficlights_intersection_detail.html:1060 +msgid "Veuillez d'abord en importer un." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:761 +#: templates/assets/trafficlights_intersection_detail.html:1068 +#, fuzzy +#| msgid "Sélection de la vue" +msgid "Sélectionner le plan DXF" +msgstr "Sélection de la vue" + +#: templates/assets/trafficlights_import_cross_plan.html:769 +#: templates/assets/trafficlights_intersection_detail.html:1076 +#, fuzzy +#| msgid "Sélectionner un type" +msgid "-- Sélectionner un plan CAO (DXF) --" +msgstr "Sélectionner un type" + +#: templates/assets/trafficlights_import_cross_plan.html:780 +#: templates/assets/trafficlights_intersection_detail.html:1087 +msgid "Couches vectorielles CAO" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:782 +#: templates/assets/trafficlights_intersection_detail.html:1089 +msgid "Décocher tout" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:787 +#: templates/assets/trafficlights_intersection_detail.html:1094 +msgid "Lecture des couches du plan..." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:800 +#: templates/assets/trafficlights_intersection_detail.html:1107 +#, fuzzy +#| msgid "Désactivé" +msgid "Désactiver tout" +msgstr "Désactivé" + +#: templates/assets/trafficlights_import_cross_plan.html:859 +msgid "" +"Voulez-vous vraiment archiver TOUS les assets actifs existants de ce " +"carrefour et recréer les nouveaux à partir du fichier Excel ? Cette action " +"est irréversible." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:1014 +msgid "Rechercher une adresse…" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:1215 +#, fuzzy +#| msgid "Position géographique" +msgid "Positionné" +msgstr "Position géographique" + +#: templates/assets/trafficlights_import_cross_plan.html:1303 +msgid "Mode: Placement de" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:1344 +msgid "Mode: Tracé" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:2058 +#, fuzzy +#| msgid "Commentaire enregistré avec succès" +msgid "Couche vectorielle chargée avec succès !" +msgstr "Commentaire enregistré avec succès" + +#: templates/assets/trafficlights_import_dxf.html:14 +msgid "Importation DXF" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:20 +msgid "Importation d'un plan DXF — Poteaux" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:36 +msgid "Choisir un plan DXF" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:53 +#, fuzzy +#| msgid "Sélectionner une catégorie" +msgid "Sélectionner un fichier existant (déjà importé)" +msgstr "Sélectionner une catégorie" + +#: templates/assets/trafficlights_import_dxf.html:56 +#, fuzzy +#| msgid "Sélectionner des fichiers" +msgid "Sélectionner un fichier" +msgstr "Sélectionner des fichiers" + +#: templates/assets/trafficlights_import_dxf.html:62 +msgid "" +"Sélectionnez un fichier DXF parmi les fichiers déjà disponibles sur le " +"serveur." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:75 +msgid "Téléverser un nouveau fichier DXF" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:82 +#, fuzzy +#| msgid "Changer les fichiers" +msgid "Analyser le fichier" +msgstr "Changer les fichiers" + +#: templates/assets/trafficlights_import_dxf.html:99 +msgid "Importer les couches vectorielles" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:108 +msgid "" +"Sélectionnez les couches vectorielles à importer depuis le fichier DXF. Les " +"couches non cochées ne seront pas converties ni affichées sur la carte." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:113 +#, fuzzy +#| msgid "Rechercher" +msgid "Rechercher un calque..." +msgstr "Rechercher" + +#: templates/assets/trafficlights_import_dxf.html:128 +#, fuzzy +#| msgid "Valider la sélection" +msgid "Lancer l'importation" +msgstr "Valider la sélection" + +#: templates/assets/trafficlights_import_dxf.html:138 +msgid "Génération de l'image géolocalisée et des couches vectorielles..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:139 +#: templates/assets/trafficlights_import_dxf.html:302 +msgid "Cette opération peut prendre un peu de temps..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:182 +msgid "Analyse..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:189 +msgid "Téléversement et analyse du fichier DXF..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:190 +msgid "Veuillez patienter pendant l\\" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:192 +#, fuzzy +#| msgid "Changer les fichiers" +msgid "Analyse du fichier DXF..." +msgstr "Changer les fichiers" + +#: templates/assets/trafficlights_import_dxf.html:193 +msgid "Lecture des calques disponibles dans le fichier..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:242 +#, fuzzy +#| msgid "Aucune rue trouvée" +msgid "Aucune couche trouvée dans le fichier." +msgstr "Aucune rue trouvée" + +#: templates/assets/trafficlights_import_dxf.html:269 +#, fuzzy +#| msgid "Erreur lors de l’ajout." +msgid "Erreur lors de l\\" +msgstr "Erreur lors de l’ajout." + +#: templates/assets/trafficlights_import_dxf.html:280 +#: views/trafficlights.py:1907 +msgid "Veuillez sélectionner un fichier DXF ou en téléverser un." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:301 +#, fuzzy +#| msgid "Général" +msgid "Génération de l\\" +msgstr "Général" + +#: templates/assets/trafficlights_index.html:18 views/core.py:613 +msgid "Voltage" +msgstr "Voltage" + +#: templates/assets/trafficlights_index.html:28 views/core.py:628 +msgid "Date de mise en service (à partir de)" +msgstr "Date de mise en service (à partir de)" + +#: templates/assets/trafficlights_index.html:31 views/core.py:619 +msgid "Connexion à la centrale" +msgstr "Connexion à la centrale" + +#: templates/assets/trafficlights_index.html:49 views/core.py:635 +msgid "Prestataire maintenance contrôleurs" +msgstr "Prestataire maintenance contrôleurs" + +#: templates/assets/trafficlights_index.html:72 +msgid "Créer une nouvelle intersection" +msgstr "Créer une nouvelle intersection" + +#: templates/assets/trafficlights_intersection_detail.html:29 +msgid "Modifier cette intersection" +msgstr "Modifier cette intersection" + +#: templates/assets/trafficlights_intersection_detail.html:39 +msgid "Tension (Voltage)" +msgstr "Tension (Voltage)" + +#: templates/assets/trafficlights_intersection_detail.html:41 +msgid "Connexion centrale" +msgstr "Connexion centrale" + +#: templates/assets/trafficlights_intersection_detail.html:53 +msgid "Carrefour principal" +msgstr "Carrefour principal" + +#: templates/assets/trafficlights_intersection_detail.html:58 +msgid "Contrats" +msgstr "Contrats" + +#: templates/assets/trafficlights_intersection_detail.html:72 +msgid "Modifier les contrats" +msgstr "Modifier les contrats" + +#: templates/assets/trafficlights_intersection_detail.html:94 +msgid "Assets importés par DXF en attente de validation" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:97 +#, python-format +msgid "" +"\n" +" Il y a %(count)s poteau importé depuis " +"un plan DXF qui est en attente de validation pour ce carrefour.\n" +" " +msgid_plural "" +"\n" +" Il y a %(count)s poteaux importés " +"depuis un plan DXF qui sont en attente de validation pour ce carrefour.\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/assets/trafficlights_intersection_detail.html:108 +#, fuzzy +#| msgid "Date d'installation" +msgid "Date d'installation :" +msgstr "Date d'installation" + +#: templates/assets/trafficlights_intersection_detail.html:119 +msgid "Option A : Remplacer par code et créer les nouveaux" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:127 +msgid "" +"Êtes-vous sûr de vouloir archiver TOUS les poteaux existants de ce carrefour " +"et d\\'activer uniquement les nouveaux poteaux importés ?" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:128 +msgid "Option B : Remplacement complet (Archiver tout le reste)" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:133 +msgid "Réimporter / Corriger" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:328 +#: templates/assets/trafficlights_intersection_detail.html:973 +#: templates/assets/unified_index.html:453 +msgid "Ajouter un asset" +msgstr "Ajouter un asset" + +#: templates/assets/trafficlights_intersection_detail.html:333 +#: templates/assets/trafficlights_intersection_detail.html:334 +msgid "Importer plan DXF" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:336 +#: templates/assets/trafficlights_intersection_detail.html:337 +#, fuzzy +#| msgid "Plan de croix" +msgid "Importer plan de croix" +msgstr "Plan de croix" + +#: templates/assets/trafficlights_intersection_detail.html:341 +msgid "Archiver tous les assets" +msgstr "Archiver tous les assets" + +#: templates/assets/trafficlights_intersection_detail.html:342 +msgid "Tout archiver" +msgstr "Tout archiver" + +#: templates/assets/trafficlights_intersection_detail.html:392 +msgid "Installation extérieure" +msgstr "Installation extérieure" + +#: templates/assets/trafficlights_intersection_detail.html:409 +msgid "Programmation" +msgstr "Programmation" + +#: templates/assets/trafficlights_intersection_detail.html:429 +msgid "Tout déplier / replier" +msgstr "Tout déplier / replier" + +#: templates/assets/trafficlights_intersection_detail.html:467 +#, fuzzy +#| msgid "Afficher les lanternes et accessoires" +msgid "Afficher les lanternes, accessoires, radars et détecteurs" +msgstr "Afficher les lanternes et accessoires" + +#: templates/assets/trafficlights_intersection_detail.html:515 +msgid "Aucun poteau" +msgstr "Aucun poteau" + +#: templates/assets/trafficlights_intersection_detail.html:528 +msgid "Armoires électriques" +msgstr "Armoires électriques" + +#: templates/assets/trafficlights_intersection_detail.html:544 +msgid "Aucune armoire électrique" +msgstr "Aucune armoire électrique" + +#: templates/assets/trafficlights_intersection_detail.html:560 +msgid "Couleur du câble sur la carte" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:572 +msgid "Aucun câble" +msgstr "Aucun câble" + +#: templates/assets/trafficlights_intersection_detail.html:581 +msgid "Radars préventifs" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:597 +#, fuzzy +#| msgid "Aucun hardware réseau" +msgid "Aucun radar préventif" +msgstr "Aucun hardware réseau" + +#: templates/assets/trafficlights_intersection_detail.html:616 +msgid "Contrôleur de trafic" +msgstr "Contrôleur de trafic" + +#: templates/assets/trafficlights_intersection_detail.html:663 +msgid "Hardwares du contrôleur" +msgstr "Hardwares du contrôleur" + +#: templates/assets/trafficlights_intersection_detail.html:687 +msgid "Aucun hardware" +msgstr "Aucun hardware" + +#: templates/assets/trafficlights_intersection_detail.html:727 +msgid "Aucun détecteur" +msgstr "Aucun détecteur" + +#: templates/assets/trafficlights_intersection_detail.html:743 +msgid "Hardware réseau" +msgstr "Hardware réseau" + +#: templates/assets/trafficlights_intersection_detail.html:767 +msgid "Aucun hardware réseau" +msgstr "Aucun hardware réseau" + +#: templates/assets/trafficlights_intersection_detail.html:783 +msgid "Hardware Tension" +msgstr "Hardware Tension" + +#: templates/assets/trafficlights_intersection_detail.html:807 +msgid "Aucun hardware tension" +msgstr "Aucun hardware tension" + +#: templates/assets/trafficlights_intersection_detail.html:820 +msgid "Aucun contrôleur" +msgstr "Aucun contrôleur" + +#: templates/assets/trafficlights_intersection_detail.html:836 +msgid "Programmations" +msgstr "Programmations" + +#: templates/assets/trafficlights_intersection_detail.html:863 +msgid "Aucune programmation" +msgstr "Aucune programmation" + +#: templates/assets/trafficlights_intersection_detail.html:881 +msgid "Fichiers de log" +msgstr "Fichiers de log" + +#: templates/assets/trafficlights_intersection_detail.html:901 +msgid "Aucun fichier de log" +msgstr "Aucun fichier de log" + +#: templates/assets/trafficlights_intersection_detail.html:981 +msgid "Sélectionner une catégorie" +msgstr "Sélectionner une catégorie" + +#: templates/assets/trafficlights_intersection_detail.html:1062 +msgid "Importer un plan DXF" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:1156 +msgid "Gérer les contrats" +msgstr "Gérer les contrats" + +#: templates/assets/trafficlights_intersection_detail.html:1162 +msgid "Contrats associés" +msgstr "Contrats associés" + +#: templates/assets/trafficlights_intersection_detail.html:1168 +#: templates/assets/trafficlights_intersection_detail.html:1259 +msgid "Numéro de contrat" +msgstr "Numéro de contrat" + +#: templates/assets/trafficlights_intersection_detail.html:1169 +#: templates/assets/trafficlights_intersection_detail.html:1260 +msgid "Entreprise" +msgstr "Entreprise" + +#: templates/assets/trafficlights_intersection_detail.html:1170 +#: templates/assets/trafficlights_intersection_detail.html:1261 +msgid "Type de maintenance" +msgstr "Type de maintenance" + +#: templates/assets/trafficlights_intersection_detail.html:1186 +#: templates/assets/trafficlights_intersection_detail.html:1337 +msgid "Retirer ce contrat" +msgstr "Retirer ce contrat" + +#: templates/assets/trafficlights_intersection_detail.html:1195 +msgid "Aucun contrat associé." +msgstr "Aucun contrat associé." + +#: templates/assets/trafficlights_intersection_detail.html:1202 +msgid "Ajouter un contrat" +msgstr "Ajouter un contrat" + +#: templates/assets/trafficlights_intersection_detail.html:1207 +msgid "Contrat" +msgstr "Contrat" + +#: templates/assets/trafficlights_intersection_detail.html:1209 +msgid "Sélectionner" +msgstr "Sélectionner" + +#: templates/assets/trafficlights_intersection_detail.html:1218 +msgid "Infrastructure" +msgstr "Infrastructure" + +#: templates/assets/trafficlights_intersection_detail.html:1219 +msgid "Contrôleur de traffic" +msgstr "Contrôleur de traffic" + +#: templates/assets/trafficlights_intersection_detail.html:1344 +msgid "Erreur lors de l’ajout." +msgstr "Erreur lors de l’ajout." + +#: templates/assets/unified_index.html:83 +msgid "Filtrer les assets" +msgstr "Filtrer les assets" + +#: templates/assets/unified_index.html:91 +#: templates/assets/unified_index.html:251 +#: templates/assets/unified_index.html:261 +msgid "Thématique" +msgstr "Thématique" + +#: templates/assets/unified_index.html:118 +msgid "Aucune" +msgstr "Aucune" + +#: templates/assets/unified_index.html:122 +#: templates/assets/unified_index.html:147 +msgid "Sélectionnez d'abord une ou plusieurs thématiques." +msgstr "Sélectionnez d'abord une ou plusieurs thématiques." + +#: templates/assets/unified_index.html:137 +msgid "Sélectionnez d'abord une ou plusieurs catégories." +msgstr "Sélectionnez d'abord une ou plusieurs catégories." + +#: templates/assets/unified_index.html:144 +msgid "Filtres thématiques" +msgstr "Filtres thématiques" + +#: templates/assets/unified_index.html:154 +msgid "Type d'élément" +msgstr "Type d'élément" + +#: templates/assets/unified_index.html:161 +msgid "Assets et localisations" +msgstr "Assets et localisations" + +#: templates/assets/unified_index.html:167 +msgid "Localisations uniquement" +msgstr "Localisations uniquement" + +#: templates/assets/unified_index.html:173 +msgid "Assets uniquement" +msgstr "Assets uniquement" + +#: templates/assets/unified_index.html:181 +#, fuzzy +#| msgid "Description du matériel" +msgid "Options de géométrie" +msgstr "Description du matériel" + +#: templates/assets/unified_index.html:186 +msgid "Inclure les assets sans géométrie" +msgstr "" + +#: templates/assets/unified_index.html:240 +msgid "Créer un élément" +msgstr "Créer un élément" + +#: templates/assets/unified_index.html:245 +msgid "Choisissez d'abord le type d'élément, puis la thématique autorisée." +msgstr "Choisissez d'abord le type d'élément, puis la thématique autorisée." + +#: templates/assets/unified_index.html:254 +#: templates/assets/unified_index.html:282 +msgid "Créer un asset" +msgstr "Créer un asset" + +#: templates/assets/unified_index.html:264 +#: templates/assets/unified_index.html:342 +msgid "Créer une localisation" +msgstr "Créer une localisation" + +#: templates/assets/unified_index.html:270 +msgid "Aucune thématique éditable n'est disponible pour la création." +msgstr "Aucune thématique éditable n'est disponible pour la création." + +#: templates/assets/unified_index.html:323 +msgid "L'asset sera créé avec le statut 'À approuver'." +msgstr "L'asset sera créé avec le statut 'À approuver'." + +#: templates/assets/unified_index.html:361 +msgid "Créer la localisation" +msgstr "Créer la localisation" + +#: templates/assets/unified_index.html:373 +msgid "Mode sélection intervention préventive" +msgstr "Mode sélection intervention préventive" + +#: templates/assets/unified_index.html:394 +msgid "Code, nom, type…" +msgstr "Code, nom, type…" + +#: templates/assets/unified_index.html:408 +#, fuzzy +#| msgid "Tous les mots doivent correspondre" +msgid "Tous les mots recherchés doivent être présents" +msgstr "Tous les mots doivent correspondre" + +#: templates/assets/unified_index.html:408 +msgid "ET" +msgstr "" + +#: templates/assets/unified_index.html:412 +#, fuzzy +#| msgid "Au moins un mot doit correspondre" +msgid "Au moins un des mots recherchés doit être présent" +msgstr "Au moins un mot doit correspondre" + +#: templates/assets/unified_index.html:460 +#: templates/assets/unified_index.html:461 +msgid "Filtres" +msgstr "Filtres" + +#: templates/assets/unified_index.html:480 +msgid "sélectionné(s)" +msgstr "sélectionné(s)" + +#: templates/assets/unified_index.html:497 +#: templates/assets/unified_index.html:616 +#, fuzzy +#| msgid "Modifier les contrats" +msgid "Modifier le statut en masse" +msgstr "Modifier les contrats" + +#: templates/assets/unified_index.html:505 +#: templates/assets/unified_index.html:645 +msgid "Modifier le statut de validation en masse" +msgstr "Modifier le statut de validation en masse" + +#: templates/assets/unified_index.html:527 +msgid "" +"Sélectionnez une ou plusieurs thématiques dans les filtres, saisissez une " +"recherche ou zoomez sur la carte." +msgstr "" +"Sélectionnez une ou plusieurs thématiques dans les filtres, saisissez une " +"recherche ou zoomez sur la carte." + +#: templates/assets/unified_index.html:560 +msgid "Choisissez une catégorie ou zoomez sur la carte pour voir les assets." +msgstr "Choisissez une catégorie ou zoomez sur la carte pour voir les assets." + +#: templates/assets/unified_index.html:621 +#, fuzzy +#| msgid "Nouveau" +msgid "Nouveau statut" +msgstr "Nouveau" + +#: templates/assets/unified_index.html:650 +msgid "Nouveau statut de validation" +msgstr "Nouveau statut de validation" + +#: templates/assets/water_index.html:17 views/core.py:691 views/locations.py:29 +msgid "GIEP" +msgstr "GIEP" + +#: templates/assets/water_index.html:31 +#, fuzzy +#| msgid "Créer une nouvelle localisation ITS" +msgid "Créer une nouvelle localisation GIEP" +msgstr "Créer une nouvelle localisation ITS" + +#: templates/assets/water_index.html:59 +#, fuzzy +#| msgid "Créer un nouvel asset ITS" +msgid "Créer un nouvel ouvrage GIEP" +msgstr "Créer un nouvel asset ITS" + +#: views/artworks.py:79 +msgid "Inactif" +msgstr "Inactif" + +#: views/artworks.py:80 +msgid "À supprimer" +msgstr "À supprimer" + +#: views/artworks.py:94 views/controlcenters.py:107 views/its.py:110 +#: views/structures.py:111 +msgid "Localisations" +msgstr "Localisations" + +#: views/artworks.py:268 +#, python-format +msgid "Type d'asset inconnu: %(asset_model)s" +msgstr "Type d'asset inconnu: %(asset_model)s" + +#: views/artworks.py:328 views/trafficlights.py:1393 +msgid "Coordonnées invalides" +msgstr "Coordonnées invalides" + +#: views/artworks.py:343 views/its.py:853 +msgid "Aucune localisation trouvée" +msgstr "Aucune localisation trouvée" + +#: views/artworks.py:436 views/artworks.py:440 +msgid "Fontaine" +msgstr "Fontaine" + +#: views/artworks.py:817 +msgid "Aucun équipement trouvé" +msgstr "Aucun équipement trouvé" + +#: views/asset_editing.py:49 views/asset_editing.py:132 +#: views/asset_editing.py:1833 views/asset_editing.py:1972 +#: views/asset_editing.py:2026 views/asset_editing.py:2084 +#: views/asset_editing.py:2160 +msgid "Permission refusée" +msgstr "Permission refusée" + +#: views/asset_editing.py:55 +msgid "Aucun fichier fourni" +msgstr "Aucun fichier fourni" + +#: views/asset_editing.py:145 +msgid "Document non trouvé" +msgstr "Document non trouvé" + +#: views/asset_editing.py:170 views/asset_editing.py:1987 +#: views/asset_editing.py:2041 views/asset_editing.py:2113 +#: views/asset_editing.py:2194 +msgid "Type d'asset inconnu" +msgstr "Type d'asset inconnu" + +#: views/asset_editing.py:289 +#, python-format +msgid "Champ inconnu: %(field)s" +msgstr "Champ inconnu: %(field)s" + +#: views/asset_editing.py:303 views/asset_editing.py:459 +#, python-format +msgid "Format de date invalide pour %(field)s" +msgstr "Format de date invalide pour %(field)s" + +#: views/asset_editing.py:318 +msgid "Modèle non trouvé" +msgstr "Modèle non trouvé" + +#: views/asset_editing.py:329 views/asset_editing.py:1521 +msgid "Catégorie non trouvée" +msgstr "Catégorie non trouvée" + +#: views/asset_editing.py:346 +msgid "Localisation non trouvée" +msgstr "Localisation non trouvée" + +#: views/asset_editing.py:355 +#, python-format +msgid "Statut invalide: %(status)s" +msgstr "Statut invalide: %(status)s" + +#: views/asset_editing.py:362 +#, python-format +msgid "Statut de validation invalide: %(status)s" +msgstr "Statut de validation invalide: %(status)s" + +#: views/asset_editing.py:376 views/asset_editing.py:413 +#: views/asset_editing.py:431 views/asset_editing.py:440 +#, python-format +msgid "Valeur numérique invalide pour %(field)s" +msgstr "Valeur numérique invalide pour %(field)s" + +#: views/asset_editing.py:403 +#, python-format +msgid "Géométrie invalide: %(error)s" +msgstr "Géométrie invalide: %(error)s" + +#: views/asset_editing.py:485 views/asset_editing.py:555 +#: views/asset_editing.py:590 views/clean.py:512 +msgid "Vous n'avez pas la permission de modifier cet asset" +msgstr "Vous n'avez pas la permission de modifier cet asset" + +#: views/asset_editing.py:495 views/asset_editing.py:605 +#: views/asset_editing.py:756 views/asset_editing.py:1449 +#: views/asset_editing.py:1977 views/asset_editing.py:2031 +#: views/asset_editing.py:2089 views/asset_editing.py:2182 +#: views/asset_editing.py:2302 views/clean.py:518 +msgid "Format JSON invalide" +msgstr "Format JSON invalide" + +#: views/asset_editing.py:504 +#, python-format +msgid "Champ non modifiable: %(field)s" +msgstr "Champ non modifiable: %(field)s" + +#: views/asset_editing.py:534 +msgid "Aucun champ mis à jour" +msgstr "Aucun champ mis à jour" + +#: views/asset_editing.py:541 +#, python-format +msgid "%(count)s champ(s) mis à jour" +msgstr "%(count)s champ(s) mis à jour" + +#: views/asset_editing.py:562 +msgid "Vous n'avez pas la permission d'archiver cet asset" +msgstr "Vous n'avez pas la permission d'archiver cet asset" + +#: views/asset_editing.py:572 +msgid "Asset archivé avec succès" +msgstr "Asset archivé avec succès" + +#: views/asset_editing.py:597 +msgid "Vous n'avez pas la permission de remplacer cet asset" +msgstr "Vous n'avez pas la permission de remplacer cet asset" + +#: views/asset_editing.py:682 +msgid "Asset remplacé avec succès" +msgstr "Asset remplacé avec succès" + +#: views/asset_editing.py:741 views/asset_editing.py:933 +#: views/asset_editing.py:1505 +msgid "Thématique non trouvée" +msgstr "Thématique non trouvée" + +#: views/asset_editing.py:748 views/asset_editing.py:2175 +msgid "Vous n'avez pas la permission de créer des assets" +msgstr "Vous n'avez pas la permission de créer des assets" + +#: views/asset_editing.py:802 +msgid "Type d'asset requis" +msgstr "Type d'asset requis" + +#: views/asset_editing.py:810 +#, python-format +msgid "Type d'asset invalide: %(type)s" +msgstr "Type d'asset invalide: %(type)s" + +#: views/asset_editing.py:821 views/asset_editing.py:951 +msgid "Type de localisation invalide" +msgstr "Type de localisation invalide" + +#: views/asset_editing.py:844 +msgid "Impossible de lier l'asset à cette localisation" +msgstr "Impossible de lier l'asset à cette localisation" + +#: views/asset_editing.py:859 +msgid "Le modèle d'asset est requis pour ce type d'asset" +msgstr "Le modèle d'asset est requis pour ce type d'asset" + +#: views/asset_editing.py:864 +msgid "Modèle d'asset non trouvé" +msgstr "Modèle d'asset non trouvé" + +#: views/asset_editing.py:897 +msgid "Asset créé avec succès" +msgstr "Asset créé avec succès" + +#: views/asset_editing.py:940 +msgid "Vous n'avez pas la permission d'archiver en masse" +msgstr "Vous n'avez pas la permission d'archiver en masse" + +#: views/asset_editing.py:958 +msgid "Cette localisation ne supporte pas la gestion des assets" +msgstr "Cette localisation ne supporte pas la gestion des assets" + +#: views/asset_editing.py:973 +#, python-format +msgid "%(count)s asset(s) archivé(s)" +msgstr "%(count)s asset(s) archivé(s)" + +#: views/asset_editing.py:1477 +msgid "Format d'ID invalide" +msgstr "Format d'ID invalide" + +#: views/asset_editing.py:1484 +msgid "Thématique requise" +msgstr "Thématique requise" + +#: views/asset_editing.py:1490 +msgid "Catégorie requise" +msgstr "Catégorie requise" + +#: views/asset_editing.py:1496 +msgid "Code requis" +msgstr "Code requis" + +#: views/asset_editing.py:1512 +msgid "Vous n'avez pas la permission de créer des équipements" +msgstr "Vous n'avez pas la permission de créer des équipements" + +#: views/asset_editing.py:1533 +msgid "Type d'asset non supporté pour cette thématique/catégorie" +msgstr "Type d'asset non supporté pour cette thématique/catégorie" + +#: views/asset_editing.py:1540 +msgid "Un équipement avec ce code existe déjà" +msgstr "Un équipement avec ce code existe déjà" + +#: views/asset_editing.py:1550 +msgid "Modèle d'équipement non trouvé" +msgstr "Modèle d'équipement non trouvé" + +#: views/asset_editing.py:1555 +msgid "Modèle requis" +msgstr "Modèle requis" + +#: views/asset_editing.py:1624 +msgid "Une localisation est requise pour créer cet asset." +msgstr "Une localisation est requise pour créer cet asset." + +#: views/asset_editing.py:1634 +msgid "Localisation introuvable" +msgstr "Localisation introuvable" + +#: views/asset_editing.py:1680 +msgid "Équipement créé avec succès" +msgstr "Équipement créé avec succès" + +#: views/asset_editing.py:1682 +#, python-format +msgid " • %(count)d opération(s) ajoutée(s)" +msgstr " • %(count)d opération(s) ajoutée(s)" + +#: views/asset_editing.py:1995 views/asset_editing.py:2100 +#: views/asset_editing.py:2103 views/asset_editing.py:2117 +#: views/asset_editing.py:2200 +msgid "Champ de liaison invalide" +msgstr "Champ de liaison invalide" + +#: views/asset_editing.py:1997 views/asset_editing.py:2061 +#: views/asset_editing.py:2105 views/asset_editing.py:2202 +msgid "Champ de liaison inconnu" +msgstr "Champ de liaison inconnu" + +#: views/asset_editing.py:2007 +msgid "Asset lié avec succès" +msgstr "Asset lié avec succès" + +#: views/asset_editing.py:2049 +msgid "Cet asset n'est pas lié à ce parent" +msgstr "Cet asset n'est pas lié à ce parent" + +#: views/asset_editing.py:2057 views/asset_editing.py:2140 +msgid "Ce champ est obligatoire et ne peut pas être vidé" +msgstr "Ce champ est obligatoire et ne peut pas être vidé" + +#: views/asset_editing.py:2068 +msgid "Asset délié avec succès" +msgstr "Asset délié avec succès" + +#: views/asset_editing.py:2129 +#, fuzzy +#| msgid "Asset lié avec succès" +msgid "Parent lié avec succès" +msgstr "Asset lié avec succès" + +#: views/asset_editing.py:2148 +#, fuzzy +#| msgid "Asset délié avec succès" +msgid "Parent délié avec succès" +msgstr "Asset délié avec succès" + +#: views/asset_editing.py:2267 +#, python-format +msgid "Le champ « %(field)s » est obligatoire." +msgstr "Le champ « %(field)s » est obligatoire." + +#: views/asset_editing.py:2269 +msgid "Impossible de créer l'asset : un champ obligatoire est manquant." +msgstr "Impossible de créer l'asset : un champ obligatoire est manquant." + +#: views/asset_editing.py:2275 +msgid "Asset créé et lié avec succès" +msgstr "Asset créé et lié avec succès" + +#: views/asset_editing.py:2309 +#, fuzzy +#| msgid "Champ non modifiable: %(field)s" +msgid "Champ non modifiable en masse" +msgstr "Champ non modifiable: %(field)s" + +#: views/asset_editing.py:2355 +#, python-format +msgid "%(updated)s équipement(s) mis à jour, %(skipped)s ignoré(s)." +msgstr "" + +#: views/asset_permissions.py:47 +msgid "Accès refusé — rôle admin requis." +msgstr "Accès refusé — rôle admin requis." + +#: views/asset_permissions.py:100 +msgid "Utilisateur requis." +msgstr "Utilisateur requis." + +#: views/asset_permissions.py:106 +msgid "Utilisateur introuvable." +msgstr "Utilisateur introuvable." + +#: views/clean.py:259 +msgid "Locations" +msgstr "Locations" + +#: views/clean.py:475 +#, python-format +msgid "Unknown model: %(asset_model)s" +msgstr "Unknown model: %(asset_model)s" + +#: views/clean.py:632 +msgid "No location found nearby" +msgstr "No location found nearby" + +#: views/controlcenters.py:396 +#, python-format +msgid "Trop d'objets à afficher (> %(max)d)" +msgstr "Trop d'objets à afficher (> %(max)d)" + +#: views/controlcenters.py:410 +#, python-format +msgid "Trop d'objets à afficher (> %(max)d). Limitez votre sélection." +msgstr "Trop d'objets à afficher (> %(max)d). Limitez votre sélection." + +#: views/controlcenters.py:528 +#, python-format +msgid "Modèle '%(model)s' inconnu." +msgstr "Modèle '%(model)s' inconnu." + +#: views/controlcenters.py:541 views/its.py:544 views/nature.py:237 +#: views/publiclighting.py:278 views/roads.py:541 views/sign.py:794 +#: views/structures.py:666 views/trafficlights.py:1273 +msgid "Aucun template trouvé pour ce type d’asset." +msgstr "Aucun template trouvé pour ce type d’asset." + +#: views/core.py:246 views/nature.py:224 views/publiclighting.py:265 +#: views/roads.py:528 views/sign.py:781 views/structures.py:653 +#: views/trafficlights.py:1260 views/water.py:523 +#, python-format +msgid "Modèle %(asset_model)s inconnu." +msgstr "Modèle %(asset_model)s inconnu." + +#: views/core.py:527 +msgid "Signaux de danger" +msgstr "Signaux de danger" + +#: views/core.py:528 +msgid "Signaux de priorité" +msgstr "Signaux de priorité" + +#: views/core.py:529 +msgid "Signaux d'interdiction" +msgstr "Signaux d'interdiction" + +#: views/core.py:530 +msgid "Signaux d'obligation" +msgstr "Signaux d'obligation" + +#: views/core.py:531 +msgid "Arrêt / Stationnement" +msgstr "Arrêt / Stationnement" + +#: views/core.py:532 +msgid "Signaux d'indication" +msgstr "Signaux d'indication" + +#: views/core.py:533 +msgid "Signaux de zone" +msgstr "Signaux de zone" + +#: views/core.py:534 +msgid "Signaux additionnels" +msgstr "Signaux additionnels" + +#: views/core.py:607 +msgid "Statut des carrefours" +msgstr "Statut des carrefours" + +#: views/core.py:700 +msgid "Niveau de qualité" +msgstr "Niveau de qualité" + +#: views/export.py:94 +msgid "ID" +msgstr "ID" + +#: views/export.py:100 +msgid "Obsolescence" +msgstr "Obsolescence" + +#: views/export.py:101 +msgid "Code localisation" +msgstr "Code localisation" + +#: views/export.py:102 +msgid "Nom localisation" +msgstr "Nom localisation" + +#: views/export.py:104 +msgid "Date de déinstallation" +msgstr "Date de déinstallation" + +#: views/export.py:106 +msgid "Garantie (mois)" +msgstr "Garantie (mois)" + +#: views/export.py:133 +msgid "Paramètre 'ids' invalide." +msgstr "Paramètre 'ids' invalide." + +#: views/export.py:136 +msgid "Aucun asset sélectionné." +msgstr "Aucun asset sélectionné." + +#: views/export.py:168 +msgid "Export XLSX indisponible (dépendance manquante)." +msgstr "Export XLSX indisponible (dépendance manquante)." + +#: views/history.py:27 views/history.py:89 views/history.py:112 +#: views/history.py:318 +msgid "Authentication required" +msgstr "Authentication required" + +#: views/history.py:35 +msgid "Cannot determine asset's thematic" +msgstr "Cannot determine asset's thematic" + +#: views/history.py:50 +msgid "You do not have permission to view assets for this thematic" +msgstr "You do not have permission to view assets for this thematic" + +#: views/history.py:148 views/history.py:149 views/history.py:280 +#: views/history.py:281 +msgid "Système" +msgstr "Système" + +#: views/history.py:196 +msgid "You do not have access to any thematics" +msgstr "You do not have access to any thematics" + +#: views/history.py:224 +msgid "You do not have permission to view this thematic" +msgstr "You do not have permission to view this thematic" + +#: views/history.py:241 +msgid "Thematic not found" +msgstr "Thematic not found" + +#: views/history.py:326 +msgid "Paramètre 'date' manquant" +msgstr "Paramètre 'date' manquant" + +#: views/history.py:342 +msgid "Format de date invalide. Utilisez ISO 8601." +msgstr "Format de date invalide. Utilisez ISO 8601." + +#: views/history.py:426 +msgid "Inexistant" +msgstr "Inexistant" + +#: views/its.py:404 views/its.py:415 views/nature.py:463 views/nature.py:543 +#: views/publiclighting.py:454 views/publiclighting.py:475 +#: views/publiclighting.py:496 views/publiclighting.py:506 views/roads.py:412 +#: views/roads.py:416 views/roads.py:420 views/roads.py:424 views/roads.py:428 +#: views/roads.py:441 views/roads.py:450 views/roads.py:459 views/roads.py:468 +#: views/roads.py:477 views/sign.py:676 views/sign.py:680 views/sign.py:721 +#: views/sign.py:730 views/structures.py:439 views/structures.py:450 +#: views/trafficlights.py:1226 views/trafficlights.py:1240 views/water.py:223 +msgid "Trop d'objets à afficher" +msgstr "Trop d'objets à afficher" + +#: views/its.py:415 views/nature.py:543 views/publiclighting.py:506 +#: views/roads.py:441 views/roads.py:450 views/roads.py:459 views/roads.py:468 +#: views/roads.py:477 views/sign.py:721 views/sign.py:730 +#: views/structures.py:450 views/trafficlights.py:1240 +msgid "Limitez votre sélection." +msgstr "Limitez votre sélection." + +#: views/location_editing.py:46 +msgid "Authentification requise." +msgstr "Authentification requise." + +#: views/location_editing.py:48 views/location_editing.py:59 +msgid "Permission refusée." +msgstr "Permission refusée." + +#: views/locations.py:25 views/water.py:86 +msgid "Ouvrages" +msgstr "Ouvrages" + +#: views/locations.py:26 +msgid "Feux" +msgstr "Feux" + +#: views/locations.py:27 +msgid "Éclairage" +msgstr "Éclairage" + +#: views/locations.py:28 +msgid "Nature" +msgstr "Nature" + +#: views/locations.py:30 +msgid "Centres de contrôle" +msgstr "Centres de contrôle" + +#: views/locations.py:31 +msgid "Signalisation" +msgstr "Signalisation" + +#: views/locations.py:32 +msgid "Voirie" +msgstr "Voirie" + +#: views/nature.py:766 +msgid "Aucune section trouvée" +msgstr "Aucune section trouvée" + +#: views/nature.py:797 views/nature.py:838 +msgid "Aucun asset sélectionné" +msgstr "Aucun asset sélectionné" + +#: views/nature.py:911 +msgid "Aucune modification fournie" +msgstr "Aucune modification fournie" + +#: views/nature.py:1252 +msgid "Commentaire enregistré avec succès" +msgstr "Commentaire enregistré avec succès" + +#: views/nature.py:1280 +msgid "Commentaire supprimé avec succès" +msgstr "Commentaire supprimé avec succès" + +#: views/nature.py:1331 +msgid "Photos ajoutées avec succès" +msgstr "Photos ajoutées avec succès" + +#: views/publiclighting.py:549 views/roads.py:655 views/sign.py:908 +msgid "Aucune rue trouvée" +msgstr "Aucune rue trouvée" + +#: views/structures.py:1181 +msgid "Aucune localisation structure trouvée" +msgstr "Aucune localisation structure trouvée" + +#: views/trafficlights.py:205 +msgid "Intersections" +msgstr "Intersections" + +#: views/trafficlights.py:629 +msgid "Contrat requis." +msgstr "Contrat requis." + +#: views/trafficlights.py:1149 +msgid "Date de mise en service invalide." +msgstr "Date de mise en service invalide." + +#: views/trafficlights.py:1356 views/trafficlights.py:1384 +msgid "Intersection introuvable" +msgstr "Intersection introuvable" + +#: views/trafficlights.py:1417 +msgid "Aucune intersection trouvée" +msgstr "Aucune intersection trouvée" + +#: views/trafficlights.py:2411 +msgid "" +"Le fichier DXF a bien été importé et le plan est disponible pour un " +"affichage sur la carte." +msgstr "" + +#: views/trafficlights.py:3809 +msgid "" +"Veuillez téléverser un fichier ou sélectionner au moins un plan existant." +msgstr "" + +#: views/trafficlights.py:3865 +msgid "Un des fichiers de plan de croix sélectionnés est introuvable." +msgstr "" + +#: views/trafficlights.py:4018 +msgid "Absent du fichier Excel (Sera conservé ou archivé)" +msgstr "" + +#: views/trafficlights.py:4063 views/trafficlights.py:4125 +#: views/trafficlights.py:4187 +#, fuzzy +#| msgid "Aucun fichier de log" +msgid "Absent du fichier Excel" +msgstr "Aucun fichier de log" + +#: views/water.py:85 +msgid "Zones GIEP" +msgstr "" + +#: views/water.py:572 +#, fuzzy +#| msgid "Aucune localisation trouvée" +msgid "Aucune localisation GIEP trouvée à proximité" +msgstr "Aucune localisation trouvée" + +#, fuzzy +#~| msgid "Changer le modèle" +#~ msgid "Charger le plan" +#~ msgstr "Changer le modèle" + +#, fuzzy +#~| msgid "Configuration du type" +#~ msgid "Configuration" +#~ msgstr "Configuration du type" + +#, fuzzy +#~| msgid "Totaux des assets" +#~ msgid "Nouveaux assets" +#~ msgstr "Totaux des assets" + +#, fuzzy +#~| msgid "Cliquez sur la carte pour ajouter des sommets." +#~ msgid "Cliquez sur la carte pour ajouter manuellement un nouveau poteau." +#~ msgstr "Cliquez sur la carte pour ajouter des sommets." + +#, fuzzy +#~| msgid "Aucun template trouvé pour ce type d’asset." +#~ msgid "Aucun poteau ajouté pour le moment." +#~ msgstr "Aucun template trouvé pour ce type d’asset." + +#, fuzzy +#~| msgid "Inexistant" +#~ msgid "Existants" +#~ msgstr "Inexistant" + +#, fuzzy +#~| msgid "Nouveau" +#~ msgid "Nouveaux" +#~ msgstr "Nouveau" + +#, fuzzy +#~| msgid "Erreur lors de la sauvegarde." +#~ msgid "Erreur lors de la lecture des couches." +#~ msgstr "Erreur lors de la sauvegarde." + +#, fuzzy +#~| msgid "Ajouter un contrat" +#~ msgid "Ajouter un poteau" +#~ msgstr "Ajouter un contrat" + +#~ msgid "Noue" +#~ msgstr "Noue" + +#~ msgid "Continu poreux" +#~ msgstr "Continu poreux" + +#~ msgid "Impermeable" +#~ msgstr "Impermeable" + +#~ msgid "Type d'EVC" +#~ msgstr "Type d'EVC" + +#~ msgid "Type de revêtement" +#~ msgstr "Type de revêtement" + +#~ msgid "Espaces verts creux" +#~ msgstr "Espaces verts creux" + +#~ msgid "Armoire électrique" +#~ msgstr "Armoire électrique" + +#~ msgid "Sélectionner un modèle" +#~ msgstr "Sélectionner un modèle" + +#~ msgid "Hauteur" +#~ msgstr "Hauteur" + +#~ msgid "Alimentation" +#~ msgstr "Alimentation" diff --git a/streetup/assets/locale/fr/LC_MESSAGES/djangojs.mo b/streetup/assets/locale/fr/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..d9908eb Binary files /dev/null and b/streetup/assets/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/assets/locale/fr/LC_MESSAGES/djangojs.po b/streetup/assets/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..e099705 --- /dev/null +++ b/streetup/assets/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,593 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: assets/static/assets/asset_add_modal.js:52 +#: assets/static/assets/asset_location_edit.js:59 +#: assets/static/assets/unified_index.js:115 +#: assets/static/assets/unified_index.js:168 +#: assets/static/assets/unified_index.js:224 +msgid "Chargement..." +msgstr "Chargement..." + +#: assets/static/assets/asset_add_modal.js:56 +#: assets/static/assets/asset_add_modal.js:426 +#: assets/static/assets/unified_index.js:114 +#: assets/static/assets/unified_index.js:220 +#: assets/static/assets/unified_index.js:262 +msgid "Sélectionner d'abord une catégorie..." +msgstr "Sélectionner d'abord une catégorie..." + +#: assets/static/assets/asset_add_modal.js:65 +#: assets/static/assets/unified_index.js:230 +msgid "Sélectionner un modèle..." +msgstr "Sélectionner un modèle..." + +#: assets/static/assets/asset_add_modal.js:77 +#: assets/static/assets/unified_index.js:243 +msgid "Aucun modèle disponible" +msgstr "Aucun modèle disponible" + +#: assets/static/assets/asset_add_modal.js:82 +#: assets/static/assets/asset_location_edit.js:102 +#: assets/static/assets/unified_index.js:134 +#: assets/static/assets/unified_index.js:205 +#: assets/static/assets/unified_index.js:247 +msgid "Erreur de chargement" +msgstr "Erreur de chargement" + +#: assets/static/assets/asset_add_modal.js:90 +#: assets/static/assets/unified_index.js:848 +#: assets/static/assets/unified_index.js:935 +msgid "Création..." +msgstr "Création..." + +#: assets/static/assets/asset_add_modal.js:146 +#: assets/static/assets/asset_add_modal.js:350 +msgid "Erreur lors de la création" +msgstr "Erreur lors de la création" + +#: assets/static/assets/asset_add_modal.js:156 +#: assets/static/assets/asset_add_modal.js:359 +msgid "Erreur de connexion" +msgstr "Erreur de connexion" + +#: assets/static/assets/asset_add_modal.js:221 +msgid "Sélection de template d'opérations" +msgstr "Sélection de template d'opérations" + +#: assets/static/assets/asset_add_modal.js:223 +msgid "Fermer" +msgstr "Fermer" + +#: assets/static/assets/asset_add_modal.js:226 +msgid "Choisissez le(s) template(s) d'opérations à appliquer à cet équipement." +msgstr "Choisissez le(s) template(s) d'opérations à appliquer à cet équipement." + +#: assets/static/assets/asset_add_modal.js:228 +msgid "Choisir le(s) template(s) d'opérations :" +msgstr "Choisir le(s) template(s) d'opérations :" + +#: assets/static/assets/asset_add_modal.js:242 +msgid "Tout sélectionner" +msgstr "Tout sélectionner" + +#: assets/static/assets/asset_add_modal.js:245 +msgid "Tout désélectionner" +msgstr "Tout désélectionner" + +#: assets/static/assets/asset_add_modal.js:253 +msgid "Ignorer et continuer" +msgstr "Ignorer et continuer" + +#: assets/static/assets/asset_add_modal.js:256 +msgid "Créer les opérations" +msgstr "Créer les opérations" + +#: assets/static/assets/asset_add_modal.js:317 +msgid "Création en cours..." +msgstr "Création en cours..." + +#: assets/static/assets/asset_location_edit.js:37 +msgid "Saisissez au moins 2 caractères pour rechercher." +msgstr "Saisissez au moins 2 caractères pour rechercher." + +#: assets/static/assets/asset_location_edit.js:76 +#: assets/static/assets/unified_index.js:177 +msgid "Sélectionner une localisation..." +msgstr "Sélectionner une localisation..." + +#: assets/static/assets/asset_location_edit.js:87 +msgid "Résultats limités. Affinez la recherche pour trouver plus facilement." +msgstr "Résultats limités. Affinez la recherche pour trouver plus facilement." + +#: assets/static/assets/asset_location_edit.js:92 +#: assets/static/assets/unified_index.js:196 +msgid "Aucune localisation disponible" +msgstr "Aucune localisation disponible" + +#: assets/static/assets/asset_location_edit.js:94 +msgid "Aucun résultat pour cette recherche." +msgstr "Aucun résultat pour cette recherche." + +#: assets/static/assets/asset_location_edit.js:104 +msgid "Erreur lors du chargement des localisations" +msgstr "Erreur lors du chargement des localisations" + +#: assets/static/assets/asset_location_edit.js:117 +msgid "Veuillez sélectionner une localisation" +msgstr "Veuillez sélectionner une localisation" + +#: assets/static/assets/asset_location_edit.js:124 +msgid "Mise à jour..." +msgstr "Mise à jour..." + +#: assets/static/assets/asset_location_edit.js:149 +msgid "Localisation mise à jour" +msgstr "Localisation mise à jour" + +#: assets/static/assets/asset_location_edit.js:154 +#: assets/static/assets/asset_location_edit.js:161 +msgid "Erreur lors de la mise à jour" +msgstr "Erreur lors de la mise à jour" + +#: assets/static/assets/assets_index.js:568 +#: assets/static/assets/assets_index.js:592 +#: assets/static/assets/unified_index.js:499 +msgid "Code" +msgstr "Code" + +#: assets/static/assets/assets_index.js:594 +#: assets/static/assets/green-surfaces-comments.js:953 +#: assets/static/assets/unified_index.js:510 +msgid "Statut" +msgstr "Statut" + +#: assets/static/assets/assets_index.js:777 +msgid "%(count)s localisation sélectionnée" +msgid_plural "%(count)s localisations sélectionnées" +msgstr[0] "%(count)s localisation sélectionnée" +msgstr[1] "%(count)s localisations sélectionnées" + +#: assets/static/assets/assets_index.js:789 +msgid "%(count)s asset sélectionné" +msgid_plural "%(count)s assets sélectionnés" +msgstr[0] "%(count)s asset sélectionné" +msgstr[1] "%(count)s assets sélectionnés" + +#: assets/static/assets/assets_index.js:880 +msgid "Sélectionner l'asset" +msgstr "Sélectionner l'asset" + +#: assets/static/assets/assets_index.js:886 +msgid "Désélectionner l'asset" +msgstr "Désélectionner l'asset" + +#: assets/static/assets/assets_index.js:997 +#: assets/static/assets/assets_index.js:1068 +msgid "Erreur lors de la récupération des données." +msgstr "Erreur lors de la récupération des données." + +#: assets/static/assets/assets_index.js:1100 +msgid "" +"Sélectionnez des localisations ou lancez une recherche pour afficher les " +"assets." +msgstr "Sélectionnez des localisations ou lancez une recherche pour afficher les assets." + +#: assets/static/assets/assets_index.js:1102 +msgid "Aucun asset ne correspond aux critères de recherche." +msgstr "Aucun asset ne correspond aux critères de recherche." + +#: assets/static/assets/assets_index.js:1309 +#: assets/static/assets/unified_index.js:2571 +#: assets/static/assets/unified_index.js:3330 +#: assets/static/assets/unified_index.js:3398 +msgid "Sans catégorie" +msgstr "Sans catégorie" + +#: assets/static/assets/assets_index.js:1330 +#: assets/static/assets/unified_index.js:3424 +msgid "%(count)s élément pris en compte" +msgid_plural "%(count)s éléments pris en compte" +msgstr[0] "%(count)s élément pris en compte" +msgstr[1] "%(count)s éléments pris en compte" + +#: assets/static/assets/assets_index.js:1338 +#: assets/static/assets/unified_index.js:3429 +msgid "(sélection)" +msgstr "(sélection)" + +#: assets/static/assets/assets_index.js:1339 +#: assets/static/assets/unified_index.js:3429 +msgid "(table)" +msgstr "(table)" + +#: assets/static/assets/assets_index.js:1350 +#: assets/static/assets/unified_index.js:3434 +msgid "Aucune donnée disponible" +msgstr "Aucune donnée disponible" + +#: assets/static/assets/assets_index.js:1383 +#: assets/static/assets/unified_index.js:3465 +msgid "au total" +msgstr "au total" + +#: assets/static/assets/assets_index.js:1389 +#: assets/static/assets/unified_index.js:509 +#: assets/static/assets/unified_index.js:3471 +msgid "Modèle" +msgstr "Modèle" + +#: assets/static/assets/assets_index.js:1390 +#: assets/static/assets/unified_index.js:3472 +msgid "Nombre" +msgstr "Nombre" + +#: assets/static/assets/assets_index.js:1391 +#: assets/static/assets/unified_index.js:3473 +msgid "Longueur (m)" +msgstr "Longueur (m)" + +#: assets/static/assets/assets_index.js:1392 +#: assets/static/assets/unified_index.js:3474 +msgid "Surface (m²)" +msgstr "Surface (m²)" + +#: assets/static/assets/assets_index.js:1396 +#: assets/static/assets/unified_index.js:3477 +msgid "Aucun modèle" +msgstr "Aucun modèle" + +#: assets/static/assets/assets_index.js:1400 +#: assets/static/assets/unified_index.js:3480 +msgid "Total catégorie" +msgstr "Total catégorie" + +#: assets/static/assets/green-surfaces-comments.js:473 +#: assets/static/assets/green-surfaces-comments.js:902 +msgid "Nouveau commentaire" +msgstr "Nouveau commentaire" + +#: assets/static/assets/green-surfaces-comments.js:480 +msgid "Textes rapides" +msgstr "Textes rapides" + +#: assets/static/assets/green-surfaces-comments.js:482 +#: assets/static/assets/green-surfaces-comments.js:483 +#: assets/static/assets/green-surfaces-comments.js:956 +msgid "Mémo" +msgstr "Mémo" + +#: assets/static/assets/green-surfaces-comments.js:485 +#: assets/static/assets/green-surfaces-comments.js:486 +msgid "Point d'attention" +msgstr "Point d'attention" + +#: assets/static/assets/green-surfaces-comments.js:488 +#: assets/static/assets/green-surfaces-comments.js:489 +#: assets/static/assets/green-surfaces-comments.js:962 +msgid "À faire" +msgstr "À faire" + +#: assets/static/assets/green-surfaces-comments.js:491 +#: assets/static/assets/green-surfaces-comments.js:492 +#: assets/static/assets/green-surfaces-comments.js:967 +msgid "OK" +msgstr "OK" + +#: assets/static/assets/green-surfaces-comments.js:494 +#: assets/static/assets/green-surfaces-comments.js:495 +#: assets/static/assets/green-surfaces-comments.js:970 +msgid "Problème" +msgstr "Problème" + +#: assets/static/assets/green-surfaces-comments.js:501 +#: assets/static/assets/green-surfaces-comments.js:977 +msgid "Commentaire" +msgstr "Commentaire" + +#: assets/static/assets/green-surfaces-comments.js:502 +msgid "Votre commentaire..." +msgstr "Votre commentaire..." + +#: assets/static/assets/green-surfaces-comments.js:506 +#: assets/static/assets/green-surfaces-comments.js:990 +msgid "Photos" +msgstr "Photos" + +#: assets/static/assets/green-surfaces-comments.js:509 +msgid "Prendre des photos (mode continu)" +msgstr "Prendre des photos (mode continu)" + +#: assets/static/assets/green-surfaces-comments.js:512 +msgid "Sélectionner des fichiers" +msgstr "Sélectionner des fichiers" + +#: assets/static/assets/green-surfaces-comments.js:521 +msgid "Position" +msgstr "Position" + +#: assets/static/assets/green-surfaces-comments.js:525 +msgid "Position actuelle sera utilisée" +msgstr "Position actuelle sera utilisée" + +#: assets/static/assets/green-surfaces-comments.js:528 +#: assets/static/assets/green-surfaces-comments.js:985 +msgid "Repositionner" +msgstr "Repositionner" + +#: assets/static/assets/green-surfaces-comments.js:536 +#: assets/static/assets/green-surfaces-comments.js:1008 +msgid "Annuler" +msgstr "Annuler" + +#: assets/static/assets/green-surfaces-comments.js:538 +#: assets/static/assets/green-surfaces-comments.js:1005 +msgid "Enregistrer" +msgstr "Enregistrer" + +#: assets/static/assets/green-surfaces-comments.js:570 +msgid "Caméra" +msgstr "Caméra" + +#: assets/static/assets/green-surfaces-comments.js:585 +#: assets/static/assets/green-surfaces-comments.js:1127 +msgid "Terminer" +msgstr "Terminer" + +#: assets/static/assets/green-surfaces-comments.js:588 +#: assets/static/assets/green-surfaces-comments.js:1124 +msgid "Capturer" +msgstr "Capturer" + +#: assets/static/assets/green-surfaces-comments.js:959 +msgid "Attention" +msgstr "Attention" + +#: assets/static/assets/green-surfaces-comments.js:977 +msgid "(optionnel)" +msgstr "(optionnel)" + +#: assets/static/assets/green-surfaces-comments.js:978 +msgid "Entrez un commentaire optionnel..." +msgstr "Entrez un commentaire optionnel..." + +#: assets/static/assets/green-surfaces-comments.js:982 +msgid "Localisation" +msgstr "Localisation" + +#: assets/static/assets/green-surfaces-comments.js:983 +msgid "Calcul en cours..." +msgstr "Calcul en cours..." + +#: assets/static/assets/green-surfaces-comments.js:993 +msgid "Appareil photo" +msgstr "Appareil photo" + +#: assets/static/assets/green-surfaces-comments.js:996 +msgid "Sélectionner fichiers" +msgstr "Sélectionner fichiers" + +#: assets/static/assets/unified_index.js:121 +#: assets/static/assets/unified_index.js:258 +msgid "Sélectionner une catégorie..." +msgstr "Sélectionner une catégorie..." + +#: assets/static/assets/unified_index.js:130 +msgid "Aucune catégorie disponible" +msgstr "Aucune catégorie disponible" + +#: assets/static/assets/unified_index.js:170 +msgid "Sélectionnez la localisation liée à ce nouvel asset." +msgstr "Sélectionnez la localisation liée à ce nouvel asset." + +#: assets/static/assets/unified_index.js:200 +msgid "Aucune localisation disponible pour créer cet asset." +msgstr "Aucune localisation disponible pour créer cet asset." + +#: assets/static/assets/unified_index.js:209 +msgid "Impossible de charger les localisations pour cette thématique." +msgstr "Impossible de charger les localisations pour cette thématique." + +#: assets/static/assets/unified_index.js:294 +msgid "Créer un asset" +msgstr "Créer un asset" + +#: assets/static/assets/unified_index.js:308 +msgid "Le code est généré automatiquement." +msgstr "Le code est généré automatiquement." + +#: assets/static/assets/unified_index.js:444 +#: assets/static/assets/unified_index.js:449 +msgid "Erreur lors du chargement du formulaire." +msgstr "Erreur lors du chargement du formulaire." + +#: assets/static/assets/unified_index.js:466 +msgid "Créer une localisation" +msgstr "Créer une localisation" + +#: assets/static/assets/unified_index.js:467 +#: assets/static/assets/unified_index.js:506 +msgid "Type" +msgstr "Type" + +#: assets/static/assets/unified_index.js:490 +msgid "Zoom" +msgstr "Zoom" + +#: assets/static/assets/unified_index.js:505 +msgid "Nom" +msgstr "Nom" + +#: assets/static/assets/unified_index.js:507 +msgid "Thématique" +msgstr "Thématique" + +#: assets/static/assets/unified_index.js:508 +msgid "Catégorie" +msgstr "Catégorie" + +#: assets/static/assets/unified_index.js:783 +#: assets/static/assets/unified_index.js:784 +msgid "Sélectionner une thématique..." +msgstr "Sélectionner une thématique..." + +#: assets/static/assets/unified_index.js:783 +#: assets/static/assets/unified_index.js:784 +msgid "Aucune thématique disponible" +msgstr "Aucune thématique disponible" + +#: assets/static/assets/unified_index.js:791 +msgid "Vous n'avez pas de droits de création" +msgstr "Vous n'avez pas de droits de création" + +#: assets/static/assets/unified_index.js:809 +msgid "Sélectionnez une thématique pour créer un asset." +msgstr "Sélectionnez une thématique pour créer un asset." + +#: assets/static/assets/unified_index.js:821 +msgid "Sélectionnez une thématique pour créer une localisation." +msgstr "Sélectionnez une thématique pour créer une localisation." + +#: assets/static/assets/unified_index.js:840 +msgid "Sélectionnez une localisation avant de créer cet asset." +msgstr "Sélectionnez une localisation avant de créer cet asset." + +#: assets/static/assets/unified_index.js:871 +msgid "Type d'asset introuvable pour ce modèle." +msgstr "Type d'asset introuvable pour ce modèle." + +#: assets/static/assets/unified_index.js:901 +msgid "Asset créé." +msgstr "Asset créé." + +#: assets/static/assets/unified_index.js:912 +#: assets/static/assets/unified_index.js:959 +msgid "Erreur lors de la création." +msgstr "Erreur lors de la création." + +#: assets/static/assets/unified_index.js:916 +#: assets/static/assets/unified_index.js:963 +msgid "Erreur de connexion." +msgstr "Erreur de connexion." + +#: assets/static/assets/unified_index.js:952 +msgid "Localisation créée." +msgstr "Localisation créée." + +#: assets/static/assets/unified_index.js:1359 +msgid "Veuillez sélectionner au moins un asset." +msgstr "Veuillez sélectionner au moins un asset." + +#: assets/static/assets/unified_index.js:1423 +msgid "" +"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre " +"pour charger le tableau et la carte." +msgstr "Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre pour charger le tableau et la carte." + +#: assets/static/assets/unified_index.js:1671 +#: assets/static/assets/unified_index.js:1702 +msgid "Voir dans le tableau" +msgstr "Voir dans le tableau" + +#: assets/static/assets/unified_index.js:1711 +msgid "autre(s) asset(s)" +msgstr "autre(s) asset(s)" + +#: assets/static/assets/unified_index.js:1715 +msgid "autre(s) localisation(s)" +msgstr "autre(s) localisation(s)" + +#: assets/static/assets/unified_index.js:1720 +msgid "Assets trouvés" +msgstr "Assets trouvés" + +#: assets/static/assets/unified_index.js:1728 +msgid "Localisations trouvées" +msgstr "Localisations trouvées" + +#: assets/static/assets/unified_index.js:2209 +msgid "Vue initiale (Bruxelles)" +msgstr "Vue initiale (Bruxelles)" + +#: assets/static/assets/unified_index.js:2349 +msgid "sélectionné(s)" +msgstr "sélectionné(s)" + +#: assets/static/assets/unified_index.js:2889 +msgid "Aucun type sélectionné" +msgstr "Aucun type sélectionné" + +#: assets/static/assets/unified_index.js:2893 +msgid "Aucun type disponible." +msgstr "Aucun type disponible." + +#: assets/static/assets/unified_index.js:3034 +#: assets/static/assets/unified_index.js:3204 +#: assets/static/assets/unified_index.js:3262 +msgid "Sélectionnez d'abord une ou plusieurs thématiques." +msgstr "Sélectionnez d'abord une ou plusieurs thématiques." + +#: assets/static/assets/unified_index.js:3104 +msgid "Tous" +msgstr "Tous" + +#: assets/static/assets/unified_index.js:3269 +msgid "Sélectionnez d'abord une ou plusieurs catégories." +msgstr "Sélectionnez d'abord une ou plusieurs catégories." + +#: assets/static/assets/unified_index.js:3285 +msgid "Chargement des modèles..." +msgstr "Chargement des modèles..." + +#: assets/static/assets/unified_index.js:3296 +msgid "Aucun modèle disponible pour les catégories sélectionnées." +msgstr "Aucun modèle disponible pour les catégories sélectionnées." + +#: assets/static/assets/unified_index.js:3307 +msgid "Afficher les modèles" +msgstr "Afficher les modèles" + +#: assets/static/assets/unified_index.js:3308 +msgid "Replier les modèles" +msgstr "Replier les modèles" + +#: assets/static/assets/unified_index.js:3313 +msgid "" +"Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options." +msgstr "Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options." + +#~ msgid "Erreur lors de la liaison" +#~ msgstr "Erreur lors de la liaison" + +#~ msgid "Erreur lors de la désélection" +#~ msgstr "Erreur lors de la désélection" + +# Asset Template Selection Modal +#~ msgid "Aucun template correspondant" +#~ msgstr "Aucun template correspondant" + +#~ msgid "" +#~ "Le modèle ou la catégorie de cet équipement ne correspond à aucun " +#~ "template d'opérations automatique." +#~ msgstr "" +#~ "Le modèle ou la catégorie de cet équipement ne correspond à aucun " +#~ "template d'opérations automatique." + +#~ msgid "-- Sélectionner un template --" +#~ msgstr "-- Sélectionner un template --" diff --git a/streetup/assets/locale/nl/LC_MESSAGES/django.mo b/streetup/assets/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..30b3d6a Binary files /dev/null and b/streetup/assets/locale/nl/LC_MESSAGES/django.mo differ diff --git a/streetup/assets/locale/nl/LC_MESSAGES/django.po b/streetup/assets/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..b08a807 --- /dev/null +++ b/streetup/assets/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,5725 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:26+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models/clean.py:16 +msgid "Cemented to ground" +msgstr "Ingecementeerd in de grond" + +#: models/clean.py:17 +msgid "Bolted to ground" +msgstr "Vastgebout in de grond" + +#: models/clean.py:18 +msgid "Pole-mounted" +msgstr "Gemonteerd op paal" + +#: models/clean.py:19 +msgid "Wall-mounted" +msgstr "Gemonteerd aan de muur" + +#: models/clean.py:20 +msgid "Freestanding" +msgstr "Vrijstaand" + +#: models/clean.py:21 +msgid "Other" +msgstr "Overig" + +#: models/clean.py:25 +msgid "Clear glass" +msgstr "Wit glas" + +#: models/clean.py:26 +msgid "Coloured glass" +msgstr "Gekleurd glas" + +#: models/clean.py:27 +msgid "Mixed" +msgstr "Gemengd" + +#: models/clean.py:82 models/clean.py:118 models/clean.py:155 +#: models/clean.py:194 +msgid "Colour" +msgstr "Kleur" + +#: models/clean.py:83 models/clean.py:156 +msgid "RAL code" +msgstr "RAL-code" + +#: models/clean.py:84 models/clean.py:119 models/clean.py:157 +#: models/clean.py:195 +msgid "Material" +msgstr "Materiaal" + +#: models/clean.py:85 models/clean.py:120 +msgid "Volume (litres)" +msgstr "Volume (liter)" + +#: models/clean.py:86 models/clean.py:121 +msgid "Cover" +msgstr "Deksel" + +#: models/clean.py:87 models/clean.py:122 +msgid "Built-in ashtray" +msgstr "Ingebouwde asbak" + +#: models/clean.py:90 +msgid "Litter bin model" +msgstr "Model vuilnisbak" + +#: models/clean.py:91 +msgid "Litter bin models" +msgstr "Modellen vuilnisbakken" + +#: models/clean.py:104 models/clean.py:177 +msgid "Location" +msgstr "Locatie" + +#: models/clean.py:110 models/clean.py:183 templates/assets/clean_index.html:28 +#: templates/assets/clean_index.html:74 +msgid "Model" +msgstr "Model" + +#: models/clean.py:116 +msgid "Fixation type" +msgstr "Type bevestiging" + +#: models/clean.py:123 +msgid "Built-in extinguisher stub (éteignoir)" +msgstr "Ingebouwde uitdover" + +#: models/clean.py:126 models/clean.py:198 +msgid "Emptying frequency" +msgstr "Ledigingsfrequentie" + +#: models/clean.py:130 +msgid "Collection route (tournée)" +msgstr "Ophaalroute" + +#: models/clean.py:134 +msgid "External ID (source system)" +msgstr "Externe ID (bronsysteem)" + +#: models/clean.py:136 models/clean.py:204 +msgid "Remarks" +msgstr "Opmerkingen" + +#: models/clean.py:139 models/clean.py:238 models/clean.py:305 +msgid "Litter bin" +msgstr "Vuilnisbak" + +#: models/clean.py:140 templates/assets/clean_location_detail.html:72 +#: templates/assets/clean_location_detail.html:110 +msgid "Litter bins" +msgstr "Vuilnisbakken" + +#: models/clean.py:158 models/clean.py:191 +msgid "Capacity (litres)" +msgstr "Capaciteit (liter)" + +#: models/clean.py:159 models/clean.py:192 +#: templates/assets/clean_location_detail.html:96 +msgid "Underground" +msgstr "Ondergronds" + +#: models/clean.py:160 models/clean.py:193 +msgid "Number of openings" +msgstr "Aantal openingen" + +#: models/clean.py:163 +msgid "Glass container model" +msgstr "Model glascontainer" + +#: models/clean.py:164 +msgid "Glass container models" +msgstr "Modellen glascontainers" + +#: models/clean.py:189 +msgid "Accepted glass type" +msgstr "Toegestane glassoort" + +#: models/clean.py:202 +msgid "Operator / manager" +msgstr "Exploitant / beheerder" + +#: models/clean.py:207 +msgid "Glass container" +msgstr "Glascontainer" + +#: models/clean.py:208 templates/assets/clean_location_detail.html:88 +#: templates/assets/clean_location_detail.html:124 +msgid "Glass containers" +msgstr "Conteneurs à verre" + +#: models/clean.py:243 +msgid "Photo" +msgstr "Foto" + +#: models/clean.py:248 +msgid "Source URL (original)" +msgstr "Bron-URL (origineel)" + +#: models/clean.py:250 +msgid "Order" +msgstr "Volgorde" + +#: models/clean.py:251 +msgid "Taken at" +msgstr "Genomen op" + +#: models/clean.py:252 +msgid "Caption" +msgstr "Bijschrift" + +#: models/clean.py:255 +msgid "Litter bin photo" +msgstr "Foto vuilnisbak" + +#: models/clean.py:256 +msgid "Litter bin photos" +msgstr "Foto's vuilnisbakken" + +#: models/clean.py:268 templates/assets/cleanlitterbin_detail.html:250 +#: templates/assets/cleanlitterbin_detail.html:378 +msgid "Important – ponctuel" +msgstr "Belangrijk – eenmalig" + +#: models/clean.py:269 templates/assets/cleanlitterbin_detail.html:251 +#: templates/assets/cleanlitterbin_detail.html:378 +msgid "Important – continu" +msgstr "Belangrijk – continu" + +#: models/clean.py:270 models/clean.py:277 +#: templates/assets/cleanlitterbin_detail.html:252 +#: templates/assets/cleanlitterbin_detail.html:262 +#: templates/assets/cleanlitterbin_detail.html:378 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Faible" +msgstr "Laag" + +#: models/clean.py:271 models/clean.py:295 models/roads.py:275 +#: models/roads.py:287 models/roads.py:297 models/trafficlights.py:203 +#: templates/assets/cleanlitterbin_detail.html:253 +#: templates/assets/cleanlitterbin_detail.html:303 +#: templates/assets/cleanlitterbin_detail.html:378 +#: templates/assets/cleanlitterbin_detail.html:401 +#: templates/assets/trafficlights_intersection_detail.html:1220 +msgid "Autre" +msgstr "Andere" + +#: models/clean.py:275 templates/assets/cleanlitterbin_detail.html:260 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Élevée" +msgstr "Hoog" + +#: models/clean.py:276 templates/assets/cleanlitterbin_detail.html:261 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Moyenne" +msgstr "Gemiddeld" + +#: models/clean.py:281 templates/assets/cleanlitterbin_detail.html:165 +#: templates/assets/cleanlitterbin_detail.html:175 +#: templates/assets/cleanlitterbin_detail.html:269 +#: templates/assets/cleanlitterbin_detail.html:278 +#: templates/assets/cleanlitterbin_detail.html:380 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Normal" +msgstr "Normaal" + +#: models/clean.py:282 templates/assets/cleanlitterbin_detail.html:166 +#: templates/assets/cleanlitterbin_detail.html:176 +#: templates/assets/cleanlitterbin_detail.html:270 +#: templates/assets/cleanlitterbin_detail.html:279 +#: templates/assets/cleanlitterbin_detail.html:380 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Difficile" +msgstr "Moeilijk" + +#: models/clean.py:283 templates/assets/cleanlitterbin_detail.html:167 +#: templates/assets/cleanlitterbin_detail.html:177 +#: templates/assets/cleanlitterbin_detail.html:271 +#: templates/assets/cleanlitterbin_detail.html:280 +#: templates/assets/cleanlitterbin_detail.html:380 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Impossible" +msgstr "Onmogelijk" + +#: models/clean.py:287 templates/assets/cleanlitterbin_detail.html:295 +#: templates/assets/cleanlitterbin_detail.html:393 +msgid "Arrêt Bus/Tram" +msgstr "Bus-/tramhalte" + +#: models/clean.py:288 templates/assets/cleanlitterbin_detail.html:296 +#: templates/assets/cleanlitterbin_detail.html:394 +msgid "Parc" +msgstr "Park" + +#: models/clean.py:289 templates/assets/cleanlitterbin_detail.html:297 +#: templates/assets/cleanlitterbin_detail.html:395 +msgid "École" +msgstr "School" + +#: models/clean.py:290 templates/assets/cleanlitterbin_detail.html:298 +#: templates/assets/cleanlitterbin_detail.html:396 +msgid "Commerce" +msgstr "Winkel / Handelszaak" + +#: models/clean.py:291 templates/assets/cleanlitterbin_detail.html:299 +#: templates/assets/cleanlitterbin_detail.html:397 views/artworks.py:420 +#: views/artworks.py:424 +msgid "Monument" +msgstr "Monument" + +#: models/clean.py:292 templates/assets/cleanlitterbin_detail.html:300 +#: templates/assets/cleanlitterbin_detail.html:398 +msgid "Passage piéton" +msgstr "Voetgangersoversteekplaats" + +#: models/clean.py:293 models/roads.py:284 +#: templates/assets/cleanlitterbin_detail.html:301 +#: templates/assets/cleanlitterbin_detail.html:399 +msgid "Place" +msgstr "Plein" + +#: models/clean.py:294 templates/assets/cleanlitterbin_detail.html:302 +#: templates/assets/cleanlitterbin_detail.html:400 +#: templates/assets/permissions/panel.html:230 +#: templates/assets/permissions/panel.html:256 +#: templates/assets/permissions/type_page.html:173 +#: templates/assets/permissions/type_page.html:196 +#: templates/assets/permissions/type_page.html:374 +#: templates/assets/permissions/type_page.html:397 +#: templates/assets/trafficlights_intersection_detail.html:68 +#: templates/assets/trafficlights_intersection_detail.html:1297 +#: templates/assets/unified_index.html:94 +#: templates/assets/unified_index.html:132 +msgid "Aucun" +msgstr "Geen" + +#: models/clean.py:307 +msgid "Observation date/time" +msgstr "Datum/tijd observatie" + +#: models/clean.py:308 +msgid "Observer notes" +msgstr "Opmerkingen observator" + +#: models/clean.py:311 +msgid "Slope too steep" +msgstr "Helling te steil" + +#: models/clean.py:312 +msgid "Unstable ground" +msgstr "Instabiele grond" + +#: models/clean.py:313 +msgid "Reflective surface" +msgstr "Reflecterend oppervlak" + +#: models/clean.py:314 +msgid "Slippery surface" +msgstr "Glad oppervlak" + +#: models/clean.py:315 +msgid "Cracks in ground" +msgstr "Scheuren in de grond" + +#: models/clean.py:316 +msgid "Ground obstacle" +msgstr "Obstakel op de grond" + +#: models/clean.py:317 +msgid "Tactile paving (dalles podotactiles)" +msgstr "Geleidetegels (noppentegels)" + +#: models/clean.py:318 +msgid "Transversal slope (%)" +msgstr "Dwarshelling (%)" + +#: models/clean.py:319 +msgid "Longitudinal slope (%)" +msgstr "Langshelling (%)" + +#: models/clean.py:320 +msgid "Ground remarks" +msgstr "Opmerkingen over de grond" + +#: models/clean.py:327 +msgid "Neighbourhood type" +msgstr "Type buurt" + +#: models/clean.py:333 +msgid "Pedestrian flow density" +msgstr "Voetgangersstroomdichtheid" + +#: models/clean.py:338 +msgid "Points of interest" +msgstr "Nuttige plaatsen (POI)" + +#: models/clean.py:340 +msgid "Clear 2 m approach" +msgstr "Vrije toegang van 2 m" + +#: models/clean.py:341 +msgid "Road to cross" +msgstr "Over te steken weg" + +#: models/clean.py:342 +msgid "Isolated bin" +msgstr "Geïsoleerde vuilnisbak" + +#: models/clean.py:343 +msgid "Obstructs road traffic" +msgstr "Hindert wegverkeer" + +#: models/clean.py:344 +msgid "Obstructs pedestrian flow" +msgstr "Hindert voetgangersstroom" + +#: models/clean.py:345 +msgid "Logical path (itinéraire logique)" +msgstr "Logische route" + +#: models/clean.py:346 +msgid "Visual contrast" +msgstr "Visueel contrast" + +#: models/clean.py:347 +msgid "Sorting indicators present" +msgstr "Sorteerindicatoren aanwezig" + +#: models/clean.py:352 +msgid "Agent access – vehicle" +msgstr "Toegang medewerker – voertuig" + +#: models/clean.py:358 +msgid "Agent access – on foot" +msgstr "Toegang medewerker – te voet" + +#: models/clean.py:362 +msgid "Other point of interest" +msgstr "Ander nuttig punt" + +#: models/clean.py:367 +msgid "Placement relevance codes" +msgstr "Relevantiecodes plaatsing" + +#: models/clean.py:371 +msgid "Obstruction codes" +msgstr "Obstakelcodes" + +#: models/clean.py:373 +msgid "Point of interest remarks" +msgstr "Opmerkingen over nuttige plaatsen" + +#: models/clean.py:374 +msgid "Accessibility remarks" +msgstr "Opmerkingen over bereikbaarheid" + +#: models/clean.py:377 +msgid "Litter bin observation" +msgstr "Observatie vuilnisbak" + +#: models/clean.py:378 +msgid "Litter bin observations" +msgstr "Observaties vuilnisbakken" + +#: models/core.py:42 +msgid "Catégorie parente" +msgstr "Bovenliggende categorie" + +#: models/core.py:43 +msgid "" +"Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne " +"peut pas avoir d'enfants." +msgstr "" + +#: models/core.py:52 +#, fuzzy +#| msgid "Modèle non trouvé" +msgid "Modèles Django autorisés" +msgstr "Model niet gevonden" + +#: models/core.py:53 +#, fuzzy +#| msgid "Type d'asset non supporté pour cette thématique/catégorie" +msgid "Les types de modèles d'assets (Django) autorisés pour cette catégorie." +msgstr "Assettype niet ondersteund voor dit thema/categorie" + +#: models/core.py:104 +msgid "À approuver" +msgstr "Te goedkeuren" + +#: models/core.py:105 +msgid "Planifié" +msgstr "Gepland" + +#: models/core.py:106 models/trafficlights.py:18 +#: templates/assets/detail_generic.html:1310 views/artworks.py:78 +msgid "Actif" +msgstr "Actief" + +#: models/core.py:107 templates/assets/detail_generic.html:1311 +msgid "Endommagé" +msgstr "Beschadigd" + +#: models/core.py:108 templates/assets/detail_generic.html:1312 +msgid "A remplacer" +msgstr "Te vervangen" + +#: models/core.py:109 models/trafficlights.py:21 +msgid "Mis hors service volontairement" +msgstr "Vrijwillig buiten dienst gesteld" + +#: models/core.py:110 templates/assets/detail_generic.html:1313 +msgid "Enlevé" +msgstr "Verwijderd" + +#: models/core.py:111 models/trafficlights.py:23 views/core.py:514 +msgid "Archivé" +msgstr "Gearchiveerd" + +#: models/core.py:115 +msgid "Bon état" +msgstr "Goede staat" + +#: models/core.py:116 +msgid "Moyen" +msgstr "Gemiddeld" + +#: models/core.py:117 +msgid "Dégradé" +msgstr "Beschadigd" + +#: models/core.py:118 +msgid "Critique" +msgstr "Kritiek" + +#: models/core.py:119 +msgid "Inconnu" +msgstr "Onbekend" + +#: models/core.py:123 +msgid "À valider" +msgstr "Te valideren" + +#: models/core.py:124 +msgid "Validé" +msgstr "Gevalideerd" + +#: models/core.py:125 +msgid "À compléter" +msgstr "Aan te vullen" + +#: models/core.py:126 templates/assets/document_version_history_list.html:41 +msgid "Rejeté" +msgstr "Afgewezen" + +#: models/history.py:186 +msgid "Type d'asset" +msgstr "Type asset" + +#: models/history.py:189 +msgid "ID de l'asset" +msgstr "Asset ID" + +#: models/history.py:200 templates/assets/components/asset_history.html:38 +#: templates/assets/permissions/panel.html:67 +#: templates/assets/permissions/panel.html:109 +#: templates/assets/permissions/panel.html:192 +#: templates/assets/permissions/type_page.html:69 +#: templates/assets/permissions/type_page.html:141 +#: templates/assets/permissions/type_page.html:235 +#: templates/assets/permissions/type_page.html:334 +msgid "Utilisateur" +msgstr "Gebruiker" + +#: models/history.py:205 +msgid "Date de modification" +msgstr "Wijzigingsdatum" + +#: models/history.py:211 +msgid "Champ modifié" +msgstr "Gewijzigd veld" + +#: models/history.py:216 templates/assets/components/asset_history.html:40 +msgid "Ancienne valeur" +msgstr "Oude waarde" + +#: models/history.py:221 templates/assets/components/asset_history.html:41 +msgid "Nouvelle valeur" +msgstr "Nieuwe waarde" + +#: models/history.py:229 +msgid "Création" +msgstr "Aanmaak" + +#: models/history.py:230 +msgid "Modification" +msgstr "Wijziging" + +#: models/history.py:231 templates/assets/cleanlitterbin_detail.html:47 +#: templates/assets/cleanlitterbin_detail.html:343 +msgid "Suppression" +msgstr "Verwijdering" + +#: models/history.py:232 templates/assets/components/asset_history.html:337 +msgid "Changement de statut" +msgstr "Statuswijziging" + +#: models/history.py:234 +msgid "Type d'action" +msgstr "Actietype" + +#: models/history.py:240 +msgid "Historique d'asset" +msgstr "Asset geschiedenis" + +#: models/history.py:241 +msgid "Historiques d'assets" +msgstr "Asset geschiedenissen" + +#: models/history.py:266 +msgid "(vide)" +msgstr "(leeg)" + +#: models/history.py:325 templates/assets/clean_index.html:65 +#: templates/assets/clean_index.html:71 +#: templates/assets/clean_location_detail.html:158 +#: templates/assets/components/add_asset_modal.html:70 +#: templates/assets/components/asset_model_card.html:17 +#: templates/assets/controlcenters_location_detail.html:242 +#: templates/assets/detail_generic.html:211 +#: templates/assets/detail_generic.html:578 +#: templates/assets/its_location_detail.html:166 +#: templates/assets/nature_locations_detail.html:72 +#: templates/assets/naturerwiz_detail.html:241 +#: templates/assets/publiclighting_street_detail.html:301 +#: templates/assets/roads_street_detail.html:316 +#: templates/assets/sign_street_detail.html:293 +#: templates/assets/structures_location_detail.html:351 +#: templates/assets/trafficlights_intersection_detail.html:932 +#: templates/assets/unified_index.html:310 views/export.py:95 +msgid "Code" +msgstr "Code" + +#: models/history.py:326 +msgid "Nom (FR)" +msgstr "Naam (FR)" + +#: models/history.py:327 +msgid "Nom (NL)" +msgstr "Naam (NL)" + +#: models/history.py:328 templates/assets/artworks_index.html:8 +#: templates/assets/artworks_index.html:36 +#: templates/assets/controlcenters_location_detail.html:245 +#: templates/assets/detail_generic.html:250 +#: templates/assets/detail_generic.html:579 +#: templates/assets/its_location_detail.html:169 +#: templates/assets/nature_locations_detail.html:75 +#: templates/assets/naturerwiz_detail.html:244 +#: templates/assets/publiclighting_street_detail.html:304 +#: templates/assets/roads_street_detail.html:319 +#: templates/assets/sign_street_detail.html:296 +#: templates/assets/structures_location_detail.html:354 +#: templates/assets/trafficlights_import_cross_plan.html:242 +#: templates/assets/trafficlights_import_cross_plan.html:325 +#: templates/assets/trafficlights_import_cross_plan.html:409 +#: templates/assets/trafficlights_import_cross_plan.html:500 +#: templates/assets/trafficlights_index.html:8 +#: templates/assets/trafficlights_intersection_detail.html:38 +#: templates/assets/trafficlights_intersection_detail.html:935 +#: templates/assets/trafficlights_intersection_detail.html:1171 +#: templates/assets/trafficlights_intersection_detail.html:1262 +#: templates/assets/unified_index.html:195 views/export.py:99 +msgid "Statut" +msgstr "Status" + +#: models/history.py:329 views/export.py:103 +msgid "Date d'installation" +msgstr "Installatiedatum" + +#: models/history.py:330 +msgid "Date de désinstallation" +msgstr "Deinstallatiedatum" + +#: models/history.py:331 templates/assets/detail_generic.html:749 +#: templates/assets/detail_generic.html:750 views/export.py:105 +msgid "Numéro de série" +msgstr "Serienummer" + +#: models/history.py:332 templates/assets/detail_generic.html:365 +msgid "Durée de garantie" +msgstr "Garantieduur" + +#: models/history.py:333 templates/assets/detail_generic.html:394 +msgid "Dernière inspection" +msgstr "Laatste inspectie" + +#: models/history.py:334 +msgid "Dernier entretien préventif" +msgstr "Laatste preventief onderhoud" + +#: models/history.py:335 +msgid "Dernier entretien correctif" +msgstr "Laatste correctief onderhoud" + +#: models/history.py:336 +msgid "Dernier relevé" +msgstr "Laatste meting" + +#: models/history.py:337 templates/assets/artworks_index.html:72 +#: templates/assets/components/add_asset_modal.html:34 +#: templates/assets/components/asset_model_card.html:32 +#: templates/assets/components/asset_model_card.html:74 +#: templates/assets/controlcenters_index.html:25 +#: templates/assets/controlcenters_index.html:68 +#: templates/assets/detail_generic.html:308 templates/assets/its_index.html:36 +#: templates/assets/its_index.html:88 templates/assets/nature_index.html:39 +#: templates/assets/nature_index.html:82 +#: templates/assets/publiclighting_index.html:29 +#: templates/assets/publiclighting_index.html:73 +#: templates/assets/roads_index.html:29 templates/assets/roads_index.html:85 +#: templates/assets/sign_index.html:29 templates/assets/sign_index.html:85 +#: templates/assets/structures_index.html:37 +#: templates/assets/structures_index.html:82 +#: templates/assets/trafficlights_index.html:81 +#: templates/assets/trafficlights_index.html:126 +#: templates/assets/trafficlights_intersection_detail.html:979 +#: templates/assets/unified_index.html:115 +#: templates/assets/unified_index.html:291 templates/assets/water_index.html:40 +#: templates/assets/water_index.html:80 views/export.py:97 +msgid "Catégorie" +msgstr "Categorie" + +#: models/history.py:338 templates/assets/components/asset_model_card.html:26 +#: templates/assets/detail_generic.html:352 +#: templates/assets/detail_generic.html:754 +#: templates/assets/detail_generic.html:755 views/export.py:107 +msgid "Marque" +msgstr "Merk" + +#: models/history.py:339 templates/assets/components/add_asset_modal.html:57 +#: templates/assets/components/asset_model_card.html:6 +#: templates/assets/controlcenters_index.html:35 +#: templates/assets/controlcenters_index.html:69 +#: templates/assets/its_index.html:46 templates/assets/its_index.html:89 +#: templates/assets/nature_index.html:49 templates/assets/nature_index.html:83 +#: templates/assets/publiclighting_index.html:39 +#: templates/assets/publiclighting_index.html:74 +#: templates/assets/roads_index.html:39 templates/assets/roads_index.html:86 +#: templates/assets/sign_index.html:39 templates/assets/sign_index.html:86 +#: templates/assets/structures_index.html:47 +#: templates/assets/structures_index.html:83 +#: templates/assets/trafficlights_index.html:91 +#: templates/assets/trafficlights_index.html:127 +#: templates/assets/trafficlights_intersection_detail.html:628 +#: templates/assets/trafficlights_intersection_detail.html:996 +#: templates/assets/unified_index.html:129 +#: templates/assets/unified_index.html:297 views/export.py:98 +msgid "Modèle" +msgstr "Model" + +#: models/history.py:340 templates/assets/components/add_asset_modal.html:101 +#: templates/assets/unified_index.html:260 +#: templates/assets/unified_index.html:303 +msgid "Localisation" +msgstr "Locatie" + +#: models/history.py:341 templates/assets/structures_location_detail.html:16 +msgid "Structure" +msgstr "Structuur" + +#: models/history.py:342 templates/assets/detail_generic.html:672 +#: views/export.py:109 +msgid "Longitude" +msgstr "Lengtegraad" + +#: models/history.py:343 templates/assets/detail_generic.html:683 +#: views/export.py:110 +msgid "Latitude" +msgstr "Breedtegraad" + +#: models/history.py:344 +msgid "Géométrie" +msgstr "Geometrie" + +#: models/history.py:345 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: models/nature.py:81 +msgid "Tulipes" +msgstr "Tulpen" + +#: models/nature.py:82 +msgid "Jacynthes" +msgstr "Hyacinten" + +#: models/nature.py:83 +msgid "Jonquilles" +msgstr "Narcissen" + +#: models/nature.py:84 +msgid "Narcisses" +msgstr "Narcissen" + +#: models/nature.py:85 +msgid "Crocus" +msgstr "Krokussen" + +#: models/nature.py:86 +msgid "Perce-neige" +msgstr "Sneeuwklokjes" + +#: models/nature.py:87 views/core.py:535 +msgid "Autres" +msgstr "Andere" + +#: models/nature.py:106 +msgid "Types de bulbes" +msgstr "Boltypen" + +#: models/nature.py:298 +msgid "Zone de gestion intégrée des eaux pluviales (ZIEP)" +msgstr "Geïntegreerde regenwaterbeheerzone" + +#: models/nature.py:299 +msgid "Zones de gestion intégrée des eaux pluviales (ZIEP)" +msgstr "Geïntegreerde regenwaterbeheerzones" + +#: models/nature.py:307 models/nature.py:334 +msgid "Stormwater inlet (Avaloir)" +msgstr "Straatkolk" + +#: models/nature.py:308 models/nature.py:335 +msgid "Sequenced border (Bordure séquencée)" +msgstr "Snoerboord" + +#: models/nature.py:309 models/nature.py:336 +msgid "Injection port (Bouche d'injection)" +msgstr "Injectiemond" + +#: models/nature.py:310 models/nature.py:337 +msgid "Non-return valve (Clapet anti-retour)" +msgstr "Terugslagklep" + +#: models/nature.py:311 models/nature.py:338 +msgid "Sedimentation (Empierrement sédimentation)" +msgstr "Sedimentatiesteenslag" + +#: models/nature.py:312 models/nature.py:339 +msgid "Kerb cell" +msgstr "Kerb cell" + +#: models/nature.py:313 models/nature.py:340 +msgid "Infiltration well (Puits d'infiltration)" +msgstr "Infiltratieput" + +#: models/nature.py:314 models/nature.py:341 +msgid "Overflow (Surverse)" +msgstr "Overloop" + +#: models/nature.py:315 models/nature.py:342 +msgid "Isolation valve (Vanne d'isolation)" +msgstr "Isolatieklep" + +#: models/nature.py:316 models/nature.py:343 +msgid "Control valve (Vanne de régulation)" +msgstr "Regelklep" + +#: models/nature.py:317 models/nature.py:345 +msgid "Sidewalk gargoyle (Gargouille de trottoir)" +msgstr "Stoepspuwer" + +#: models/nature.py:318 models/nature.py:346 +msgid "Trickle of water (Filet d'eau)" +msgstr "Watergreppel" + +#: models/nature.py:319 models/nature.py:347 +msgid "Rail drain (Avaloir de rail)" +msgstr "Railkolk" + +#: models/nature.py:320 models/nature.py:348 +msgid "Leveled edge (Bordure arasée)" +msgstr "Verlaagde boord" + +#: models/nature.py:321 models/nature.py:349 +msgid "Infiltration channel (Caniveau infiltrant)" +msgstr "Infiltratiegoot" + +#: models/nature.py:322 models/nature.py:350 +#, fuzzy +#| msgid "Road Drain" +msgid "Drain" +msgstr "Straatkolk" + +#: models/nature.py:323 models/nature.py:351 +msgid "Overflowing (Trop plein)" +msgstr "Overloop" + +#: models/nature.py:324 models/nature.py:352 +msgid "Separate sewer system (Réseau égout séparatif)" +msgstr "Gescheiden rioolstelsel" + +#: models/nature.py:325 +#, fuzzy +#| msgid "Espace vert creux" +msgid "Hollow green space (Espace vert creux)" +msgstr "Verlaagde groenzone" + +#: models/nature.py:326 +msgid "Decentralized underground structure (Ouvrage enterré décentralisé)" +msgstr "Decentraal ondergronds bouwwerk" + +#: models/nature.py:327 +msgid "Work on an impermeable surface (Ouvrage sur surface imperméable)" +msgstr "Constructie op ondoorlatend oppervlak" + +#: models/nature.py:328 +msgid "Alternative technique (Technique alternative)" +msgstr "Alternatieve techniek" + +#: models/nature.py:329 +msgid "Supplementary work (Ouvrage annexe)" +msgstr "Bijkomend bouwwerk" + +#: models/nature.py:354 +#, fuzzy +#| msgid "Bassin sec/d'infiltration" +msgid "Dry/infiltration basin (Bassin sec/infiltration)" +msgstr "Droog-/infiltratiebekken" + +#: models/nature.py:355 +#, fuzzy +#| msgid "Bassin en eau" +msgid "Wet basin (Bassin en eau)" +msgstr "Nat bekken" + +#: models/nature.py:356 +#, fuzzy +#| msgid "Fossé" +msgid "Ditch (Fossé)" +msgstr "Greppel" + +#: models/nature.py:357 +#, fuzzy +#| msgid "Jardin de pluie" +msgid "Rain garden (Jardin de pluie)" +msgstr "Regentuin" + +#: models/nature.py:358 +msgid "Swale (Noue)" +msgstr "Wadi" + +#: models/nature.py:359 +msgid "Draining structure (Massif drainant)" +msgstr "Drainagekoffer" + +#: models/nature.py:360 +msgid "Draining trench (Tranchée drainante)" +msgstr "Drainagesleuf" + +#: models/nature.py:361 +msgid "Retention roof (Toiture stockante)" +msgstr "Retentiedak" + +#: models/nature.py:362 +msgid "Green roof (Toiture verte)" +msgstr "Groendak" + +#: models/nature.py:363 +msgid "Stormwater basin (Bassin d'orage)" +msgstr "Regenwaterbekken" + +#: models/nature.py:364 +msgid "SAUL-empty (SAUL-vide)" +msgstr "SAUL (leeg)" + +#: models/nature.py:365 +msgid "SAUL-earth (SAUL-terre)" +msgstr "SAUL (aarde)" + +#: models/nature.py:366 +msgid "Cistern (Citerne)" +msgstr "Regenwaterput" + +#: models/nature.py:367 +msgid "Green space on slab (Espace vert sur dalle)" +msgstr "Groenzone op plaat" + +#: models/nature.py:368 +msgid "Filter strip (Bande filtrante)" +msgstr "Filterstrook" + +#: models/nature.py:369 +msgid "Porous concrete collector (Collecteur en béton poreux)" +msgstr "Poreuze betonnen collector" + +#: models/nature.py:384 +msgid "Design (Étude)" +msgstr "Ontwerp" + +#: models/nature.py:385 +#, fuzzy +#| msgid "Construction status" +msgid "Construction (Travaux)" +msgstr "Bouwstatus" + +#: models/nature.py:386 +msgid "Provisional acceptance (Réception provisoire)" +msgstr "Voorlopige oplevering" + +#: models/nature.py:387 +msgid "Final acceptance (Réception définitive)" +msgstr "Definitieve oplevering" + +#: models/nature.py:391 +msgid "Good (Bon)" +msgstr "Goed" + +#: models/nature.py:392 +msgid "Inspection to schedule (Examen à programmer)" +msgstr "Te plannen inspectie" + +#: models/nature.py:393 +msgid "Curative action needed (Intervention curative nécessaire)" +msgstr "Curatieve interventie nodig" + +#: models/nature.py:394 +msgid "Total rehabilitation needed (Réfection totale nécessaire)" +msgstr "Volledige vernieuwing nodig" + +#: models/nature.py:395 +msgid "Maintenance works to perform (Travaux entretien à réaliser)" +msgstr "Uit te voeren onderhoudswerken" + +#: models/nature.py:396 +msgid "Maintenance works (Travaux maintenance)" +msgstr "Onderhoudswerken" + +#: models/nature.py:397 +msgid "Maintenance works to schedule (Travaux maintenance à programmer)" +msgstr "Te plannen onderhoudswerken" + +#: models/nature.py:401 +msgid "Connected (Connecté)" +msgstr "Aangesloten" + +#: models/nature.py:402 +msgid "Not connected (Non connecté)" +msgstr "Niet aangesloten" + +#: models/nature.py:406 +#, fuzzy +#| msgid "Végétal" +msgid "Vegetal (Végétal)" +msgstr "Plantaardig" + +#: models/nature.py:407 +#, fuzzy +#| msgid "Structure enherbée" +msgid "Grassed structure (Structure enherbée)" +msgstr "Met gras begroeide structuur" + +#: models/nature.py:408 +#, fuzzy +#| msgid "Matériau non agrégé" +msgid "Unbound material (Matériau non agrégé)" +msgstr "Niet-geaggregeerd materiaal" + +#: models/nature.py:409 +#, fuzzy +#| msgid "Structure à remplissage minéral" +msgid "Mineral-filled structure (Structure à remplissage minéral)" +msgstr "Met mineraal gevulde structuur" + +#: models/nature.py:410 +#, fuzzy +#| msgid "Revêtement surélevé" +msgid "Raised coating (Revêtement surélevé)" +msgstr "Verhoogde verharding" + +#: models/nature.py:411 +#, fuzzy +#| msgid "Modulaire poreux" +msgid "Porous modular (Modulaire poreux)" +msgstr "Poreus modulair" + +#: models/nature.py:412 +msgid "Porous continuous (Continu poreux)" +msgstr "Poreuze doorlopende verharding" + +#: models/nature.py:413 +msgid "Impermeable (Imperméable)" +msgstr "Ondoorlatend" + +#: models/nature.py:417 +msgid "Open soil (Pleine terre)" +msgstr "Volle grond" + +#: models/nature.py:418 +#, fuzzy +#| msgid "Mélange terre-pierre" +msgid "Soil-stone mixture (Mélange terre-pierre)" +msgstr "Grond-steenmengsel" + +#: models/nature.py:419 +msgid "Grassed wide-joint pavers (Pavés à joints larges enherbés)" +msgstr "Klinkers met brede grasvoegen" + +#: models/nature.py:420 +msgid "Concrete grass grids (Dalles gazon en béton)" +msgstr "Betonnen grasdalen" + +#: models/nature.py:421 +msgid "Vegetated cellular tiles (Dalles alvéolaires végétalisées)" +msgstr "Groene kunststof grasplaten" + +#: models/nature.py:422 +msgid "Woodchips/mulch (Mulch végétal)" +msgstr "Houtsnippers" + +#: models/nature.py:423 +msgid "Limestone gravel (Graviers calcaires)" +msgstr "Kalkgrind" + +#: models/nature.py:424 +msgid "Non-limestone gravel (Graviers non calcaires)" +msgstr "Niet-kalkgrind" + +#: models/nature.py:425 +msgid "Dirt ground (Terre battue)" +msgstr "Aangestampte aarde" + +#: models/nature.py:426 +msgid "Gravel wide-joint pavers (Pavés à joints larges engravillonnés)" +msgstr "Klinkers met brede grindvoegen" + +#: models/nature.py:427 +msgid "Gravel cellular tiles (Dalles alvéolaires en gravier)" +msgstr "Grindplaten" + +#: models/nature.py:428 +msgid "HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)" +msgstr "HDPE-platen gevuld met klinkers" + +#: models/nature.py:429 +msgid "Grates (Grilles)" +msgstr "Roosters" + +#: models/nature.py:430 +msgid "Wooden decking (Platelages bois)" +msgstr "Houten vlonder" + +#: models/nature.py:431 +msgid "Porous concrete pavers (Pavé en béton poreux)" +msgstr "Poreuze betonstraatstenen" + +#: models/nature.py:432 +msgid "Porous concrete slab (Dalle en béton poreux)" +msgstr "Poreuze betonplaat" + +#: models/nature.py:433 +msgid "Porous asphalt (Enrobés bitumineux poreux)" +msgstr "Poreus asfalt" + +#: models/nature.py:434 +msgid "Porous concrete (Bétons poreux)" +msgstr "Poreus beton" + +#: models/nature.py:435 +msgid "Resin-bound gravel (Graviers liés à la résine)" +msgstr "Harsgebonden grind" + +#: models/nature.py:436 +msgid "Polymer-bound crushed gravel (Graviers concassés liés aux polymères)" +msgstr "Polymeergebonden steenslag" + +#: models/nature.py:437 +msgid "EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)" +msgstr "EPDM en SBR rubberbekledingen" + +#: models/nature.py:438 +msgid "" +"Non-porous cement concrete coatings (Revêtements en béton de ciment non " +"perméables)" +msgstr "" + +#: models/nature.py:439 +msgid "Non-porous asphalt coatings (Revêtements bitumineux non perméables)" +msgstr "Ondoorlatend asfalt" + +#: models/nature.py:440 +msgid "Non-porous tiling (Dallages non perméables)" +msgstr "Ondoorlatende plaveisel" + +#: models/nature.py:441 +msgid "Non-porous paving (Pavages non perméables)" +msgstr "Ondoorlatende bestrating" + +#: models/nature.py:442 +msgid "Other non-porous coatings (Autres revêtements non perméables)" +msgstr "Andere ondoorlatende bekledingen" + +#: models/nature.py:447 +msgid "Point" +msgstr "" + +#: models/nature.py:448 +msgid "Line" +msgstr "" + +#: models/nature.py:449 +msgid "Polygon" +msgstr "" + +#: models/nature.py:456 +msgid "External ID" +msgstr "Externe ID" + +#: models/nature.py:457 +msgid "Code name" +msgstr "Codenaam" + +#: models/nature.py:458 +msgid "Structure type" +msgstr "Type constructie" + +#: models/nature.py:459 +msgid "Geometry type" +msgstr "Geometrietype" + +#: models/nature.py:460 +msgid "Project manager" +msgstr "Projectmanager" + +#: models/nature.py:462 +msgid "Construction status" +msgstr "Bouwstatus" + +#: models/nature.py:463 +msgid "Commissioning date" +msgstr "Datum van ingebruikname" + +#: models/nature.py:464 +msgid "Condition" +msgstr "Staat" + +#: models/nature.py:465 +msgid "Administrator" +msgstr "Beheerder" + +#: models/nature.py:466 +msgid "Monitoring" +msgstr "Opvolging" + +#: models/nature.py:467 +msgid "Comment" +msgstr "Opmerking" + +#: models/nature.py:468 +msgid "Has filter" +msgstr "Heeft filter" + +#: models/nature.py:470 +msgid "Sewer connection" +msgstr "Roolaansluiting" + +#: models/nature.py:471 +msgid "Separate network connection" +msgstr "Aansluiting gescheiden rioolstelsel" + +#: models/nature.py:472 +msgid "Hydro network connection" +msgstr "Aansluiting hydrografisch netwerk" + +#: models/nature.py:474 +msgid "Surface area (m²)" +msgstr "Oppervlakte (m²)" + +#: models/nature.py:475 +msgid "Length (m)" +msgstr "Lengte (m)" + +#: models/nature.py:477 +msgid "Coating category" +msgstr "Type verharding" + +#: models/nature.py:478 +msgid "Coating type" +msgstr "Type bekleding" + +#: models/nature.py:485 models/nature.py:493 +#, fuzzy +#| msgid "Asset" +msgid "RWI Asset" +msgstr "Asset" + +#: models/nature.py:486 +#, fuzzy +#| msgid "Assets" +msgid "RWI Assets" +msgstr "Assets" + +#: models/nature.py:494 +msgid "File" +msgstr "" + +#: models/nature.py:498 +#, fuzzy +#| msgid "Assets uniquement" +msgid "RWI Asset Document" +msgstr "Alleen assets" + +#: models/nature.py:499 +#, fuzzy +#| msgid "Documents" +msgid "RWI Asset Documents" +msgstr "Documenten" + +#: models/publiclighting.py:85 +msgid "Public lighting pole" +msgstr "" + +#: models/publiclighting.py:86 +msgid "Public lighting poles" +msgstr "" + +#: models/publiclighting.py:108 +msgid "Luminaire" +msgstr "Armatuur" + +#: models/publiclighting.py:109 +msgid "Luminaires" +msgstr "Armaturen" + +#: models/roads.py:224 +msgid "Protection Asset" +msgstr "Beschermingselement" + +#: models/roads.py:225 +msgid "Antiterroriste" +msgstr "Antiterrorisme" + +#: models/roads.py:226 +msgid "Neutralisation stationnement" +msgstr "Parkeerneutralisatie" + +#: models/roads.py:227 +msgid "Sécurité routière" +msgstr "Verkeersveiligheid" + +#: models/roads.py:228 +msgid "Espace public" +msgstr "Openbare ruimte" + +#: models/roads.py:229 +msgid "Chantier" +msgstr "Werf" + +#: models/roads.py:230 +msgid "Non identifié" +msgstr "Niet geïdentificeerd" + +#: models/roads.py:234 +msgid "Maintenir" +msgstr "Behouden" + +#: models/roads.py:235 +msgid "Enlever" +msgstr "Verwijderen" + +#: models/roads.py:236 templates/assets/detail_generic.html:139 +#: templates/assets/detail_generic.html:140 +#: templates/assets/detail_generic.html:183 +#: templates/assets/detail_generic.html:184 +#: templates/assets/detail_generic.html:761 +msgid "Remplacer" +msgstr "Vervangen" + +#: models/roads.py:258 +msgid "Road Concrete Block Set" +msgstr "Betonblokkenset" + +#: models/roads.py:264 +msgid "Béton" +msgstr "Beton" + +#: models/roads.py:265 +msgid "Asphalte" +msgstr "Asfalt" + +#: models/roads.py:266 +msgid "Pierre" +msgstr "Natuursteen" + +#: models/roads.py:267 +msgid "Dalles en béton" +msgstr "Betontegels" + +#: models/roads.py:268 +msgid "Pavés en béton" +msgstr "Betonklinkers" + +#: models/roads.py:269 +msgid "Dalles en pierre" +msgstr "Natuursteentegels" + +#: models/roads.py:270 +msgid "Pavés en pierre" +msgstr "Natuursteenklinkers" + +#: models/roads.py:271 +msgid "Béton/Gazon" +msgstr "Grasbetontegels" + +#: models/roads.py:272 +msgid "Gravier" +msgstr "Grind" + +#: models/roads.py:273 +msgid "Gazon" +msgstr "Gras" + +#: models/roads.py:274 +msgid "Terre" +msgstr "Aarde" + +#: models/roads.py:276 models/roads.py:288 +msgid "N/A" +msgstr "N/B" + +#: models/roads.py:280 +msgid "Filet d'eau" +msgstr "Goot / watergreppel" + +#: models/roads.py:281 +msgid "Trottoir" +msgstr "Stoep / Trottoir" + +#: models/roads.py:282 +msgid "Berme" +msgstr "Berm" + +#: models/roads.py:283 +msgid "Trottoir façade vers chaussée" +msgstr "Trottoir van gevel naar rijbaan" + +#: models/roads.py:285 +msgid "Le long du trottoir" +msgstr "Langs het trottoir" + +#: models/roads.py:286 +msgid "Milieu chaussée" +msgstr "Midden op de weg" + +#: models/roads.py:292 +msgid "Obstrué" +msgstr "Verstopt / Belemmerd" + +#: models/roads.py:293 +msgid "Coupe-odeur manquant" +msgstr "Stankafsluiter ontbreekt" + +#: models/roads.py:294 +msgid "Grille cassée" +msgstr "Gebroken rooster" + +#: models/roads.py:295 +msgid "Grille manquante" +msgstr "Ontbrekend rooster" + +#: models/roads.py:296 +msgid "Mauvais écoulement" +msgstr "Slechte afwatering" + +#: models/roads.py:330 +msgid "Road Drain" +msgstr "Straatkolk" + +#: models/sign.py:173 +msgid "Panneau de signalisation" +msgstr "Verkeersbord" + +#: models/sign.py:174 +msgid "Panneaux de signalisation" +msgstr "Verkeersborden" + +#: models/trafficlights.py:19 +msgid "En préparation" +msgstr "In voorbereiding" + +#: models/trafficlights.py:20 +msgid "En cours de réparation" +msgstr "In herstelling" + +#: models/trafficlights.py:22 +msgid "Désactivé" +msgstr "Uitgeschakeld" + +#: models/trafficlights.py:73 +msgid "Est un radar ?" +msgstr "" + +#: models/trafficlights.py:202 +msgid "Plan de croix" +msgstr "Kruisplan" + +#: models/trafficlights.py:204 +msgid "Fichier de programmation" +msgstr "Programmeerbestand" + +#: models/trafficlights.py:205 +msgid "Plan de raccordement" +msgstr "Aansluitschema" + +#: models/trafficlights.py:211 +msgid "Programmation associée" +msgstr "Bijbehorende programmering" + +#: models/trafficlights.py:262 models/trafficlights.py:273 +#: models/trafficlights.py:533 +msgid "Couleur" +msgstr "Kleur" + +#: models/trafficlights.py:263 models/trafficlights.py:274 +#: models/trafficlights.py:534 +msgid "RAL" +msgstr "RAL" + +#: models/trafficlights.py:264 +msgid "Hauteur (m)" +msgstr "Hoogte (m)" + +#: models/trafficlights.py:271 +msgid "Longueur de l'arc (m)" +msgstr "Booglengte (m)" + +#: models/trafficlights.py:272 +msgid "Extension" +msgstr "Uitbreiding" + +#: models/trafficlights.py:293 +#, fuzzy +#| msgid "Traffic Light Radar Model" +msgid "Traffic light pole" +msgstr "Radarmodel" + +#: models/trafficlights.py:294 +#, fuzzy +#| msgid "Traffic Light Radar Models" +msgid "Traffic light poles" +msgstr "Radarmodellen" + +#: models/trafficlights.py:299 +msgid "Nombre de brins" +msgstr "Aantal draden" + +#: models/trafficlights.py:308 +msgid "Câble" +msgstr "Kabel" + +#: models/trafficlights.py:309 +#: templates/assets/publiclighting_street_detail.html:269 +#: templates/assets/trafficlights_import_cross_plan.html:215 +#: templates/assets/trafficlights_import_cross_plan.html:608 +#: templates/assets/trafficlights_intersection_detail.html:553 +msgid "Câbles" +msgstr "Kabels" + +#: models/trafficlights.py:314 +msgid "Tension (V)" +msgstr "Spanning (V)" + +#: models/trafficlights.py:323 +msgid "Lanterne" +msgstr "Lantaarn" + +#: models/trafficlights.py:324 +#: templates/assets/trafficlights_import_cross_plan.html:205 +msgid "Lanternes" +msgstr "Lantaarns" + +#: models/trafficlights.py:335 +msgid "Connecté à la centrale de contrôle" +msgstr "Verbonden met de verkeerscentrale" + +#: models/trafficlights.py:337 models/trafficlights.py:514 +#: templates/assets/trafficlights_intersection_detail.html:636 +#: templates/assets/trafficlights_intersection_detail.html:960 +msgid "Plan" +msgstr "Plan" + +#: models/trafficlights.py:338 templates/assets/sign_index.html:87 +#: templates/assets/structures_index.html:76 +#: templates/assets/trafficlights_index.html:120 +#: templates/assets/trafficlights_intersection_detail.html:632 +#: templates/assets/trafficlights_intersection_detail.html:1216 +#: views/export.py:93 +msgid "Type" +msgstr "Type" + +#: models/trafficlights.py:339 +#: templates/assets/trafficlights_intersection_detail.html:640 +msgid "Puissance" +msgstr "Vermogen" + +#: models/trafficlights.py:340 +msgid "Version du firmware" +msgstr "Firmwareversie" + +#: models/trafficlights.py:341 +msgid "RMS" +msgstr "RMS" + +#: models/trafficlights.py:342 +msgid "URL" +msgstr "URL" + +#: models/trafficlights.py:345 +#: templates/assets/trafficlights_intersection_detail.html:401 +msgid "Contrôleur" +msgstr "Regelaar" + +#: models/trafficlights.py:346 +msgid "Contrôleurs" +msgstr "Regelaars" + +#: models/trafficlights.py:352 models/trafficlights.py:379 +#: models/trafficlights.py:395 models/trafficlights.py:411 +msgid "Hardware" +msgstr "Hardware" + +#: models/trafficlights.py:353 models/trafficlights.py:394 +#: models/trafficlights.py:410 +msgid "Nom du matériel" +msgstr "Hardwarenaam" + +#: models/trafficlights.py:354 +msgid "Description du matériel" +msgstr "Hardwarebeschrijving" + +#: models/trafficlights.py:357 +msgid "Hardware contrôleur" +msgstr "Controleurhardware" + +#: models/trafficlights.py:358 +msgid "Hardwares contrôleur" +msgstr "Controleurhardwares" + +#: models/trafficlights.py:365 +msgid "Date du log" +msgstr "Logdatum" + +#: models/trafficlights.py:368 +msgid "Fichier de log du contrôleur" +msgstr "Logbestand van de controleur" + +#: models/trafficlights.py:369 +msgid "Fichiers de log du contrôleur" +msgstr "Logbestanden van de controleur" + +#: models/trafficlights.py:378 +msgid "Nom du détecteur" +msgstr "Detectornaam" + +#: models/trafficlights.py:384 +msgid "Phases associées" +msgstr "Bijbehorende fasen" + +#: models/trafficlights.py:385 models/trafficlights.py:398 +#: models/trafficlights.py:414 +msgid "Mentionné sur le plan" +msgstr "Vermeld op het plan" + +#: models/trafficlights.py:388 +msgid "Détecteur" +msgstr "Detector" + +#: models/trafficlights.py:389 +#: templates/assets/trafficlights_intersection_detail.html:703 +msgid "Détecteurs" +msgstr "Detectoren" + +#: models/trafficlights.py:404 +msgid "Network Hardware" +msgstr "Netwerkhardware" + +#: models/trafficlights.py:405 +msgid "Network Hardwares" +msgstr "Netwerkhardwares" + +#: models/trafficlights.py:420 +msgid "Voltage Hardware" +msgstr "Spanningshardware" + +#: models/trafficlights.py:421 +msgid "Voltage Hardwares" +msgstr "Spanningshardwares" + +#: models/trafficlights.py:427 +msgid "Largeur (cm)" +msgstr "Breedte (cm)" + +#: models/trafficlights.py:428 +msgid "Hauteur (cm)" +msgstr "Hoogte (cm)" + +#: models/trafficlights.py:429 +msgid "Profondeur (cm)" +msgstr "Diepte (cm)" + +#: models/trafficlights.py:430 +msgid "Indice de protection IP" +msgstr "Beschermingsgraad IP" + +#: models/trafficlights.py:455 models/trafficlights.py:485 +msgid "Code EAN" +msgstr "Code EAN" + +#: models/trafficlights.py:456 +msgid "Alimentation électrique" +msgstr "Elektrische voeding" + +#: models/trafficlights.py:457 +msgid "Numéro de compteur" +msgstr "Teller nummer" + +#: models/trafficlights.py:458 +msgid "Publicité" +msgstr "Reclame" + +#: models/trafficlights.py:459 +msgid "Type de clé" +msgstr "Sleuteltype" + +#: models/trafficlights.py:460 +msgid "Type d'armoire" +msgstr "Kastype" + +#: models/trafficlights.py:461 +msgid "Géré par" +msgstr "Beheerd door" + +#: models/trafficlights.py:462 +msgid "Présence Compteur" +msgstr "Aanwezigheid teller" + +#: models/trafficlights.py:463 +msgid "Interrupteur différentiel" +msgstr "Differentiaalschakelaar" + +#: models/trafficlights.py:464 +msgid "Calibre du disjoncteur différentiel" +msgstr "Differentiaalautomaat kaliber" + +#: models/trafficlights.py:465 +msgid "Disjoncteur différentiel" +msgstr "Differentiaalautomaat" + +#: models/trafficlights.py:466 +msgid "Calibre du disjoncteur" +msgstr "Automaat kaliber" + +#: models/trafficlights.py:467 +msgid "Calibre différentiel" +msgstr "Differentieel kaliber" + +#: models/trafficlights.py:468 +msgid "Départ vers" +msgstr "Vertrek naar" + +#: models/trafficlights.py:469 +msgid "Conformité visuelle" +msgstr "Visuele conformiteit" + +#: models/trafficlights.py:470 views/locations.py:33 +msgid "Propreté" +msgstr "Netheid" + +#: models/trafficlights.py:473 +msgid "Armoire" +msgstr "Kast" + +#: models/trafficlights.py:474 +msgid "Armoires" +msgstr "Kasten" + +#: models/trafficlights.py:483 +msgid "Description du contenu" +msgstr "Inhoudsbeschrijving" + +#: models/trafficlights.py:484 +msgid "Numéro de référence" +msgstr "Referentienummer" + +#: models/trafficlights.py:488 +msgid "Contenu d'armoire" +msgstr "Kasteinhoud" + +#: models/trafficlights.py:489 +msgid "Contenus d'armoires" +msgstr "Kasteninhoudslijst" + +#: models/trafficlights.py:504 +msgid "Accessoire" +msgstr "Accessoire" + +#: models/trafficlights.py:505 +msgid "Accessoires" +msgstr "Accessoires" + +#: models/trafficlights.py:512 +#: templates/assets/trafficlights_intersection_detail.html:644 +msgid "Date de mise en service" +msgstr "Datum ingebruikname" + +#: models/trafficlights.py:513 +msgid "Date de mise hors service" +msgstr "Datum buitengebruikstelling" + +#: models/trafficlights.py:515 +#: templates/assets/trafficlights_intersection_detail.html:50 +msgid "Index" +msgstr "Index" + +#: models/trafficlights.py:516 +msgid "Informations" +msgstr "Informatie" + +#: models/trafficlights.py:549 +msgid "Radar Pole" +msgstr "Radarpaal" + +#: models/trafficlights.py:550 +msgid "Radar Poles" +msgstr "Radarpalen" + +#: models/trafficlights.py:555 +msgid "Traffic Light Radar Model" +msgstr "Radarmodel" + +#: models/trafficlights.py:556 +msgid "Traffic Light Radar Models" +msgstr "Radarmodellen" + +#: models/trafficlights.py:564 +msgid "Radar Pole (Support)" +msgstr "Radarpaal (Ondersteuning)" + +#: models/trafficlights.py:565 +msgid "Public Lighting Pole (Support)" +msgstr "Verlichtingspaal (Ondersteuning)" + +#: models/trafficlights.py:566 +msgid "Traffic Light Pole (Support)" +msgstr "Verkeerslichtpaal (Ondersteuning)" + +#: models/trafficlights.py:569 +msgid "Public Lighting Pole (Power)" +msgstr "Verlichtingspaal (Voeding)" + +#: models/trafficlights.py:570 +msgid "Traffic Light Pole (Power)" +msgstr "Verkeerslichtpaal (Voeding)" + +#: models/trafficlights.py:573 +msgid "Radar" +msgstr "Radar" + +#: models/trafficlights.py:574 +msgid "Radars" +msgstr "Radars" + +#: permissions.py:502 +msgid "Missing asset information" +msgstr "Ontbrekende asset informatie" + +#: permissions.py:509 +msgid "Invalid asset type" +msgstr "Ongeldig asset type" + +#: permissions.py:512 +msgid "You do not have permission to edit this asset" +msgstr "U hebt geen toestemming om dit asset te bewerken" + +#: permissions.py:549 +msgid "Invalid thematic" +msgstr "Ongeldig thema" + +#: permissions.py:553 views/history.py:44 +msgid "You do not have permission to edit assets for this thematic" +msgstr "U hebt geen toestemming om assets voor dit thema te bewerken" + +#: templates/assets/artworks_index.html:18 +#: templates/assets/artworks_index.html:46 templates/assets/clean_index.html:8 +#: templates/assets/controlcenters_index.html:16 +#: templates/assets/controlcenters_index.html:45 +#: templates/assets/its_index.html:16 templates/assets/its_index.html:56 +#: templates/assets/nature_index.html:28 templates/assets/nature_index.html:59 +#: templates/assets/publiclighting_index.html:18 +#: templates/assets/publiclighting_index.html:49 +#: templates/assets/roads_index.html:18 templates/assets/roads_index.html:61 +#: templates/assets/sign_index.html:18 templates/assets/sign_index.html:61 +#: templates/assets/structures_index.html:26 +#: templates/assets/structures_index.html:57 +#: templates/assets/trafficlights_index.html:61 +#: templates/assets/trafficlights_index.html:101 +#: templates/assets/unified_index.html:229 templates/assets/water_index.html:22 +#: templates/assets/water_index.html:50 +msgid "Filtrer" +msgstr "Filteren" + +#: templates/assets/artworks_index.html:19 +#: templates/assets/artworks_index.html:47 templates/assets/clean_index.html:9 +#: templates/assets/controlcenters_index.html:17 +#: templates/assets/controlcenters_index.html:46 +#: templates/assets/its_index.html:17 templates/assets/its_index.html:57 +#: templates/assets/nature_index.html:29 templates/assets/nature_index.html:60 +#: templates/assets/publiclighting_index.html:19 +#: templates/assets/publiclighting_index.html:50 +#: templates/assets/roads_index.html:19 templates/assets/roads_index.html:62 +#: templates/assets/sign_index.html:19 templates/assets/sign_index.html:62 +#: templates/assets/structures_index.html:27 +#: templates/assets/structures_index.html:58 +#: templates/assets/trafficlights_index.html:62 +#: templates/assets/trafficlights_index.html:102 +#: templates/assets/unified_index.html:227 templates/assets/water_index.html:23 +#: templates/assets/water_index.html:51 +msgid "Réinitialiser" +msgstr "Resetten" + +#: templates/assets/artworks_index.html:22 +#: templates/assets/artworks_index.html:64 +#: templates/assets/artworks_street_detail.html:15 +#: templates/assets/publiclighting_index.html:8 +#: templates/assets/publiclighting_index.html:67 +#: templates/assets/roads_index.html:8 templates/assets/roads_index.html:79 +#: templates/assets/sign_index.html:8 templates/assets/sign_index.html:79 +#: templates/assets/trafficlights_intersection_detail.html:79 views/core.py:597 +msgid "Commune" +msgstr "Gemeente" + +#: templates/assets/artworks_index.html:63 +#: templates/assets/artworks_index.html:71 +#: templates/assets/components/add_asset_modal.html:82 +#: templates/assets/components/add_asset_modal.html:91 +#: templates/assets/components/asset_model_card.html:21 +#: templates/assets/controlcenters_index.html:62 +#: templates/assets/controlcenters_index.html:67 +#: templates/assets/detail_generic.html:224 +#: templates/assets/detail_generic.html:237 templates/assets/its_index.html:82 +#: templates/assets/its_index.html:87 templates/assets/nature_index.html:76 +#: templates/assets/publiclighting_index.html:66 +#: templates/assets/publiclighting_index.html:72 +#: templates/assets/roads_index.html:78 templates/assets/roads_index.html:84 +#: templates/assets/sign_index.html:78 templates/assets/sign_index.html:84 +#: templates/assets/structures_index.html:75 +#: templates/assets/structures_index.html:81 +#: templates/assets/trafficlights_import_cross_plan.html:241 +#: templates/assets/trafficlights_import_cross_plan.html:323 +#: templates/assets/trafficlights_import_cross_plan.html:407 +#: templates/assets/trafficlights_import_cross_plan.html:499 +#: templates/assets/trafficlights_index.html:119 +#: templates/assets/trafficlights_index.html:125 +#: templates/assets/unified_index.html:315 +#: templates/assets/unified_index.html:319 templates/assets/water_index.html:75 +#: views/export.py:96 +msgid "Nom" +msgstr "Naam" + +#: templates/assets/artworks_index.html:65 +#: templates/assets/artworks_street_detail.html:19 +msgid "Code voirie" +msgstr "Wegcode" + +#: templates/assets/artworks_index.html:66 +#: templates/assets/artworks_street_detail.html:23 +msgid "Section" +msgstr "Sectie" + +#: templates/assets/artworks_index.html:73 +msgid "Sous-type" +msgstr "Subtype" + +#: templates/assets/artworks_street_detail.html:7 +msgid "Retour à la vue Artworks" +msgstr "Terug naar Kunstwerken overzicht" + +#: templates/assets/artworks_street_detail.html:27 +msgid "Niveau" +msgstr "Niveau" + +#: templates/assets/artworks_street_detail.html:38 +msgid "Monuments" +msgstr "Monumenten" + +#: templates/assets/artworks_street_detail.html:51 +#: templates/assets/artworks_street_detail.html:78 +#: templates/assets/controlcenters_location_detail.html:251 +#: templates/assets/its_location_detail.html:175 +#: templates/assets/nature_locations_detail.html:81 +#: templates/assets/naturerwiz_detail.html:250 +#: templates/assets/publiclighting_street_detail.html:310 +#: templates/assets/roads_street_detail.html:325 +#: templates/assets/sign_street_detail.html:302 +#: templates/assets/structures_location_detail.html:117 +#: templates/assets/structures_location_detail.html:360 +#: templates/assets/trafficlights_intersection_detail.html:941 +msgid "Détail" +msgstr "Detail" + +#: templates/assets/artworks_street_detail.html:57 +msgid "Aucun monument actif n'est lié à cette localisation." +msgstr "Geen actief monument is gekoppeld aan deze locatie." + +#: templates/assets/artworks_street_detail.html:65 +msgid "Fontaines" +msgstr "Fonteinen" + +#: templates/assets/artworks_street_detail.html:84 +msgid "Aucune fontaine active n'est liée à cette localisation." +msgstr "Geen actieve fontein is gekoppeld aan deze locatie." + +#: templates/assets/base_assets_index.html:22 +msgid "Thématique :" +msgstr "Thema:" + +#: templates/assets/base_assets_index.html:33 +msgid "Sélectionnez une thématique" +msgstr "Selecteer een thema" + +#: templates/assets/base_assets_index.html:56 +#: templates/assets/base_assets_index.html:69 +msgid "Filtre" +msgstr "Filter" + +#: templates/assets/base_assets_index.html:81 +msgid "Sélection de la vue" +msgstr "Weergaveselectie" + +#: templates/assets/base_assets_index.html:82 +#: templates/assets/unified_index.html:425 +msgid "Vue tableau uniquement" +msgstr "Alleen tabelweergave" + +#: templates/assets/base_assets_index.html:85 +#: templates/assets/unified_index.html:428 +msgid "Vue partagée" +msgstr "Gedeelde weergave" + +#: templates/assets/base_assets_index.html:88 +#: templates/assets/unified_index.html:431 +msgid "Vue carte uniquement" +msgstr "Alleen kaartweergave" + +#: templates/assets/base_assets_index.html:102 +msgid "Code, nom…" +msgstr "Code, naam..." + +#: templates/assets/base_assets_index.html:107 +msgid "Effacer la recherche" +msgstr "Zoekopdracht wissen" + +#: templates/assets/base_assets_index.html:113 +msgid "Mode" +msgstr "Modus" + +#: templates/assets/base_assets_index.html:115 +msgid "Tous les mots doivent correspondre" +msgstr "Alle woorden moeten overeenkomen" + +#: templates/assets/base_assets_index.html:117 +msgid "Au moins un mot doit correspondre" +msgstr "Minstens één woord moet overeenkomen" + +#: templates/assets/base_assets_index.html:123 +msgid "Rechercher" +msgstr "Zoeken" + +#: templates/assets/base_assets_index.html:151 +#: templates/assets/base_assets_index.html:188 +#: templates/assets/trafficlights_import_cross_plan.html:64 +#: templates/assets/unified_index.html:475 +msgid "Tout sélectionner" +msgstr "Alles selecteren" + +#: templates/assets/base_assets_index.html:152 +#: templates/assets/base_assets_index.html:189 +#: templates/assets/unified_index.html:476 +msgid "Tout désélectionner" +msgstr "Alles deselecteren" + +#: templates/assets/base_assets_index.html:154 +#: templates/assets/base_assets_index.html:191 +#: templates/assets/unified_index.html:378 +#: templates/assets/unified_index.html:478 +msgid "Valider la sélection" +msgstr "Selectie bevestigen" + +#: templates/assets/base_assets_index.html:157 +#, python-format +msgid "" +"\n" +" %(counter)s localisation sélectionnée\n" +" " +msgid_plural "" +"\n" +" %(counter)s localisations sélectionnées\n" +" " +msgstr[0] "" +"\n" +"%(counter)s geselecteerde locatie" +msgstr[1] "" +"\n" +"%(counter)s geselecteerde locaties" + +#: templates/assets/base_assets_index.html:171 +#: templates/assets/base_assets_index.html:225 +#: templates/assets/unified_index.html:520 +msgid "Loading..." +msgstr "Bezig met laden..." + +#: templates/assets/base_assets_index.html:194 +#, python-format +msgid "" +"\n" +" %(counter)s asset sélectionné\n" +" " +msgid_plural "" +"\n" +" %(counter)s assets sélectionnés\n" +" " +msgstr[0] "" +"\n" +"%(counter)s geselecteerde asset" +msgstr[1] "" +"\n" +"%(counter)s geselecteerde assets" + +#: templates/assets/base_assets_index.html:205 +#: templates/assets/unified_index.html:484 +msgid "Action" +msgstr "Actie" + +#: templates/assets/base_assets_index.html:211 +#: templates/assets/base_assets_index.html:321 +#: templates/assets/unified_index.html:490 +#: templates/assets/unified_index.html:603 +msgid "Exporter" +msgstr "Exporteren" + +#: templates/assets/base_assets_index.html:261 +msgid "Carte" +msgstr "Kaart" + +#: templates/assets/base_assets_index.html:269 +msgid "Filtrer automatiquement sur la zone visible" +msgstr "Automatisch filteren op het zichtbare gebied" + +#: templates/assets/base_assets_index.html:280 +#: templates/assets/unified_index.html:566 +msgid "Totaux des assets" +msgstr "Totalen van assets" + +#: templates/assets/base_assets_index.html:282 +#: templates/assets/base_assets_index.html:305 +#: templates/assets/components/add_asset_modal.html:19 +#: templates/assets/components/asset_documents.html:37 +#: templates/assets/components/asset_location_link.html:50 +#: templates/assets/components/asset_model_card.html:62 +#: templates/assets/components/location_edit_modals.html:24 +#: templates/assets/components/location_edit_modals.html:56 +#: templates/assets/components/location_edit_modals.html:82 +#: templates/assets/controlcenters_location_detail.html:112 +#: templates/assets/controlcenters_location_detail.html:145 +#: templates/assets/controlcenters_location_detail.html:170 +#: templates/assets/its_location_detail.html:55 +#: templates/assets/its_location_detail.html:88 +#: templates/assets/naturerwiz_detail.html:273 +#: templates/assets/naturerwiz_detail.html:306 +#: templates/assets/publiclighting_street_detail.html:161 +#: templates/assets/publiclighting_street_detail.html:194 +#: templates/assets/publiclighting_street_detail.html:219 +#: templates/assets/roads_street_detail.html:160 +#: templates/assets/roads_street_detail.html:193 +#: templates/assets/roads_street_detail.html:218 +#: templates/assets/sign_street_detail.html:160 +#: templates/assets/sign_street_detail.html:193 +#: templates/assets/sign_street_detail.html:218 +#: templates/assets/structures_location_detail.html:190 +#: templates/assets/structures_location_detail.html:232 +#: templates/assets/structures_location_detail.html:257 +#: templates/assets/trafficlights_import_cross_plan.html:802 +#: templates/assets/trafficlights_intersection_detail.html:248 +#: templates/assets/trafficlights_intersection_detail.html:281 +#: templates/assets/trafficlights_intersection_detail.html:306 +#: templates/assets/trafficlights_intersection_detail.html:1109 +#: templates/assets/trafficlights_intersection_detail.html:1158 +#: templates/assets/trafficlights_intersection_detail.html:1234 +#: templates/assets/unified_index.html:84 +#: templates/assets/unified_index.html:241 +#: templates/assets/unified_index.html:283 +#: templates/assets/unified_index.html:343 +#: templates/assets/unified_index.html:568 +#: templates/assets/unified_index.html:587 +msgid "Fermer" +msgstr "Sluiten" + +#: templates/assets/base_assets_index.html:293 +#: templates/assets/unified_index.html:575 +msgid "Totaux" +msgstr "Totalen" + +#: templates/assets/base_assets_index.html:304 +#: templates/assets/unified_index.html:586 +msgid "Exporter les assets sélectionnés" +msgstr "Geselecteerde assets exporteren" + +#: templates/assets/base_assets_index.html:308 +#: templates/assets/unified_index.html:590 +msgid "Choisissez le format d'export :" +msgstr "Kies exportformaat:" + +#: templates/assets/base_assets_index.html:320 +#: templates/assets/components/add_asset_modal.html:138 +#: templates/assets/components/asset_documents.html:87 +#: templates/assets/components/asset_location_link.html:75 +#: templates/assets/components/asset_model_card.html:98 +#: templates/assets/components/location_edit_modals.html:38 +#: templates/assets/components/location_edit_modals.html:63 +#: templates/assets/components/location_edit_modals.html:96 +#: templates/assets/controlcenters_location_detail.html:156 +#: templates/assets/detail_generic.html:150 +#: templates/assets/detail_generic.html:194 +#: templates/assets/detail_generic.html:759 +#: templates/assets/its_location_detail.html:99 +#: templates/assets/permissions/access_edit.html:121 +#: templates/assets/permissions/type_access_edit.html:126 +#: templates/assets/publiclighting_street_detail.html:205 +#: templates/assets/roads_street_detail.html:204 +#: templates/assets/sign_street_detail.html:204 +#: templates/assets/structures_location_detail.html:243 +#: templates/assets/trafficlights_import_dxf.html:126 +#: templates/assets/trafficlights_intersection_detail.html:292 +#: templates/assets/trafficlights_intersection_detail.html:1003 +#: templates/assets/unified_index.html:228 +#: templates/assets/unified_index.html:327 +#: templates/assets/unified_index.html:359 +#: templates/assets/unified_index.html:602 +#: templates/assets/unified_index.html:631 +#: templates/assets/unified_index.html:661 +msgid "Annuler" +msgstr "Annuleren" + +#: templates/assets/clean_index.html:18 templates/assets/clean_index.html:73 +msgid "Category" +msgstr "Categorie" + +#: templates/assets/clean_index.html:38 +#: templates/assets/clean_location_detail.html:161 +msgid "Status" +msgstr "Status" + +#: templates/assets/clean_index.html:40 +msgid "To be approved" +msgstr "Te goedkeuren" + +#: templates/assets/clean_index.html:41 +#: templates/assets/clean_location_detail.html:53 +msgid "Active" +msgstr "Actief" + +#: templates/assets/clean_index.html:42 +msgid "Damaged" +msgstr "Beschadigd" + +#: templates/assets/clean_index.html:43 +msgid "To replace" +msgstr "Te vervangen" + +#: templates/assets/clean_index.html:44 +msgid "Removed" +msgstr "Verwijderd" + +#: templates/assets/clean_index.html:48 +msgid "Filter" +msgstr "Filteren" + +#: templates/assets/clean_index.html:49 +msgid "Reset" +msgstr "Resetten" + +#: templates/assets/clean_index.html:66 templates/assets/clean_index.html:72 +msgid "Name" +msgstr "Naam" + +#: templates/assets/clean_location_detail.html:19 +msgid "Municipality:" +msgstr "Gemeente:" + +#: templates/assets/clean_location_detail.html:22 +msgid "Road code:" +msgstr "Wegcode:" + +#: templates/assets/clean_location_detail.html:25 +#: templates/assets/roads_street_detail.html:20 +msgid "Section:" +msgstr "Sectie:" + +#: templates/assets/clean_location_detail.html:28 +msgid "Street level:" +msgstr "Straatniveau:" + +#: templates/assets/clean_location_detail.html:35 +#: templates/assets/controlcenters_location_detail.html:34 +#: templates/assets/its_location_detail.html:30 +#: templates/assets/nature_locations_detail.html:20 +#: templates/assets/naturerwiz_detail.html:20 +#: templates/assets/publiclighting_street_detail.html:18 +#: templates/assets/roads_street_detail.html:27 +#: templates/assets/sign_street_detail.html:18 +#: templates/assets/structures_location_detail.html:35 +#: templates/assets/trafficlights_intersection_detail.html:82 +msgid "Street View" +msgstr "Street View" + +#: templates/assets/clean_location_detail.html:36 +#: templates/assets/controlcenters_location_detail.html:35 +#: templates/assets/its_location_detail.html:31 +#: templates/assets/nature_locations_detail.html:21 +#: templates/assets/naturerwiz_detail.html:21 +#: templates/assets/publiclighting_street_detail.html:19 +#: templates/assets/roads_street_detail.html:28 +#: templates/assets/sign_street_detail.html:19 +#: templates/assets/structures_location_detail.html:36 +#: templates/assets/trafficlights_intersection_detail.html:83 +msgid "View in Google Street View" +msgstr "Bekijk in Google Street View" + +#: templates/assets/clean_location_detail.html:45 +#: templates/assets/controlcenters_location_detail.html:186 +#: templates/assets/its_location_detail.html:110 +#: templates/assets/naturerwiz_detail.html:180 +#: templates/assets/publiclighting_street_detail.html:233 +#: templates/assets/roads_street_detail.html:232 +#: templates/assets/sign_street_detail.html:232 +#: templates/assets/structures_location_detail.html:273 +#: templates/assets/trafficlights_intersection_detail.html:324 +#: templates/assets/unified_index.html:448 views/artworks.py:95 +#: views/clean.py:260 views/controlcenters.py:108 +msgid "Assets" +msgstr "Assets" + +#: templates/assets/clean_location_detail.html:60 +msgid "All" +msgstr "Alle" + +#: templates/assets/clean_location_detail.html:82 +#: templates/assets/clean_location_detail.html:119 +msgid "No litter bins" +msgstr "Geen vuilnisbakken" + +#: templates/assets/clean_location_detail.html:99 +#: templates/assets/clean_location_detail.html:133 +msgid "No glass containers" +msgstr "Geen glascontainers" + +#: templates/assets/clean_location_detail.html:147 +#: templates/assets/controlcenters_location_detail.html:231 +#: templates/assets/its_location_detail.html:155 +#: templates/assets/nature_locations_detail.html:61 +#: templates/assets/naturerwiz_detail.html:230 +#: templates/assets/publiclighting_street_detail.html:290 +#: templates/assets/roads_street_detail.html:305 +#: templates/assets/sign_street_detail.html:282 +#: templates/assets/structures_location_detail.html:340 +#: templates/assets/trafficlights_intersection_detail.html:921 +msgid "Interventions" +msgstr "Interventies" + +#: templates/assets/clean_location_detail.html:159 +msgid "Title" +msgstr "Titel" + +#: templates/assets/clean_location_detail.html:160 +msgid "Start" +msgstr "Begin" + +#: templates/assets/clean_location_detail.html:168 +msgid "Detail" +msgstr "Detail" + +#: templates/assets/clean_location_detail.html:179 +msgid "No interventions" +msgstr "Geen interventies" + +#: templates/assets/cleanlitterbin_detail.html:18 +msgid "Observations" +msgstr "Observaties" + +#: templates/assets/cleanlitterbin_detail.html:23 +#: templates/assets/components/asset_documents.html:14 +#: templates/assets/controlcenters_location_detail.html:46 +#: templates/assets/controlcenters_location_detail.html:129 +#: templates/assets/its_location_detail.html:41 +#: templates/assets/its_location_detail.html:72 +#: templates/assets/naturerwiz_detail.html:82 +#: templates/assets/naturerwiz_detail.html:290 +#: templates/assets/publiclighting_street_detail.html:29 +#: templates/assets/publiclighting_street_detail.html:178 +#: templates/assets/roads_street_detail.html:38 +#: templates/assets/roads_street_detail.html:177 +#: templates/assets/sign_street_detail.html:29 +#: templates/assets/sign_street_detail.html:177 +#: templates/assets/structures_location_detail.html:47 +#: templates/assets/structures_location_detail.html:207 +#: templates/assets/trafficlights_intersection_detail.html:157 +#: templates/assets/trafficlights_intersection_detail.html:265 +#: templates/assets/trafficlights_intersection_detail.html:329 +#: templates/assets/trafficlights_intersection_detail.html:1226 +msgid "Ajouter" +msgstr "Toevoegen" + +#: templates/assets/cleanlitterbin_detail.html:31 +msgid "Aucune observation enregistrée." +msgstr "Geen observatie geregistreerd." + +#: templates/assets/cleanlitterbin_detail.html:42 +msgid "Date inconnue" +msgstr "Onbekende datum" + +#: templates/assets/cleanlitterbin_detail.html:65 +#: templates/assets/cleanlitterbin_detail.html:221 +#: templates/assets/cleanlitterbin_detail.html:362 +msgid "Sol" +msgstr "Grond" + +#: templates/assets/cleanlitterbin_detail.html:70 +#: templates/assets/cleanlitterbin_detail.html:223 +#: templates/assets/cleanlitterbin_detail.html:364 +msgid "Pente trop forte" +msgstr "Helling te steil" + +#: templates/assets/cleanlitterbin_detail.html:74 +#: templates/assets/cleanlitterbin_detail.html:224 +#: templates/assets/cleanlitterbin_detail.html:365 +msgid "Sol instable" +msgstr "Instabiele grond" + +#: templates/assets/cleanlitterbin_detail.html:78 +#: templates/assets/cleanlitterbin_detail.html:225 +#: templates/assets/cleanlitterbin_detail.html:366 +msgid "Surface réfléchissante" +msgstr "Reflecterend oppervlak" + +#: templates/assets/cleanlitterbin_detail.html:82 +#: templates/assets/cleanlitterbin_detail.html:226 +#: templates/assets/cleanlitterbin_detail.html:367 +msgid "Surface glissante" +msgstr "Glad oppervlak" + +#: templates/assets/cleanlitterbin_detail.html:86 +#: templates/assets/cleanlitterbin_detail.html:227 +#: templates/assets/cleanlitterbin_detail.html:368 +msgid "Fissures" +msgstr "Scheuren" + +#: templates/assets/cleanlitterbin_detail.html:90 +#: templates/assets/cleanlitterbin_detail.html:228 +#: templates/assets/cleanlitterbin_detail.html:369 +msgid "Entrave" +msgstr "Obstakel" + +#: templates/assets/cleanlitterbin_detail.html:94 +#: templates/assets/cleanlitterbin_detail.html:229 +#: templates/assets/cleanlitterbin_detail.html:370 +msgid "Dalles podotactiles" +msgstr "Geleidetegels" + +#: templates/assets/cleanlitterbin_detail.html:99 +msgid "Pente transversale" +msgstr "Dwarshelling" + +#: templates/assets/cleanlitterbin_detail.html:103 +msgid "Pente longitudinale" +msgstr "Langshelling" + +#: templates/assets/cleanlitterbin_detail.html:108 +#: templates/assets/cleanlitterbin_detail.html:194 +msgid "Remarque" +msgstr "Opmerking" + +#: templates/assets/cleanlitterbin_detail.html:118 +#: templates/assets/cleanlitterbin_detail.html:244 +#: templates/assets/cleanlitterbin_detail.html:376 +msgid "Accessibilité" +msgstr "Bereikbaarheid" + +#: templates/assets/cleanlitterbin_detail.html:122 +#: templates/assets/cleanlitterbin_detail.html:247 +#: templates/assets/cleanlitterbin_detail.html:378 +msgid "Type de quartier" +msgstr "Type buurt" + +#: templates/assets/cleanlitterbin_detail.html:126 +#: templates/assets/cleanlitterbin_detail.html:257 +#: templates/assets/cleanlitterbin_detail.html:379 +msgid "Densité flux piéton" +msgstr "Voetgangersstroomdichtheid" + +#: templates/assets/cleanlitterbin_detail.html:130 +#: templates/assets/cleanlitterbin_detail.html:283 +#: templates/assets/cleanlitterbin_detail.html:382 +msgid "Approche 2 m dégagée" +msgstr "Vrije toegang van 2 m" + +#: templates/assets/cleanlitterbin_detail.html:134 +#: templates/assets/cleanlitterbin_detail.html:284 +#: templates/assets/cleanlitterbin_detail.html:383 +msgid "Voirie à traverser" +msgstr "Over te steken weg" + +#: templates/assets/cleanlitterbin_detail.html:138 +#: templates/assets/cleanlitterbin_detail.html:285 +#: templates/assets/cleanlitterbin_detail.html:384 +msgid "Poubelle isolée" +msgstr "Geïsoleerde vuilnisbak" + +#: templates/assets/cleanlitterbin_detail.html:142 +#: templates/assets/cleanlitterbin_detail.html:286 +#: templates/assets/cleanlitterbin_detail.html:385 +msgid "Gêne flux routier" +msgstr "Hindert wegverkeer" + +#: templates/assets/cleanlitterbin_detail.html:146 +#: templates/assets/cleanlitterbin_detail.html:287 +#: templates/assets/cleanlitterbin_detail.html:386 +msgid "Gêne flux piéton" +msgstr "Hindert voetgangersstroom" + +#: templates/assets/cleanlitterbin_detail.html:150 +#: templates/assets/cleanlitterbin_detail.html:288 +#: templates/assets/cleanlitterbin_detail.html:387 +msgid "Itinéraire logique" +msgstr "Logische route" + +#: templates/assets/cleanlitterbin_detail.html:154 +#: templates/assets/cleanlitterbin_detail.html:289 +#: templates/assets/cleanlitterbin_detail.html:388 +msgid "Contraste visuel" +msgstr "Visueel contrast" + +#: templates/assets/cleanlitterbin_detail.html:158 +#: templates/assets/cleanlitterbin_detail.html:290 +#: templates/assets/cleanlitterbin_detail.html:389 +msgid "Indicateurs de tri" +msgstr "Sorteerindicatoren" + +#: templates/assets/cleanlitterbin_detail.html:162 +#: templates/assets/cleanlitterbin_detail.html:266 +#: templates/assets/cleanlitterbin_detail.html:380 +msgid "Accès agent (véhicule)" +msgstr "Toegang medewerker (voertuig)" + +#: templates/assets/cleanlitterbin_detail.html:172 +#: templates/assets/cleanlitterbin_detail.html:275 +#: templates/assets/cleanlitterbin_detail.html:381 +msgid "Accès agent (pied)" +msgstr "Toegang medewerker (te voet)" + +#: templates/assets/cleanlitterbin_detail.html:183 +#: templates/assets/cleanlitterbin_detail.html:292 +#: templates/assets/cleanlitterbin_detail.html:391 +msgid "Points d'intérêt" +msgstr "Nuttige plaatsen (POI)" + +#: templates/assets/cleanlitterbin_detail.html:208 +#: templates/assets/cleanlitterbin_detail.html:350 +msgid "Général" +msgstr "Algemeen" + +#: templates/assets/cleanlitterbin_detail.html:211 +#: templates/assets/cleanlitterbin_detail.html:353 +msgid "Date / heure" +msgstr "Datum / tijd" + +#: templates/assets/cleanlitterbin_detail.html:216 +#: templates/assets/cleanlitterbin_detail.html:357 views/export.py:108 +msgid "Note" +msgstr "Opmerking" + +#: templates/assets/cleanlitterbin_detail.html:231 +#: templates/assets/cleanlitterbin_detail.html:371 +#, python-format +msgid "Pente transversale (%%)" +msgstr "Dwarshelling (%%)" + +#: templates/assets/cleanlitterbin_detail.html:235 +#: templates/assets/cleanlitterbin_detail.html:372 +#, python-format +msgid "Pente longitudinale (%%)" +msgstr "Langshelling (%%)" + +#: templates/assets/cleanlitterbin_detail.html:239 +#: templates/assets/cleanlitterbin_detail.html:373 +msgid "Remarque sol" +msgstr "Opmerking over de grond" + +#: templates/assets/cleanlitterbin_detail.html:308 +#: templates/assets/cleanlitterbin_detail.html:404 +msgid "Autre point d'intérêt" +msgstr "Ander nuttig punt" + +#: templates/assets/cleanlitterbin_detail.html:312 +#: templates/assets/cleanlitterbin_detail.html:405 +msgid "Remarque accessibilité" +msgstr "Opmerking over bereikbaarheid" + +#: templates/assets/cleanlitterbin_detail.html:319 +#: templates/assets/cleanlitterbin_detail.html:409 +msgid "Supprimer cette observation" +msgstr "Deze observatie verwijderen" + +#: templates/assets/components/add_asset_modal.html:17 +msgid "Ajouter un nouvel équipement" +msgstr "Nieuw asset toevoegen" + +#: templates/assets/components/add_asset_modal.html:37 +#: templates/assets/unified_index.html:293 +msgid "Sélectionner une catégorie..." +msgstr "Selecteer een categorie..." + +#: templates/assets/components/add_asset_modal.html:47 +#: templates/assets/trafficlights_intersection_detail.html:988 +#, fuzzy +#| msgid "Type d'élément" +msgid "Type d'équipement" +msgstr "Type element" + +#: templates/assets/components/add_asset_modal.html:50 +#: templates/assets/trafficlights_intersection_detail.html:991 +#, fuzzy +#| msgid "Sélectionner un type" +msgid "Sélectionner un type..." +msgstr "Selecteer een type" + +#: templates/assets/components/add_asset_modal.html:60 +#: templates/assets/trafficlights_intersection_detail.html:998 +#: templates/assets/unified_index.html:299 +msgid "Sélectionner d'abord une catégorie..." +msgstr "Selecteer eerst een categorie..." + +#: templates/assets/components/add_asset_modal.html:63 +msgid "Le modèle détermine les caractéristiques de l'équipement." +msgstr "Het model bepaalt de kenmerken van het asset." + +#: templates/assets/components/add_asset_modal.html:73 +msgid "Ex: STR-001" +msgstr "Bijv: STR-001" + +#: templates/assets/components/add_asset_modal.html:75 +msgid "Code unique pour identifier l'équipement." +msgstr "Unieke code om het asset te identificeren." + +#: templates/assets/components/add_asset_modal.html:85 +msgid "Nom en français (optionnel)" +msgstr "Naam in het Frans (optioneel)" + +#: templates/assets/components/add_asset_modal.html:94 +msgid "Naam in het Nederlands (optioneel)" +msgstr "Naam in het Nederlands (optioneel)" + +#: templates/assets/components/add_asset_modal.html:104 +msgid "Aucune localisation (optionnel)" +msgstr "Geen locatie (optioneel)" + +#: templates/assets/components/add_asset_modal.html:110 +msgid "Associez l'équipement à une localisation spécifique." +msgstr "Koppel het asset aan een specifieke locatie." + +#: templates/assets/components/add_asset_modal.html:119 +#: templates/assets/structures_index.html:74 +msgid "Ouvrage" +msgstr "Kunstwerk" + +#: templates/assets/components/add_asset_modal.html:122 +msgid "Aucun ouvrage (optionnel)" +msgstr "Geen kunstwerk (optioneel)" + +#: templates/assets/components/add_asset_modal.html:132 +msgid "L'équipement sera créé avec le statut 'À approuver'." +msgstr "Het asset wordt aangemaakt met de status 'Te goedkeuren'." + +#: templates/assets/components/add_asset_modal.html:141 +msgid "Créer l'équipement" +msgstr "Asset aanmaken" + +#: templates/assets/components/asset_children_tree.html:25 +#: templates/assets/detail_generic.html:792 +#: templates/assets/detail_generic.html:802 +#: templates/assets/detail_generic.html:1390 +msgid "Lier un asset existant" +msgstr "Een bestaand asset koppelen" + +#: templates/assets/components/asset_children_tree.html:36 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Changer le parent" +msgstr "Wijzig model" + +#: templates/assets/components/asset_children_tree.html:82 +#: templates/assets/components/asset_children_tree.html:96 +#: templates/assets/detail_generic.html:1357 +msgid "Délier cet asset" +msgstr "Dit asset ontkoppelen" + +#: templates/assets/components/asset_documents.html:6 +msgid "Documents associés" +msgstr "Monumenten" + +#: templates/assets/components/asset_documents.html:34 +#: templates/assets/controlcenters_location_detail.html:111 +#: templates/assets/its_location_detail.html:54 +#: templates/assets/naturerwiz_detail.html:272 +#: templates/assets/publiclighting_street_detail.html:160 +#: templates/assets/roads_street_detail.html:159 +#: templates/assets/sign_street_detail.html:159 +#: templates/assets/structures_location_detail.html:189 +#: templates/assets/trafficlights_intersection_detail.html:247 +msgid "Ajouter des documents" +msgstr "Documenten toevoegen" + +#: templates/assets/components/asset_documents.html:48 +msgid "Sélectionnez un ou plusieurs fichiers pour continuer." +msgstr "Selecteer één of meerdere bestanden om door te gaan." + +#: templates/assets/components/asset_documents.html:50 +msgid "Choisir des fichiers" +msgstr "Bestanden kiezen" + +#: templates/assets/components/asset_documents.html:59 +msgid "Changer les fichiers" +msgstr "Bestanden wijzigen" + +#: templates/assets/components/asset_documents.html:66 +msgid "Tag" +msgstr "Tag" + +#: templates/assets/components/asset_documents.html:78 +msgid "Aucun tag disponible pour cette thématique." +msgstr "Geen tags beschikbaar voor dit thema." + +#: templates/assets/components/asset_documents.html:89 +msgid "Uploader" +msgstr "Uploaden" + +#: templates/assets/components/asset_history.html:8 +msgid "Historique des modifications" +msgstr "Wijzigingsgeschiedenis" + +#: templates/assets/components/asset_history.html:12 +msgid "Tableau" +msgstr "Tabel" + +#: templates/assets/components/asset_history.html:17 +msgid "Ligne du temps" +msgstr "Tijdlijn" + +#: templates/assets/components/asset_history.html:28 +#: templates/assets/detail_generic.html:787 +#: templates/assets/unified_index.html:305 +msgid "Chargement..." +msgstr "Laden..." + +#: templates/assets/components/asset_history.html:37 +msgid "Date" +msgstr "Datum" + +#: templates/assets/components/asset_history.html:39 +msgid "Champ" +msgstr "Veld" + +#: templates/assets/components/asset_history.html:50 +#: templates/assets/components/asset_history.html:67 +msgid "Aucune modification enregistrée pour cet asset." +msgstr "Geen wijzigingen geregistreerd voor dit asset." + +#: templates/assets/components/asset_history.html:211 +msgid "Authentification requise" +msgstr "Authenticatie vereist" + +#: templates/assets/components/asset_history.html:213 +msgid "Vous n'avez pas accès à cet historique" +msgstr "U heeft geen toegang tot deze geschiedenis" + +#: templates/assets/components/asset_history.html:214 +#: templates/assets/components/asset_history.html:237 +#: templates/assets/components/asset_history.html:242 +msgid "Erreur lors du chargement de l'historique" +msgstr "Fout bij het laden van de geschiedenis" + +#: templates/assets/components/asset_history.html:331 +msgid "Asset créé" +msgstr "Asset aangemaakt" + +#: templates/assets/components/asset_location_link.html:8 +msgid "Rue associée" +msgstr "Bijbehorende straat" + +#: templates/assets/components/asset_location_link.html:10 +msgid "Carrefour associé" +msgstr "Bijbehorend kruispunt" + +#: templates/assets/components/asset_location_link.html:12 +msgid "Localisation associée" +msgstr "Bijbehorende locatie" + +#: templates/assets/components/asset_location_link.html:17 +#: templates/assets/components/asset_model_card.html:9 +msgid "Changer" +msgstr "Wijzigen" + +#: templates/assets/components/asset_location_link.html:26 +msgid "Voir le détail de la localisation" +msgstr "Locatiedetails bekijken" + +#: templates/assets/components/asset_location_link.html:35 +msgid "Aucune localisation associée" +msgstr "Geen gekoppelde locatie" + +#: templates/assets/components/asset_location_link.html:48 +msgid "Changer la localisation" +msgstr "Wijzig locatie" + +#: templates/assets/components/asset_location_link.html:55 +msgid "Localisation actuelle :" +msgstr "Huidige locatie:" + +#: templates/assets/components/asset_location_link.html:59 +msgid "Recherche" +msgstr "Zoeken" + +#: templates/assets/components/asset_location_link.html:62 +msgid "Code ou nom de localisation…" +msgstr "Locatiecode of -naam..." + +#: templates/assets/components/asset_location_link.html:67 +msgid "Nouvelle localisation" +msgstr "Nieuwe locatie" + +#: templates/assets/components/asset_location_link.html:70 +msgid "Sélectionner une localisation..." +msgstr "Selecteer een locatie" + +#: templates/assets/components/asset_location_link.html:77 +#: templates/assets/components/asset_model_card.html:101 +msgid "Appliquer" +msgstr "Toepassen" + +#: templates/assets/components/asset_model_card.html:38 +msgid "Description" +msgstr "Beschrijving" + +#: templates/assets/components/asset_model_card.html:60 +msgid "Changer le modèle" +msgstr "Wijzig model" + +#: templates/assets/components/asset_model_card.html:69 +msgid "Modèle actuel :" +msgstr "Huidig model:" + +#: templates/assets/components/asset_model_card.html:77 +msgid "Toutes les catégories" +msgstr "Alle categorieën" + +#: templates/assets/components/asset_model_card.html:86 +msgid "Nouveau modèle" +msgstr "Nieuw model" + +#: templates/assets/components/asset_model_card.html:89 +msgid "Sélectionner un modèle..." +msgstr "Selecteer een model" + +#: templates/assets/components/location_edit_modals.html:22 +#, python-format +msgid "Modifier %(label)s" +msgstr "%(label)s bewerken" + +#: templates/assets/components/location_edit_modals.html:40 +#: templates/assets/permissions/access_edit.html:117 +#: templates/assets/permissions/panel.html:278 +#: templates/assets/permissions/type_access_edit.html:122 +#: templates/assets/permissions/type_page.html:215 +#: templates/assets/permissions/type_page.html:419 +#: templates/assets/unified_index.html:632 +#: templates/assets/unified_index.html:662 +msgid "Enregistrer" +msgstr "Opslaan" + +#: templates/assets/components/location_edit_modals.html:54 +#: templates/assets/controlcenters_location_detail.html:144 +#: templates/assets/its_location_detail.html:87 +#: templates/assets/publiclighting_street_detail.html:193 +#: templates/assets/roads_street_detail.html:192 +#: templates/assets/sign_street_detail.html:192 +#: templates/assets/structures_location_detail.html:231 +#: templates/assets/trafficlights_intersection_detail.html:280 +msgid "Confirmer la suppression" +msgstr "Verwijdering bevestigen" + +#: templates/assets/components/location_edit_modals.html:59 +#, python-format +msgid "Êtes-vous sûr de vouloir supprimer cette localisation (%(label)s) ?" +msgstr "Weet u zeker dat u deze locatie (%(label)s) wilt verwijderen?" + +#: templates/assets/components/location_edit_modals.html:60 +msgid "" +"Cette action est irréversible. Tous les assets associés seront également " +"affectés." +msgstr "" + +#: templates/assets/components/location_edit_modals.html:65 +#: templates/assets/controlcenters_location_detail.html:95 +#: templates/assets/controlcenters_location_detail.html:157 +#: templates/assets/document_version_history_list.html:59 +#: templates/assets/its_location_detail.html:100 +#: templates/assets/permissions/panel.html:70 +#: templates/assets/permissions/panel.html:112 +#: templates/assets/permissions/panel.html:215 +#: templates/assets/permissions/type_page.html:72 +#: templates/assets/permissions/type_page.html:161 +#: templates/assets/permissions/type_page.html:239 +#: templates/assets/permissions/type_page.html:364 +#: templates/assets/publiclighting_street_detail.html:88 +#: templates/assets/publiclighting_street_detail.html:143 +#: templates/assets/publiclighting_street_detail.html:206 +#: templates/assets/roads_street_detail.html:92 +#: templates/assets/roads_street_detail.html:142 +#: templates/assets/roads_street_detail.html:205 +#: templates/assets/sign_street_detail.html:87 +#: templates/assets/sign_street_detail.html:142 +#: templates/assets/sign_street_detail.html:205 +#: templates/assets/structures_location_detail.html:244 +#: templates/assets/trafficlights_intersection_detail.html:293 +msgid "Supprimer" +msgstr "Verwijderen" + +# , python-format +#: templates/assets/components/location_edit_modals.html:80 +#, python-format +msgid "Créer une nouvelle localisation (%(label)s)" +msgstr "Nieuwe locatie aanmaken (%(label)s)" + +#: templates/assets/components/location_edit_modals.html:98 +#: templates/assets/trafficlights_intersection_detail.html:1005 +msgid "Créer" +msgstr "Aanmaken" + +#: templates/assets/components/location_edit_modals.html:179 +msgid "Cliquez pour fermer le polygone" +msgstr "Klik om de polygoon te sluiten" + +#: templates/assets/components/location_edit_modals.html:207 +msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." +msgstr "Polygoon opgeslagen. Klik op \"Tekenen\" om deze opnieuw te tekenen." + +#: templates/assets/components/location_edit_modals.html:286 +msgid "Sommets" +msgstr "Hoekpunten" + +#: templates/assets/components/location_edit_modals.html:286 +msgid "Cliquez sur le premier sommet ou « Terminer » pour clore." +msgstr "Klik op het eerste hoekpunt of op \"Voltooien\" om te sluiten." + +#: templates/assets/components/location_edit_modals.html:301 +msgid "Cliquez sur la carte pour ajouter des sommets." +msgstr "Klik op de kaart om hoekpunten toe te voegen." + +#: templates/assets/components/location_edit_modals.html:321 +msgid "Polygone effacé. Cliquez sur « Dessiner » pour en tracer un nouveau." +msgstr "Polygoon gewist. Klik op \"Tekenen\" om een nieuwe te tekenen." + +#: templates/assets/components/location_edit_modals.html:436 +#: templates/assets/components/location_edit_modals.html:515 +#: templates/assets/detail_generic.html:1191 +msgid "Erreur lors du chargement du formulaire." +msgstr "Fout bij het laden van het formulier." + +#: templates/assets/components/location_edit_modals.html:460 +msgid "Erreur lors de la sauvegarde." +msgstr "Fout bij het opslaan." + +#: templates/assets/components/location_edit_modals.html:463 +#: templates/assets/components/location_edit_modals.html:544 +msgid "Erreur réseau." +msgstr "Netwerkfout." + +#: templates/assets/components/location_edit_modals.html:541 +msgid "Erreur lors de la création." +msgstr "Fout bij het aanmaken." + +#: templates/assets/components/map_location_picker.html:23 +msgid "Position géographique" +msgstr "Geografische positie" + +#: templates/assets/components/map_location_picker.html:24 +#: templates/assets/components/map_polygon_picker.html:26 +msgid "facultatif" +msgstr "optioneel" + +#: templates/assets/components/map_location_picker.html:29 +msgid "Utiliser ma position" +msgstr "Mijn locatie gebruiken" + +#: templates/assets/components/map_location_picker.html:34 +msgid "Effacer le marqueur" +msgstr "Markering wissen" + +#: templates/assets/components/map_location_picker.html:45 +msgid "" +"Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour " +"l'ajuster." +msgstr "" + +#: templates/assets/components/map_polygon_picker.html:25 +msgid "Géométrie (polygone)" +msgstr "Geometrie (polygoon)" + +#: templates/assets/components/map_polygon_picker.html:31 +msgid "Dessiner un polygone" +msgstr "Polygoon tekenen" + +#: templates/assets/components/map_polygon_picker.html:32 +msgid "Dessiner" +msgstr "Tekenen" + +#: templates/assets/components/map_polygon_picker.html:36 +msgid "Terminer le polygone" +msgstr "Polygoon voltooien" + +#: templates/assets/components/map_polygon_picker.html:37 +msgid "Terminer" +msgstr "Voltooien" + +#: templates/assets/components/map_polygon_picker.html:41 +msgid "Effacer le polygone" +msgstr "Polygoon wissen" + +#: templates/assets/components/map_polygon_picker.html:51 +msgid "" +"Cliquez sur « Dessiner » pour commencer à tracer un polygone sur la carte." +msgstr "" + +#: templates/assets/controlcenters_index.html:8 +#: templates/assets/its_index.html:8 templates/assets/structures_index.html:18 +msgid "Type de Localisation" +msgstr "Type locatie" + +#: templates/assets/controlcenters_index.html:10 +#: templates/assets/its_index.html:10 templates/assets/structures_index.html:20 +#: templates/assets/unified_index.html:117 +msgid "Toutes" +msgstr "Alle" + +#: templates/assets/controlcenters_index.html:11 +#: templates/assets/its_index.html:11 templates/assets/structures_index.html:21 +msgid "Principale" +msgstr "Hoofdlocatie" + +#: templates/assets/controlcenters_index.html:12 +#: templates/assets/its_index.html:12 templates/assets/structures_index.html:22 +msgid "Secondaire" +msgstr "Nevenlocatie" + +#: templates/assets/controlcenters_index.html:61 +msgid "Centre de contrôle" +msgstr "Controlecentrum" + +#: templates/assets/controlcenters_location_detail.html:15 +msgid "Centre de contrôle:" +msgstr "Controlecentrum" + +#: templates/assets/controlcenters_location_detail.html:18 +#: templates/assets/its_location_detail.html:19 +msgid "Localisation principale:" +msgstr "Hoofdlocatie:" + +#: templates/assets/controlcenters_location_detail.html:26 +msgid "Parent:" +msgstr "Bovenliggende locatie:" + +#: templates/assets/controlcenters_location_detail.html:45 +#: templates/assets/its_location_detail.html:40 +#: templates/assets/nature_locations_detail.html:37 +#: templates/assets/naturerwiz_detail.html:80 +#: templates/assets/publiclighting_street_detail.html:28 +#: templates/assets/roads_street_detail.html:37 +#: templates/assets/sign_street_detail.html:28 +#: templates/assets/structures_location_detail.html:46 +#: templates/assets/trafficlights_intersection_detail.html:156 +msgid "Documents" +msgstr "Documenten" + +#: templates/assets/controlcenters_location_detail.html:86 +#: templates/assets/detail_generic.html:134 +#: templates/assets/detail_generic.html:135 +#: templates/assets/detail_generic.html:178 +#: templates/assets/detail_generic.html:179 +#: templates/assets/publiclighting_street_detail.html:78 +#: templates/assets/publiclighting_street_detail.html:133 +#: templates/assets/sign_street_detail.html:78 +#: templates/assets/sign_street_detail.html:132 +#: templates/assets/trafficlights_import_cross_plan.html:274 +#: templates/assets/trafficlights_import_cross_plan.html:358 +#: templates/assets/trafficlights_import_cross_plan.html:442 +#: templates/assets/trafficlights_import_cross_plan.html:532 +msgid "Archiver" +msgstr "Archiveren" + +#: templates/assets/controlcenters_location_detail.html:119 +#: templates/assets/its_location_detail.html:62 +#: templates/assets/naturerwiz_detail.html:280 +#: templates/assets/publiclighting_street_detail.html:168 +#: templates/assets/roads_street_detail.html:167 +#: templates/assets/sign_street_detail.html:167 +#: templates/assets/structures_location_detail.html:197 +#: templates/assets/trafficlights_intersection_detail.html:255 +msgid "Sélectionner des fichiers" +msgstr "Bestanden selecteren" + +#: templates/assets/controlcenters_location_detail.html:149 +#: templates/assets/its_location_detail.html:92 +#: templates/assets/publiclighting_street_detail.html:198 +#: templates/assets/roads_street_detail.html:197 +#: templates/assets/sign_street_detail.html:197 +#: templates/assets/structures_location_detail.html:236 +#: templates/assets/trafficlights_intersection_detail.html:285 +msgid "Voulez-vous vraiment supprimer ce document ?" +msgstr "Wilt u dit document echt verwijderen?" + +#: templates/assets/controlcenters_location_detail.html:150 +#: templates/assets/its_location_detail.html:93 +#: templates/assets/publiclighting_street_detail.html:199 +#: templates/assets/roads_street_detail.html:198 +#: templates/assets/sign_street_detail.html:198 +#: templates/assets/structures_location_detail.html:237 +#: templates/assets/trafficlights_intersection_detail.html:286 +msgid "Nom du fichier" +msgstr "Bestandsnaam" + +#: templates/assets/controlcenters_location_detail.html:169 +#: templates/assets/naturerwiz_detail.html:305 +#: templates/assets/publiclighting_street_detail.html:218 +#: templates/assets/roads_street_detail.html:217 +#: templates/assets/sign_street_detail.html:217 +#: templates/assets/structures_location_detail.html:256 +#: templates/assets/trafficlights_intersection_detail.html:305 +msgid "Historique des documents" +msgstr "Documentgeschiedenis" + +#: templates/assets/controlcenters_location_detail.html:195 +#: templates/assets/its_location_detail.html:119 +#: templates/assets/naturerwiz_detail.html:195 +#: templates/assets/publiclighting_street_detail.html:238 +#: templates/assets/roads_street_detail.html:237 +#: templates/assets/sign_street_detail.html:237 +#: templates/assets/structures_location_detail.html:287 +msgid "Arborescence" +msgstr "Boomstructuur" + +#: templates/assets/controlcenters_location_detail.html:202 +#: templates/assets/its_location_detail.html:126 +#: templates/assets/publiclighting_street_detail.html:241 +#: templates/assets/roads_street_detail.html:240 +#: templates/assets/sign_street_detail.html:240 +#: templates/assets/structures_location_detail.html:294 +msgid "Archives" +msgstr "Archief" + +#: templates/assets/controlcenters_location_detail.html:212 +#: templates/assets/controlcenters_location_tree.html:21 +#: templates/assets/its_location_detail.html:136 +#: templates/assets/its_location_tree.html:21 +#: templates/assets/structures_location_tree.html:21 +msgid "sous-localisations" +msgstr "sublocaties" + +#: templates/assets/controlcenters_location_detail.html:215 +#: templates/assets/its_location_detail.html:139 +#: templates/assets/its_location_tree.html:24 +#: templates/assets/naturerwiz_detail.html:206 +#: templates/assets/naturerwiz_location_tree.html:35 +msgid "assets" +msgstr "assets" + +#: templates/assets/controlcenters_location_detail.html:243 +#: templates/assets/its_location_detail.html:167 +#: templates/assets/nature_locations_detail.html:73 +#: templates/assets/naturerwiz_detail.html:242 +#: templates/assets/publiclighting_street_detail.html:302 +#: templates/assets/roads_street_detail.html:317 +#: templates/assets/sign_street_detail.html:294 +#: templates/assets/structures_location_detail.html:352 +#: templates/assets/trafficlights_intersection_detail.html:933 +msgid "Titre" +msgstr "Titel" + +#: templates/assets/controlcenters_location_detail.html:244 +#: templates/assets/its_location_detail.html:168 +#: templates/assets/nature_locations_detail.html:74 +#: templates/assets/naturerwiz_detail.html:243 +#: templates/assets/publiclighting_street_detail.html:303 +#: templates/assets/roads_street_detail.html:318 +#: templates/assets/sign_street_detail.html:295 +#: templates/assets/structures_location_detail.html:353 +#: templates/assets/trafficlights_intersection_detail.html:934 +msgid "Début" +msgstr "Start" + +#: templates/assets/detail_generic.html:73 +#: templates/assets/detail_generic.html:816 +#: templates/assets/detail_generic.html:833 +msgid "Retour" +msgstr "Terug" + +#: templates/assets/detail_generic.html:85 +msgid "Détails" +msgstr "Details" + +#: templates/assets/detail_generic.html:87 +msgid "StreetUp" +msgstr "StreetUp" + +#: templates/assets/detail_generic.html:118 +#: templates/assets/detail_generic.html:160 +#: templates/assets/unified_index.html:250 +msgid "Asset" +msgstr "Asset" + +#: templates/assets/detail_generic.html:144 +#: templates/assets/detail_generic.html:188 +msgid "Éditer" +msgstr "Bewerken" + +#: templates/assets/detail_generic.html:147 +#: templates/assets/detail_generic.html:191 +msgid "Sauvegarder" +msgstr "Opslaan" + +#: templates/assets/detail_generic.html:204 +msgid "Informations générales" +msgstr "Algemene informatie" + +#: templates/assets/detail_generic.html:277 +#: templates/assets/unified_index.html:212 +msgid "Statut de validation" +msgstr "Validatiestatus" + +#: templates/assets/detail_generic.html:326 +#: templates/assets/detail_generic.html:580 +msgid "Installé le" +msgstr "Geïnstalleerd op" + +#: templates/assets/detail_generic.html:339 +msgid "N° de série" +msgstr "Serienummer" + +#: templates/assets/detail_generic.html:370 +msgid "mois" +msgstr "maanden" + +#: templates/assets/detail_generic.html:375 +#, python-format +msgid "%(n)s mois restants" +msgstr "%(n)s maanden resterend" + +#: templates/assets/detail_generic.html:377 +msgid "Expire ce mois-ci" +msgstr "Verloopt deze maand" + +#: templates/assets/detail_generic.html:379 +#, python-format +msgid "Expirée (%(n)s mois)" +msgstr "Verlopen (%(n)s maanden)" + +#: templates/assets/detail_generic.html:417 +msgid "Attributs spécifiques" +msgstr "Specifieke kenmerken" + +#: templates/assets/detail_generic.html:432 +#: templates/assets/trafficlights_index.html:35 templatetags/asset_tags.py:129 +#: views/core.py:622 views/core.py:648 views/trafficlights.py:186 +msgid "Oui" +msgstr "Ja" + +#: templates/assets/detail_generic.html:433 +#: templates/assets/trafficlights_index.html:39 templatetags/asset_tags.py:129 +#: views/core.py:623 views/core.py:649 views/trafficlights.py:186 +msgid "Non" +msgstr "Nee" + +#: templates/assets/detail_generic.html:474 +msgid "Permissions spécifiques" +msgstr "Specifieke machtigingen" + +#: templates/assets/detail_generic.html:479 +msgid "Voir tous les accès pour ce type" +msgstr "Bekijk alle toegang voor dit type" + +#: templates/assets/detail_generic.html:481 +msgid "Vue par type" +msgstr "Weergave per type" + +#: templates/assets/detail_generic.html:492 +msgid "Chargement des permissions…" +msgstr "Machtigingen laden..." + +#: templates/assets/detail_generic.html:510 +msgid "Erreur lors du chargement." +msgstr "Fout bij het laden." + +#: templates/assets/detail_generic.html:522 +msgid "Assets liés" +msgstr "Gekoppelde assets" + +#: templates/assets/detail_generic.html:528 +msgid "Lier ou créer un asset" +msgstr "Een asset koppelen of aanmaken" + +#: templates/assets/detail_generic.html:529 +msgid "Lier / Créer" +msgstr "Koppelen / Aanmaken" + +#: templates/assets/detail_generic.html:535 +msgid "Aucun asset lié." +msgstr "Geen gekoppeld asset." + +#: templates/assets/detail_generic.html:547 +msgid "Historique des remplacements" +msgstr "Geschiedenis van vervangingen" + +#: templates/assets/detail_generic.html:581 +msgid "Désinstallé le" +msgstr "Verwijderd op" + +#: templates/assets/detail_generic.html:582 +msgid "Durée" +msgstr "Duur" + +#: templates/assets/detail_generic.html:618 +msgid "En service" +msgstr "In dienst" + +#: templates/assets/detail_generic.html:644 +msgid "Géolocalisation" +msgstr "Geolocatie" + +#: templates/assets/detail_generic.html:651 +msgid "Voir sur Street View" +msgstr "Bekijk in Street View" + +#: templates/assets/detail_generic.html:657 +msgid "Voir sur Mapillary" +msgstr "Bekijk op Mapillary" + +#: templates/assets/detail_generic.html:728 +msgid "Remplacer l'asset" +msgstr "Vervang het asset" + +#: templates/assets/detail_generic.html:733 +msgid "L'asset actuel sera archivé et un nouvel asset sera créé." +msgstr "" +"Het huidige asset wordt gearchiveerd en een nieuw asset wordt aangemaakt.Het " +"huidige asset wordt gearchiveerd en een nieuw asset wordt aangemaakt." + +#: templates/assets/detail_generic.html:734 +msgid "Les noms (FR/NL) de l'ancien asset seront automatiquement copiés." +msgstr "De namen (FR/NL) van het oude asset worden automatisch gekopieerd." + +#: templates/assets/detail_generic.html:738 +msgid "Modèle du nouvel asset" +msgstr "Model van het nieuwe asset" + +#: templates/assets/detail_generic.html:740 +msgid "Garder le même modèle" +msgstr "Houd hetzelfde model" + +#: templates/assets/detail_generic.html:749 +#: templates/assets/detail_generic.html:754 +msgid "optionnel" +msgstr "optioneel" + +#: templates/assets/detail_generic.html:777 +msgid "Lier / Créer un asset" +msgstr "Een asset koppelen / aanmaken" + +#: templates/assets/detail_generic.html:785 +msgid "Type d'asset à lier" +msgstr "Te koppelen assettype" + +#: templates/assets/detail_generic.html:795 +#: templates/assets/detail_generic.html:826 +msgid "Créer un nouvel asset" +msgstr "Nieuw asset aanmaken" + +#: templates/assets/detail_generic.html:804 +#: templates/assets/detail_generic.html:1080 +msgid "Rechercher l'asset à lier" +msgstr "Het te koppelen asset zoeken" + +#: templates/assets/detail_generic.html:806 +msgid "Code ou nom..." +msgstr "Code of naam..." + +#: templates/assets/detail_generic.html:819 +msgid "Lier" +msgstr "Koppelen" + +#: templates/assets/detail_generic.html:827 +msgid "Un nouvel asset sera créé et lié automatiquement à cet asset parent." +msgstr "" +"Een nieuw asset wordt aangemaakt en automatisch gekoppeld aan dit " +"bovenliggende asset." + +#: templates/assets/detail_generic.html:836 +msgid "Créer et lier" +msgstr "Aanmaken en koppelen" + +#: templates/assets/detail_generic.html:942 +#, fuzzy +#| msgid "Assets uniquement" +msgid "Assets enfants" +msgstr "Alleen assets" + +#: templates/assets/detail_generic.html:959 +#, fuzzy +#| msgid "Assets uniquement" +msgid "Assets parents" +msgstr "Alleen assets" + +#: templates/assets/detail_generic.html:1048 +#: templates/assets/detail_generic.html:1079 +#, fuzzy +#| msgid "Rechercher l'asset à lier" +msgid "Rechercher le parent à lier" +msgstr "Het te koppelen asset zoeken" + +#: templates/assets/detail_generic.html:1180 +msgid "Chargement du formulaire…" +msgstr "Formulier laden..." + +#: templates/assets/detail_generic.html:1198 +msgid "Aucun champ supplémentaire requis." +msgstr "Geen extra velden vereist." + +#: templates/assets/detail_generic.html:1223 +msgid "— Sélectionner —" +msgstr "— Selecteren —" + +#: templates/assets/detail_generic.html:1223 +msgid "— Aucun —" +msgstr "— Geen —" + +#: templates/assets/detail_generic.html:1513 +msgid "Champs obligatoires manquants" +msgstr "Verplichte velden ontbreken" + +#: templates/assets/detail_generic.html:1596 +#: templates/assets/detail_generic.html:1629 +msgid "Délier cet asset de son parent ?" +msgstr "Dit asset ontkoppelen van zijn bovenliggend asset?" + +#: templates/assets/document_history_list.html:19 +#: templates/assets/document_version_history_list.html:27 +msgid "Ajouté le" +msgstr "Toegevoegd op" + +#: templates/assets/document_history_list.html:21 +#: templates/assets/document_version_history_list.html:29 +msgid "par" +msgstr "door" + +#: templates/assets/document_history_list.html:42 +msgid "Aucun document trouvé." +msgstr "Geen document gevonden." + +#: templates/assets/document_version_history_list.html:37 +msgid "Approuvé" +msgstr "Goedgekeurd" + +#: templates/assets/document_version_history_list.html:39 +msgid "En attente" +msgstr "In afwachting" + +#: templates/assets/document_version_history_list.html:43 +msgid "Brouillon" +msgstr "Concept" + +#: templates/assets/document_version_history_list.html:50 +msgid "Télécharger" +msgstr "Downloaden" + +#: templates/assets/document_version_history_list.html:66 +msgid "Aucune version trouvée." +msgstr "Geen versie gevonden." + +#: templates/assets/its_index.html:27 +msgid "Créer une nouvelle localisation ITS" +msgstr "Nieuwe ITS-locatie aanmaken" + +#: templates/assets/its_index.html:66 +msgid "Créer un nouvel asset ITS" +msgstr "Nieuwe ITS-asset aanmaken" + +#: templates/assets/its_location_detail.html:22 +msgid "Localisation parente:" +msgstr "Bovenliggende locatie:" + +#: templates/assets/nature_index.html:8 templates/assets/water_index.html:7 +#: views/core.py:667 views/core.py:687 +msgid "Type de localisation" +msgstr "Locatietype" + +#: templates/assets/nature_index.html:13 +#: templates/assets/permissions/panel.html:147 +#: templates/assets/permissions/panel.html:228 +#: templates/assets/permissions/panel.html:254 +#: templates/assets/permissions/type_page.html:98 +#: templates/assets/permissions/type_page.html:171 +#: templates/assets/permissions/type_page.html:194 +#: templates/assets/permissions/type_page.html:282 +#: templates/assets/permissions/type_page.html:372 +#: templates/assets/permissions/type_page.html:395 +#: templates/assets/trafficlights_index.html:43 +#: templates/assets/trafficlights_index.html:51 +#: templates/assets/unified_index.html:93 +#: templates/assets/unified_index.html:131 templates/assets/water_index.html:12 +#: views/core.py:670 views/core.py:690 +msgid "Tous" +msgstr "Alle" + +#: templates/assets/nature_index.html:18 views/core.py:671 views/nature.py:104 +#: views/publiclighting.py:93 views/roads.py:99 views/sign.py:105 +msgid "Voiries" +msgstr "Wegen" + +#: templates/assets/nature_index.html:24 views/core.py:676 +msgid "Ring et autoroutes" +msgstr "Ring en autosnelwegen" + +#: templates/assets/nature_index.html:77 +msgid "Ring/Autoroute" +msgstr "Ring/Autosnelweg" + +#: templates/assets/nature_index.html:84 templates/assets/water_index.html:81 +msgid "Superficie" +msgstr "Oppervlakte" + +#: templates/assets/nature_locations_detail.html:14 +msgid "Zone:" +msgstr "Zone:" + +#: templates/assets/nature_locations_detail.html:15 +msgid "Autoroute:" +msgstr "Autosnelweg:" + +#: templates/assets/nature_locations_detail.html:28 +msgid "Nombre d'arbres:" +msgstr "Aantal bomen:" + +#: templates/assets/nature_locations_detail.html:29 +msgid "Superficie massif arboré:" +msgstr "Oppervlakte bomenmassief:" + +#: templates/assets/nature_locations_detail.html:30 +msgid "Superficie pré fleuri:" +msgstr "Oppervlakte bloemenweide:" + +#: templates/assets/naturerwiz_detail.html:116 +msgid "Documents de la zone" +msgstr "Documenten van de zone" + +#: templates/assets/naturerwiz_detail.html:124 +msgid "Documents des assets" +msgstr "Documenten van de assets" + +#: templates/assets/naturerwiz_detail.html:135 +#: templates/assets/structures_location_detail.html:54 +#: templates/assets/trafficlights_intersection_detail.html:165 +msgid "Répertoire principal" +msgstr "Hoofdmap" + +#: templates/assets/naturerwiz_detail.html:143 +#, fuzzy +#| msgid "Aucun document trouvé." +msgid "Aucun document de zone configuré." +msgstr "Geen document gevonden." + +#: templates/assets/naturerwiz_detail.html:169 +#, fuzzy +#| msgid "Aucun contrat associé." +msgid "Aucun document d'asset associé." +msgstr "Geen gekoppeld contract." + +#: templates/assets/naturerwiz_detail.html:211 +#: templates/assets/structures_location_detail.html:321 +msgid "Masquer les non-actifs" +msgstr "Niet-actieve verbergen" + +#: templates/assets/naturerwiz_detail.html:213 +#: templates/assets/structures_location_detail.html:323 +msgid "Afficher les non-actifs" +msgstr "Niet-actieve tonen" + +#: templates/assets/naturerwiz_detail.html:220 +#, fuzzy +#| msgid "Aucun asset lié." +msgid "Aucun asset associé." +msgstr "Geen gekoppeld asset." + +#: templates/assets/permissions/access_edit.html:10 +#: templates/assets/permissions/type_access_edit.html:10 +#: templates/assets/permissions/type_page.html:10 +msgid "Accueil" +msgstr "Home" + +#: templates/assets/permissions/access_edit.html:11 +msgid "Modifier un accès" +msgstr "Toegang bewerken" + +#: templates/assets/permissions/access_edit.html:18 +msgid "Modifier l'accès" +msgstr "Toegang bewerken" + +#: templates/assets/permissions/access_edit.html:29 +msgid "Type :" +msgstr "Type:" + +#: templates/assets/permissions/access_edit.html:31 +msgid "Objet #" +msgstr "Object #" + +#: templates/assets/permissions/access_edit.html:37 +#: templates/assets/permissions/panel.html:204 +#: templates/assets/permissions/type_access_edit.html:42 +#: templates/assets/permissions/type_page.html:150 +#: templates/assets/permissions/type_page.html:353 +msgid "Droits" +msgstr "Rechten" + +#: templates/assets/permissions/access_edit.html:41 +#: templates/assets/permissions/type_access_edit.html:46 +msgid "Peut visualiser" +msgstr "Kan bekijken" + +#: templates/assets/permissions/access_edit.html:46 +#: templates/assets/permissions/type_access_edit.html:51 +msgid "Peut modifier" +msgstr "Kan bewerken" + +#: templates/assets/permissions/access_edit.html:51 +#: templates/assets/permissions/type_access_edit.html:56 +msgid "Peut supprimer" +msgstr "Kan verwijderen" + +#: templates/assets/permissions/access_edit.html:61 +#: templates/assets/permissions/panel.html:113 +#: templates/assets/permissions/panel.html:224 +#: templates/assets/permissions/type_access_edit.html:66 +#: templates/assets/permissions/type_page.html:73 +#: templates/assets/permissions/type_page.html:169 +#: templates/assets/permissions/type_page.html:240 +#: templates/assets/permissions/type_page.html:393 +msgid "Champs visibles" +msgstr "Zichtbare velden" + +#: templates/assets/permissions/access_edit.html:62 +#: templates/assets/permissions/type_access_edit.html:67 +msgid "vide = tous les champs" +msgstr "leeg = alle velden" + +#: templates/assets/permissions/access_edit.html:65 +#: templates/assets/permissions/access_edit.html:93 +#: templates/assets/permissions/type_access_edit.html:70 +#: templates/assets/permissions/type_access_edit.html:98 +#: templates/assets/trafficlights_import_dxf.html:117 +msgid "Tout cocher" +msgstr "Alles aanvinken" + +#: templates/assets/permissions/access_edit.html:67 +#: templates/assets/permissions/access_edit.html:95 +#: templates/assets/permissions/type_access_edit.html:72 +#: templates/assets/permissions/type_access_edit.html:100 +#: templates/assets/trafficlights_import_dxf.html:118 +msgid "Tout décocher" +msgstr "Alles uitvinken" + +#: templates/assets/permissions/access_edit.html:80 +#: templates/assets/permissions/access_edit.html:108 +#: templates/assets/permissions/panel.html:242 +#: templates/assets/permissions/panel.html:268 +#: templates/assets/permissions/type_access_edit.html:85 +#: templates/assets/permissions/type_access_edit.html:113 +#: templates/assets/permissions/type_page.html:185 +#: templates/assets/permissions/type_page.html:208 +#: templates/assets/permissions/type_page.html:386 +#: templates/assets/permissions/type_page.html:409 +msgid "Aucun champ disponible" +msgstr "Geen velden beschikbaar" + +#: templates/assets/permissions/access_edit.html:89 +#: templates/assets/permissions/panel.html:114 +#: templates/assets/permissions/panel.html:250 +#: templates/assets/permissions/type_access_edit.html:94 +#: templates/assets/permissions/type_page.html:74 +#: templates/assets/permissions/type_page.html:192 +#: templates/assets/permissions/type_page.html:241 +#: templates/assets/permissions/type_page.html:370 +msgid "Champs modifiables" +msgstr "Bewerkbare velden" + +#: templates/assets/permissions/access_edit.html:90 +#: templates/assets/permissions/type_access_edit.html:95 +msgid "vide = droits par rôle" +msgstr "leeg = rechten per rol" + +#: templates/assets/permissions/panel.html:29 +msgid "Type d'objet :" +msgstr "Objecttype:" + +#: templates/assets/permissions/panel.html:38 +msgid "Droits explicites requis (droits thématiques insuffisants)" +msgstr "Expliciete rechten vereist (onvoldoende thematische rechten)" + +#: templates/assets/permissions/panel.html:43 +msgid "Accès restreint" +msgstr "Beperkte toegang" + +#: templates/assets/permissions/panel.html:55 +msgid "Accès au niveau du type (tous les objets)" +msgstr "Toegang op typeniveau (alle objecten)" + +#: templates/assets/permissions/panel.html:58 +msgid "Gérer" +msgstr "Beheren" + +#: templates/assets/permissions/panel.html:62 +msgid "" +"Les utilisateurs ci-dessous ont un accès défini pour TOUS les objets de ce " +"type. Ces droits s'appliquent à cet objet sauf si une ligne d'accès " +"spécifique ci-dessous les écrase." +msgstr "" + +#: templates/assets/permissions/panel.html:68 +#: templates/assets/permissions/panel.html:110 +#: templates/assets/permissions/panel.html:207 +#: templates/assets/permissions/type_page.html:70 +#: templates/assets/permissions/type_page.html:153 +#: templates/assets/permissions/type_page.html:237 +#: templates/assets/permissions/type_page.html:356 +msgid "Voir" +msgstr "Bekijken" + +#: templates/assets/permissions/panel.html:69 +#: templates/assets/permissions/panel.html:111 +#: templates/assets/permissions/panel.html:211 +#: templates/assets/permissions/type_page.html:71 +#: templates/assets/permissions/type_page.html:157 +#: templates/assets/permissions/type_page.html:238 +#: templates/assets/permissions/type_page.html:360 +msgid "Modifier" +msgstr "Bewerken" + +#: templates/assets/permissions/panel.html:71 +msgid "Champs" +msgstr "Velden" + +#: templates/assets/permissions/panel.html:94 +#: templates/assets/permissions/panel.html:154 +#: templates/assets/permissions/type_page.html:105 +#: templates/assets/permissions/type_page.html:289 +msgid "Par rôle" +msgstr "Per rol" + +#: templates/assets/permissions/panel.html:165 +#: templates/assets/permissions/type_page.html:300 +msgid "Supprimer cet accès ?" +msgstr "Deze toegang verwijderen?" + +#: templates/assets/permissions/panel.html:177 +msgid "Aucun accès spécifique défini pour cet objet." +msgstr "Geen specifieke toegang gedefinieerd voor dit object." + +#: templates/assets/permissions/panel.html:183 +msgid "Ajouter / mettre à jour un accès" +msgstr "Toegang toevoegen / bijwerken" + +#: templates/assets/permissions/panel.html:194 +#: templates/assets/permissions/type_page.html:143 +#: templates/assets/permissions/type_page.html:336 +msgid "choisir" +msgstr "kiezen" + +#: templates/assets/permissions/panel.html:225 +#: templates/assets/permissions/type_page.html:169 +#: templates/assets/permissions/type_page.html:393 +msgid "vide = tous" +msgstr "leeg = alle" + +#: templates/assets/permissions/panel.html:251 +#: templates/assets/permissions/type_page.html:192 +#: templates/assets/permissions/type_page.html:370 +msgid "vide = rôle" +msgstr "leeg = rol" + +#: templates/assets/permissions/type_access_edit.html:13 +#: templates/assets/permissions/type_page.html:12 +msgid "Permissions" +msgstr "Machtigingen" + +#: templates/assets/permissions/type_access_edit.html:16 +#: templates/assets/permissions/type_access_edit.html:23 +msgid "Modifier l'accès de type" +msgstr "Toegang op typeniveau bewerken" + +#: templates/assets/permissions/type_access_edit.html:34 +msgid "Type d'asset :" +msgstr "Assettype:" + +#: templates/assets/permissions/type_access_edit.html:36 +msgid "s'applique à tous les objets de ce type" +msgstr "geldt voor alle objecten van dit type" + +#: templates/assets/permissions/type_page.html:26 +msgid "Configuration du type" +msgstr "Typeconfiguratie" + +#: templates/assets/permissions/type_page.html:38 +msgid "Droits explicites requis" +msgstr "Expliciete rechten vereist" + +#: templates/assets/permissions/type_page.html:42 +msgid "" +"Si activé, les droits thématiques seuls ne suffisent pas pour accéder aux " +"objets de ce type. Chaque utilisateur doit avoir une ligne d'accès explicite " +"ci-dessous." +msgstr "" + +#: templates/assets/permissions/type_page.html:47 +msgid "" +"Accès restreint actif — les droits thématiques ne sont pas pris en compte " +"pour ce type." +msgstr "" + +#: templates/assets/permissions/type_page.html:59 +msgid "Accès utilisateur (tous les objets de ce type)" +msgstr "Gebruikerstoegang (alle objecten van dit type)" + +#: templates/assets/permissions/type_page.html:116 +msgid "Supprimer cet accès de type ?" +msgstr "Deze toegang op typeniveau verwijderen?" + +#: templates/assets/permissions/type_page.html:130 +msgid "" +"Aucun accès de type défini. Les droits thématiques s'appliquent (sauf " +"restriction ci-dessus)." +msgstr "" + +#: templates/assets/permissions/type_page.html:226 +msgid "Accès par utilisateur" +msgstr "Toegang per gebruiker" + +#: templates/assets/permissions/type_page.html:236 +msgid "Objet" +msgstr "Object" + +#: templates/assets/permissions/type_page.html:315 +msgid "Aucun accès spécifique défini pour ce type d'objet." +msgstr "Geen specifieke toegang gedefinieerd voor dit type object." + +#: templates/assets/permissions/type_page.html:324 +msgid "Ajouter un accès sur un objet spécifique" +msgstr "Toegang toevoegen voor een specifiek object" + +#: templates/assets/permissions/type_page.html:346 +msgid "ID objet" +msgstr "Object-ID" + +#: templates/assets/permissions/type_page.html:349 +msgid "ID de l'instance précise" +msgstr "ID van de specifieke instantie" + +#: templates/assets/publiclighting_street_detail.html:15 +#: templates/assets/roads_street_detail.html:15 +#: templates/assets/sign_street_detail.html:15 +msgid "Commune:" +msgstr "Gemeente:" + +#: templates/assets/publiclighting_street_detail.html:74 +#: templates/assets/roads_street_detail.html:83 +#: templates/assets/sign_street_detail.html:74 +#: templates/assets/structures_location_detail.html:111 +#: templates/assets/structures_location_detail.html:171 +#: templates/assets/trafficlights_intersection_detail.html:217 +msgid "Historique" +msgstr "Geschiedenis" + +#: templates/assets/publiclighting_street_detail.html:250 +#: templates/assets/sign_street_detail.html:249 +#: templates/assets/trafficlights_import_cross_plan.html:200 +#: templates/assets/trafficlights_import_cross_plan.html:603 +#: templates/assets/trafficlights_intersection_detail.html:425 +msgid "Poteaux" +msgstr "Palen" + +#: templates/assets/publiclighting_street_detail.html:267 +msgid "Alimentation:" +msgstr "Voeding:" + +#: templates/assets/roads_index.html:49 +msgid "Qualité globale" +msgstr "Globale kwaliteit" + +#: templates/assets/roads_index.html:87 +#: templates/assets/roads_street_detail.html:255 +#: templates/assets/roads_street_detail.html:270 +msgid "Qualité" +msgstr "Kwaliteit" + +#: templates/assets/roads_street_detail.html:17 +msgid "Code voirie:" +msgstr "Wegcode:" + +#: templates/assets/roads_street_detail.html:23 +msgid "Niveau:" +msgstr "Niveau:" + +#: templates/assets/roads_street_detail.html:249 +msgid "Chaussées" +msgstr "Rijbanen" + +#: templates/assets/roads_street_detail.html:259 +msgid "Aucune chaussée" +msgstr "Geen rijbaan" + +#: templates/assets/roads_street_detail.html:264 +msgid "Trottoirs" +msgstr "Voetpaden" + +#: templates/assets/roads_street_detail.html:274 +msgid "Aucun trottoir" +msgstr "Geen voetpad" + +#: templates/assets/roads_street_detail.html:279 +msgid "Mobilier urbain" +msgstr "Stedelijk meubilair" + +#: templates/assets/roads_street_detail.html:287 +msgid "Aucun mobilier urbain" +msgstr "Geen stedelijk meubilair" + +#: templates/assets/roads_street_detail.html:296 +msgid "Aucun asset archivé" +msgstr "Geen gearchiveerd asset" + +#: templates/assets/roads_street_detail.html:336 +msgid "Aucune intervention" +msgstr "Geen interventie" + +#: templates/assets/sign_index.html:49 views/core.py:709 +msgid "Type de panneau" +msgstr "Type bord" + +#: templates/assets/sign_index.html:88 +msgid "Texte" +msgstr "Tekst" + +#: templates/assets/structures_index.html:8 views/core.py:657 +msgid "Type d'ouvrage" +msgstr "Type kunstwerk" + +#: templates/assets/structures_location_detail.html:19 views/core.py:645 +msgid "Localisation principale" +msgstr "Hoofdlocatie" + +#: templates/assets/structures_location_detail.html:27 +msgid "Parent" +msgstr "Bovenliggende locatie" + +#: templates/assets/structures_location_detail.html:275 +msgid "Créer un nouvel équipement" +msgstr "Maak een nieuw asset aan" + +#: templates/assets/structures_location_detail.html:276 +#: templates/assets/trafficlights_import_cross_plan.html:259 +#: templates/assets/trafficlights_import_cross_plan.html:343 +#: templates/assets/trafficlights_import_cross_plan.html:427 +#: templates/assets/trafficlights_import_cross_plan.html:517 +msgid "Nouveau" +msgstr "Nieuw" + +#: templates/assets/structures_location_detail.html:305 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(c)s sous-localisation\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(c)s sous-localisations\n" +#| " " +msgid "" +"\n" +" %(c)s sous-localisation\n" +" " +msgid_plural "" +"\n" +" %(c)s sous-localisations\n" +" " +msgstr[0] "" +"\n" +"%(c)s sublocatie" +msgstr[1] "" +"\n" +"%(c)s sublocaties" + +#: templates/assets/structures_location_detail.html:312 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " %(a)s asset\n" +#| " " +#| msgid_plural "" +#| "\n" +#| " %(a)s assets\n" +#| " " +msgid "" +"\n" +" %(a)s asset\n" +" " +msgid_plural "" +"\n" +" %(a)s assets\n" +" " +msgstr[0] "" +"\n" +"%(a)s asset" +msgstr[1] "" +"\n" +"%(a)s assets" + +#: templates/assets/trafficlights_import_cross_plan.html:18 +#: templates/assets/trafficlights_import_dxf.html:12 +#, fuzzy +#| msgid "Thématique" +msgid "Thématiques" +msgstr "Thema" + +#: templates/assets/trafficlights_import_cross_plan.html:20 +#, fuzzy +#| msgid "Plan de croix" +msgid "Importation Plan de croix" +msgstr "Kruisplan" + +#: templates/assets/trafficlights_import_cross_plan.html:26 +msgid "Importation d'un plan de croix (Excel)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:29 +#: templates/assets/trafficlights_import_dxf.html:23 +#, fuzzy +#| msgid "Statut des carrefours" +msgid "Retour au carrefour" +msgstr "Status van de kruispunten" + +#: templates/assets/trafficlights_import_cross_plan.html:42 +msgid "Sélectionner / Téléverser le plan de croix (.xlsx)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:60 +msgid "Utiliser des plans de croix déjà rattachés" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:80 +msgid "" +"Sélectionnez un ou plusieurs plans de croix déjà associés à ce carrefour, ou " +"téléversez-en de nouveaux ci-dessous." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:85 +#: templates/assets/trafficlights_import_dxf.html:68 +#: templates/assets/unified_index.html:412 +msgid "OU" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:92 +msgid "Fichier Excel du plan de croix" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:96 +msgid "" +"Téléversez un ou plusieurs classeurs Excel de plans de croix (contenant " +"l'onglet 'kruisjesplan' ou 'Plan croix')." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:102 +#, fuzzy +#| msgid "Filtrer les assets" +msgid "Analyser et comparer les assets" +msgstr "Assets filteren" + +#: templates/assets/trafficlights_import_cross_plan.html:132 +msgid "" +"Veuillez téléverser un fichier Excel ou sélectionner au moins un plan " +"existant." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:171 +msgid "Aperçu de la comparaison des assets" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:174 +#, fuzzy +#| msgid "Fichier" +msgid "Fichiers :" +msgstr "Bestand" + +#: templates/assets/trafficlights_import_cross_plan.html:176 +#, fuzzy +#| msgid "Fichier" +msgid "Fichier :" +msgstr "Bestand" + +#: templates/assets/trafficlights_import_cross_plan.html:178 +#, fuzzy +#| msgid "Carrefour associé" +msgid "Carrefour :" +msgstr "Bijbehorend kruispunt" + +#: templates/assets/trafficlights_import_cross_plan.html:183 +msgid "Tout remplacer (Archiver l'existant + Recréer)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:186 +msgid "Appliquer les décisions" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:210 +#, fuzzy +#| msgid "Détecteurs" +msgid "Détecteurs & BP" +msgstr "Detectoren" + +#: templates/assets/trafficlights_import_cross_plan.html:229 +msgid "" +"Vous pouvez géolocaliser les poteaux en les plaçant sur le plan de la carte." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:232 +msgid "Positionner les poteaux sur la carte" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:240 +#: templates/assets/trafficlights_import_cross_plan.html:322 +#: templates/assets/trafficlights_import_cross_plan.html:406 +#: templates/assets/trafficlights_import_cross_plan.html:498 +#, fuzzy +#| msgid "Code EAN" +msgid "Code Asset" +msgstr "Code EAN" + +#: templates/assets/trafficlights_import_cross_plan.html:243 +#: templates/assets/trafficlights_import_cross_plan.html:326 +#: templates/assets/trafficlights_import_cross_plan.html:410 +#: templates/assets/trafficlights_import_cross_plan.html:501 +msgid "Modèle Déduit (Excel)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:244 +#: templates/assets/trafficlights_import_cross_plan.html:327 +#: templates/assets/trafficlights_import_cross_plan.html:411 +#: templates/assets/trafficlights_import_cross_plan.html:502 +#, fuzzy +#| msgid "Modèle actuel :" +msgid "Modèle Actuel (DB)" +msgstr "Huidig model:" + +#: templates/assets/trafficlights_import_cross_plan.html:245 +#: templates/assets/trafficlights_import_cross_plan.html:328 +#: templates/assets/trafficlights_import_cross_plan.html:412 +#: templates/assets/trafficlights_import_cross_plan.html:503 +msgid "Décision" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:246 +#: templates/assets/trafficlights_import_cross_plan.html:329 +#: templates/assets/trafficlights_import_cross_plan.html:413 +#: templates/assets/trafficlights_import_cross_plan.html:504 +#, fuzzy +#| msgid "Changer le modèle" +msgid "Forcer le Modèle" +msgstr "Wijzig model" + +#: templates/assets/trafficlights_import_cross_plan.html:261 +#: templates/assets/trafficlights_import_cross_plan.html:345 +#: templates/assets/trafficlights_import_cross_plan.html:429 +#: templates/assets/trafficlights_import_cross_plan.html:519 +#, fuzzy +#| msgid "Modifier" +msgid "Modifié" +msgstr "Bewerken" + +#: templates/assets/trafficlights_import_cross_plan.html:263 +#: templates/assets/trafficlights_import_cross_plan.html:347 +#: templates/assets/trafficlights_import_cross_plan.html:431 +#: templates/assets/trafficlights_import_cross_plan.html:521 +msgid "Identique" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:265 +#: templates/assets/trafficlights_import_cross_plan.html:349 +#: templates/assets/trafficlights_import_cross_plan.html:433 +#: templates/assets/trafficlights_import_cross_plan.html:523 +#, fuzzy +#| msgid "Assets uniquement" +msgid "DB uniquement" +msgstr "Alleen assets" + +#: templates/assets/trafficlights_import_cross_plan.html:273 +#: templates/assets/trafficlights_import_cross_plan.html:357 +#: templates/assets/trafficlights_import_cross_plan.html:441 +#: templates/assets/trafficlights_import_cross_plan.html:531 +msgid "Conserver en DB" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:279 +#: templates/assets/trafficlights_import_cross_plan.html:363 +#: templates/assets/trafficlights_import_cross_plan.html:447 +#: templates/assets/trafficlights_import_cross_plan.html:537 +#: templates/assets/unified_index.html:329 +msgid "Créer l'asset" +msgstr "De asset aanmaken" + +#: templates/assets/trafficlights_import_cross_plan.html:280 +#: templates/assets/trafficlights_import_cross_plan.html:364 +#: templates/assets/trafficlights_import_cross_plan.html:448 +#: templates/assets/trafficlights_import_cross_plan.html:538 +msgid "Ignorer" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:282 +#: templates/assets/trafficlights_import_cross_plan.html:366 +#: templates/assets/trafficlights_import_cross_plan.html:450 +#: templates/assets/trafficlights_import_cross_plan.html:540 +#, fuzzy +#| msgid "Créer l'asset" +msgid "Mettre à jour l'asset" +msgstr "De asset aanmaken" + +#: templates/assets/trafficlights_import_cross_plan.html:283 +#: templates/assets/trafficlights_import_cross_plan.html:287 +#: templates/assets/trafficlights_import_cross_plan.html:367 +#: templates/assets/trafficlights_import_cross_plan.html:371 +#: templates/assets/trafficlights_import_cross_plan.html:451 +#: templates/assets/trafficlights_import_cross_plan.html:455 +#: templates/assets/trafficlights_import_cross_plan.html:541 +#: templates/assets/trafficlights_import_cross_plan.html:545 +msgid "Remplacer (Archiver + Créer)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:284 +#: templates/assets/trafficlights_import_cross_plan.html:368 +#: templates/assets/trafficlights_import_cross_plan.html:452 +#: templates/assets/trafficlights_import_cross_plan.html:542 +#, fuzzy +#| msgid "Inexistant" +msgid "Conserver existant" +msgstr "Niet bestaand" + +#: templates/assets/trafficlights_import_cross_plan.html:286 +#: templates/assets/trafficlights_import_cross_plan.html:370 +#: templates/assets/trafficlights_import_cross_plan.html:454 +#: templates/assets/trafficlights_import_cross_plan.html:544 +msgid "Ne rien faire (Identique)" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:295 +#: templates/assets/trafficlights_import_cross_plan.html:379 +#: templates/assets/trafficlights_import_cross_plan.html:463 +#: templates/assets/trafficlights_import_cross_plan.html:553 +msgid "Laisser par défaut" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:307 +#, fuzzy +#| msgid "Aucun poteau" +msgid "Aucun poteau détecté." +msgstr "Geen paal" + +#: templates/assets/trafficlights_import_cross_plan.html:324 +#: templates/assets/trafficlights_import_cross_plan.html:408 +msgid "Poteau" +msgstr "Paal" + +#: templates/assets/trafficlights_import_cross_plan.html:391 +#, fuzzy +#| msgid "Aucun détecteur" +msgid "Aucune lanterne détectée." +msgstr "Geen detector" + +#: templates/assets/trafficlights_import_cross_plan.html:475 +#, fuzzy +#| msgid "Aucun détecteur" +msgid "Aucun détecteur détecté." +msgstr "Geen detector" + +#: templates/assets/trafficlights_import_cross_plan.html:487 +msgid "" +"Vous pouvez tracer précisément les câbles d'interconnexion le long des " +"tranchées de la carte." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:490 +msgid "Tracer les câbles sur la carte" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:565 +#, fuzzy +#| msgid "Aucun câble" +msgid "Aucun câble détecté." +msgstr "Geen kabel" + +#: templates/assets/trafficlights_import_cross_plan.html:590 +msgid "Retour au tableau comparatif" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:593 +msgid "Poteaux positionnés :" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:623 +msgid "Aide au placement" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:626 +msgid "" +"Cliquez sur 'Placer sur la carte' pour un poteau, puis cliquez sur la carte " +"pour le positionner." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:636 +#, fuzzy +#| msgid "A remplacer" +msgid "À placer" +msgstr "Te vervangen" + +#: templates/assets/trafficlights_import_cross_plan.html:645 +msgid "Placer sur la carte" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:660 +msgid "Tracé des câbles" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:663 +msgid "" +"Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer " +"la ligne du câble." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:677 +#, fuzzy +#| msgid "Éditer" +msgid "Tracer / Éditer" +msgstr "Bewerken" + +#: templates/assets/trafficlights_import_cross_plan.html:697 +#: templates/assets/trafficlights_import_cross_plan.html:1319 +#: templates/assets/trafficlights_import_cross_plan.html:1373 +msgid "Mode: Lecture" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:699 +#: templates/assets/unified_index.html:399 +msgid "Effacer" +msgstr "Wissen" + +#: templates/assets/trafficlights_import_cross_plan.html:715 +#: templates/assets/trafficlights_intersection_detail.html:1019 +msgid "Plans géoréférencés" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:723 +#: templates/assets/trafficlights_intersection_detail.html:1027 +msgid "Plan image" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:728 +#: templates/assets/trafficlights_intersection_detail.html:1032 +#, fuzzy +#| msgid "Plan" +msgid "Plan CAO" +msgstr "Plan" + +#: templates/assets/trafficlights_import_cross_plan.html:743 +#: templates/assets/trafficlights_intersection_detail.html:1047 +msgid "Aucun plan d'image géoréférencé disponible" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:744 +#: templates/assets/trafficlights_intersection_detail.html:1048 +msgid "" +"Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la " +"fiche." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:755 +#: templates/assets/trafficlights_intersection_detail.html:1059 +#, fuzzy +#| msgid "Aucun tag disponible pour cette thématique." +msgid "Aucun fichier DXF disponible pour ce carrefour" +msgstr "Geen tags beschikbaar voor dit thema." + +#: templates/assets/trafficlights_import_cross_plan.html:756 +#: templates/assets/trafficlights_intersection_detail.html:1060 +msgid "Veuillez d'abord en importer un." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:761 +#: templates/assets/trafficlights_intersection_detail.html:1068 +#, fuzzy +#| msgid "Sélection de la vue" +msgid "Sélectionner le plan DXF" +msgstr "Weergaveselectie" + +#: templates/assets/trafficlights_import_cross_plan.html:769 +#: templates/assets/trafficlights_intersection_detail.html:1076 +#, fuzzy +#| msgid "Sélectionner un type" +msgid "-- Sélectionner un plan CAO (DXF) --" +msgstr "Selecteer een type" + +#: templates/assets/trafficlights_import_cross_plan.html:780 +#: templates/assets/trafficlights_intersection_detail.html:1087 +msgid "Couches vectorielles CAO" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:782 +#: templates/assets/trafficlights_intersection_detail.html:1089 +msgid "Décocher tout" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:787 +#: templates/assets/trafficlights_intersection_detail.html:1094 +msgid "Lecture des couches du plan..." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:800 +#: templates/assets/trafficlights_intersection_detail.html:1107 +#, fuzzy +#| msgid "Désactivé" +msgid "Désactiver tout" +msgstr "Uitgeschakeld" + +#: templates/assets/trafficlights_import_cross_plan.html:859 +msgid "" +"Voulez-vous vraiment archiver TOUS les assets actifs existants de ce " +"carrefour et recréer les nouveaux à partir du fichier Excel ? Cette action " +"est irréversible." +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:1014 +msgid "Rechercher une adresse…" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:1215 +#, fuzzy +#| msgid "Position géographique" +msgid "Positionné" +msgstr "Geografische positie" + +#: templates/assets/trafficlights_import_cross_plan.html:1303 +msgid "Mode: Placement de" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:1344 +msgid "Mode: Tracé" +msgstr "" + +#: templates/assets/trafficlights_import_cross_plan.html:2058 +#, fuzzy +#| msgid "Commentaire enregistré avec succès" +msgid "Couche vectorielle chargée avec succès !" +msgstr "Opmerking succesvol opgeslagen" + +#: templates/assets/trafficlights_import_dxf.html:14 +msgid "Importation DXF" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:20 +msgid "Importation d'un plan DXF — Poteaux" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:36 +msgid "Choisir un plan DXF" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:53 +#, fuzzy +#| msgid "Sélectionner une catégorie" +msgid "Sélectionner un fichier existant (déjà importé)" +msgstr "Selecteer een categorie" + +#: templates/assets/trafficlights_import_dxf.html:56 +#, fuzzy +#| msgid "Sélectionner des fichiers" +msgid "Sélectionner un fichier" +msgstr "Bestanden selecteren" + +#: templates/assets/trafficlights_import_dxf.html:62 +msgid "" +"Sélectionnez un fichier DXF parmi les fichiers déjà disponibles sur le " +"serveur." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:75 +msgid "Téléverser un nouveau fichier DXF" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:82 +#, fuzzy +#| msgid "Changer les fichiers" +msgid "Analyser le fichier" +msgstr "Bestanden wijzigen" + +#: templates/assets/trafficlights_import_dxf.html:99 +msgid "Importer les couches vectorielles" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:108 +msgid "" +"Sélectionnez les couches vectorielles à importer depuis le fichier DXF. Les " +"couches non cochées ne seront pas converties ni affichées sur la carte." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:113 +#, fuzzy +#| msgid "Rechercher" +msgid "Rechercher un calque..." +msgstr "Zoeken" + +#: templates/assets/trafficlights_import_dxf.html:128 +#, fuzzy +#| msgid "Valider la sélection" +msgid "Lancer l'importation" +msgstr "Selectie bevestigen" + +#: templates/assets/trafficlights_import_dxf.html:138 +msgid "Génération de l'image géolocalisée et des couches vectorielles..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:139 +#: templates/assets/trafficlights_import_dxf.html:302 +msgid "Cette opération peut prendre un peu de temps..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:182 +msgid "Analyse..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:189 +msgid "Téléversement et analyse du fichier DXF..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:190 +msgid "Veuillez patienter pendant l\\" +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:192 +#, fuzzy +#| msgid "Changer les fichiers" +msgid "Analyse du fichier DXF..." +msgstr "Bestanden wijzigen" + +#: templates/assets/trafficlights_import_dxf.html:193 +msgid "Lecture des calques disponibles dans le fichier..." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:242 +#, fuzzy +#| msgid "Aucune rue trouvée" +msgid "Aucune couche trouvée dans le fichier." +msgstr "Geen straat gevonden." + +#: templates/assets/trafficlights_import_dxf.html:269 +#, fuzzy +#| msgid "Erreur lors de l’ajout." +msgid "Erreur lors de l\\" +msgstr "Fout bij het toevoegen." + +#: templates/assets/trafficlights_import_dxf.html:280 +#: views/trafficlights.py:1907 +msgid "Veuillez sélectionner un fichier DXF ou en téléverser un." +msgstr "" + +#: templates/assets/trafficlights_import_dxf.html:301 +#, fuzzy +#| msgid "Général" +msgid "Génération de l\\" +msgstr "Algemeen" + +#: templates/assets/trafficlights_index.html:18 views/core.py:613 +msgid "Voltage" +msgstr "Spanning" + +#: templates/assets/trafficlights_index.html:28 views/core.py:628 +msgid "Date de mise en service (à partir de)" +msgstr "Datum ingebruikname (vanaf)" + +#: templates/assets/trafficlights_index.html:31 views/core.py:619 +msgid "Connexion à la centrale" +msgstr "Aansluiting op de centrale" + +#: templates/assets/trafficlights_index.html:49 views/core.py:635 +msgid "Prestataire maintenance contrôleurs" +msgstr "Onderhoudsprovider controllers" + +#: templates/assets/trafficlights_index.html:72 +msgid "Créer une nouvelle intersection" +msgstr "Nieuw kruispunt aanmaken" + +#: templates/assets/trafficlights_intersection_detail.html:29 +msgid "Modifier cette intersection" +msgstr "Dit kruispunt bewerken" + +#: templates/assets/trafficlights_intersection_detail.html:39 +msgid "Tension (Voltage)" +msgstr "Spanning" + +#: templates/assets/trafficlights_intersection_detail.html:41 +msgid "Connexion centrale" +msgstr "Aansluiting op de centrale" + +#: templates/assets/trafficlights_intersection_detail.html:53 +msgid "Carrefour principal" +msgstr "Hoofdkruispunt" + +#: templates/assets/trafficlights_intersection_detail.html:58 +msgid "Contrats" +msgstr "Contracten" + +#: templates/assets/trafficlights_intersection_detail.html:72 +msgid "Modifier les contrats" +msgstr "Contracten bewerken" + +#: templates/assets/trafficlights_intersection_detail.html:94 +msgid "Assets importés par DXF en attente de validation" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:97 +#, python-format +msgid "" +"\n" +" Il y a %(count)s poteau importé depuis " +"un plan DXF qui est en attente de validation pour ce carrefour.\n" +" " +msgid_plural "" +"\n" +" Il y a %(count)s poteaux importés " +"depuis un plan DXF qui sont en attente de validation pour ce carrefour.\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/assets/trafficlights_intersection_detail.html:108 +#, fuzzy +#| msgid "Date d'installation" +msgid "Date d'installation :" +msgstr "Installatiedatum" + +#: templates/assets/trafficlights_intersection_detail.html:119 +msgid "Option A : Remplacer par code et créer les nouveaux" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:127 +msgid "" +"Êtes-vous sûr de vouloir archiver TOUS les poteaux existants de ce carrefour " +"et d\\'activer uniquement les nouveaux poteaux importés ?" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:128 +msgid "Option B : Remplacement complet (Archiver tout le reste)" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:133 +msgid "Réimporter / Corriger" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:328 +#: templates/assets/trafficlights_intersection_detail.html:973 +#: templates/assets/unified_index.html:453 +msgid "Ajouter un asset" +msgstr "Asset toevoegen" + +#: templates/assets/trafficlights_intersection_detail.html:333 +#: templates/assets/trafficlights_intersection_detail.html:334 +msgid "Importer plan DXF" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:336 +#: templates/assets/trafficlights_intersection_detail.html:337 +#, fuzzy +#| msgid "Plan de croix" +msgid "Importer plan de croix" +msgstr "Kruisplan" + +#: templates/assets/trafficlights_intersection_detail.html:341 +msgid "Archiver tous les assets" +msgstr "Alle assets archiveren" + +#: templates/assets/trafficlights_intersection_detail.html:342 +msgid "Tout archiver" +msgstr "Alles archiveren" + +#: templates/assets/trafficlights_intersection_detail.html:392 +msgid "Installation extérieure" +msgstr "Buiteninstallatie" + +#: templates/assets/trafficlights_intersection_detail.html:409 +msgid "Programmation" +msgstr "Programmering" + +#: templates/assets/trafficlights_intersection_detail.html:429 +msgid "Tout déplier / replier" +msgstr "Alles uitvouwen / inklappen" + +#: templates/assets/trafficlights_intersection_detail.html:467 +#, fuzzy +#| msgid "Afficher les lanternes et accessoires" +msgid "Afficher les lanternes, accessoires, radars et détecteurs" +msgstr "Lantaarns en accessoires weergeven" + +#: templates/assets/trafficlights_intersection_detail.html:515 +msgid "Aucun poteau" +msgstr "Geen paal" + +#: templates/assets/trafficlights_intersection_detail.html:528 +msgid "Armoires électriques" +msgstr "Elektrische kasten" + +#: templates/assets/trafficlights_intersection_detail.html:544 +msgid "Aucune armoire électrique" +msgstr "Geen elektrische kast" + +#: templates/assets/trafficlights_intersection_detail.html:560 +msgid "Couleur du câble sur la carte" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:572 +msgid "Aucun câble" +msgstr "Geen kabel" + +#: templates/assets/trafficlights_intersection_detail.html:581 +msgid "Radars préventifs" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:597 +#, fuzzy +#| msgid "Aucun hardware réseau" +msgid "Aucun radar préventif" +msgstr "Geen netwerkhardware" + +#: templates/assets/trafficlights_intersection_detail.html:616 +msgid "Contrôleur de trafic" +msgstr "Verkeerscontroller" + +#: templates/assets/trafficlights_intersection_detail.html:663 +msgid "Hardwares du contrôleur" +msgstr "Hardware van de controleur" + +#: templates/assets/trafficlights_intersection_detail.html:687 +msgid "Aucun hardware" +msgstr "Geen hardware" + +#: templates/assets/trafficlights_intersection_detail.html:727 +msgid "Aucun détecteur" +msgstr "Geen detector" + +#: templates/assets/trafficlights_intersection_detail.html:743 +msgid "Hardware réseau" +msgstr "Netwerkhardware" + +#: templates/assets/trafficlights_intersection_detail.html:767 +msgid "Aucun hardware réseau" +msgstr "Geen netwerkhardware" + +#: templates/assets/trafficlights_intersection_detail.html:783 +msgid "Hardware Tension" +msgstr "Spanningshardware" + +#: templates/assets/trafficlights_intersection_detail.html:807 +msgid "Aucun hardware tension" +msgstr "Geen spanningshardware" + +#: templates/assets/trafficlights_intersection_detail.html:820 +msgid "Aucun contrôleur" +msgstr "Geen regelaar" + +#: templates/assets/trafficlights_intersection_detail.html:836 +msgid "Programmations" +msgstr "Programmeringen" + +#: templates/assets/trafficlights_intersection_detail.html:863 +msgid "Aucune programmation" +msgstr "Geen programmering" + +#: templates/assets/trafficlights_intersection_detail.html:881 +msgid "Fichiers de log" +msgstr "Logbestanden" + +#: templates/assets/trafficlights_intersection_detail.html:901 +msgid "Aucun fichier de log" +msgstr "Geen logbestand" + +#: templates/assets/trafficlights_intersection_detail.html:981 +msgid "Sélectionner une catégorie" +msgstr "Selecteer een categorie" + +#: templates/assets/trafficlights_intersection_detail.html:1062 +msgid "Importer un plan DXF" +msgstr "" + +#: templates/assets/trafficlights_intersection_detail.html:1156 +msgid "Gérer les contrats" +msgstr "Contracten beheren" + +#: templates/assets/trafficlights_intersection_detail.html:1162 +msgid "Contrats associés" +msgstr "Gekoppelde contracten" + +#: templates/assets/trafficlights_intersection_detail.html:1168 +#: templates/assets/trafficlights_intersection_detail.html:1259 +msgid "Numéro de contrat" +msgstr "Contractnummer" + +#: templates/assets/trafficlights_intersection_detail.html:1169 +#: templates/assets/trafficlights_intersection_detail.html:1260 +msgid "Entreprise" +msgstr "Onderneming" + +#: templates/assets/trafficlights_intersection_detail.html:1170 +#: templates/assets/trafficlights_intersection_detail.html:1261 +msgid "Type de maintenance" +msgstr "Type onderhoud" + +#: templates/assets/trafficlights_intersection_detail.html:1186 +#: templates/assets/trafficlights_intersection_detail.html:1337 +msgid "Retirer ce contrat" +msgstr "Dit contract verwijderen" + +#: templates/assets/trafficlights_intersection_detail.html:1195 +msgid "Aucun contrat associé." +msgstr "Geen gekoppeld contract." + +#: templates/assets/trafficlights_intersection_detail.html:1202 +msgid "Ajouter un contrat" +msgstr "Contract toevoegen" + +#: templates/assets/trafficlights_intersection_detail.html:1207 +msgid "Contrat" +msgstr "Contract" + +#: templates/assets/trafficlights_intersection_detail.html:1209 +msgid "Sélectionner" +msgstr "Selecteren" + +#: templates/assets/trafficlights_intersection_detail.html:1218 +msgid "Infrastructure" +msgstr "Infrastructuur" + +#: templates/assets/trafficlights_intersection_detail.html:1219 +msgid "Contrôleur de traffic" +msgstr "Verkeersregelaar" + +#: templates/assets/trafficlights_intersection_detail.html:1344 +msgid "Erreur lors de l’ajout." +msgstr "Fout bij het toevoegen." + +#: templates/assets/unified_index.html:83 +msgid "Filtrer les assets" +msgstr "Assets filteren" + +#: templates/assets/unified_index.html:91 +#: templates/assets/unified_index.html:251 +#: templates/assets/unified_index.html:261 +msgid "Thématique" +msgstr "Thema" + +#: templates/assets/unified_index.html:118 +msgid "Aucune" +msgstr "Geen" + +#: templates/assets/unified_index.html:122 +#: templates/assets/unified_index.html:147 +msgid "Sélectionnez d'abord une ou plusieurs thématiques." +msgstr "Selecteer eerst een of meer thema's." + +#: templates/assets/unified_index.html:137 +msgid "Sélectionnez d'abord une ou plusieurs catégories." +msgstr "Selecteer eerst een of meer categorieën." + +#: templates/assets/unified_index.html:144 +msgid "Filtres thématiques" +msgstr "Thematische filters" + +#: templates/assets/unified_index.html:154 +msgid "Type d'élément" +msgstr "Type element" + +#: templates/assets/unified_index.html:161 +msgid "Assets et localisations" +msgstr "Assets en locaties" + +#: templates/assets/unified_index.html:167 +msgid "Localisations uniquement" +msgstr "Alleen locaties" + +#: templates/assets/unified_index.html:173 +msgid "Assets uniquement" +msgstr "Alleen assets" + +#: templates/assets/unified_index.html:181 +#, fuzzy +#| msgid "Description du matériel" +msgid "Options de géométrie" +msgstr "Hardwarebeschrijving" + +#: templates/assets/unified_index.html:186 +msgid "Inclure les assets sans géométrie" +msgstr "" + +#: templates/assets/unified_index.html:240 +msgid "Créer un élément" +msgstr "Een element aanmaken" + +#: templates/assets/unified_index.html:245 +msgid "Choisissez d'abord le type d'élément, puis la thématique autorisée." +msgstr "Kies eerst het type element en vervolgens het toegestane thema." + +#: templates/assets/unified_index.html:254 +#: templates/assets/unified_index.html:282 +msgid "Créer un asset" +msgstr "Een asset aanmaken" + +#: templates/assets/unified_index.html:264 +#: templates/assets/unified_index.html:342 +msgid "Créer une localisation" +msgstr "Een locatie aanmaken" + +#: templates/assets/unified_index.html:270 +msgid "Aucune thématique éditable n'est disponible pour la création." +msgstr "Er is geen bewerkbaar thema beschikbaar om aan te maken." + +#: templates/assets/unified_index.html:323 +msgid "L'asset sera créé avec le statut 'À approuver'." +msgstr "De asset wordt aangemaakt met de status 'Goed te keuren'." + +#: templates/assets/unified_index.html:361 +msgid "Créer la localisation" +msgstr "De locatie aanmaken" + +#: templates/assets/unified_index.html:373 +msgid "Mode sélection intervention préventive" +msgstr "Selectiemodus preventieve interventie" + +#: templates/assets/unified_index.html:394 +msgid "Code, nom, type…" +msgstr "Code, naam, type..." + +#: templates/assets/unified_index.html:408 +#, fuzzy +#| msgid "Tous les mots doivent correspondre" +msgid "Tous les mots recherchés doivent être présents" +msgstr "Alle woorden moeten overeenkomen" + +#: templates/assets/unified_index.html:408 +msgid "ET" +msgstr "" + +#: templates/assets/unified_index.html:412 +#, fuzzy +#| msgid "Au moins un mot doit correspondre" +msgid "Au moins un des mots recherchés doit être présent" +msgstr "Minstens één woord moet overeenkomen" + +#: templates/assets/unified_index.html:460 +#: templates/assets/unified_index.html:461 +msgid "Filtres" +msgstr "Filters" + +#: templates/assets/unified_index.html:480 +msgid "sélectionné(s)" +msgstr "geselecteerd" + +#: templates/assets/unified_index.html:497 +#: templates/assets/unified_index.html:616 +#, fuzzy +#| msgid "Modifier les contrats" +msgid "Modifier le statut en masse" +msgstr "Contracten bewerken" + +#: templates/assets/unified_index.html:505 +#: templates/assets/unified_index.html:645 +msgid "Modifier le statut de validation en masse" +msgstr "Validatiestatus in bulk wijzigen" + +#: templates/assets/unified_index.html:527 +msgid "" +"Sélectionnez une ou plusieurs thématiques dans les filtres, saisissez une " +"recherche ou zoomez sur la carte." +msgstr "" + +#: templates/assets/unified_index.html:560 +msgid "Choisissez une catégorie ou zoomez sur la carte pour voir les assets." +msgstr "Kies een categorie of zoom in op de kaart om de assets te bekijken." + +#: templates/assets/unified_index.html:621 +#, fuzzy +#| msgid "Nouveau" +msgid "Nouveau statut" +msgstr "Nieuw" + +#: templates/assets/unified_index.html:650 +msgid "Nouveau statut de validation" +msgstr "Nieuwe validatiestatus" + +#: templates/assets/water_index.html:17 views/core.py:691 views/locations.py:29 +msgid "GIEP" +msgstr "GIEP" + +#: templates/assets/water_index.html:31 +#, fuzzy +#| msgid "Créer une nouvelle localisation ITS" +msgid "Créer une nouvelle localisation GIEP" +msgstr "Nieuwe ITS-locatie aanmaken" + +#: templates/assets/water_index.html:59 +#, fuzzy +#| msgid "Créer un nouvel asset ITS" +msgid "Créer un nouvel ouvrage GIEP" +msgstr "Nieuwe ITS-asset aanmaken" + +#: views/artworks.py:79 +msgid "Inactif" +msgstr "Inactief" + +#: views/artworks.py:80 +msgid "À supprimer" +msgstr "Te verwijderen" + +#: views/artworks.py:94 views/controlcenters.py:107 views/its.py:110 +#: views/structures.py:111 +msgid "Localisations" +msgstr "Locaties" + +#: views/artworks.py:268 +#, python-format +msgid "Type d'asset inconnu: %(asset_model)s" +msgstr "Onbekend assettype: %(asset_model)s" + +#: views/artworks.py:328 views/trafficlights.py:1393 +msgid "Coordonnées invalides" +msgstr "Ongeldige coördinaten" + +#: views/artworks.py:343 views/its.py:853 +msgid "Aucune localisation trouvée" +msgstr "Geen locatie gevonden." + +#: views/artworks.py:436 views/artworks.py:440 +msgid "Fontaine" +msgstr "Fontein" + +#: views/artworks.py:817 +msgid "Aucun équipement trouvé" +msgstr "Geen apparatuur gevonden" + +#: views/asset_editing.py:49 views/asset_editing.py:132 +#: views/asset_editing.py:1833 views/asset_editing.py:1972 +#: views/asset_editing.py:2026 views/asset_editing.py:2084 +#: views/asset_editing.py:2160 +msgid "Permission refusée" +msgstr "Toestemming geweigerd" + +#: views/asset_editing.py:55 +msgid "Aucun fichier fourni" +msgstr "Geen bestand opgegeven" + +#: views/asset_editing.py:145 +msgid "Document non trouvé" +msgstr "Document niet gevonden" + +#: views/asset_editing.py:170 views/asset_editing.py:1987 +#: views/asset_editing.py:2041 views/asset_editing.py:2113 +#: views/asset_editing.py:2194 +msgid "Type d'asset inconnu" +msgstr "Onbekend assettype" + +#: views/asset_editing.py:289 +#, python-format +msgid "Champ inconnu: %(field)s" +msgstr "Onbekend veld: %(field)s" + +#: views/asset_editing.py:303 views/asset_editing.py:459 +#, python-format +msgid "Format de date invalide pour %(field)s" +msgstr "Ongeldig datumformaat voor %(field)s" + +#: views/asset_editing.py:318 +msgid "Modèle non trouvé" +msgstr "Model niet gevonden" + +#: views/asset_editing.py:329 views/asset_editing.py:1521 +msgid "Catégorie non trouvée" +msgstr "Categorie niet gevonden" + +#: views/asset_editing.py:346 +msgid "Localisation non trouvée" +msgstr "Locatie niet gevonden" + +#: views/asset_editing.py:355 +#, python-format +msgid "Statut invalide: %(status)s" +msgstr "Ongeldige status: %(status)s" + +#: views/asset_editing.py:362 +#, python-format +msgid "Statut de validation invalide: %(status)s" +msgstr "Ongeldige validatiestatus: %(status)s" + +#: views/asset_editing.py:376 views/asset_editing.py:413 +#: views/asset_editing.py:431 views/asset_editing.py:440 +#, python-format +msgid "Valeur numérique invalide pour %(field)s" +msgstr "Ongeldige numerieke waarde voor %(field)s" + +#: views/asset_editing.py:403 +#, python-format +msgid "Géométrie invalide: %(error)s" +msgstr "Ongeldige geometrie: %(error)s" + +#: views/asset_editing.py:485 views/asset_editing.py:555 +#: views/asset_editing.py:590 views/clean.py:512 +msgid "Vous n'avez pas la permission de modifier cet asset" +msgstr "Je hebt geen toestemming om dit asset te bewerken" + +#: views/asset_editing.py:495 views/asset_editing.py:605 +#: views/asset_editing.py:756 views/asset_editing.py:1449 +#: views/asset_editing.py:1977 views/asset_editing.py:2031 +#: views/asset_editing.py:2089 views/asset_editing.py:2182 +#: views/asset_editing.py:2302 views/clean.py:518 +msgid "Format JSON invalide" +msgstr "Ongeldige JSON-indeling" + +#: views/asset_editing.py:504 +#, python-format +msgid "Champ non modifiable: %(field)s" +msgstr "Niet-bewerkbaar veld: %(field)s" + +#: views/asset_editing.py:534 +msgid "Aucun champ mis à jour" +msgstr "Geen veld bijgewerkt" + +#: views/asset_editing.py:541 +#, python-format +msgid "%(count)s champ(s) mis à jour" +msgstr "%(count)s veld(en) bijgewerkt" + +#: views/asset_editing.py:562 +msgid "Vous n'avez pas la permission d'archiver cet asset" +msgstr "Je hebt geen toestemming om dit asset te archiveren" + +#: views/asset_editing.py:572 +msgid "Asset archivé avec succès" +msgstr "Asset succesvol gearchiveerd" + +#: views/asset_editing.py:597 +msgid "Vous n'avez pas la permission de remplacer cet asset" +msgstr "Je hebt geen toestemming om dit asset te vervangen" + +#: views/asset_editing.py:682 +msgid "Asset remplacé avec succès" +msgstr "Asset succesvol vervangen" + +#: views/asset_editing.py:741 views/asset_editing.py:933 +#: views/asset_editing.py:1505 +msgid "Thématique non trouvée" +msgstr "Thema niet gevonden" + +#: views/asset_editing.py:748 views/asset_editing.py:2175 +msgid "Vous n'avez pas la permission de créer des assets" +msgstr "Je hebt geen toestemming om assets aan te maken" + +#: views/asset_editing.py:802 +msgid "Type d'asset requis" +msgstr "Assettype vereist" + +#: views/asset_editing.py:810 +#, python-format +msgid "Type d'asset invalide: %(type)s" +msgstr "Ongeldig assettype: %(type)s" + +#: views/asset_editing.py:821 views/asset_editing.py:951 +msgid "Type de localisation invalide" +msgstr "Ongeldig locatie type" + +#: views/asset_editing.py:844 +msgid "Impossible de lier l'asset à cette localisation" +msgstr "Kan het asset niet koppelen aan deze locatie" + +#: views/asset_editing.py:859 +msgid "Le modèle d'asset est requis pour ce type d'asset" +msgstr "Het assetmodel is vereist voor dit type asset" + +#: views/asset_editing.py:864 +msgid "Modèle d'asset non trouvé" +msgstr "Assetmodel niet gevonden" + +#: views/asset_editing.py:897 +msgid "Asset créé avec succès" +msgstr "Asset succesvol aangemaakt" + +#: views/asset_editing.py:940 +msgid "Vous n'avez pas la permission d'archiver en masse" +msgstr "Je hebt geen toestemming voor massale archivering" + +#: views/asset_editing.py:958 +msgid "Cette localisation ne supporte pas la gestion des assets" +msgstr "Deze locatie ondersteunt geen assetbeheer" + +#: views/asset_editing.py:973 +#, python-format +msgid "%(count)s asset(s) archivé(s)" +msgstr "%(count)s asset(s) gearchiveerd" + +#: views/asset_editing.py:1477 +msgid "Format d'ID invalide" +msgstr "Ongeldig ID-formaat" + +#: views/asset_editing.py:1484 +msgid "Thématique requise" +msgstr "Thema vereist" + +#: views/asset_editing.py:1490 +msgid "Catégorie requise" +msgstr "Categorie vereist" + +#: views/asset_editing.py:1496 +msgid "Code requis" +msgstr "Code vereist" + +#: views/asset_editing.py:1512 +msgid "Vous n'avez pas la permission de créer des équipements" +msgstr "Je hebt geen toestemming om apparatuur aan te maken" + +#: views/asset_editing.py:1533 +msgid "Type d'asset non supporté pour cette thématique/catégorie" +msgstr "Assettype niet ondersteund voor dit thema/categorie" + +#: views/asset_editing.py:1540 +msgid "Un équipement avec ce code existe déjà" +msgstr "Er bestaat al apparatuur met deze code" + +#: views/asset_editing.py:1550 +msgid "Modèle d'équipement non trouvé" +msgstr "Apparaatmodel niet gevonden" + +#: views/asset_editing.py:1555 +msgid "Modèle requis" +msgstr "Model vereist" + +#: views/asset_editing.py:1624 +msgid "Une localisation est requise pour créer cet asset." +msgstr "Une localisation est requise pour créer cet asset." + +#: views/asset_editing.py:1634 +msgid "Localisation introuvable" +msgstr "Locatie onvindbaar" + +#: views/asset_editing.py:1680 +msgid "Équipement créé avec succès" +msgstr "Apparaat succesvol aangemaakt" + +#: views/asset_editing.py:1682 +#, python-format +msgid " • %(count)d opération(s) ajoutée(s)" +msgstr " • %(count)d bewerking(en) toegevoegd" + +#: views/asset_editing.py:1995 views/asset_editing.py:2100 +#: views/asset_editing.py:2103 views/asset_editing.py:2117 +#: views/asset_editing.py:2200 +msgid "Champ de liaison invalide" +msgstr "Ongeldig koppelingsveld" + +#: views/asset_editing.py:1997 views/asset_editing.py:2061 +#: views/asset_editing.py:2105 views/asset_editing.py:2202 +msgid "Champ de liaison inconnu" +msgstr "Onbekend koppelingsveld" + +#: views/asset_editing.py:2007 +msgid "Asset lié avec succès" +msgstr "Asset succesvol gekoppeld" + +#: views/asset_editing.py:2049 +msgid "Cet asset n'est pas lié à ce parent" +msgstr "Dit asset is niet gekoppeld aan dit bovenliggend element" + +#: views/asset_editing.py:2057 views/asset_editing.py:2140 +msgid "Ce champ est obligatoire et ne peut pas être vidé" +msgstr "Dit veld is verplicht en kan niet leeg worden gemaakt" + +#: views/asset_editing.py:2068 +msgid "Asset délié avec succès" +msgstr "Asset succesvol ontkoppeld" + +#: views/asset_editing.py:2129 +#, fuzzy +#| msgid "Asset lié avec succès" +msgid "Parent lié avec succès" +msgstr "Asset succesvol gekoppeld" + +#: views/asset_editing.py:2148 +#, fuzzy +#| msgid "Asset délié avec succès" +msgid "Parent délié avec succès" +msgstr "Asset succesvol ontkoppeld" + +#: views/asset_editing.py:2267 +#, python-format +msgid "Le champ « %(field)s » est obligatoire." +msgstr "Het veld \"%(field)s\" is verplicht." + +#: views/asset_editing.py:2269 +msgid "Impossible de créer l'asset : un champ obligatoire est manquant." +msgstr "Kan asset niet aanmaken: er ontbreekt een verplicht veld." + +#: views/asset_editing.py:2275 +msgid "Asset créé et lié avec succès" +msgstr "Asset succesvol aangemaakt en gekoppeld" + +#: views/asset_editing.py:2309 +#, fuzzy +#| msgid "Champ non modifiable: %(field)s" +msgid "Champ non modifiable en masse" +msgstr "Niet-bewerkbaar veld: %(field)s" + +#: views/asset_editing.py:2355 +#, python-format +msgid "%(updated)s équipement(s) mis à jour, %(skipped)s ignoré(s)." +msgstr "" + +#: views/asset_permissions.py:47 +msgid "Accès refusé — rôle admin requis." +msgstr "Toegang geweigerd — adminrol vereist." + +#: views/asset_permissions.py:100 +msgid "Utilisateur requis." +msgstr "Gebruiker vereist." + +#: views/asset_permissions.py:106 +msgid "Utilisateur introuvable." +msgstr "Gebruiker niet gevonden." + +#: views/clean.py:259 +msgid "Locations" +msgstr "Locaties" + +#: views/clean.py:475 +#, python-format +msgid "Unknown model: %(asset_model)s" +msgstr "Onbekend model: %(asset_model)s" + +#: views/clean.py:632 +msgid "No location found nearby" +msgstr "Geen locatie gevonden in de buurt" + +#: views/controlcenters.py:396 +#, python-format +msgid "Trop d'objets à afficher (> %(max)d)" +msgstr "Te veel objecten om te tonen (> %(max)d)" + +#: views/controlcenters.py:410 +#, python-format +msgid "Trop d'objets à afficher (> %(max)d). Limitez votre sélection." +msgstr "Te veel objecten om te tonen (> %(max)d). Beperk uw keuze." + +#: views/controlcenters.py:528 +#, python-format +msgid "Modèle '%(model)s' inconnu." +msgstr "Model '%(model)s' onbekend." + +#: views/controlcenters.py:541 views/its.py:544 views/nature.py:237 +#: views/publiclighting.py:278 views/roads.py:541 views/sign.py:794 +#: views/structures.py:666 views/trafficlights.py:1273 +msgid "Aucun template trouvé pour ce type d’asset." +msgstr "Er is geen template gevonden voor dit type asset." + +#: views/core.py:246 views/nature.py:224 views/publiclighting.py:265 +#: views/roads.py:528 views/sign.py:781 views/structures.py:653 +#: views/trafficlights.py:1260 views/water.py:523 +#, python-format +msgid "Modèle %(asset_model)s inconnu." +msgstr "Model %(asset_model)s onbekend." + +#: views/core.py:527 +msgid "Signaux de danger" +msgstr "Gevaarsignalen" + +#: views/core.py:528 +msgid "Signaux de priorité" +msgstr "Prioritaire signalen" + +#: views/core.py:529 +msgid "Signaux d'interdiction" +msgstr "Verbodsborden" + +#: views/core.py:530 +msgid "Signaux d'obligation" +msgstr "Verplichte signalen" + +#: views/core.py:531 +msgid "Arrêt / Stationnement" +msgstr "Stoppen/parkeren" + +#: views/core.py:532 +msgid "Signaux d'indication" +msgstr "Indicatiesignalen" + +#: views/core.py:533 +msgid "Signaux de zone" +msgstr "Zonesignalen" + +#: views/core.py:534 +msgid "Signaux additionnels" +msgstr "Extra signalen" + +#: views/core.py:607 +msgid "Statut des carrefours" +msgstr "Status van de kruispunten" + +#: views/core.py:700 +msgid "Niveau de qualité" +msgstr "Kwaliteitsniveau" + +#: views/export.py:94 +msgid "ID" +msgstr "ID" + +#: views/export.py:100 +msgid "Obsolescence" +msgstr "Veroudering" + +#: views/export.py:101 +msgid "Code localisation" +msgstr "Locatiecode" + +#: views/export.py:102 +msgid "Nom localisation" +msgstr "Locatienaam" + +#: views/export.py:104 +msgid "Date de déinstallation" +msgstr "Datum van buitendienststelling" + +#: views/export.py:106 +msgid "Garantie (mois)" +msgstr "Garantie (maanden)" + +#: views/export.py:133 +msgid "Paramètre 'ids' invalide." +msgstr "Ongeldige parameter 'ids'." + +#: views/export.py:136 +msgid "Aucun asset sélectionné." +msgstr "Geen asset geselecteerd." + +#: views/export.py:168 +msgid "Export XLSX indisponible (dépendance manquante)." +msgstr "XLSX-export niet beschikbaar (ontbrekende afhankelijkheid)." + +#: views/history.py:27 views/history.py:89 views/history.py:112 +#: views/history.py:318 +msgid "Authentication required" +msgstr "Authenticatie vereist" + +#: views/history.py:35 +msgid "Cannot determine asset's thematic" +msgstr "Kan de thematiek van het asset niet bepalen" + +#: views/history.py:50 +msgid "You do not have permission to view assets for this thematic" +msgstr "U heeft geen toestemming om assets voor dit thema te bekijken" + +#: views/history.py:148 views/history.py:149 views/history.py:280 +#: views/history.py:281 +msgid "Système" +msgstr "Systeem" + +#: views/history.py:196 +msgid "You do not have access to any thematics" +msgstr "U heeft geen toegang tot enig thema" + +#: views/history.py:224 +msgid "You do not have permission to view this thematic" +msgstr "U heeft geen toestemming om dit thema te bekijken" + +#: views/history.py:241 +msgid "Thematic not found" +msgstr "Thema niet gevonden" + +#: views/history.py:326 +msgid "Paramètre 'date' manquant" +msgstr "Parameter 'date' ontbreekt" + +#: views/history.py:342 +msgid "Format de date invalide. Utilisez ISO 8601." +msgstr "Ongeldig datumformaat. Gebruik ISO 8601." + +#: views/history.py:426 +msgid "Inexistant" +msgstr "Niet bestaand" + +#: views/its.py:404 views/its.py:415 views/nature.py:463 views/nature.py:543 +#: views/publiclighting.py:454 views/publiclighting.py:475 +#: views/publiclighting.py:496 views/publiclighting.py:506 views/roads.py:412 +#: views/roads.py:416 views/roads.py:420 views/roads.py:424 views/roads.py:428 +#: views/roads.py:441 views/roads.py:450 views/roads.py:459 views/roads.py:468 +#: views/roads.py:477 views/sign.py:676 views/sign.py:680 views/sign.py:721 +#: views/sign.py:730 views/structures.py:439 views/structures.py:450 +#: views/trafficlights.py:1226 views/trafficlights.py:1240 views/water.py:223 +msgid "Trop d'objets à afficher" +msgstr "Te veel objecten om te tonen" + +#: views/its.py:415 views/nature.py:543 views/publiclighting.py:506 +#: views/roads.py:441 views/roads.py:450 views/roads.py:459 views/roads.py:468 +#: views/roads.py:477 views/sign.py:721 views/sign.py:730 +#: views/structures.py:450 views/trafficlights.py:1240 +msgid "Limitez votre sélection." +msgstr "Beperk uw keuze." + +#: views/location_editing.py:46 +msgid "Authentification requise." +msgstr "Authenticatie vereist." + +#: views/location_editing.py:48 views/location_editing.py:59 +msgid "Permission refusée." +msgstr "Toegang geweigerd." + +#: views/locations.py:25 views/water.py:86 +msgid "Ouvrages" +msgstr "Kunstwerken" + +#: views/locations.py:26 +msgid "Feux" +msgstr "Verkeerslichten" + +#: views/locations.py:27 +msgid "Éclairage" +msgstr "Verlichting" + +#: views/locations.py:28 +msgid "Nature" +msgstr "Natuur" + +#: views/locations.py:30 +msgid "Centres de contrôle" +msgstr "Controlecentrum" + +#: views/locations.py:31 +msgid "Signalisation" +msgstr "Signalisatie" + +#: views/locations.py:32 +msgid "Voirie" +msgstr "Weg" + +#: views/nature.py:766 +msgid "Aucune section trouvée" +msgstr "Geen versie gevonden." + +#: views/nature.py:797 views/nature.py:838 +msgid "Aucun asset sélectionné" +msgstr "Geen asset geselecteerd" + +#: views/nature.py:911 +msgid "Aucune modification fournie" +msgstr "Geen wijziging opgegeven" + +#: views/nature.py:1252 +msgid "Commentaire enregistré avec succès" +msgstr "Opmerking succesvol opgeslagen" + +#: views/nature.py:1280 +msgid "Commentaire supprimé avec succès" +msgstr "Opmerking succesvol verwijderd" + +#: views/nature.py:1331 +msgid "Photos ajoutées avec succès" +msgstr "Foto's succesvol toegevoegd" + +#: views/publiclighting.py:549 views/roads.py:655 views/sign.py:908 +msgid "Aucune rue trouvée" +msgstr "Geen straat gevonden." + +#: views/structures.py:1181 +msgid "Aucune localisation structure trouvée" +msgstr "Geen structuurlocatie gevonden" + +#: views/trafficlights.py:205 +msgid "Intersections" +msgstr "Kruispunten" + +#: views/trafficlights.py:629 +msgid "Contrat requis." +msgstr "Contract vereist." + +#: views/trafficlights.py:1149 +msgid "Date de mise en service invalide." +msgstr "Ongeldige datum van ingebruikname." + +#: views/trafficlights.py:1356 views/trafficlights.py:1384 +msgid "Intersection introuvable" +msgstr "Kruispunt niet gevonden" + +#: views/trafficlights.py:1417 +msgid "Aucune intersection trouvée" +msgstr "Geen kruispunt gevonden." + +#: views/trafficlights.py:2411 +msgid "" +"Le fichier DXF a bien été importé et le plan est disponible pour un " +"affichage sur la carte." +msgstr "" + +#: views/trafficlights.py:3809 +msgid "" +"Veuillez téléverser un fichier ou sélectionner au moins un plan existant." +msgstr "" + +#: views/trafficlights.py:3865 +msgid "Un des fichiers de plan de croix sélectionnés est introuvable." +msgstr "" + +#: views/trafficlights.py:4018 +msgid "Absent du fichier Excel (Sera conservé ou archivé)" +msgstr "" + +#: views/trafficlights.py:4063 views/trafficlights.py:4125 +#: views/trafficlights.py:4187 +#, fuzzy +#| msgid "Aucun fichier de log" +msgid "Absent du fichier Excel" +msgstr "Geen logbestand" + +#: views/water.py:85 +msgid "Zones GIEP" +msgstr "" + +#: views/water.py:572 +#, fuzzy +#| msgid "Aucune localisation trouvée" +msgid "Aucune localisation GIEP trouvée à proximité" +msgstr "Geen locatie gevonden." + +#, fuzzy +#~| msgid "Changer le modèle" +#~ msgid "Charger le plan" +#~ msgstr "Wijzig model" + +#, fuzzy +#~| msgid "Configuration du type" +#~ msgid "Configuration" +#~ msgstr "Typeconfiguratie" + +#, fuzzy +#~| msgid "Totaux des assets" +#~ msgid "Nouveaux assets" +#~ msgstr "Totalen van assets" + +#, fuzzy +#~| msgid "Cliquez sur la carte pour ajouter des sommets." +#~ msgid "Cliquez sur la carte pour ajouter manuellement un nouveau poteau." +#~ msgstr "Klik op de kaart om hoekpunten toe te voegen." + +#, fuzzy +#~| msgid "Aucun template trouvé pour ce type d’asset." +#~ msgid "Aucun poteau ajouté pour le moment." +#~ msgstr "Er is geen template gevonden voor dit type asset." + +#, fuzzy +#~| msgid "Inexistant" +#~ msgid "Existants" +#~ msgstr "Niet bestaand" + +#, fuzzy +#~| msgid "Nouveau" +#~ msgid "Nouveaux" +#~ msgstr "Nieuw" + +#, fuzzy +#~| msgid "Erreur lors de la sauvegarde." +#~ msgid "Erreur lors de la lecture des couches." +#~ msgstr "Fout bij het opslaan." + +#, fuzzy +#~| msgid "Ajouter un contrat" +#~ msgid "Ajouter un poteau" +#~ msgstr "Contract toevoegen" + +#~ msgid "Noue" +#~ msgstr "Wadi" + +#~ msgid "Continu poreux" +#~ msgstr "Poreus continu" + +#~ msgid "Impermeable" +#~ msgstr "Impermeabel" + +#~ msgid "Type d'EVC" +#~ msgstr "Type EVC" + +#~ msgid "Type de revêtement" +#~ msgstr "Type verharding" + +#~ msgid "Espaces verts creux" +#~ msgstr "Verlaagde groenzones" + +#~ msgid "Armoire électrique" +#~ msgstr "Elektrische kast" + +#~ msgid "Sélectionner un modèle" +#~ msgstr "Selecteer een model" + +#~ msgid "Hauteur" +#~ msgstr "Hoogte" + +#~ msgid "Alimentation" +#~ msgstr "Voeding" diff --git a/streetup/assets/locale/nl/LC_MESSAGES/djangojs.mo b/streetup/assets/locale/nl/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..18477de Binary files /dev/null and b/streetup/assets/locale/nl/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/assets/locale/nl/LC_MESSAGES/djangojs.po b/streetup/assets/locale/nl/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..83d100e --- /dev/null +++ b/streetup/assets/locale/nl/LC_MESSAGES/djangojs.po @@ -0,0 +1,595 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: assets/static/assets/asset_add_modal.js:52 +#: assets/static/assets/asset_location_edit.js:59 +#: assets/static/assets/unified_index.js:115 +#: assets/static/assets/unified_index.js:168 +#: assets/static/assets/unified_index.js:224 +msgid "Chargement..." +msgstr "Laden..." + +#: assets/static/assets/asset_add_modal.js:56 +#: assets/static/assets/asset_add_modal.js:426 +#: assets/static/assets/unified_index.js:114 +#: assets/static/assets/unified_index.js:220 +#: assets/static/assets/unified_index.js:262 +msgid "Sélectionner d'abord une catégorie..." +msgstr "Selecteer eerst een categorie..." + +#: assets/static/assets/asset_add_modal.js:65 +#: assets/static/assets/unified_index.js:230 +msgid "Sélectionner un modèle..." +msgstr "Selecteer een model..." + +#: assets/static/assets/asset_add_modal.js:77 +#: assets/static/assets/unified_index.js:243 +msgid "Aucun modèle disponible" +msgstr "Geen model beschikbaar" + +#: assets/static/assets/asset_add_modal.js:82 +#: assets/static/assets/asset_location_edit.js:102 +#: assets/static/assets/unified_index.js:134 +#: assets/static/assets/unified_index.js:205 +#: assets/static/assets/unified_index.js:247 +msgid "Erreur de chargement" +msgstr "Fout bij het laden" + +#: assets/static/assets/asset_add_modal.js:90 +#: assets/static/assets/unified_index.js:848 +#: assets/static/assets/unified_index.js:935 +msgid "Création..." +msgstr "Bezig met aanmaken..." + +#: assets/static/assets/asset_add_modal.js:146 +#: assets/static/assets/asset_add_modal.js:350 +msgid "Erreur lors de la création" +msgstr "Fout bij het aanmaken" + +#: assets/static/assets/asset_add_modal.js:156 +#: assets/static/assets/asset_add_modal.js:359 +msgid "Erreur de connexion" +msgstr "Verbindingsfout" + +#: assets/static/assets/asset_add_modal.js:221 +msgid "Sélection de template d'opérations" +msgstr "Selectie van bewerkingssjabloon" + +#: assets/static/assets/asset_add_modal.js:223 +msgid "Fermer" +msgstr "Sluiten" + +#: assets/static/assets/asset_add_modal.js:226 +msgid "Choisissez le(s) template(s) d'opérations à appliquer à cet équipement." +msgstr "" +"Kies de bewerkingssjabloon(s) die op deze uitrusting moeten worden toegepast." + +#: assets/static/assets/asset_add_modal.js:228 +msgid "Choisir le(s) template(s) d'opérations :" +msgstr "Kies de bewerkingssjabloon(s) :" + +#: assets/static/assets/asset_add_modal.js:242 +msgid "Tout sélectionner" +msgstr "Alles selecteren" + +#: assets/static/assets/asset_add_modal.js:245 +msgid "Tout désélectionner" +msgstr "Alles deselecteren" + +#: assets/static/assets/asset_add_modal.js:253 +msgid "Ignorer et continuer" +msgstr "Negeren en doorgaan" + +#: assets/static/assets/asset_add_modal.js:256 +msgid "Créer les opérations" +msgstr "Bewerkingen aanmaken" + +#: assets/static/assets/asset_add_modal.js:317 +msgid "Création en cours..." +msgstr "Bezig met aanmaken..." + +#: assets/static/assets/asset_location_edit.js:37 +msgid "Saisissez au moins 2 caractères pour rechercher." +msgstr "Typ ten minste 2 tekens om te zoeken." + +#: assets/static/assets/asset_location_edit.js:76 +#: assets/static/assets/unified_index.js:177 +msgid "Sélectionner une localisation..." +msgstr "Selecteer een locatie..." + +#: assets/static/assets/asset_location_edit.js:87 +msgid "Résultats limités. Affinez la recherche pour trouver plus facilement." +msgstr "" +"Beperkte resultaten. Verfijn de zoekopdracht om gemakkelijker te vinden." + +#: assets/static/assets/asset_location_edit.js:92 +#: assets/static/assets/unified_index.js:196 +msgid "Aucune localisation disponible" +msgstr "Geen locatie beschikbaar" + +#: assets/static/assets/asset_location_edit.js:94 +msgid "Aucun résultat pour cette recherche." +msgstr "Geen resultaten voor deze zoekopdracht." + +#: assets/static/assets/asset_location_edit.js:104 +msgid "Erreur lors du chargement des localisations" +msgstr "Fout bij het laden van locaties" + +#: assets/static/assets/asset_location_edit.js:117 +msgid "Veuillez sélectionner une localisation" +msgstr "Selecteer een locatie" + +#: assets/static/assets/asset_location_edit.js:124 +msgid "Mise à jour..." +msgstr "Bezig met bijwerken..." + +#: assets/static/assets/asset_location_edit.js:149 +msgid "Localisation mise à jour" +msgstr "Locatie bijgewerkt" + +#: assets/static/assets/asset_location_edit.js:154 +#: assets/static/assets/asset_location_edit.js:161 +msgid "Erreur lors de la mise à jour" +msgstr "Fout bij het bijwerken" + +#: assets/static/assets/assets_index.js:568 +#: assets/static/assets/assets_index.js:592 +#: assets/static/assets/unified_index.js:499 +msgid "Code" +msgstr "Code" + +#: assets/static/assets/assets_index.js:594 +#: assets/static/assets/green-surfaces-comments.js:953 +#: assets/static/assets/unified_index.js:510 +msgid "Statut" +msgstr "Status" + +#: assets/static/assets/assets_index.js:777 +msgid "%(count)s localisation sélectionnée" +msgid_plural "%(count)s localisations sélectionnées" +msgstr[0] "%(count)s locatie geselecteerd" +msgstr[1] "%(count)s locaties geselecteerd" + +#: assets/static/assets/assets_index.js:789 +msgid "%(count)s asset sélectionné" +msgid_plural "%(count)s assets sélectionnés" +msgstr[0] "%(count)s asset geselecteerd" +msgstr[1] "%(count)s assets geselecteerd" + +#: assets/static/assets/assets_index.js:880 +msgid "Sélectionner l'asset" +msgstr "Asset selecteren" + +#: assets/static/assets/assets_index.js:886 +msgid "Désélectionner l'asset" +msgstr "Asset deselecteren" + +#: assets/static/assets/assets_index.js:997 +#: assets/static/assets/assets_index.js:1068 +msgid "Erreur lors de la récupération des données." +msgstr "Fout bij het ophalen van de gegevens." + +#: assets/static/assets/assets_index.js:1100 +msgid "" +"Sélectionnez des localisations ou lancez une recherche pour afficher les " +"assets." +msgstr "" + +#: assets/static/assets/assets_index.js:1102 +msgid "Aucun asset ne correspond aux critères de recherche." +msgstr "Geen enkele asset voldoet aan de zoekcriteria." + +#: assets/static/assets/assets_index.js:1309 +#: assets/static/assets/unified_index.js:2571 +#: assets/static/assets/unified_index.js:3330 +#: assets/static/assets/unified_index.js:3398 +msgid "Sans catégorie" +msgstr "Zonder categorie" + +#: assets/static/assets/assets_index.js:1330 +#: assets/static/assets/unified_index.js:3424 +msgid "%(count)s élément pris en compte" +msgid_plural "%(count)s éléments pris en compte" +msgstr[0] "%(count)s item meegenomen" +msgstr[1] "%(count)s items meegenomen" + +#: assets/static/assets/assets_index.js:1338 +#: assets/static/assets/unified_index.js:3429 +msgid "(sélection)" +msgstr "(selectie)" + +#: assets/static/assets/assets_index.js:1339 +#: assets/static/assets/unified_index.js:3429 +msgid "(table)" +msgstr "(tabel)" + +#: assets/static/assets/assets_index.js:1350 +#: assets/static/assets/unified_index.js:3434 +msgid "Aucune donnée disponible" +msgstr "Geen gegevens beschikbaar" + +#: assets/static/assets/assets_index.js:1383 +#: assets/static/assets/unified_index.js:3465 +msgid "au total" +msgstr "in totaal" + +#: assets/static/assets/assets_index.js:1389 +#: assets/static/assets/unified_index.js:509 +#: assets/static/assets/unified_index.js:3471 +msgid "Modèle" +msgstr "Model" + +#: assets/static/assets/assets_index.js:1390 +#: assets/static/assets/unified_index.js:3472 +msgid "Nombre" +msgstr "Nummer" + +#: assets/static/assets/assets_index.js:1391 +#: assets/static/assets/unified_index.js:3473 +msgid "Longueur (m)" +msgstr "Lengte (m)" + +#: assets/static/assets/assets_index.js:1392 +#: assets/static/assets/unified_index.js:3474 +msgid "Surface (m²)" +msgstr "Oppervlakte (m²)" + +#: assets/static/assets/assets_index.js:1396 +#: assets/static/assets/unified_index.js:3477 +msgid "Aucun modèle" +msgstr "Geen model" + +#: assets/static/assets/assets_index.js:1400 +#: assets/static/assets/unified_index.js:3480 +msgid "Total catégorie" +msgstr "Totaal per categorie" + +#: assets/static/assets/green-surfaces-comments.js:473 +#: assets/static/assets/green-surfaces-comments.js:902 +msgid "Nouveau commentaire" +msgstr "Nieuwe opmerking" + +#: assets/static/assets/green-surfaces-comments.js:480 +msgid "Textes rapides" +msgstr "Snelle teksten" + +#: assets/static/assets/green-surfaces-comments.js:482 +#: assets/static/assets/green-surfaces-comments.js:483 +#: assets/static/assets/green-surfaces-comments.js:956 +msgid "Mémo" +msgstr "Memo" + +#: assets/static/assets/green-surfaces-comments.js:485 +#: assets/static/assets/green-surfaces-comments.js:486 +msgid "Point d'attention" +msgstr "Aandachtspunt" + +#: assets/static/assets/green-surfaces-comments.js:488 +#: assets/static/assets/green-surfaces-comments.js:489 +#: assets/static/assets/green-surfaces-comments.js:962 +msgid "À faire" +msgstr "Te doen" + +#: assets/static/assets/green-surfaces-comments.js:491 +#: assets/static/assets/green-surfaces-comments.js:492 +#: assets/static/assets/green-surfaces-comments.js:967 +msgid "OK" +msgstr "OK" + +#: assets/static/assets/green-surfaces-comments.js:494 +#: assets/static/assets/green-surfaces-comments.js:495 +#: assets/static/assets/green-surfaces-comments.js:970 +msgid "Problème" +msgstr "Probleem" + +#: assets/static/assets/green-surfaces-comments.js:501 +#: assets/static/assets/green-surfaces-comments.js:977 +msgid "Commentaire" +msgstr "Opmerking" + +#: assets/static/assets/green-surfaces-comments.js:502 +msgid "Votre commentaire..." +msgstr "Uw opmerking..." + +#: assets/static/assets/green-surfaces-comments.js:506 +#: assets/static/assets/green-surfaces-comments.js:990 +msgid "Photos" +msgstr "Foto's" + +#: assets/static/assets/green-surfaces-comments.js:509 +msgid "Prendre des photos (mode continu)" +msgstr "Foto's maken (continue modus)" + +#: assets/static/assets/green-surfaces-comments.js:512 +msgid "Sélectionner des fichiers" +msgstr "Bestanden selecteren" + +#: assets/static/assets/green-surfaces-comments.js:521 +msgid "Position" +msgstr "Positie" + +#: assets/static/assets/green-surfaces-comments.js:525 +msgid "Position actuelle sera utilisée" +msgstr "Huidige positie zal worden gebruikt" + +#: assets/static/assets/green-surfaces-comments.js:528 +#: assets/static/assets/green-surfaces-comments.js:985 +msgid "Repositionner" +msgstr "Opnieuw positioneren" + +#: assets/static/assets/green-surfaces-comments.js:536 +#: assets/static/assets/green-surfaces-comments.js:1008 +msgid "Annuler" +msgstr "Annuleren" + +#: assets/static/assets/green-surfaces-comments.js:538 +#: assets/static/assets/green-surfaces-comments.js:1005 +msgid "Enregistrer" +msgstr "Opslaan" + +#: assets/static/assets/green-surfaces-comments.js:570 +msgid "Caméra" +msgstr "Camera" + +#: assets/static/assets/green-surfaces-comments.js:585 +#: assets/static/assets/green-surfaces-comments.js:1127 +msgid "Terminer" +msgstr "Voltooien" + +#: assets/static/assets/green-surfaces-comments.js:588 +#: assets/static/assets/green-surfaces-comments.js:1124 +msgid "Capturer" +msgstr "Opnemen" + +#: assets/static/assets/green-surfaces-comments.js:959 +msgid "Attention" +msgstr "Let op" + +#: assets/static/assets/green-surfaces-comments.js:977 +msgid "(optionnel)" +msgstr "(optioneel)" + +#: assets/static/assets/green-surfaces-comments.js:978 +msgid "Entrez un commentaire optionnel..." +msgstr "Voer een optionele opmerking in..." + +#: assets/static/assets/green-surfaces-comments.js:982 +msgid "Localisation" +msgstr "Locatie" + +#: assets/static/assets/green-surfaces-comments.js:983 +msgid "Calcul en cours..." +msgstr "Bezig met berekenen..." + +#: assets/static/assets/green-surfaces-comments.js:993 +msgid "Appareil photo" +msgstr "Camera" + +#: assets/static/assets/green-surfaces-comments.js:996 +msgid "Sélectionner fichiers" +msgstr "Bestanden selecteren" + +#: assets/static/assets/unified_index.js:121 +#: assets/static/assets/unified_index.js:258 +msgid "Sélectionner une catégorie..." +msgstr "Selecteer een categorie..." + +#: assets/static/assets/unified_index.js:130 +msgid "Aucune catégorie disponible" +msgstr "Geen categorie beschikbaar" + +#: assets/static/assets/unified_index.js:170 +msgid "Sélectionnez la localisation liée à ce nouvel asset." +msgstr "Selecteer de locatie gekoppeld aan deze nieuwe asset." + +#: assets/static/assets/unified_index.js:200 +msgid "Aucune localisation disponible pour créer cet asset." +msgstr "Geen locatie beschikbaar om deze asset aan te maken." + +#: assets/static/assets/unified_index.js:209 +msgid "Impossible de charger les localisations pour cette thématique." +msgstr "Kan locaties voor dit thema niet laden." + +#: assets/static/assets/unified_index.js:294 +msgid "Créer un asset" +msgstr "Een asset aanmaken" + +#: assets/static/assets/unified_index.js:308 +msgid "Le code est généré automatiquement." +msgstr "De code wordt automatisch gegenereerd." + +#: assets/static/assets/unified_index.js:444 +#: assets/static/assets/unified_index.js:449 +msgid "Erreur lors du chargement du formulaire." +msgstr "Fout bij het laden van het formulier." + +#: assets/static/assets/unified_index.js:466 +msgid "Créer une localisation" +msgstr "Een locatie aanmaken" + +#: assets/static/assets/unified_index.js:467 +#: assets/static/assets/unified_index.js:506 +msgid "Type" +msgstr "Type" + +#: assets/static/assets/unified_index.js:490 +msgid "Zoom" +msgstr "Zoom" + +#: assets/static/assets/unified_index.js:505 +msgid "Nom" +msgstr "Naam" + +#: assets/static/assets/unified_index.js:507 +msgid "Thématique" +msgstr "Thema" + +#: assets/static/assets/unified_index.js:508 +msgid "Catégorie" +msgstr "Categorie" + +#: assets/static/assets/unified_index.js:783 +#: assets/static/assets/unified_index.js:784 +msgid "Sélectionner une thématique..." +msgstr "Selecteer een thema..." + +#: assets/static/assets/unified_index.js:783 +#: assets/static/assets/unified_index.js:784 +msgid "Aucune thématique disponible" +msgstr "Geen thema beschikbaar" + +#: assets/static/assets/unified_index.js:791 +msgid "Vous n'avez pas de droits de création" +msgstr "U heeft geen rechten om aan te maken" + +#: assets/static/assets/unified_index.js:809 +msgid "Sélectionnez une thématique pour créer un asset." +msgstr "Selecteer een thema om een asset aan te maken." + +#: assets/static/assets/unified_index.js:821 +msgid "Sélectionnez une thématique pour créer une localisation." +msgstr "Selecteer een thema om een locatie aan te maken." + +#: assets/static/assets/unified_index.js:840 +msgid "Sélectionnez une localisation avant de créer cet asset." +msgstr "Selecteer een locatie voordat u deze asset aanmaakt." + +#: assets/static/assets/unified_index.js:871 +msgid "Type d'asset introuvable pour ce modèle." +msgstr "Assettype niet gevonden voor dit model." + +#: assets/static/assets/unified_index.js:901 +msgid "Asset créé." +msgstr "Asset aangemaakt." + +#: assets/static/assets/unified_index.js:912 +#: assets/static/assets/unified_index.js:959 +msgid "Erreur lors de la création." +msgstr "Fout bij het aanmaken." + +#: assets/static/assets/unified_index.js:916 +#: assets/static/assets/unified_index.js:963 +msgid "Erreur de connexion." +msgstr "Verbindingsfout." + +#: assets/static/assets/unified_index.js:952 +msgid "Localisation créée." +msgstr "Locatie aangemaakt." + +#: assets/static/assets/unified_index.js:1359 +msgid "Veuillez sélectionner au moins un asset." +msgstr "Selecteer ten minste één asset." + +#: assets/static/assets/unified_index.js:1423 +msgid "" +"Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre " +"pour charger le tableau et la carte." +msgstr "" + +#: assets/static/assets/unified_index.js:1671 +#: assets/static/assets/unified_index.js:1702 +msgid "Voir dans le tableau" +msgstr "Bekijk in de tabel" + +#: assets/static/assets/unified_index.js:1711 +msgid "autre(s) asset(s)" +msgstr "andere asset(s)" + +#: assets/static/assets/unified_index.js:1715 +msgid "autre(s) localisation(s)" +msgstr "andere locatie(s)" + +#: assets/static/assets/unified_index.js:1720 +msgid "Assets trouvés" +msgstr "Gevonden assets" + +#: assets/static/assets/unified_index.js:1728 +msgid "Localisations trouvées" +msgstr "Gevonden locaties" + +#: assets/static/assets/unified_index.js:2209 +msgid "Vue initiale (Bruxelles)" +msgstr "Eerste weergave (Brussel)" + +#: assets/static/assets/unified_index.js:2349 +msgid "sélectionné(s)" +msgstr "geselecteerd" + +#: assets/static/assets/unified_index.js:2889 +msgid "Aucun type sélectionné" +msgstr "Geen type geselecteerd" + +#: assets/static/assets/unified_index.js:2893 +msgid "Aucun type disponible." +msgstr "Geen type beschikbaar." + +#: assets/static/assets/unified_index.js:3034 +#: assets/static/assets/unified_index.js:3204 +#: assets/static/assets/unified_index.js:3262 +msgid "Sélectionnez d'abord une ou plusieurs thématiques." +msgstr "Selecteer eerst een of meer thema's." + +#: assets/static/assets/unified_index.js:3104 +msgid "Tous" +msgstr "Alle" + +#: assets/static/assets/unified_index.js:3269 +msgid "Sélectionnez d'abord une ou plusieurs catégories." +msgstr "Selecteer eerst een of meer categorieën." + +#: assets/static/assets/unified_index.js:3285 +msgid "Chargement des modèles..." +msgstr "Modellen laden..." + +#: assets/static/assets/unified_index.js:3296 +msgid "Aucun modèle disponible pour les catégories sélectionnées." +msgstr "Geen model beschikbaar voor de geselecteerde categorieën." + +#: assets/static/assets/unified_index.js:3307 +msgid "Afficher les modèles" +msgstr "Modellen tonen" + +#: assets/static/assets/unified_index.js:3308 +msgid "Replier les modèles" +msgstr "Modellen inklappen" + +#: assets/static/assets/unified_index.js:3313 +msgid "" +"Bloc replié. Cliquez sur \"Afficher les modèles\" pour voir les options." +msgstr "" + +#~ msgid "Erreur lors de la liaison" +#~ msgstr "Fout bij het koppelen" + +#~ msgid "Erreur lors de la désélection" +#~ msgstr "Fout bij het deselecteren" + +# Asset Template Selection Modal +#~ msgid "Aucun template correspondant" +#~ msgstr "Geen overeenkomstige sjabloon" + +#~ msgid "" +#~ "Le modèle ou la catégorie de cet équipement ne correspond à aucun " +#~ "template d'opérations automatique." +#~ msgstr "" +#~ "Het model of de categorie van deze uitrusting komt niet overeen met een " +#~ "automatisch bewerkingssjabloon." + +#~ msgid "-- Sélectionner un template --" +#~ msgstr "-- Selecteer een sjabloon --" diff --git a/streetup/assets/management/__init__.py b/streetup/assets/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/assets/management/commands/__init__.py b/streetup/assets/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/assets/management/commands/assets_at_date.py b/streetup/assets/management/commands/assets_at_date.py new file mode 100644 index 0000000..efffc27 --- /dev/null +++ b/streetup/assets/management/commands/assets_at_date.py @@ -0,0 +1,199 @@ +""" +Management command to query assets by status at a specific date. +Usage: python manage.py assets_at_date +Examples: + python manage.py assets_at_date signpole active 2025-01-15 + python manage.py assets_at_date assets.signpole active 2025-01-15 # Specify app + python manage.py assets_at_date sign.signpole active 2025-01-15 # Specify app +""" + +from django.core.management.base import BaseCommand +from django.contrib.contenttypes.models import ContentType +from django.utils.dateparse import parse_datetime, parse_date +from django.utils import timezone +from datetime import datetime +from assets.models import AssetHistory + + +class Command(BaseCommand): + help = 'Query assets by status at a specific date (supports app_label.model_name format)' + + def add_arguments(self, parser): + parser.add_argument( + 'asset_model', + type=str, + help='The asset model, optionally with app label (e.g., signpole or assets.signpole)' + ) + parser.add_argument( + 'status', + type=str, + nargs='?', + default=None, + help='Status to filter by (e.g., active, removed) - optional' + ) + parser.add_argument( + 'date', + type=str, + help='Date in ISO format (e.g., 2025-01-15 or 2025-01-15T12:00:00)' + ) + + def handle(self, *args, **options): + asset_model_input = options['asset_model'] + status_filter = options.get('status') + date_str = options['date'] + + # Parse date + try: + target_date = parse_datetime(date_str) + if not target_date: + parsed_date = parse_date(date_str) + if not parsed_date: + raise ValueError("Invalid date format") + target_date = datetime.combine(parsed_date, datetime.min.time()) + target_date = timezone.make_aware(target_date) + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Invalid date format: {e}") + ) + return + + # Parse asset model (may include app_label) + if '.' in asset_model_input: + app_label, model_name = asset_model_input.split('.', 1) + content_types = ContentType.objects.filter(app_label=app_label, model=model_name.lower()) + else: + model_name = asset_model_input.lower() + content_types = list(ContentType.objects.filter(model=model_name)) + + # Handle multiple matches + if not content_types: + self.stdout.write( + self.style.ERROR(f"Model '{asset_model_input}' not found") + ) + return + + if len(content_types) > 1: + self.stdout.write( + self.style.WARNING(f"Multiple models found for '{asset_model_input}':") + ) + for ct in content_types: + self.stdout.write(f" - {ct.app_label}.{ct.model}") + self.stdout.write( + self.style.WARNING("Using the first one. Specify with 'app_label.model' to choose.\n") + ) + + content_type = content_types[0] + asset_model_class = content_type.model_class() + + self.stdout.write( + self.style.SUCCESS( + f"\nAssets of type '{content_type.app_label}.{content_type.model}' on {target_date.date()}" + ) + ) + if status_filter: + self.stdout.write(f" Status: {status_filter}\n") + + # Get all assets created by the date + created_by_date = list(AssetHistory.objects.filter( + content_type=content_type, + action_type='create', + timestamp__lte=target_date + ).values_list('object_id', flat=True).distinct()) + + if not created_by_date: + self.stdout.write(self.style.WARNING(" No assets created by this date")) + return + + # Get all deletions in one query + deleted_ids = set(AssetHistory.objects.filter( + content_type=content_type, + object_id__in=created_by_date, + action_type='delete', + timestamp__lte=target_date + ).values_list('object_id', flat=True)) + + # Get last status for each asset in one query using subquery + from django.db.models import OuterRef, Subquery + latest_status = AssetHistory.objects.filter( + content_type=content_type, + object_id=OuterRef('object_id'), + field_name='status', + timestamp__lte=target_date + ).order_by('-timestamp').values('new_value')[:1] + + statuses = {} + status_entries = AssetHistory.objects.filter( + content_type=content_type, + object_id__in=created_by_date, + field_name='status', + timestamp__lte=target_date + ).values('object_id').annotate( + last_status=Subquery(latest_status) + ).values('object_id', 'last_status') + + for entry in status_entries: + statuses[entry['object_id']] = entry['last_status'] + + # Get all assets in one query for faster access + assets_dict = {asset.id: asset for asset in asset_model_class.objects.filter(pk__in=created_by_date)} + + # Build results + results = [] + for asset_id in created_by_date: + # Check if deleted + if asset_id in deleted_ids: + status = 'DELETED' + else: + # Get status from pre-fetched data + status = statuses.get(asset_id) + if not status: + # Fallback to current asset status + asset_obj = assets_dict.get(asset_id) + if asset_obj: + status = getattr(asset_obj, 'status', None) + + if status_filter and status != status_filter: + continue + + # Get asset details from pre-fetched dict + asset = assets_dict.get(asset_id) + if asset: + results.append({ + 'id': asset_id, + 'code': asset.code, + 'status': status, + 'name': asset.get_name() if hasattr(asset, 'get_name') else str(asset) + }) + else: + results.append({ + 'id': asset_id, + 'code': f'ID: {asset_id}', + 'status': status, + 'name': '(Deleted)' + }) + + # Display results + if not results: + self.stdout.write( + self.style.WARNING( + f" No assets found with the given criteria" + ) + ) + return + + self.stdout.write(f" Found {len(results)} asset(s):\n") + + for item in results: + status_color = { + 'active': self.style.SUCCESS, + 'removed': self.style.ERROR, + 'damaged': self.style.WARNING, + 'DELETED': self.style.ERROR, + }.get(item['status'], self.style.WARNING) + + status_display = status_color(f"[{item['status']}]") + self.stdout.write( + f" • {item['code']:20} {status_display:20} {item['name']}" + ) + + self.stdout.write(self.style.SUCCESS(f"\n Total: {len(results)} asset(s)\n")) diff --git a/streetup/assets/management/commands/assets_import_template.xlsx b/streetup/assets/management/commands/assets_import_template.xlsx new file mode 100644 index 0000000..3107518 Binary files /dev/null and b/streetup/assets/management/commands/assets_import_template.xlsx differ diff --git a/streetup/assets/management/commands/export_greensurfaces_to_xlsx.py b/streetup/assets/management/commands/export_greensurfaces_to_xlsx.py new file mode 100644 index 0000000..a15f671 --- /dev/null +++ b/streetup/assets/management/commands/export_greensurfaces_to_xlsx.py @@ -0,0 +1,165 @@ +from collections import defaultdict +from datetime import datetime +import re + +from openpyxl import Workbook + +from django.core.management.base import BaseCommand + +from assets.models import NatureGreenSurface + + +ILLEGAL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") + + +def sanitize(value): + if isinstance(value, str): + return ILLEGAL_CHAR_RE.sub("", value) + return value + + +def autosize_columns(ws): + for column in ws.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + value = "" if cell.value is None else str(cell.value) + if len(value) > max_length: + max_length = len(value) + ws.column_dimensions[column_letter].width = min(max(12, max_length + 2), 60) + + +class Command(BaseCommand): + help = ( + "Exporte les assets NatureGreenSurface dans un fichier XLSX avec " + "une feuille de donnees brutes et une feuille de synthese par type de surface." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--output", + type=str, + default=None, + help="Chemin du fichier XLSX de sortie.", + ) + parser.add_argument( + "--include-removed", + action="store_true", + default=False, + help="Inclut les surfaces avec le statut 'removed'.", + ) + + def handle(self, *args, **options): + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = options["output"] or f"greensurfaces_export_{timestamp}.xlsx" + + queryset = NatureGreenSurface.objects.select_related("model", "location").order_by("code") + if not options["include_removed"]: + queryset = queryset.exclude(status="removed") + + wb = Workbook() + + # Feuille 1: donnees brutes + ws_raw = wb.active + ws_raw.title = "Donnees" + raw_headers = [ + "ID", + "Code", + "Nom FR", + "Nom NL", + "Statut", + "Type surface (code)", + "Type surface (nom FR)", + "Type surface (nom NL)", + "Categorie (code)", + "Categorie (nom FR)", + "Categorie (nom NL)", + "Localisation (code)", + "Localisation (nom FR)", + "Localisation (nom NL)", + "Superficie", + "Pente", + "Densite", + "Largeur", + "Longueur", + "Longitude", + "Latitude", + ] + ws_raw.append(raw_headers) + + summary = defaultdict(lambda: {"count": 0, "surface_sum": 0.0}) + + for surface in queryset: + model = surface.model + category = model.category if model and model.category else None + location = surface.location + + model_key = ( + model.code if model and model.code else "SANS_TYPE", + model.name_fr if model and model.name_fr else "Sans type", + model.name_nl if model and model.name_nl else "", + ) + + surface_area = float(surface.surface_area or 0) + summary[model_key]["count"] += 1 + summary[model_key]["surface_sum"] += surface_area + + ws_raw.append( + [ + surface.id, + sanitize(surface.code), + sanitize(surface.name_fr), + sanitize(surface.name_nl), + sanitize(surface.status), + sanitize(model.code if model else None), + sanitize(model.name_fr if model else None), + sanitize(model.name_nl if model else None), + sanitize(category.code if category else None), + sanitize(category.name_fr if category else None), + sanitize(category.name_nl if category else None), + sanitize(location.code if location else None), + sanitize(location.name_fr if location else None), + sanitize(location.name_nl if location else None), + surface.surface_area, + surface.slope, + surface.density, + surface.width, + surface.length, + surface.lon, + surface.lat, + ] + ) + + autosize_columns(ws_raw) + + # Feuille 2: synthese + ws_summary = wb.create_sheet(title="Synthese") + summary_headers = [ + "Type surface (code)", + "Type surface (nom FR)", + "Type surface (nom NL)", + "Nombre", + "Superficie totale", + ] + ws_summary.append(summary_headers) + + for (model_code, model_name_fr, model_name_nl), values in sorted( + summary.items(), key=lambda item: (item[0][1], item[0][0]) + ): + ws_summary.append( + [ + sanitize(model_code), + sanitize(model_name_fr), + sanitize(model_name_nl), + values["count"], + round(values["surface_sum"], 2), + ] + ) + + ws_summary.append(["TOTAL", "", "", f"=SUM(D2:D{ws_summary.max_row})", f"=SUM(E2:E{ws_summary.max_row})"]) + + autosize_columns(ws_summary) + + wb.save(output_path) + self.stdout.write(self.style.SUCCESS(f"Export termine: {output_path}")) + self.stdout.write(self.style.SUCCESS(f"Nombre de surfaces exportees: {queryset.count()}")) diff --git a/streetup/assets/management/commands/import_assets_from_excel.py b/streetup/assets/management/commands/import_assets_from_excel.py new file mode 100644 index 0000000..e8527fd --- /dev/null +++ b/streetup/assets/management/commands/import_assets_from_excel.py @@ -0,0 +1,909 @@ +""" +Commande Django pour importer des assets depuis un fichier Excel. + +La commande lit un fichier .xlsx dont les colonnes sont : + thématique | nom_modèle_objet | code | nom_fr | nom_nl | + code_catégorie | code_modèle | code_localisation | geojson | + lon | lat | statut | codes_parents | codes_enfants | + marque | date_installation | structure + +Usage : + python manage.py import_assets_from_excel chemin/vers/fichier.xlsx + python manage.py import_assets_from_excel chemin/vers/fichier.xlsx --dry-run + python manage.py import_assets_from_excel chemin/vers/fichier.xlsx --update + python manage.py import_assets_from_excel --generate-template [--template-output assets_template.xlsx] +""" + +import ast +import json +import re +from datetime import datetime + +import openpyxl +from openpyxl.styles import Alignment, Border, Font, PatternFill, Side +from openpyxl.utils import get_column_letter + +from django.apps import apps +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.geos import GEOSGeometry, Point +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from django.utils import timezone + +from assets.models import ( + AbstractAsset, + AbstractGeoAsset, + AbstractLocation, + AssetCategory, + ASSET_STATUS_CHOICES, +) +from common.models import Thematic + + +# --------------------------------------------------------------------------- +# Column definitions (normalized header → Excel label) +# --------------------------------------------------------------------------- + +COLUMNS = [ + ("thématique", "Thématique (code)"), + ("nom_modèle_objet", "Nom du modèle d'objet (ex: structuregeoasset)"), + ("code", "Code"), + ("nom_fr", "Nom FR"), + ("nom_nl", "Nom NL"), + ("code_catégorie", "Code de la catégorie"), + ("code_modèle", "Code du modèle"), + ("code_localisation", "Code de la localisation"), + ("geojson", "GeoJSON"), + ("lon", "Lon"), + ("lat", "Lat"), + ("statut", "Statut"), + ("codes_parents", "Codes des assets parents ( [code1, code2] )"), + ("codes_enfants", "Codes des assets enfants ( [code1, code2] )"), + ("marque", "Marque"), + ("date_installation", "Date d'installation (YYYY-MM-DD)"), + ("structure", "Structure (code)"), +] + +HEADER_KEYS = [col[0] for col in COLUMNS] + +COLUMN_HINTS = { + "thématique": "Code de la thématique, ex: structures, trafficlights, controlcenters…", + "nom_modèle_objet": "Nom du modèle Django en minuscules, ex: structuregeoasset, itsgeoasset, controlcentergeoasset", + "code": "Code unique de l'asset", + "nom_fr": "Nom en français", + "nom_nl": "Naam in het Nederlands", + "code_catégorie": "Code de l'AssetCategory", + "code_modèle": "Code du modèle d'asset (StructureAssetModel.code, etc.)", + "code_localisation": "Code de la localisation (StructureLocation.code, ITSLocation.code…)", + "geojson": 'GeoJSON valide (type Point/Polygon…), ex: {"type":"Point","coordinates":[4.35,50.85]}', + "lon": "Longitude (WGS84) – facultatif si geojson fourni", + "lat": "Latitude (WGS84) – facultatif si geojson fourni", + "statut": "active | planned | damaged | to_replace | removed | to_be_approved", + "codes_parents": 'Codes des assets parents entre crochets, ex: [CODE1, CODE2]', + "codes_enfants": 'Codes des assets enfants entre crochets, ex: [CODE3]', + "marque": "Marque / fabricant", + "date_installation": "Date d'installation au format YYYY-MM-DD", + "structure": "Code de l'objet Structure (uniquement pour StructureGeoAsset)", +} + +EXAMPLE_ROW = { + "thématique": "structures", + "nom_modèle_objet": "structuregeoasset", + "code": "STR-001", + "nom_fr": "Pont de l'exemple", + "nom_nl": "Voorbeeldbrug", + "code_catégorie": "CAT_STRUCTURE", + "code_modèle": "MOD_PONT", + "code_localisation": "LOC_001", + "geojson": '{"type":"Point","coordinates":[4.3517,50.8503]}', + "lon": "4.3517", + "lat": "50.8503", + "statut": "active", + "codes_parents": "[STR-000]", + "codes_enfants": "[STR-002, STR-003]", + "marque": "Fabricant SA", + "date_installation": "2024-01-15", + "structure": "OA-001", +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _normalize_key(s: str) -> str: + """Lowercases and strips a string for column matching.""" + return s.strip().lower().replace(" ", "_").replace("'", "_").replace("(", "").replace(")", "") + + +def _parse_codes_list(raw: str) -> list[str]: + """Parse '[CODE1, CODE2]' or 'CODE1,CODE2' into a list of strings.""" + if not raw: + return [] + raw = str(raw).strip() + if raw.startswith("["): + # Remove brackets and split + inner = raw.strip("[]") + parts = [p.strip() for p in inner.split(",") if p.strip()] + return parts + return [p.strip() for p in raw.split(",") if p.strip()] + + +def _parse_date(raw) -> datetime | None: + """Try to parse a date string (YYYY-MM-DD or DD/MM/YYYY).""" + if not raw: + return None + if isinstance(raw, datetime): + return raw + raw = str(raw).strip() + for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y", "%Y/%m/%d"): + try: + return datetime.strptime(raw, fmt) + except ValueError: + continue + return None + + +def _get_model_class(model_name: str): + """Return the Django model class for a given model name (case-insensitive).""" + model_name_lower = model_name.strip().lower() + cts = ContentType.objects.filter(model=model_name_lower) + if not cts.exists(): + return None + # Prefer assets app + cts_sorted = sorted(cts, key=lambda ct: (0 if ct.app_label == "assets" else 1)) + for ct in cts_sorted: + mc = ct.model_class() + if mc is not None: + return mc + return None + + +def _find_asset_by_code(code: str): + """Search all AbstractAsset subclasses for an asset with the given code.""" + for model in apps.get_models(): + if model._meta.abstract: + continue + try: + if not issubclass(model, AbstractAsset): + continue + except TypeError: + continue + obj = model.objects.filter(code=code).first() + if obj: + return obj + return None + + +def _find_location_by_code(model_class, code: str): + """ + Find a location instance for an asset model class by code. + Looks at FK fields that point to AbstractLocation subclasses. + """ + for field in model_class._meta.get_fields(): + if not hasattr(field, "related_model") or field.related_model is None: + continue + if not hasattr(field, "column"): + continue # skip reverse relations + try: + if issubclass(field.related_model, AbstractLocation): + location = field.related_model.objects.filter(code=code).first() + if location: + return field.name, location + except TypeError: + continue + return None, None + + +def _find_model_instance_by_code(model_class, code: str): + """ + Find an AssetModel instance for an asset class by code. + Looks for the 'model' FK field. + """ + try: + model_field = model_class._meta.get_field("model") + related_model = model_field.related_model + return related_model.objects.filter(code=code).first() + except Exception: + return None + + +def _set_geom_from_geojson(asset, geojson_str: str): + """Parse a GeoJSON string and set asset.geom (in SRID 3812).""" + try: + geom_data = json.loads(geojson_str) + if geom_data.get("type") == "Feature": + geom_json = json.dumps(geom_data.get("geometry", {})) + else: + geom_json = json.dumps(geom_data) + geom = GEOSGeometry(geom_json, srid=4326) + geom.transform(3812) + asset.geom = geom + # Update lon/lat from geometry centroid + geom_wgs84 = geom.transform(4326, clone=True) + if geom_wgs84.geom_type == "Point": + asset.lon = geom_wgs84.x + asset.lat = geom_wgs84.y + else: + centroid = geom_wgs84.centroid + asset.lon = centroid.x + asset.lat = centroid.y + return True + except Exception as e: + return False + + +def _set_geom_from_lon_lat(asset, lon: float, lat: float): + """Set asset.geom (in SRID 3812) from WGS84 lon/lat.""" + pt = Point(lon, lat, srid=4326) + pt.transform(3812) + asset.geom = pt + asset.lon = lon + asset.lat = lat + + +def _set_parent_fk(asset, parent_asset, model_class, excluded=None): + """ + Find the FK field on 'asset' (model_class) that points to the type of + 'parent_asset' and set it. + Returns (True, field_name) on success or (False, tried_field_names) on failure. + """ + if excluded is None: + excluded = {"model", "category", "replaced_by", "marked_as_removed_by", "updated_by", + "marked_as_removed_by"} + parent_class = type(parent_asset) + tried = [] + for field in model_class._meta.get_fields(): + if not hasattr(field, "related_model") or field.related_model is None: + continue + if not hasattr(field, "column"): + continue # skip reverse relations + if field.name in excluded: + continue + # Only consider FK fields pointing to AbstractAsset subclasses + try: + if not issubclass(field.related_model, AbstractAsset): + continue + except TypeError: + continue + tried.append(f"{field.name}→{field.related_model.__name__}") + try: + if issubclass(parent_class, field.related_model): + setattr(asset, field.name, parent_asset) + return True, field.name + except TypeError: + continue + return False, tried + + +def _set_child_fk(child_asset, current_asset, excluded=None): + """ + Find the FK field on 'child_asset' that points to the type of 'current_asset' + and set it on the child. + Returns (True, field_name) on success or (False, tried_field_names) on failure. + """ + if excluded is None: + excluded = {"model", "category", "replaced_by", "marked_as_removed_by", "updated_by", + "marked_as_removed_by"} + current_class = type(current_asset) + child_class = type(child_asset) + tried = [] + for field in child_class._meta.get_fields(): + if not hasattr(field, "related_model") or field.related_model is None: + continue + if not hasattr(field, "column"): + continue + if field.name in excluded: + continue + # Only consider FK fields pointing to AbstractAsset subclasses + try: + if not issubclass(field.related_model, AbstractAsset): + continue + except TypeError: + continue + tried.append(f"{field.name}→{field.related_model.__name__}") + try: + if issubclass(current_class, field.related_model): + setattr(child_asset, field.name, current_asset) + return True, field.name + except TypeError: + continue + return False, tried + + +# --------------------------------------------------------------------------- +# Excel template generation +# --------------------------------------------------------------------------- + +def generate_template(output_path: str): + """Generate a nicely formatted Excel template file.""" + wb = openpyxl.Workbook() + ws_data = wb.active + ws_data.title = "Assets" + + # --- Styles --- + header_fill = PatternFill("solid", fgColor="1F4E79") + hint_fill = PatternFill("solid", fgColor="D6E4F0") + example_fill = PatternFill("solid", fgColor="E2EFDA") + header_font = Font(bold=True, color="FFFFFF", size=11) + hint_font = Font(italic=True, color="555555", size=9) + example_font = Font(color="375623", size=10) + wrap = Alignment(wrap_text=True, vertical="top") + thin_border_side = Side(border_style="thin", color="AAAAAA") + thin_border = Border( + left=thin_border_side, right=thin_border_side, + top=thin_border_side, bottom=thin_border_side, + ) + + # Row 1 – Column keys (hidden row used for mapping) + for col_idx, (key, label) in enumerate(COLUMNS, start=1): + cell = ws_data.cell(row=1, column=col_idx, value=key) + cell.font = Font(size=8, color="999999") + cell.fill = PatternFill("solid", fgColor="F2F2F2") + + # Row 2 – Human-readable headers + for col_idx, (key, label) in enumerate(COLUMNS, start=1): + cell = ws_data.cell(row=2, column=col_idx, value=label) + cell.font = header_font + cell.fill = header_fill + cell.alignment = wrap + cell.border = thin_border + + # Row 3 – Hints + for col_idx, key in enumerate(HEADER_KEYS, start=1): + cell = ws_data.cell(row=3, column=col_idx, value=COLUMN_HINTS.get(key, "")) + cell.font = hint_font + cell.fill = hint_fill + cell.alignment = wrap + cell.border = thin_border + + # Row 4 – Example + for col_idx, key in enumerate(HEADER_KEYS, start=1): + cell = ws_data.cell(row=4, column=col_idx, value=EXAMPLE_ROW.get(key, "")) + cell.font = example_font + cell.fill = example_fill + cell.alignment = wrap + cell.border = thin_border + + # Row 5+ – Empty data rows + for row in range(5, 25): + for col_idx in range(1, len(COLUMNS) + 1): + cell = ws_data.cell(row=row, column=col_idx, value="") + cell.border = thin_border + cell.alignment = Alignment(vertical="top") + + # Column widths + COLUMN_WIDTHS = { + 1: 20, # thématique + 2: 35, # nom_modèle_objet + 3: 20, # code + 4: 30, # nom_fr + 5: 30, # nom_nl + 6: 25, # code_catégorie + 7: 25, # code_modèle + 8: 25, # code_localisation + 9: 50, # geojson + 10: 12, # lon + 11: 12, # lat + 12: 18, # statut + 13: 35, # codes_parents + 14: 35, # codes_enfants + 15: 25, # marque + 16: 22, # date_installation + 17: 20, # structure + } + for col_idx, width in COLUMN_WIDTHS.items(): + ws_data.column_dimensions[get_column_letter(col_idx)].width = width + + ws_data.row_dimensions[1].height = 14 + ws_data.row_dimensions[2].height = 30 + ws_data.row_dimensions[3].height = 50 + ws_data.row_dimensions[4].height = 25 + + # Freeze panes below row 4 and after column A + ws_data.freeze_panes = "B5" + + # --- Second sheet: reference values --- + ws_ref = wb.create_sheet("Référence") + ref_header = Font(bold=True, color="FFFFFF") + ref_fill = PatternFill("solid", fgColor="2E75B6") + + ws_ref["A1"] = "Valeurs valides pour le champ 'statut'" + ws_ref["A1"].font = ref_header + ws_ref["A1"].fill = ref_fill + ws_ref["A1"].alignment = Alignment(horizontal="center") + ws_ref.merge_cells("A1:B1") + + ws_ref["A2"] = "Clé" + ws_ref["B2"] = "Libellé" + ws_ref["A2"].font = Font(bold=True) + ws_ref["B2"].font = Font(bold=True) + + status_labels = { + "to_be_approved": "À approuver", + "planned": "Planifié", + "active": "Actif", + "damaged": "Endommagé", + "to_replace": "À remplacer", + "removed": "Enlevé", + } + for row_idx, (k, v) in enumerate(status_labels.items(), start=3): + ws_ref.cell(row=row_idx, column=1, value=k) + ws_ref.cell(row=row_idx, column=2, value=v) + + row_after_status = 3 + len(status_labels) + 2 + + ws_ref.cell(row=row_after_status, column=1, value="Exemples de modèles d'objets (nom_modèle_objet)").font = Font(bold=True) + ws_ref.merge_cells(f"A{row_after_status}:B{row_after_status}") + + examples = [ + ("structuregeoasset", "StructureGeoAsset – Ouvrages d'art"), + ("itsgeoasset", "ITSGeoAsset – ITS"), + ("controlcentergeoasset", "ControlCenterGeoAsset – Centres de contrôle"), + ("trafficlightpole", "TrafficLightPole – Feux de circulation (poteaux)"), + ("trafficlightlantern", "TrafficLightLantern – Lanternes"), + ("trafficlightcontroller","TrafficLightController – Régulateurs"), + ("trafficlightdetector", "TrafficLightDetector – Détecteurs"), + ("trafficlightcabinet", "TrafficLightElectricalCabinet – Armoires"), + ("publiclightingpole", "PublicLightingPole – Éclairage public (mâts)"), + ("publiclightinglightfixture", "PublicLightingLightFixture – Luminaires"), + ("naturetree", "NatureTree – Arbres"), + ("natureshrub", "NatureShrub – Arbustes"), + ("naturebench", "NatureBench – Bancs"), + ("naturegreensurface", "NatureGreenSurface – Surfaces vertes"), + ("roadfurniture", "RoadFurniture – Mobilier routier"), + ] + for row_idx, (model, desc) in enumerate(examples, start=row_after_status + 1): + ws_ref.cell(row=row_idx, column=1, value=model) + ws_ref.cell(row=row_idx, column=2, value=desc) + + ws_ref.column_dimensions["A"].width = 35 + ws_ref.column_dimensions["B"].width = 50 + + wb.save(output_path) + return output_path + + +# --------------------------------------------------------------------------- +# Main command +# --------------------------------------------------------------------------- + +class Command(BaseCommand): + help = ( + "Importe des assets depuis un fichier Excel (.xlsx). " + "Utilisez --generate-template pour créer le fichier modèle." + ) + + def add_arguments(self, parser): + parser.add_argument( + "excel_file", + nargs="?", + type=str, + help="Chemin vers le fichier Excel à importer.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + default=False, + help="Simule l'import sans écrire dans la base de données.", + ) + parser.add_argument( + "--update", + action="store_true", + default=False, + help="Met à jour les assets existants si le code existe déjà.", + ) + parser.add_argument( + "--generate-template", + action="store_true", + default=False, + help="Génère un fichier Excel modèle.", + ) + parser.add_argument( + "--template-output", + type=str, + default="assets_import_template.xlsx", + help="Chemin de sortie du fichier modèle (défaut: assets_import_template.xlsx).", + ) + + # ------------------------------------------------------------------ + def handle(self, *args, **options): + # --- Template generation mode --- + if options["generate_template"]: + output = options["template_output"] + path = generate_template(output) + self.stdout.write(self.style.SUCCESS(f"✓ Template généré: {path}")) + return + + # --- Import mode --- + excel_file = options.get("excel_file") + if not excel_file: + raise CommandError( + "Veuillez fournir le chemin du fichier Excel, " + "ou utilisez --generate-template pour créer le modèle." + ) + + dry_run = options["dry_run"] + update = options["update"] + + if dry_run: + self.stdout.write(self.style.WARNING("⚠ Mode dry-run activé – aucune donnée ne sera enregistrée.")) + + self._import(excel_file, dry_run=dry_run, update=update) + + # ------------------------------------------------------------------ + def _import(self, excel_file: str, dry_run: bool, update: bool): + try: + wb = openpyxl.load_workbook(excel_file, data_only=True) + except FileNotFoundError: + raise CommandError(f"Fichier introuvable: {excel_file}") + except Exception as e: + raise CommandError(f"Impossible d'ouvrir le fichier: {e}") + + ws = wb.active + + # --- Detect header row --- + header_row_idx, col_map, data_start_row = self._detect_header(ws) + if header_row_idx is None: + raise CommandError( + "Impossible de trouver la ligne d'en-tête. " + "Assurez-vous que la ligne 1 ou 2 contient les clés de colonnes." + ) + + self.stdout.write(f"→ En-tête détectée à la ligne {header_row_idx}, données à partir de la ligne {data_start_row}.") + + # Collect data rows (skip header, hint and example rows) + data_rows = [] + for row in ws.iter_rows(min_row=data_start_row, values_only=True): + # Skip completely empty rows + if all(cell is None or str(cell).strip() == "" for cell in row): + continue + data_rows.append(row) + + self.stdout.write(f"→ {len(data_rows)} ligne(s) à traiter.") + + # Two-pass approach + # Pass 1: create / update assets + # Pass 2: resolve parent/child FK links + created_assets = {} # row_index → asset instance + rows_data = [] # store parsed row dicts + + errors = [] + stats = {"created": 0, "updated": 0, "skipped": 0, "errors": 0} + + # Consume data_rows into parsed dicts, skip hint row(s) written by template + parsed_rows = [] + for row in data_rows: + row_dict = {} + for key, col_idx in col_map.items(): + value = row[col_idx] if col_idx < len(row) else None + row_dict[key] = value if value is not None else "" + # Skip if 'code' is empty + if not str(row_dict.get("code", "")).strip(): + continue + parsed_rows.append(row_dict) + + self.stdout.write(f"→ {len(parsed_rows)} ligne(s) valides (avec code).") + + with transaction.atomic(): + # ------ PASS 1: create / update assets ------ + for line_num, row_dict in enumerate(parsed_rows, start=1): + try: + asset, action = self._process_row(row_dict, update=update, dry_run=dry_run) + if asset is not None: + created_assets[line_num] = asset + rows_data.append((line_num, row_dict)) + stats[action] += 1 + self.stdout.write( + f" Ligne {line_num}: [{action.upper()}] code={row_dict['code']}" + ) + else: + stats["skipped"] += 1 + self.stdout.write( + self.style.WARNING(f" Ligne {line_num}: [IGNORÉ] code={row_dict['code']}") + ) + except Exception as e: + stats["errors"] += 1 + msg = f" Ligne {line_num}: [ERREUR] code={row_dict.get('code', '?')} – {e}" + self.stderr.write(self.style.ERROR(msg)) + errors.append(msg) + + # ------ PASS 2: parent/child links ------ + for line_num, row_dict in rows_data: + asset = created_assets.get(line_num) + if asset is None: + continue + + model_class = type(asset) + save_needed = False + + # Parent links + parent_codes = _parse_codes_list(str(row_dict.get("codes_parents", ""))) + for pcode in parent_codes: + parent = _find_asset_by_code(pcode) + if parent is None: + self.stderr.write( + f" Ligne {line_num}: [LIEN] parent '{pcode}' introuvable dans la base – lien ignoré." + ) + continue + if not dry_run: + # Direction 1 : FK sur le nouvel asset → parent + ok, tried_fields = _set_parent_fk(asset, parent, model_class) + if ok: + save_needed = True + self.stdout.write( + f" Ligne {line_num}: [LIEN] parent '{pcode}' ({type(parent).__name__}) lié via FK." + ) + else: + # Direction 2 (fallback) : FK sur le parent → nouvel asset + ok2, tried_fields2 = _set_parent_fk(parent, asset, type(parent)) + if ok2: + parent.save() + self.stdout.write( + f" Ligne {line_num}: [LIEN] parent '{pcode}' ({type(parent).__name__}) " + f"lié via FK inverse (FK sur le parent)." + ) + else: + all_tried = (tried_fields or []) + (tried_fields2 or []) + self.stderr.write( + f" Ligne {line_num}: [LIEN] impossible de lier le parent '{pcode}' " + f"({type(parent).__name__}) – aucun champ FK compatible trouvé " + f"(champs vérifiés: {all_tried or 'aucun'})." + ) + else: + self.stdout.write( + f" Ligne {line_num}: [LIEN dry-run] parent '{pcode}' trouvé ({type(parent).__name__})." + ) + + if save_needed and not dry_run: + asset.save() + save_needed = False + + # Child links + child_codes = _parse_codes_list(str(row_dict.get("codes_enfants", ""))) + for ccode in child_codes: + child = _find_asset_by_code(ccode) + if child is None: + self.stderr.write( + f" Ligne {line_num}: [LIEN] enfant '{ccode}' introuvable dans la base – lien ignoré." + ) + continue + if not dry_run: + # Direction 1 : FK sur l'enfant → nouvel asset + ok, tried_fields = _set_child_fk(child, asset) + if ok: + child.save() + self.stdout.write( + f" Ligne {line_num}: [LIEN] enfant '{ccode}' ({type(child).__name__}) lié via FK." + ) + else: + # Direction 2 (fallback) : FK sur le nouvel asset → enfant + ok2, tried_fields2 = _set_child_fk(asset, child) + if ok2: + asset.save() + self.stdout.write( + f" Ligne {line_num}: [LIEN] enfant '{ccode}' ({type(child).__name__}) " + f"lié via FK inverse (FK sur l'asset importé)." + ) + else: + all_tried = (tried_fields or []) + (tried_fields2 or []) + self.stderr.write( + f" Ligne {line_num}: [LIEN] impossible de lier l'enfant '{ccode}' " + f"({type(child).__name__}) – aucun champ FK compatible trouvé " + f"(champs vérifiés: {all_tried or 'aucun'})." + ) + else: + self.stdout.write( + f" Ligne {line_num}: [LIEN dry-run] enfant '{ccode}' trouvé ({type(child).__name__})." + ) + + if dry_run: + # Roll back so nothing is persisted + transaction.set_rollback(True) + + # --- Summary --- + self.stdout.write("") + self.stdout.write(self.style.SUCCESS("=== Résumé de l'import ===")) + self.stdout.write(f" Créés : {stats['created']}") + self.stdout.write(f" Mis à jour: {stats['updated']}") + self.stdout.write(f" Ignorés : {stats['skipped']}") + self.stdout.write(f" Erreurs : {stats['errors']}") + if errors: + self.stdout.write("") + self.stdout.write(self.style.ERROR("Détail des erreurs:")) + for err in errors: + self.stdout.write(self.style.ERROR(err)) + if dry_run: + self.stdout.write(self.style.WARNING("(dry-run: aucune donnée enregistrée)")) + + # ------------------------------------------------------------------ + def _detect_header(self, ws): + """ + Detect which row contains the column headers and where data starts. + Supports two layouts: + - Row 1: machine-key row (from our template) → data starts at row 5 + - Row 1 or 2: human-readable labels → data starts the row after + Returns (header_row_idx [1-based], col_map {key: 0-based col index}, + data_start_row [1-based]). + """ + # Build reverse mapping: normalised label → key + label_to_key = {_normalize_key(label): key for key, label in COLUMNS} + + HEADER_KEYS_SET = set(HEADER_KEYS) + + for row_idx in range(1, 4): + row_values = [ + str(cell.value).strip() if cell.value is not None else "" + for cell in ws[row_idx] + ] + normalized = [_normalize_key(v) for v in row_values] + matches_key = sum(1 for n in normalized if n in HEADER_KEYS_SET) + matches_label = sum(1 for n in normalized if n in label_to_key) + + if matches_key >= 3: + col_map = {} + for col_idx, norm in enumerate(normalized): + if norm in HEADER_KEYS_SET: + col_map[norm] = col_idx + # If ALL values are exact machine keys, this is our template row 1. + # In that case skip rows 2-4 (human labels, hints, example). + non_empty = [n for n in normalized if n] + is_template_machine_row = non_empty and all(n in HEADER_KEYS_SET for n in non_empty) + data_start = 5 if is_template_machine_row else row_idx + 1 + return row_idx, col_map, data_start + + if matches_label >= 3: + col_map = {} + for col_idx, norm in enumerate(normalized): + if norm in label_to_key: + col_map[label_to_key[norm]] = col_idx + return row_idx, col_map, row_idx + 1 + + return None, {}, None + + # ------------------------------------------------------------------ + def _process_row(self, row_dict: dict, update: bool, dry_run: bool): + """ + Process a single Excel row and create/update the asset. + Returns (asset_instance, action_string) or (None, 'skipped'). + """ + code = str(row_dict.get("code", "")).strip() + if not code: + return None, "skipped" + + model_name = str(row_dict.get("nom_modèle_objet", "")).strip().lower() + if not model_name: + raise ValueError("La colonne 'nom_modèle_objet' est requise.") + + model_class = _get_model_class(model_name) + if model_class is None: + raise ValueError(f"Modèle d'objet inconnu: '{model_name}'") + + # Check for existing asset + existing = model_class.objects.filter(code=code).first() + if existing and not update: + return None, "skipped" + + asset = existing if existing else model_class() + action = "updated" if existing else "created" + + # --- Thematic (informational, used for validation only) --- + thematic_code = str(row_dict.get("thématique", "")).strip() + if thematic_code and not dry_run: + try: + Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + raise ValueError(f"Thématique inconnue: '{thematic_code}'") + + # --- Basic fields --- + asset.code = code + + nom_fr = str(row_dict.get("nom_fr", "")).strip() + if nom_fr and hasattr(asset, "name_fr"): + asset.name_fr = nom_fr + + nom_nl = str(row_dict.get("nom_nl", "")).strip() + if nom_nl and hasattr(asset, "name_nl"): + asset.name_nl = nom_nl + + marque = str(row_dict.get("marque", "")).strip() + if marque and hasattr(asset, "brand"): + asset.brand = marque + + # --- Status --- + statut = str(row_dict.get("statut", "")).strip() + if statut: + valid_keys = list(ASSET_STATUS_CHOICES.keys()) + if statut in valid_keys: + asset.status = statut + else: + raise ValueError( + f"Statut invalide: '{statut}'. Valeurs acceptées: {valid_keys}" + ) + else: + if not existing: + asset.status = "active" + + # --- Installation date --- + date_raw = row_dict.get("date_installation", "") + parsed_date = _parse_date(date_raw) + if parsed_date and hasattr(asset, "installation_date"): + from django.utils import timezone as dj_tz + if dj_tz.is_aware(parsed_date): + asset.installation_date = parsed_date + else: + asset.installation_date = dj_tz.make_aware(parsed_date) + + # --- Category --- + cat_code = str(row_dict.get("code_catégorie", "")).strip() + if cat_code and hasattr(asset, "category"): + try: + asset.category = AssetCategory.objects.get(code=cat_code) + except AssetCategory.DoesNotExist: + raise ValueError(f"Catégorie inconnue: '{cat_code}'") + + # --- Asset model object (e.g. StructureAssetModel) --- + model_code = str(row_dict.get("code_modèle", "")).strip() + if model_code and hasattr(model_class, "_meta"): + model_instance = _find_model_instance_by_code(model_class, model_code) + if model_instance is None: + raise ValueError(f"Modèle d'asset introuvable: code='{model_code}'") + asset.model = model_instance + elif not existing: + # Check if 'model' field is required + try: + model_field = model_class._meta.get_field("model") + if not model_field.null and not model_field.blank: + raise ValueError( + "Le champ 'code_modèle' est requis pour ce type d'asset." + ) + except Exception: + pass + + # --- Location --- + loc_code = str(row_dict.get("code_localisation", "")).strip() + if loc_code: + field_name, location = _find_location_by_code(model_class, loc_code) + if location: + setattr(asset, field_name, location) + else: + raise ValueError(f"Localisation introuvable: code='{loc_code}'") + + # --- Structure --- + structure_code = str(row_dict.get("structure", "")).strip() + if structure_code and hasattr(asset, "structure"): + from assets.models.structures import Structure + try: + asset.structure = Structure.objects.get(code=structure_code) + except Structure.DoesNotExist: + raise ValueError(f"Structure introuvable: code='{structure_code}'") + + # --- Geometry --- + geojson_raw = str(row_dict.get("geojson", "")).strip() + lon_raw = row_dict.get("lon", "") + lat_raw = row_dict.get("lat", "") + + if geojson_raw and hasattr(asset, "geom"): + ok = _set_geom_from_geojson(asset, geojson_raw) + if not ok: + raise ValueError(f"GeoJSON invalide: '{geojson_raw[:80]}'") + # Also store raw geojson string + if hasattr(asset, "geojson"): + asset.geojson = geojson_raw + elif lon_raw != "" and lat_raw != "" and hasattr(asset, "geom"): + try: + lon = float(str(lon_raw).replace(",", ".")) + lat = float(str(lat_raw).replace(",", ".")) + _set_geom_from_lon_lat(asset, lon, lat) + if hasattr(asset, "geojson"): + asset.geojson = json.dumps({ + "type": "Point", + "coordinates": [lon, lat], + }) + except ValueError: + raise ValueError(f"Lon/lat invalide: lon='{lon_raw}', lat='{lat_raw}'") + + if not dry_run: + asset.save() + + return asset, action diff --git a/streetup/assets/management/commands/import_concrete_blocks_from_observations.py b/streetup/assets/management/commands/import_concrete_blocks_from_observations.py new file mode 100644 index 0000000..24395cd --- /dev/null +++ b/streetup/assets/management/commands/import_concrete_blocks_from_observations.py @@ -0,0 +1,446 @@ +""" +Commande Django pour créer des objets RoadConcreteBlockSet +à partir des observations d'inventaire de blocs en béton. + +Les observations concernées sont celles qui contiennent une réponse +de type choice_multiple dont la valeur correspond à un type de bloc +(concrete_1m, concrete_2m, red_white). C'est le même critère que +celui utilisé par maps/views.py::concrete_blocks_geojson. + +Mapping des fonctions (valeurs checklist → choix du modèle) : + works → construction + security → antiterrorist + parking → parking_neutralization + protection → protection + deposit → unidentified + circulation → unidentified + test → unidentified + +Usage : + python manage.py import_concrete_blocks_from_observations + python manage.py import_concrete_blocks_from_observations --dry-run + python manage.py import_concrete_blocks_from_observations --update +""" + +import os + +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.contrib.gis.geos import GEOSGeometry, Point +from django.core.files.base import ContentFile +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q + +from assets.models.roads import RoadConcreteBlockSet +from documents.models import DocumentAttachment, DocumentTag, DocumentVersion, ManagedDocument +from observations.models import Observation, ObservationChecklistResponse, ObservationPhoto + + +# --------------------------------------------------------------------------- +# Constants – keep in sync with maps/views.py +# --------------------------------------------------------------------------- + +BLOCK_TYPE_VALUES = ['concrete_1m', 'concrete_2m', 'red_white'] +FUNCTION_VALUES = ['works', 'security', 'parking', 'test', 'deposit', 'circulation', 'protection'] + +# Map observation checklist function keys → RoadConcreteBlockSet.main_function choices +FUNCTION_TO_CB = { + 'protection': 'protection', + 'security': 'antiterrorist', + 'parking': 'parking_neutralization', + 'works': 'construction', + 'deposit': 'unidentified', + 'circulation': 'unidentified', + 'test': 'unidentified', + 'antiterrorist': 'antiterrorist', # in case of direct checklist response with this value + 'parking_neutralization': 'parking_neutralization', # idem + 'construction': 'construction', # idem + 'unidentified': 'unidentified', # idem + 'road_safety': 'road_safety', # idem + 'public_space': 'public_space', # idem +} + +# Coordinate transform helpers (built once) +_srs_4326 = SpatialReference(4326) +_srs_3812 = SpatialReference(3812) +_ct_to_3812 = CoordTransform(_srs_4326, _srs_3812) +_ct_to_4326 = CoordTransform(_srs_3812, _srs_4326) + + +def _json_contains_any(field, values): + """Q expression: JSON array field contains at least one of the given values.""" + q = Q() + for v in values: + q |= Q(**{f'{field}__contains': [v]}) + return q + + +def _make_geom_3812(obs): + """ + Return a GEOS Point in SRID 3812 for *obs*, or None if coordinates + are unavailable. Prefers obs.geom (already in 3812) over lat/lon. + """ + if obs.geom: + return obs.geom # PointField srid=3812 + + if obs.latitude is not None and obs.longitude is not None: + pt = Point(obs.longitude, obs.latitude, srid=4326) + pt.transform(_ct_to_3812) + return pt + + return None + + +def _make_geojson(geom_3812): + """Return a GeoJSON string (WGS84) for the given Point in SRID 3812.""" + if geom_3812 is None: + return None + wgs84 = geom_3812.clone() + wgs84.transform(_ct_to_4326) + return wgs84.geojson + + +def _make_lon_lat(obs, geom_3812): + """Return (lon, lat) in WGS84 from observation fields or geom.""" + if obs.longitude is not None and obs.latitude is not None: + return obs.longitude, obs.latitude + if geom_3812 is not None: + wgs84 = geom_3812.clone() + wgs84.transform(_ct_to_4326) + return wgs84.x, wgs84.y + return None, None + + +def _obs_code_to_asset_code(obs): + """Derive a unique asset code from the observation code.""" + return f'CBX-{obs.code}' if obs.code else f'CBX-OBS{obs.pk:05d}' + + +# --------------------------------------------------------------------------- +# Fetch observation data (mirrors concrete_blocks_geojson logic) +# --------------------------------------------------------------------------- + +def _fetch_observation_data(): + """ + Return a dict keyed by observation id, each value being a dict with: + obs – Observation instance + block_types – list of matching block-type keys + functions – list of matching function keys + block_count – float or None + """ + # Observations that have at least one block-type checklist response + obs_ids = list( + ObservationChecklistResponse.objects + .filter(_json_contains_any('value_choices', BLOCK_TYPE_VALUES)) + .values_list('observation_id', flat=True) + .distinct() + ) + if not obs_ids: + return {} + + observations = { + obs.pk: obs + for obs in Observation.objects.filter(pk__in=obs_ids) + } + + # Block-type checklist responses + bt_responses = ( + ObservationChecklistResponse.objects + .filter(observation_id__in=obs_ids) + .filter(_json_contains_any('value_choices', BLOCK_TYPE_VALUES)) + .values('observation_id', 'value_choices') + ) + # Function checklist responses + fn_responses = ( + ObservationChecklistResponse.objects + .filter(observation_id__in=obs_ids) + .filter(_json_contains_any('value_choices', FUNCTION_VALUES)) + .values('observation_id', 'value_choices') + ) + # Block count (numeric checklist item) + cnt_responses = ( + ObservationChecklistResponse.objects + .filter( + observation_id__in=obs_ids, + checklist_item__item_type='number', + value_number__isnull=False, + ) + .values('observation_id', 'value_number') + ) + + result = {obs_id: {'obs': obs, 'block_types': [], 'functions': [], 'block_count': None} + for obs_id, obs in observations.items()} + + for r in bt_responses: + obs_id = r['observation_id'] + for v in (r['value_choices'] or []): + if v in BLOCK_TYPE_VALUES and v not in result[obs_id]['block_types']: + result[obs_id]['block_types'].append(v) + + for r in fn_responses: + obs_id = r['observation_id'] + for v in (r['value_choices'] or []): + if v in FUNCTION_VALUES and v not in result[obs_id]['functions']: + result[obs_id]['functions'].append(v) + + for r in cnt_responses: + obs_id = r['observation_id'] + if result[obs_id]['block_count'] is None: + result[obs_id]['block_count'] = float(r['value_number']) + + return result + + +# --------------------------------------------------------------------------- +# Management command +# --------------------------------------------------------------------------- + +class Command(BaseCommand): + help = ( + "Crée des objets RoadConcreteBlockSet à partir des observations " + "d'inventaire de blocs en béton (lecture des réponses de checklist)." + ) + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + default=False, + help="Afficher les opérations sans écrire en base.", + ) + parser.add_argument( + '--update', + action='store_true', + default=False, + help=( + "Mettre à jour les RoadConcreteBlockSet existants " + "(identifiés via observation_source) au lieu de les ignorer." + ), + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + do_update = options['update'] + + if dry_run: + self.stdout.write(self.style.WARNING("Mode dry-run : aucune écriture en base.\n")) + + # ── Fetch data from observations ───────────────────────────────────── + data = _fetch_observation_data() + self.stdout.write(f"Observations avec blocs en béton trouvées : {len(data)}") + + if not data: + self.stdout.write(self.style.WARNING("Aucune observation trouvée. Fin.")) + return + + # ── Existing RoadConcreteBlockSet keyed by observation_source_id ───── + existing = { + cbs.observation_source_id: cbs + for cbs in RoadConcreteBlockSet.objects.filter( + observation_source_id__in=list(data.keys()) + ) + } + self.stdout.write(f"RoadConcreteBlockSet déjà présents : {len(existing)}") + + # ── Build objects ──────────────────────────────────────────────────── + to_create: list[RoadConcreteBlockSet] = [] + to_update_instances: list[RoadConcreteBlockSet] = [] + update_fields: list[str] = [] + skipped = created = updated = 0 + + for obs_id, row in data.items(): + obs = row['obs'] + block_types = row['block_types'] + functions = row['functions'] + block_count = row['block_count'] + + # Map functions to model choices; deduplicate while preserving order + cb_functions = [] + for fn in functions: + mapped = FUNCTION_TO_CB.get(fn) + if mapped and mapped not in cb_functions: + cb_functions.append(mapped) + + main_function = cb_functions[0] if cb_functions else None + secondary_functions = cb_functions[1:] if len(cb_functions) > 1 else None + + geom_3812 = _make_geom_3812(obs) + geojson = _make_geojson(geom_3812) + lon, lat = _make_lon_lat(obs, geom_3812) + + if obs_id in existing: + if not do_update: + skipped += 1 + continue + + # ── Update path ─────────────────────────────────────────── + cbs = existing[obs_id] + cbs.address = obs.address + # cbs.main_function = main_function + # cbs.secondary_functions = secondary_functions or [] + cbs.number_of_blocks = int(block_count) if block_count is not None else None + cbs.geom = geom_3812 + cbs.geojson = geojson + cbs.lon = lon + cbs.lat = lat + to_update_instances.append(cbs) + if not update_fields: + update_fields = [ + 'address', 'main_function', 'secondary_functions', + 'number_of_blocks', 'geom', 'geojson', 'lon', 'lat', + ] + self.stdout.write( + f" [MÀJP] CBX id={cbs.id} ← OBS {obs.code} " + f"({len(functions)} fonction(s), {block_count} blocs)" + ) + else: + # ── Create path ─────────────────────────────────────────── + code = _obs_code_to_asset_code(obs) + cbs = RoadConcreteBlockSet( + code = code, + name_fr = obs.address or code, + status = 'active', + address = obs.address, + # main_function = main_function, + # secondary_functions = secondary_functions or [], + number_of_blocks = int(block_count) if block_count is not None else None, + geom = geom_3812, + geojson = geojson, + lon = lon, + lat = lat, + observation_source = obs, + ) + to_create.append(cbs) + self.stdout.write( + f" [CRÉER] {code} ← OBS {obs.code} " + f"({len(functions)} fonction(s), {block_count} blocs)" + ) + + # ── Write to DB ────────────────────────────────────────────────────── + if not dry_run: + with transaction.atomic(): + if to_create: + RoadConcreteBlockSet.objects.bulk_create(to_create, batch_size=200) + created = len(to_create) + + if to_update_instances and update_fields: + RoadConcreteBlockSet.objects.bulk_update( + to_update_instances, update_fields, batch_size=200 + ) + updated = len(to_update_instances) + else: + created = len(to_create) + updated = len(to_update_instances) + + # ── Attach photos ──────────────────────────────────────────────────── + # Process photos for ALL observations that now have a linked CBS + # (newly created + pre-existing). Idempotency is handled inside. + all_obs_ids = list(data.keys()) + photos_attached, photos_skipped = self._attach_photos(all_obs_ids, dry_run) + + # ── Summary ────────────────────────────────────────────────────────── + suffix = " (dry-run)" if dry_run else "" + self.stdout.write("") + self.stdout.write(self.style.SUCCESS( + f"Terminé{suffix} : {created} CBS créé(s), {updated} mis à jour, {skipped} ignoré(s) — " + f"{photos_attached} photo(s) attachée(s), {photos_skipped} déjà présente(s)." + )) + + # ------------------------------------------------------------------------- + + def _attach_photos(self, obs_ids: list, dry_run: bool) -> tuple[int, int]: + """ + Crée des ManagedDocument (tag 'photo') et les attache aux + RoadConcreteBlockSet correspondants via DocumentAttachment. + + Retourne (nb_attachées, nb_déjà_présentes). + """ + if not obs_ids: + return 0, 0 + + # Get-or-create the 'photo' DocumentTag + photo_tag, tag_created = DocumentTag.objects.get_or_create( + slug='photo', + defaults={'name_fr': 'photo', 'name_nl': 'foto', 'order': 0}, + ) + if tag_created: + self.stdout.write(self.style.SUCCESS(' DocumentTag "photo" créé.')) + + cbs_ct = ContentType.objects.get_for_model(RoadConcreteBlockSet) + + # CBS keyed by observation_source_id + cbs_by_obs = { + cbs.observation_source_id: cbs + for cbs in RoadConcreteBlockSet.objects.filter( + observation_source_id__in=obs_ids + ) + } + + # Photos grouped by observation_id + photos_by_obs: dict[int, list[ObservationPhoto]] = {obs_id: [] for obs_id in obs_ids} + for photo in ObservationPhoto.objects.filter(observation_id__in=obs_ids): + photos_by_obs[photo.observation_id].append(photo) + + # Existing attachments for these CBS (to avoid per-photo DB queries) + cbs_pks = [cbs.pk for cbs in cbs_by_obs.values()] + existing_titles: set[tuple[int, str]] = set( + DocumentAttachment.objects + .filter(content_type=cbs_ct, object_id__in=cbs_pks) + .values_list('object_id', 'document__title') + ) + + attached = skipped = 0 + + for obs_id, photos in photos_by_obs.items(): + cbs = cbs_by_obs.get(obs_id) + if cbs is None or not photos: + continue + + for obs_photo in photos: + if not obs_photo.photo: + continue + + filename = os.path.basename(obs_photo.photo.name) + title = os.path.splitext(filename)[0] + + if (cbs.pk, title) in existing_titles: + skipped += 1 + continue + + if dry_run: + self.stdout.write(f" [DRY] Photo : {filename} → CBS {cbs.code}") + attached += 1 + continue + + try: + with obs_photo.photo.open('rb') as fh: + image_data = fh.read() + except Exception as exc: + self.stdout.write( + self.style.WARNING(f" Impossible de lire {filename} : {exc}") + ) + continue + + with transaction.atomic(): + doc = ManagedDocument.objects.create(title=title) + doc.tags.add(photo_tag) + + version = DocumentVersion( + document=doc, + status=DocumentVersion.STATUS_APPROVED, + ) + version.file.save(filename, ContentFile(image_data), save=False) + version.save() + + DocumentAttachment.objects.create( + document=doc, + content_type=cbs_ct, + object_id=cbs.pk, + ) + + existing_titles.add((cbs.pk, title)) # update local cache + attached += 1 + self.stdout.write(f" Photo : {filename} → CBS {cbs.code}") + + return attached, skipped diff --git a/streetup/assets/management/commands/import_drains_from_csv.py b/streetup/assets/management/commands/import_drains_from_csv.py new file mode 100644 index 0000000..a1f6d3c --- /dev/null +++ b/streetup/assets/management/commands/import_drains_from_csv.py @@ -0,0 +1,418 @@ +""" +Management command to import / update RoadDrain objects from +external/avaloirs_2026.csv. + +CSV → RoadDrain field mapping +───────────────────────────── +geom → geom (PointField srid=3812, dim=3) + lon / lat / geojson +DR_ID → code (e.g. "DR-001") +DR_STREET → street (ForeignKey to RoadStreet, matched by name_fr) +DR_SOURCE → positionning_source (with DR_SURVEY_TYPE appended) +DR_SOILTYPE → soil_type +DR_LOCATION → drain_location_type +DR_VEGETATION → nearby_vegetation +DR_ROOTING → possible_rooting +DR_STATE → is_defect +DR_BAD_STATE_TYPE → defect_types (all tokens as list) +DR_STATE_NOTE → defect_note (appended) +DR_Z → altitude +DR_BEGIN_LIFE → installation_date +DR_MEASURE_POSITION / DR_POSITION / DR_BAD_POSITION_TYPE / DR_POSITION_NOTE + → positionning_note (combined) +DR_MODEL → model (RoadDrainModel, looked-up or created by code) +DR_MODEL_NOTE / DR_NOTE → note (combined) +DR_TYPE → related_image_type +DR_OPERATOR → brand +""" + +import csv +import json +import os +import re +from datetime import datetime + +from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.contrib.gis.geos import Point +from django.core.management.base import BaseCommand +from django.db import transaction + +from assets.models.roads import RoadDrain, RoadDrainModel, RoadStreet + +# ── Path to CSV ─────────────────────────────────────────────────────────────── +CSV_PATH = os.path.normpath( + os.path.join( + os.path.dirname(__file__), # …/assets/management/commands/ + "..", "..", "..", # → repo root + "scripts", "data", "avaloirs_2026.csv", + ) +) + +BULK_BATCH_SIZE = 500 + +# ── Lookup / mapping tables ─────────────────────────────────────────────────── + +SOIL_TYPE_MAP = { + "CONCRETE": "concrete", + "ASPHALT": "asphalt", + "STONE": "stone", + "CONCRETE SLABS": "concrete_slabs", + "CONCRETE PAVING": "concrete_paving", + "STONE SLABS": "stone_slabs", + "STONE PAVING": "stone_paving", + "CONCRETE/GRASS": "concrete_grass", + "GRAVEL": "gravel", + "GRASS": "grass", + "EARTH": "earth", +} + +LOCATION_TYPE_MAP = { + "WATERWAY": "waterway", + "SIDEWALK": "sidewalk", + "BERME": "berme", + "SIDEWALK-FACADE TO ROAD": "sidewalk_facade_to_road", + "SQUARE": "square", + "ALONG SIDEWALK": "along_sidewalk", + "MIDDLE ROAD": "middle_road", +} + +DEFECT_TYPE_MAP = { + "OBSTRUCTED": "obstructed", + "SMELL CUT MISSING": "smell_cut_missing", + "GRATE BROKEN": "grate_broken", + "GRATE MISSING": "grate_missing", + "POOR WATER FLOW": "poor_water_flow", + "OTHER": "other", +} + +# Fields updated on an existing drain (update path) +UPDATE_FIELDS = [ + "street_id", "model_id", "soil_type", "drain_location_type", + "nearby_vegetation", "possible_rooting", "is_defect", "defect_types", + "defect_note", "positionning_source", "positionning_note", + "positionning_date", "altitude", "related_image_type", + "brand", "note", "installation_date", + "geom", "lon", "lat", "geojson", +] + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def _parse_bool(val: str): + """Return True/False/None from 'true'/'false'/'' strings.""" + v = (val or "").strip().lower() + if v == "true": + return True + if v == "false": + return False + return None + + +def _parse_defect_tokens(raw: str) -> list[str]: + """ + Parse values like '{OBSTRUCTED}', '{GRATE BROKEN, OTHER}', '{}'. + Returns a list of upper-cased token strings. + """ + raw = raw.strip() + if not raw or raw == "{}": + return [] + inner = re.sub(r"^\{|\}$", "", raw) + return [t.strip() for t in inner.split(",") if t.strip()] + + +def _extract_model_code(model_path: str) -> str: + """'MODEL/AV/AV_TYPE-002.png' → 'AV_TYPE-002'""" + return os.path.splitext(os.path.basename(model_path))[0] + + +def _parse_geom(wkt: str): + """ + Parse 'POINT ZM (x y z m)' (already in EPSG:3812) into a 3-D Point. + Returns a GEOSGeometry Point(srid=3812) or None on failure. + """ + m = re.match( + r"POINT\s+ZM\s*\(\s*([\d.+-]+)\s+([\d.+-]+)\s+([\d.+-]+)\s+([\d.+-]+)\s*\)", + wkt, + re.IGNORECASE, + ) + if not m: + return None + x, y, z = float(m.group(1)), float(m.group(2)), float(m.group(3)) + return Point(x, y, z, srid=3812) + + +def _lon_lat_geojson(geom_3812): + """Transform a 3812 Point to WGS84, return (lon, lat, geojson_str).""" + srs_3812 = SpatialReference(3812) + srs_4326 = SpatialReference(4326) + ct = CoordTransform(srs_3812, srs_4326) + g = geom_3812.clone() + g.transform(ct) + lon, lat = round(g.x, 8), round(g.y, 8) + geojson = json.dumps({"type": "Point", "coordinates": [lon, lat]}) + return lon, lat, geojson + + +def _parse_date(raw: str): + """Parse 'YYYY/MM/DD' → date, or None.""" + raw = (raw or "").strip() + if not raw: + return None + try: + return datetime.strptime(raw, "%Y/%m/%d").date() + except ValueError: + return None + + +def _build_drain(row: dict, street_map: dict, drain_model_map: dict) -> dict: + """ + Convert one CSV row into a dict of RoadDrain field values. + Returns None if the row must be skipped. + """ + if (row.get("DR_TYPE") or "").strip().upper() != "DRAIN POINT": + return None + + dr_id = (row.get("DR_ID") or "").strip() + if not dr_id: + return None + + code = f"DR-{int(dr_id):05d}" + + # ── Geometry ────────────────────────────────────────────────────────────── + wkt = (row.get("geom") or "").strip() + geom = _parse_geom(wkt) if wkt else None + if geom is None: + return None + lon, lat, geojson = _lon_lat_geojson(geom) + + # ── Street ──────────────────────────────────────────────────────────────── + # street_name = (row.get("DR_STREET") or "").strip() + # street = street_map.get(street_name) # may be None + street = None + + # ── Drain model ─────────────────────────────────────────────────────────── + model_path = (row.get("DR_MODEL") or "").strip() + drain_model = None + if model_path: + model_code = _extract_model_code(model_path) + drain_model = drain_model_map.get(model_code) + + # ── Soil type ───────────────────────────────────────────────────────────── + soil_raw = (row.get("DR_SOILTYPE") or "").strip().upper() + soil_type = SOIL_TYPE_MAP.get(soil_raw) + + # ── Location type ───────────────────────────────────────────────────────── + loc_raw = (row.get("DR_LOCATION") or "").strip().upper() + drain_location_type = LOCATION_TYPE_MAP.get(loc_raw) + + # ── Boolean flags ───────────────────────────────────────────────────────── + nearby_vegetation = _parse_bool(row.get("DR_VEGETATION", "")) + possible_rooting = _parse_bool(row.get("DR_ROOTING", "")) + is_defect = _parse_bool(row.get("DR_STATE", "")) + + # ── Defect type / note ──────────────────────────────────────────────────── + defect_tokens = _parse_defect_tokens(row.get("DR_BAD_STATE_TYPE") or "") + defect_types = [DEFECT_TYPE_MAP[t] for t in defect_tokens if t in DEFECT_TYPE_MAP] or None + + state_note = (row.get("DR_STATE_NOTE") or "").strip() + defect_note = state_note or None + + # ── Positionning ────────────────────────────────────────────────────────── + survey_source = (row.get("DR_SOURCE") or "").strip() + survey_type = (row.get("DR_SURVEY_TYPE") or "").strip() + positionning_source = f"{survey_source} / {survey_type}" if survey_type else survey_source or None + + pos_note_parts = [] + measure_pos = (row.get("DR_MEASURE_POSITION") or "").strip() + if measure_pos: + pos_note_parts.append(measure_pos) + bad_position = _parse_bool(row.get("DR_POSITION", "")) + if bad_position: + bad_pos_tokens = _parse_defect_tokens(row.get("DR_BAD_POSITION_TYPE") or "") + if bad_pos_tokens: + pos_note_parts.append("Position: " + ", ".join(bad_pos_tokens)) + pos_note_raw = (row.get("DR_POSITION_NOTE") or "").strip() + if pos_note_raw: + pos_note_parts.append(pos_note_raw) + positionning_note = "\n".join(pos_note_parts) or None + + positionning_date = _parse_date(row.get("DR_BEGIN_LIFE") or "") + + # ── Altitude ────────────────────────────────────────────────────────────── + z_raw = (row.get("DR_Z") or "").strip() + altitude = float(z_raw) if z_raw else None + + # ── Note ───────────────────────────────────────────────────────────────── + note_parts = [] + dr_note = (row.get("DR_NOTE") or "").strip() + model_note = (row.get("DR_MODEL_NOTE") or "").strip() + veg_note = (row.get("DR_VEGETATION_NOTE") or "").strip() + if dr_note: + note_parts.append(dr_note) + if model_note: + note_parts.append(f"Modèle : {model_note}") + if veg_note: + note_parts.append(f"Végétation : {veg_note}") + note = "\n".join(note_parts) or None + + # ── Misc ────────────────────────────────────────────────────────────────── + related_image_type = (row.get("DR_TYPE") or "").strip() or None + brand = None + installation_date = None + + return dict( + code=code, + street=street, + model=drain_model, + soil_type=soil_type, + drain_location_type=drain_location_type, + nearby_vegetation=nearby_vegetation, + possible_rooting=possible_rooting, + is_defect=is_defect, + defect_types=defect_types, + defect_note=defect_note, + positionning_source=positionning_source, + positionning_note=positionning_note, + positionning_date=positionning_date, + altitude=altitude, + related_image_type=related_image_type, + brand=brand, + note=note, + installation_date=installation_date, + geom=geom, + lon=lon, + lat=lat, + geojson=geojson, + ) + + +# ── Management command ──────────────────────────────────────────────────────── + +class Command(BaseCommand): + help = "Import / update RoadDrain objects from external/avaloirs_2026.csv" + + def add_arguments(self, parser): + parser.add_argument( + "--csv", + default=CSV_PATH, + help="Path to the CSV file (default: external/avaloirs_2026.csv)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Parse and validate without writing to the database", + ) + + def handle(self, *args, **options): + csv_path = options["csv"] + dry_run = options["dry_run"] + + if not os.path.isfile(csv_path): + self.stderr.write(self.style.ERROR(f"CSV file not found: {csv_path}")) + return + + # ── Pre-load DB lookups ─────────────────────────────────────────────── + street_map = { + s.name_fr: s + for s in RoadStreet.objects.all() + if s.name_fr + } + self.stdout.write(f" Streets loaded : {len(street_map)}") + + # Get-or-create drain models keyed by code + drain_model_map: dict[str, RoadDrainModel] = { + m.code: m for m in RoadDrainModel.objects.all() + } + self.stdout.write(f" DrainModels loaded : {len(drain_model_map)}") + + # Existing drains keyed by code + existing_drains: dict[str, RoadDrain] = { + d.code: d for d in RoadDrain.objects.all() + } + self.stdout.write(f" Existing drains : {len(existing_drains)}") + + # ── Read CSV ────────────────────────────────────────────────────────── + with open(csv_path, newline="", encoding="utf-8-sig") as f: + rows = list(csv.DictReader(f)) + self.stdout.write(f" CSV rows : {len(rows)}") + + # ── Ensure drain models exist for all DR_MODEL values ───────────────── + if not dry_run: + needed_codes = set() + for row in rows: + mp = (row.get("DR_MODEL") or "").strip() + if mp: + needed_codes.add(_extract_model_code(mp)) + + new_models = [] + for code in needed_codes: + if code not in drain_model_map: + dm = RoadDrainModel(code=code, name_fr=code) + new_models.append(dm) + if new_models: + RoadDrainModel.objects.bulk_create(new_models, batch_size=BULK_BATCH_SIZE) + # Reload + drain_model_map = {m.code: m for m in RoadDrainModel.objects.all()} + self.stdout.write( + f" DrainModels created: {len(new_models)} " + f"(total {len(drain_model_map)})" + ) + + # ── Parse rows ──────────────────────────────────────────────────────── + to_create: list[RoadDrain] = [] + to_update: list[RoadDrain] = [] + skipped = 0 + streets_not_found: set[str] = set() + + for row in rows: + fields = _build_drain(row, street_map, drain_model_map) + if fields is None: + skipped += 1 + continue + + street_name = (row.get("DR_STREET") or "").strip() + if street_name and fields["street"] is None: + streets_not_found.add(street_name) + + code = fields.pop("code") + existing = existing_drains.get(code) + + if existing is None: + obj = RoadDrain(code=code, **fields) + to_create.append(obj) + else: + for attr, val in fields.items(): + setattr(existing, attr, val) + to_update.append(existing) + + # ── Report unmatched streets ────────────────────────────────────────── + for name in sorted(streets_not_found): + self.stdout.write( + self.style.WARNING(f" Street not found in DB: {name!r}") + ) + + self.stdout.write( + f" To create : {len(to_create)}, " + f"to update : {len(to_update)}, " + f"skipped : {skipped}" + ) + + if dry_run: + self.stdout.write(self.style.WARNING(" Dry-run: no changes written.")) + return + + # ── Write to DB ─────────────────────────────────────────────────────── + with transaction.atomic(): + if to_create: + RoadDrain.objects.bulk_create( + to_create, batch_size=BULK_BATCH_SIZE + ) + if to_update: + RoadDrain.objects.bulk_update( + to_update, UPDATE_FIELDS, batch_size=BULK_BATCH_SIZE + ) + + self.stdout.write( + self.style.SUCCESS( + f" Done — created {len(to_create)}, updated {len(to_update)} drains." + ) + ) diff --git a/streetup/assets/management/commands/import_rwi.py b/streetup/assets/management/commands/import_rwi.py new file mode 100644 index 0000000..4604991 --- /dev/null +++ b/streetup/assets/management/commands/import_rwi.py @@ -0,0 +1,291 @@ +import os +from django.core.management.base import BaseCommand +from django.conf import settings +from django.contrib.gis.gdal import DataSource +from django.contrib.gis.geos import GEOSGeometry +from django.utils.dateparse import parse_date +from django.db import transaction + +from assets.models import NatureRWIAsset + +# Dutch translations for type_ouvrage +TYPE_OUVRAGE_NL_MAP = { + # Points + 'avaloir': 'afvoerput', + 'bordure sequencee': 'rand met sequentie', + 'bouche injection': 'injectieopening', + 'clapet anti-retour': 'terugslagklep', + 'empierrement sedimentation': 'verstening sedimentatie', + 'kerb cell': 'kerbcel', + 'puits infiltration': 'infiltratieput', + 'surverse': 'overloop', + 'vanne isolation': 'afsluitklep', + 'vanne regulation': 'regelklep', + # Lines + 'gargouille de trottoir': 'stoepgootsteen', + 'filet eau': 'druppel water', + 'avaloir de rail': 'spoorafvoer', + 'bordure arasee': 'afgewerkte rand', + 'caniveau infiltrant': 'infiltratiegoot', + 'drain': 'afvoer', + 'trop plein': 'overvol', + 'reseau egout separatif': 'gescheiden rioolnetwerk', + # Polygons + 'bassin sec/infiltration': 'droog bassin / infiltratiebekken', + 'bassin en eau': 'waterbak', + 'fosse': 'greppel', + 'jardin de pluie': 'regentuin', + 'noue': 'knoop', + 'massif drainant': 'drainageblok', + 'tranchee drainante': 'afwateringsgreppel', + 'toiture stockante': 'stokkend dak', + 'toiture verte': 'groendak', + 'bassin orage': 'regenwaterreservoir', + 'SAUL-vide': 'ultralichte holle structuur', + 'SAUL-terre': 'ultralichte honingraatstructuur aarde', + 'citerne': 'tankwagen', + 'espace vert sur dalle': 'groene ruimte op betonnen plaat', + 'bande filtrante': 'filterband', + 'collecteur en beton poreux': 'poreuze betonnen collector', +} + +def get_conn(feature, keys): + for k in keys: + if k in feature.fields: + val = feature.get(k) + if val is not None: + return str(val).strip() + return None + +def get_field_if_exists(feature, name): + if name in feature.fields: + return feature.get(name) + return None + +def get_rwi_asset_acronym(geometry_type, structure_type, code_name): + struct_type_lower = str(structure_type).strip().lower() if structure_type else None + + if code_name: + return str(code_name).strip().upper() + + mappings = { + 'avaloir': 'AV', + 'bordure sequencee': 'BS', + 'bouche injection': 'BI', + 'clapet anti-retour': 'CA', + 'empierrement sedimentation': 'ES', + 'kerb cell': 'KC', + 'puits infiltration': 'PI', + 'surverse': 'SU', + 'vanne isolation': 'VI', + 'vanne regulation': 'VR', + 'gargouille de trottoir': 'GT', + 'filet eau': 'FE', + 'avaloir de rail': 'AR', + 'bordure arasee': 'BA', + 'caniveau infiltrant': 'CI', + 'drain': 'D', + 'trop plein': 'TP', + 'reseau egout separatif': 'RES', + 'bassin sec/infiltration': 'OED', + 'bassin en eau': 'OED', + 'fosse': 'EVC', + 'jardin de pluie': 'EVC', + 'noue': 'EVC', + 'massif drainant': 'OED', + 'tranchee drainante': 'OED', + 'toiture stockante': 'OSI', + 'toiture verte': 'OSI', + 'bassin orage': 'OED', + 'SAUL-vide': 'OED', + 'SAUL-terre': 'TA', + 'citerne': 'OED', + 'espace vert sur dalle': 'EVC', + 'bande filtrante': 'EVC', + 'collecteur en beton poreux': 'OED', + 'chambre de visite': 'CV', + } + + if struct_type_lower in mappings: + return mappings[struct_type_lower] + + if geometry_type: + return geometry_type.upper() + + return 'UNK' + +class Command(BaseCommand): + help = "Imports RWI (Rain Water Integration) assets from the GeoPackage database." + + def add_arguments(self, parser): + parser.add_argument( + "gpkg_path", + nargs="?", + type=str, + help="Path to the GeoPackage file. Defaults to external_files/giep/inventaire_giep.gpkg if not provided.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + default=False, + help="Simulates the import process without writing changes to the database.", + ) + + def handle(self, *args, **options): + dry_run = options["dry_run"] + gpkg_path = options.get("gpkg_path") + if not gpkg_path: + gpkg_path = os.path.abspath(os.path.join(settings.BASE_DIR, "../external_files/giep/inventaire_giep.gpkg")) + + if not os.path.exists(gpkg_path): + self.stderr.write(self.style.ERROR(f"Error: GeoPackage file not found at: {gpkg_path}")) + return + + self.stdout.write(f"→ Opening GeoPackage file at: {gpkg_path}") + ds = DataSource(gpkg_path) + + if dry_run: + self.stdout.write(self.style.WARNING("⚠ Dry-run mode active – no database changes will be saved.")) + + try: + with transaction.atomic(): + layer_mappings = [ + ('point', 'point'), + ('ligne', 'line'), + ('polygone', 'polygon'), + ] + + total_created = 0 + total_updated = 0 + for layer_name, geometry_type in layer_mappings: + layer = ds[layer_name] + self.stdout.write(f"→ Importing layer: {layer_name} ({len(layer)} features)...") + + created_count = 0 + updated_count = 0 + for feature in layer: + # Defensive geometry access + try: + if not feature.geom: + continue + ogr_geom = feature.geom.clone() + except Exception as ge: + self.stdout.write(self.style.WARNING(f" [Warning] Skipping feature {feature.fid} due to geometry error: {ge}")) + continue + + # Force geometry to 2D + ogr_geom.set_3d(False) + + # EPSG 31370 (Lambert 72) -> EPSG 3812 (Lambert 2008) + geom = GEOSGeometry(ogr_geom.wkb, srid=31370) + if geom.empty: + continue + geom.transform(3812) + + # Coordinate transformation to EPSG 4326 (WGS 84) to extract lon/lat + geom_wgs84 = geom.clone() + geom_wgs84.transform(4326) + if geom_wgs84.empty: + continue + centroid = geom_wgs84.centroid + lon = centroid.x + lat = centroid.y + + type_ouvrage = get_field_if_exists(feature, 'type_ouvra') + code_name = get_field_if_exists(feature, 'nom_code') + + # Format standard RWI codes with acronym + acronym = get_rwi_asset_acronym(geometry_type, type_ouvrage, code_name) + code = f"RWI_{acronym}_{feature.fid:03d}" + + name_fr = str(type_ouvrage).capitalize() if type_ouvrage else code + name_nl = TYPE_OUVRAGE_NL_MAP.get(type_ouvrage, "").capitalize() or name_fr + + # Connection strings + sewer_conn = get_conn(feature, ['connexion_egout', 'connexion egout', 'Conexion egout']) + separate_conn = get_conn(feature, ['connexion_r.separatif', 'connexion r.separatif', 'connexion_r_separatif']) + hydro_conn = get_conn(feature, ['connexion_r.hydro', 'connexion r.hydro', 'connexion_r_hydro']) + + # Filter mapping + has_filter = None + if 'filtre' in feature.fields: + filtre_val = feature.get('filtre') + if filtre_val is not None: + has_filter = True if str(filtre_val).lower().strip() in ('oui', '1', 'true') else False + + # Commissioning dates parsing + comm_date_str = None + for k in ['date_mise_en_service', 'mise_serv', 'mise_servi']: + if k in feature.fields: + val = feature.get(k) + if val: + comm_date_str = str(val).strip() + break + + commissioning_date = None + if comm_date_str: + commissioning_date = parse_date(comm_date_str) + + # Dimensional metrics + surface_area = get_field_if_exists(feature, 'surf_m2') + length = get_field_if_exists(feature, 'length_m') + + # Fetch existing or instantiate a new asset using external_id and geometry_type + asset = NatureRWIAsset.objects.filter(external_id=feature.fid, geometry_type=geometry_type).first() + is_new = asset is None + if is_new: + asset = NatureRWIAsset(code=code) + else: + # Update code to the new format if it changed + asset.code = code + + # Populate attributes + asset.name_fr = name_fr + asset.name_nl = name_nl + asset.geom = geom + asset.lon = lon + asset.lat = lat + asset.geojson = geom.json + asset.geometry_type = geometry_type + asset.external_id = feature.fid + asset.code_name = get_field_if_exists(feature, 'nom_code') + asset.structure_type = type_ouvrage + asset.project_manager = get_field_if_exists(feature, 'chef_proj') + asset.construction_status = str(get_field_if_exists(feature, 'travaux')).strip() if get_field_if_exists(feature, 'travaux') else None + asset.commissioning_date = commissioning_date + asset.condition = get_field_if_exists(feature, 'etat') + asset.administrator = get_field_if_exists(feature, 'admin') + asset.monitoring = get_field_if_exists(feature, 'suivi') + asset.comment = get_field_if_exists(feature, 'comment') or get_field_if_exists(feature, 'commentaire') + asset.has_filter = has_filter + asset.sewer_connection = sewer_conn + asset.separate_network_connection = separate_conn + asset.hydro_network_connection = hydro_conn + asset.surface_area = surface_area + asset.length = length + asset.coating_category = get_field_if_exists(feature, 'cat_rev') + asset.coating_type = get_field_if_exists(feature, 'type_revetement') + asset.status = 'active' + + asset.save() + + if is_new: + created_count += 1 + else: + updated_count += 1 + + self.stdout.write(self.style.SUCCESS(f"✓ Layer '{layer_name}': {created_count} created, {updated_count} updated.")) + total_created += created_count + total_updated += updated_count + + if dry_run: + # Roll back changes so database remains unmodified + transaction.set_rollback(True) + self.stdout.write(self.style.WARNING("✓ Dry-run completed. All changes rolled back successfully.")) + else: + self.stdout.write(self.style.SUCCESS( + f"✓ Import completed! Total: {total_created} assets created, {total_updated} assets updated in the database." + )) + + except Exception as e: + self.stderr.write(self.style.ERROR(f"Error during import: {e}")) diff --git a/streetup/assets/management/commands/import_tl_cabinet_photos_from_mobigis.py b/streetup/assets/management/commands/import_tl_cabinet_photos_from_mobigis.py new file mode 100644 index 0000000..1068a8b --- /dev/null +++ b/streetup/assets/management/commands/import_tl_cabinet_photos_from_mobigis.py @@ -0,0 +1,172 @@ +""" +Management command to import photos for TrafficLightElectricalCabinet objects +from the MobiGIS CSV export. + +For each cabinet row that has an image field, the file is fetched from: + https://data.mobility.brussels/ + +e.g. media/maintain/inventory/cabin/TCN360_02_20210607_03_000410C.jpg + → https://data.mobility.brussels/media/maintain/inventory/cabin/TCN360_02_... + +A ManagedDocument + DocumentVersion are created and linked to the cabinet via a +DocumentAttachment. The DocumentTag "photo" is applied to every document. + +Re-running the command is safe: existing attachments (same document title + +same cabinet) are skipped. + +Usage +----- + python manage.py import_tl_cabinet_photos_from_mobigis + python manage.py import_tl_cabinet_photos_from_mobigis --limit 100 +""" + +import csv +import os +import urllib.request + +from django.contrib.contenttypes.models import ContentType +from django.core.files.base import ContentFile +from django.core.management.base import BaseCommand +from django.db import transaction +from django.utils.text import slugify + +from assets.models.trafficlights import TrafficLightElectricalCabinet +from documents.models import ( + DocumentAttachment, + DocumentTag, + DocumentVersion, + ManagedDocument, +) + +BASE_DIR = os.path.normpath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "scripts", "data", "tl_mobigis", "electrical_cabinet", + ) +) +CSV_PATH = os.path.join(BASE_DIR, "electricalcabinet.csv") + +PHOTO_BASE_URL = "https://data.mobility.brussels/" +DEFAULT_LIMIT = 50 + + +class Command(BaseCommand): + help = ( + "Fetch and attach photos for TrafficLightElectricalCabinet objects " + "from scripts/data/tl_mobigis/electrical_cabinet/electricalcabinet.csv" + ) + + def add_arguments(self, parser): + parser.add_argument( + "--limit", + type=int, + default=DEFAULT_LIMIT, + help=f"Maximum number of photos to download (default: {DEFAULT_LIMIT}).", + ) + + def handle(self, *args, **options): + limit = options["limit"] + + # ── Get-or-create the "photo" DocumentTag ──────────────────────────── + photo_tag, tag_created = DocumentTag.objects.get_or_create( + slug="photo", + defaults={"name_fr": "photo", "name_nl": "foto", "order": 0}, + ) + if tag_created: + self.stdout.write(self.style.SUCCESS(' DocumentTag "photo" created.')) + else: + self.stdout.write(f' DocumentTag "photo" found (id={photo_tag.pk}).') + + # ── Pre-load cabinets keyed by code ─────────────────────────────────── + cabinets_by_code: dict[str, TrafficLightElectricalCabinet] = {} + for cab in TrafficLightElectricalCabinet.objects.all(): + # Keep first occurrence only; codes should be unique within DB + if cab.code not in cabinets_by_code: + cabinets_by_code[cab.code] = cab + + cabinet_ct = ContentType.objects.get_for_model(TrafficLightElectricalCabinet) + self.stdout.write(f" Cabinets loaded: {len(cabinets_by_code)}") + + # ── Read CSV, keep rows with image and a matching cabinet ───────────── + with open(CSV_PATH, newline="", encoding="utf-8-sig") as f: + rows = list(csv.DictReader(f, delimiter=";")) + + candidates = [] + for row in rows: + image_path = (row.get("image") or "").strip() + if not image_path: + continue + arma_key = (row.get("armoire_key") or "").strip() + arma_id = (row.get("armoire_id") or row.get("id") or "").strip() + code = arma_key if arma_key else f"CAB_{arma_id}" + cabinet = cabinets_by_code.get(code) + if cabinet is None: + continue + candidates.append((cabinet, image_path)) + + self.stdout.write( + f" CSV rows with image matching a DB cabinet: {len(candidates)}" + ) + + # ── Apply limit ─────────────────────────────────────────────────────── + to_process = candidates[:limit] + self.stdout.write(f" Processing (limit={limit}): {len(to_process)}") + + created = already_present = skipped = errors = 0 + + for cabinet, image_path in to_process: + filename = os.path.basename(image_path) + url = PHOTO_BASE_URL + image_path + title = os.path.splitext(filename)[0] + + # ── Idempotency: skip if attachment already exists ──────────── + already = DocumentAttachment.objects.filter( + content_type=cabinet_ct, + object_id=cabinet.pk, + document__title=title, + ).exists() + if already: + already_present += 1 + continue + + # ── Download the image (outside transaction) ────────────────── + try: + with urllib.request.urlopen(url, timeout=15) as response: + image_data = response.read() + except Exception as exc: + self.stdout.write( + self.style.WARNING(f" Download failed {url}: {exc}") + ) + errors += 1 + continue + + # ── Persist document + attachment (committed immediately) ───── + with transaction.atomic(): + doc = ManagedDocument.objects.create(title=title) + doc.tags.add(photo_tag) + + version = DocumentVersion( + document=doc, + status=DocumentVersion.STATUS_APPROVED, + ) + version.file.save(filename, ContentFile(image_data), save=False) + version.save() + + DocumentAttachment.objects.create( + document=doc, + content_type=cabinet_ct, + object_id=cabinet.pk, + ) + + created += 1 + self.stdout.write(f" ✓ {cabinet.code} → {filename}") + + self.stdout.write( + self.style.SUCCESS( + f"\nCabinet photo import complete:\n" + f" Downloaded & attached : {created}\n" + f" Already present : {already_present}\n" + f" Skipped (no cabinet) : {skipped}\n" + f" Errors (download) : {errors}" + ) + ) diff --git a/streetup/assets/management/commands/import_tl_electrical_cabinets_from_mobigis.py b/streetup/assets/management/commands/import_tl_electrical_cabinets_from_mobigis.py new file mode 100644 index 0000000..dd8c970 --- /dev/null +++ b/streetup/assets/management/commands/import_tl_electrical_cabinets_from_mobigis.py @@ -0,0 +1,409 @@ +""" +Management command to import TrafficLightElectricalCabinet objects +from the MobiGIS CSV export. + +Source file : scripts/data/tl_mobigis/electrical_cabinet/electricalcabinet.csv +Lookup files: cabinettype.csv, calibre.csv, cleanliness.csv, compliance.csv, + content.csv, destination.csv, keytype.csv, obsolescence.csv +M2M files : electricalcabinet_calibre_differential_switch.csv + electricalcabinet_content.csv + electricalcabinet_exit_to.csv + +Rules +----- +- Skip rows that have no tl_key_ep (link to TrafficLightIntersection). +- Geometry is in EPSG 31370 (Belgian Lambert 72); transform to EPSG 3812 for + the geom field, and derive lon/lat (WGS84) + geojson from the same point. +- code = armoire_key if non-empty, else "CAB_{armoire_id}". +- Existing cabinets (same intersection + same code) are updated, not duplicated. +- After cabinets, TrafficLightElectricalCabinetContent objects are synced from + electricalcabinet_content.csv (one record per content-type/cabinet pair). +- Multi-valued calibre differential-switch values are joined with "; ". +- Multi-valued exit-to destinations are joined with ", " (truncated to 50 chars). + +Performance +----------- +All DB work is done in bulk_create + bulk_update batches to minimise round-trips. +""" + +import csv +import json +import os +from collections import defaultdict +from datetime import datetime + +from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.contrib.gis.geos import GEOSGeometry +from django.core.management.base import BaseCommand +from django.db import transaction + +from assets.models.trafficlights import ( + TrafficLightElectricalCabinet, + TrafficLightElectricalCabinetContent, + TrafficLightElectricalCabinetContentModel, + TrafficLightIntersection, +) + +BASE_DIR = os.path.normpath( + os.path.join( + os.path.dirname(__file__), # …/assets/management/commands/ + "..", "..", "..", "scripts", "data", "tl_mobigis", "electrical_cabinet", + ) +) + +BULK_BATCH_SIZE = 500 + +CABINET_UPDATE_FIELDS = [ + "code", "name_fr", "name_nl", + "geom", "lon", "lat", "geojson", + "meter_inside", "meter_number", "ean_code", "power_supply", + "differential_breaker", "differential_switch", "publicity", + "note", "cleanliness", "key_type", "obsolescence", "visual_conformity", + "differential_breaker_caliber", "differential_caliber", + "differential_switch_caliber", "cabinet_type", "managed_by", + "departure_towards", "installation_date", +] + +CONTENT_UPDATE_FIELDS = ["model_id", "name_fr", "name_nl"] + +# cabinettype.code → CABINET_TYPE_CHOICES key +CABINET_TYPE_MAP = { + "Feux rouges": "traffic_lights", + "Luminaire": "other", + "STIB": "other", + "Mixte": "mixed", + "Autre": "other", + "Inconnu": "other", +} + +# keytype.code → KEY_TYPE_CHOICES key +KEY_TYPE_MAP = { + "Yale 5": "yale5", + "Siemens": "siemens", + "58335": "58335", + "EU4": "eu4", + "Pas de clef": "no_key", + "BUR 16": "bur16", + "BKS": "bks", +} + + +# ── helpers ─────────────────────────────────────────────────────────────────── + +def _csv_path(filename): + return os.path.join(BASE_DIR, filename) + + +def _read_lookup(filename, key_col="id"): + """Return {id_string: row_dict} for a semicolon-delimited CSV.""" + result = {} + with open(_csv_path(filename), newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + result[(row.get(key_col) or "").strip()] = row + return result + + +def _bool(value): + """'t' / 'true' / '1' → True, anything else → False.""" + return (value or "").strip().lower() in ("t", "true", "1") + + +def _str(value): + """Strip and return None if empty.""" + v = (value or "").strip() + return v if v else None + + +def _parse_date(value): + v = (value or "").strip() + if not v: + return None + for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"): + try: + return datetime.strptime(v, fmt) + except ValueError: + continue + return None + + +def _lut_code(lut, id_val): + """Return lut[id_val]['code'] stripped, or None.""" + if id_val and id_val in lut: + return lut[id_val]["code"].strip() or None + return None + + +# ── command ─────────────────────────────────────────────────────────────────── + +class Command(BaseCommand): + help = ( + "Import TrafficLightElectricalCabinet objects from " + "scripts/data/tl_mobigis/electrical_cabinet/electricalcabinet.csv" + ) + + def handle(self, *args, **options): + + # ── Lookup tables (CSV only, no DB) ─────────────────────────────────── + cabinettype_lut = _read_lookup("cabinettype.csv") + calibre_lut = _read_lookup("calibre.csv") + cleanliness_lut = _read_lookup("cleanliness.csv") + compliance_lut = _read_lookup("compliance.csv") + content_lut = _read_lookup("content.csv") + destination_lut = _read_lookup("destination.csv") + keytype_lut = _read_lookup("keytype.csv") + obsolescence_lut = _read_lookup("obsolescence.csv") + + # ── Pre-load M2M CSVs into dicts keyed by electricalcabinet_id ──────── + calibre_diff_switch: dict[str, list[str]] = defaultdict(list) + with open(_csv_path("electricalcabinet_calibre_differential_switch.csv"), + newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + cab_id = (row.get("electricalcabinet_id") or "").strip() + cal_id = (row.get("eccalibre_id") or "").strip() + if cab_id and cal_id in calibre_lut: + calibre_diff_switch[cab_id].append(calibre_lut[cal_id]["code"].strip()) + + content_m2m: dict[str, list[str]] = defaultdict(list) + with open(_csv_path("electricalcabinet_content.csv"), + newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + cab_id = (row.get("electricalcabinet_id") or "").strip() + content_id = (row.get("eccontent_id") or "").strip() + if cab_id and content_id in content_lut: + content_m2m[cab_id].append(content_id) + + exit_to_m2m: dict[str, list[str]] = defaultdict(list) + with open(_csv_path("electricalcabinet_exit_to.csv"), + newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + cab_id = (row.get("electricalcabinet_id") or "").strip() + dest_id = (row.get("ecdestination_id") or "").strip() + if cab_id and dest_id in destination_lut: + exit_to_m2m[cab_id].append(destination_lut[dest_id]["code"].strip()) + + # ── DB lookups ──────────────────────────────────────────────────────── + intersections = {i.code: i for i in TrafficLightIntersection.objects.all()} + self.stdout.write(f" Intersections loaded : {len(intersections)}") + + existing_cabinets: dict[tuple, TrafficLightElectricalCabinet] = { + (c.intersection_id, c.code): c + for c in TrafficLightElectricalCabinet.objects.all() + } + self.stdout.write(f" Existing cabinets loaded : {len(existing_cabinets)}") + + # ── Coordinate transforms (built once) ──────────────────────────────── + srs_31370 = SpatialReference(31370) + srs_3812 = SpatialReference(3812) + srs_4326 = SpatialReference(4326) + ct_to_3812 = CoordTransform(srs_31370, srs_3812) + ct_to_4326 = CoordTransform(srs_31370, srs_4326) + + # ── Parse main CSV ──────────────────────────────────────────────────── + with open(_csv_path("electricalcabinet.csv"), newline="", encoding="utf-8-sig") as f: + rows = list(csv.DictReader(f, delimiter=";")) + + to_create: list[TrafficLightElectricalCabinet] = [] + to_update: list[TrafficLightElectricalCabinet] = [] + # mob_id (CSV 'id') → cabinet instance for the content sync pass + mob_id_to_cabinet: dict[str, TrafficLightElectricalCabinet] = {} + + skipped_no_key = skipped_no_intersection = skipped_no_geom = 0 + + for row in rows: + tl_key_ep = _str(row.get("tl_key_ep")) + if not tl_key_ep: + skipped_no_key += 1 + continue + + intersection = intersections.get(tl_key_ep) + if intersection is None: + skipped_no_intersection += 1 + self.stdout.write( + self.style.WARNING(f" Intersection not found: {tl_key_ep!r}") + ) + continue + + # ── Geometry ────────────────────────────────────────────────────── + wkt = _str(row.get("wkt_geom")) + if not wkt: + skipped_no_geom += 1 + continue + try: + geom_31370 = GEOSGeometry(wkt, srid=31370) + geom_3812 = geom_31370.clone() + geom_3812.transform(ct_to_3812) + geom_wgs84 = geom_31370.clone() + geom_wgs84.transform(ct_to_4326) + lon = geom_wgs84.x + lat = geom_wgs84.y + geojson = json.dumps( + {"type": "Point", "coordinates": [round(lon, 8), round(lat, 8)]} + ) + except Exception as exc: + self.stdout.write( + self.style.WARNING( + f" Geometry error row id={row.get('id')}: {exc}" + ) + ) + skipped_no_geom += 1 + continue + + # ── Scalar fields ───────────────────────────────────────────────── + mob_id = (row.get("id") or "").strip() + arma_key = _str(row.get("armoire_key")) + code = arma_key if arma_key else f"CAB_{(row.get('armoire_id') or mob_id).strip()}" + + # Cabinet type mapping + cab_type_id = _str(row.get("cabinet_type_id")) + cab_type_code = _lut_code(cabinettype_lut, cab_type_id) + cabinet_type = CABINET_TYPE_MAP.get(cab_type_code, "other") if cab_type_code else "traffic_lights" + + # Key type mapping + key_type_id = _str(row.get("key_type_id")) + key_type_code = _lut_code(keytype_lut, key_type_id) + key_type = KEY_TYPE_MAP.get(key_type_code) if key_type_code else None + + # Free-text lookups (stored as code label) + cleanliness = _lut_code(cleanliness_lut, _str(row.get("cleanliness_id"))) + obsolescence = _lut_code(obsolescence_lut, _str(row.get("obsolescence_id"))) or 'unknown' + visual_conformity = _lut_code(compliance_lut, _str(row.get("visual_compliance_id"))) + + # Calibre fields + differential_breaker_caliber = _lut_code(calibre_lut, _str(row.get("calibre_cb_id"))) + differential_caliber = _lut_code(calibre_lut, _str(row.get("calibre_differential_id"))) + + # M2M aggregated fields + diff_switch_caliber = "; ".join(calibre_diff_switch[mob_id]) or None + departure_towards = ", ".join(exit_to_m2m[mob_id])[:50] or None + + managed_by = "BM" if _bool(row.get("bm_admin")) else None + + vals = dict( + code = code, + name_fr = code, + name_nl = code, + geom = geom_3812, + lon = lon, + lat = lat, + geojson = geojson, + meter_inside = _bool(row.get("counter_present")), + meter_number = _str(row.get("nr_counter")), + ean_code = _str(row.get("ean_counter")), + power_supply = _str(row.get("power")), + differential_breaker = _bool(row.get("circuit_breaker")), + differential_switch = _bool(row.get("differential_switch")), + publicity = _bool(row.get("publicity")), + note = _str(row.get("remark")), + cleanliness = cleanliness, + key_type = key_type, + obsolescence = obsolescence, + visual_conformity = visual_conformity, + differential_breaker_caliber = differential_breaker_caliber, + differential_caliber = differential_caliber, + differential_switch_caliber = diff_switch_caliber, + cabinet_type = cabinet_type, + managed_by = managed_by, + departure_towards = departure_towards, + installation_date = _parse_date(row.get("install_date")), + ) + + key = (intersection.pk, code) + cabinet = existing_cabinets.get(key) + if cabinet is not None: + for attr, val in vals.items(): + setattr(cabinet, attr, val) + to_update.append(cabinet) + else: + cabinet = TrafficLightElectricalCabinet(intersection=intersection, **vals) + to_create.append(cabinet) + + mob_id_to_cabinet[mob_id] = cabinet + + # ── Bulk write cabinets ─────────────────────────────────────────────── + with transaction.atomic(): + TrafficLightElectricalCabinet.objects.bulk_create( + to_create, batch_size=BULK_BATCH_SIZE + ) + TrafficLightElectricalCabinet.objects.bulk_update( + to_update, fields=CABINET_UPDATE_FIELDS, batch_size=BULK_BATCH_SIZE + ) + # After bulk_create the ORM fills in pk on each object in to_create, + # so mob_id_to_cabinet already has correct PKs for all cabinets. + + # ── Sync content objects ────────────────────────────────────────────── + # Ensure a TrafficLightElectricalCabinetContentModel exists per content type. + content_id_to_model: dict[str, TrafficLightElectricalCabinetContentModel] = {} + for c_id, c_row in content_lut.items(): + c_code = c_row.get("code", "").strip() + name_fr = c_row["descr_fr"].strip() + name_nl = (c_row.get("descr_nl") or "").strip() or name_fr + if not c_code: + continue + obj, _ = TrafficLightElectricalCabinetContentModel.objects.get_or_create( + code=c_code, + defaults={"name_fr": name_fr, "name_nl": name_nl}, + ) + content_id_to_model[c_id] = obj + + existing_contents: dict[tuple, TrafficLightElectricalCabinetContent] = { + (c.cabinet_id, c.code): c + for c in TrafficLightElectricalCabinetContent.objects.all() + } + + contents_to_create: list[TrafficLightElectricalCabinetContent] = [] + contents_to_update: list[TrafficLightElectricalCabinetContent] = [] + content_model_missing = 0 + + for mob_id, cabinet in mob_id_to_cabinet.items(): + if not cabinet.pk: + continue # Safety guard (should not happen after bulk_create) + for eccontent_id in content_m2m[mob_id]: + content_model = content_id_to_model.get(eccontent_id) + if content_model is None: + content_model_missing += 1 + continue + c_row = content_lut[eccontent_id] + code = f"CONT_{eccontent_id}" + name_fr = c_row["descr_fr"].strip() + name_nl = (c_row.get("descr_nl") or "").strip() or name_fr + key = (cabinet.pk, code) + + existing = existing_contents.get(key) + if existing is not None: + existing.model_id = content_model.pk + existing.name_fr = name_fr + existing.name_nl = name_nl + contents_to_update.append(existing) + else: + contents_to_create.append( + TrafficLightElectricalCabinetContent( + cabinet = cabinet, + code = code, + name_fr = name_fr, + name_nl = name_nl, + model = content_model, + ) + ) + + with transaction.atomic(): + TrafficLightElectricalCabinetContent.objects.bulk_create( + contents_to_create, batch_size=BULK_BATCH_SIZE + ) + TrafficLightElectricalCabinetContent.objects.bulk_update( + contents_to_update, fields=CONTENT_UPDATE_FIELDS, batch_size=BULK_BATCH_SIZE + ) + + # ── Summary ─────────────────────────────────────────────────────────── + self.stdout.write( + self.style.SUCCESS( + f"\nTrafficLightElectricalCabinet import complete:\n" + f" Cabinets created : {len(to_create)}\n" + f" Cabinets updated : {len(to_update)}\n" + f" Skipped (no tl_key_ep) : {skipped_no_key}\n" + f" Skipped (intersection missing) : {skipped_no_intersection}\n" + f" Skipped (bad/missing geom) : {skipped_no_geom}\n" + f"\n Content objects created : {len(contents_to_create)}\n" + f" Content objects updated : {len(contents_to_update)}\n" + f" Content skipped (no model) : {content_model_missing}" + ) + ) diff --git a/streetup/assets/management/commands/import_tl_lanterns_accessories_from_mobigis.py b/streetup/assets/management/commands/import_tl_lanterns_accessories_from_mobigis.py new file mode 100644 index 0000000..05d58bb --- /dev/null +++ b/streetup/assets/management/commands/import_tl_lanterns_accessories_from_mobigis.py @@ -0,0 +1,303 @@ +""" +Management command to import TrafficLightLantern and TrafficLightAccessory objects +from the MobiGIS CSV exports. + +Source files : scripts/data/tl_mobigis/tllanterns.csv + scripts/data/tl_mobigis/tlaccessories.csv +Lookup files : tllanterntype.csv, tlaccessorytype.csv, tltrafficlights.csv + +Mapping +------- + lantern/accessory.trafficlight_id + → tltrafficlights.trafficlight_id + → (tl_key, nr_trafficlight) + → TrafficLightPole in DB + +Idempotency : keyed on (pole, code) where code = MobiGIS row id. +Performance : bulk_create + bulk_update; CoordTransform built once (no geometry + in these CSVs, kept for consistency with other import commands). +""" + +import csv +import os + +from django.core.management.base import BaseCommand +from django.db import transaction + +from assets.models.trafficlights import ( + TrafficLightAccessory, + TrafficLightAccessoryModel, + TrafficLightLantern, + TrafficLightLanternModel, + TrafficLightPole, +) + +BASE_DIR = os.path.normpath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "scripts", "data", "tl_mobigis", + ) +) + +BULK_BATCH_SIZE = 500 + +LANTERN_UPDATE_FIELDS = ["code", "model_id", "serial_number", "name_fr", "name_nl"] +ACCESSORY_UPDATE_FIELDS = ["code", "model_id", "serial_number", "name_fr", "name_nl", + "intersection_id"] + + +def _csv_path(filename): + return os.path.join(BASE_DIR, filename) + + +def _read_lookup(filename, key_col="id"): + """Return {id_string: row_dict} for a semicolon-delimited CSV.""" + result = {} + with open(_csv_path(filename), newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + result[(row.get(key_col) or "").strip()] = row + return result + + +def _serial(value): + """Return None for the MobiGIS placeholder '0', else the trimmed value.""" + v = (value or "").strip() + return None if v in ("0", "") else v + + +class Command(BaseCommand): + help = ( + "Import TrafficLightLantern and TrafficLightAccessory from " + "scripts/data/tl_mobigis/tllanterns.csv and tlaccessories.csv" + ) + + def handle(self, *args, **options): + # ── Lookup tables (CSV only, no DB) ────────────────────────────────── + lantern_type_lut = _read_lookup("tllanterntype.csv") # id → {descr_fr, …} + accessory_type_lut = _read_lookup("tlaccessorytype.csv") # id → {descr_fr, …} + + # lanterns/accessories reference poles by tltrafficlights.id (not trafficlight_id) + tl_lut = {} + with open(_csv_path("tltrafficlights.csv"), newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + row_id = (row.get("id") or "").strip() + tl_key = (row.get("tl_key") or "").strip() + code = (row.get("nr_trafficlight") or "").strip() + if row_id and tl_key: + tl_lut[row_id] = (tl_key, code) + + # ── DB lookups (4 queries total) ────────────────────────────────────── + # Poles keyed by (intersection_code, pole.code) — after the poles import + # pole.code is already in the prefixed format "SUL21_A01". + poles = { + (p.intersection.code, p.code): p + for p in TrafficLightPole.objects.select_related("intersection").all() + } + self.stdout.write(f" Poles loaded : {len(poles)}") + + lantern_models_by_name = { + m.name_fr: m for m in TrafficLightLanternModel.objects.all() if m.name_fr + } + accessory_models_by_name = { + m.name_fr: m for m in TrafficLightAccessoryModel.objects.all() if m.name_fr + } + self.stdout.write( + f" Lantern models loaded : {len(lantern_models_by_name)}\n" + f" Accessory models loaded : {len(accessory_models_by_name)}" + ) + + existing_lanterns = { + (l.pole_id, l.code): l + for l in TrafficLightLantern.objects.all() + } + existing_accessories = { + (a.pole_id, a.code): a + for a in TrafficLightAccessory.objects.filter(pole__isnull=False) + } + self.stdout.write( + f" Existing lanterns loaded : {len(existing_lanterns)}\n" + f" Existing accessories loaded: {len(existing_accessories)}" + ) + + # ── Read lanterns + accessories CSVs ───────────────────────────────── + with open(_csv_path("tllanterns.csv"), newline="", encoding="utf-8-sig") as f: + lan_rows = list(csv.DictReader(f, delimiter=";")) + with open(_csv_path("tlaccessories.csv"), newline="", encoding="utf-8-sig") as f: + acc_rows = list(csv.DictReader(f, delimiter=";")) + + # ── Pre-pass: assign sequential codes per pole ──────────────────────── + # Lanterns within a pole: SUL21_A01_LAN01, SUL21_A01_LAN02, … + # Accessories within a pole: SUL21_A01_ACC01, SUL21_A01_ACC02, … + # Ordering is by MobiGIS row id (integer sort for stability). + from collections import defaultdict + + def _build_seq_codes(rows, id_col, suffix): + """Return {mob_id: new_sequential_code} for a CSV row list.""" + groups = defaultdict(list) # (tl_key, raw_nr) → [mob_id, …] + for row in rows: + tl_id = (row.get("trafficlight_id") or "").strip() + mob_id = (row.get(id_col) or "").strip() + mapping = tl_lut.get(tl_id) + if not mapping: + continue + tl_key, raw_nr = mapping + new_pole_code = f"{tl_key}_{raw_nr}" + if (tl_key, new_pole_code) not in poles: + continue + groups[(tl_key, raw_nr)].append(mob_id) + result = {} + for (tl_key, raw_nr), mob_ids in groups.items(): + sorted_ids = sorted(mob_ids, key=lambda x: int(x) if x.isdigit() else x) + for seq, mid in enumerate(sorted_ids, 1): + result[mid] = f"{tl_key}_{raw_nr}_{suffix}{seq:02d}" + return result + + lantern_new_codes = _build_seq_codes(lan_rows, "id", "LAN") + accessory_new_codes = _build_seq_codes(acc_rows, "id", "ACC") + + # ── Parse lanterns CSV ──────────────────────────────────────────────── + lan_create: list[TrafficLightLantern] = [] + lan_update: list[TrafficLightLantern] = [] + lan_skip_pole = lan_skip_model = 0 + + for row in lan_rows: + tl_id = (row.get("trafficlight_id") or "").strip() + mob_id = (row.get("id") or "").strip() + + new_code = lantern_new_codes.get(mob_id) + if new_code is None: + lan_skip_pole += 1 + continue + + mapping = tl_lut.get(tl_id) + tl_key, raw_nr = mapping + new_pole_code = f"{tl_key}_{raw_nr}" + pole = poles.get((tl_key, new_pole_code)) + if pole is None: + lan_skip_pole += 1 + continue + + type_id = (row.get("lantern_type_id") or "").strip() + type_row = lantern_type_lut.get(type_id) + lantern_model = ( + lantern_models_by_name.get(type_row["descr_fr"].strip()) + if type_row else None + ) + if lantern_model is None: + lan_skip_model += 1 + continue + + serial = _serial(row.get("serial_number")) + name = lantern_model.name_fr or new_code + + # Dual lookup: new code (re-run) or old mob_id (first migration) + obj = existing_lanterns.get((pole.pk, new_code)) \ + or existing_lanterns.get((pole.pk, mob_id)) + if obj is not None: + obj.code = new_code + obj.model_id = lantern_model.pk + obj.serial_number = serial + obj.name_fr = name + obj.name_nl = name + lan_update.append(obj) + else: + lan_create.append( + TrafficLightLantern( + pole = pole, + code = new_code, + name_fr = name, + name_nl = name, + model_id = lantern_model.pk, + serial_number = serial, + ) + ) + + # ── Parse accessories CSV ───────────────────────────────────────────── + acc_create: list[TrafficLightAccessory] = [] + acc_update: list[TrafficLightAccessory] = [] + acc_skip_pole = acc_skip_model = 0 + + for row in acc_rows: + tl_id = (row.get("trafficlight_id") or "").strip() + mob_id = (row.get("id") or "").strip() + + new_code = accessory_new_codes.get(mob_id) + if new_code is None: + acc_skip_pole += 1 + continue + + mapping = tl_lut.get(tl_id) + tl_key, raw_nr = mapping + new_pole_code = f"{tl_key}_{raw_nr}" + pole = poles.get((tl_key, new_pole_code)) + if pole is None: + acc_skip_pole += 1 + continue + + type_id = (row.get("accessory_type_id") or "").strip() + type_row = accessory_type_lut.get(type_id) + accessory_model = ( + accessory_models_by_name.get(type_row["descr_fr"].strip()) + if type_row else None + ) + if accessory_model is None: + acc_skip_model += 1 + continue + + serial = _serial(row.get("serial_number")) + name = accessory_model.name_fr or new_code + + # Dual lookup: new code (re-run) or old mob_id (first migration) + obj = existing_accessories.get((pole.pk, new_code)) \ + or existing_accessories.get((pole.pk, mob_id)) + if obj is not None: + obj.code = new_code + obj.model_id = accessory_model.pk + obj.serial_number = serial + obj.name_fr = name + obj.name_nl = name + obj.intersection_id = pole.intersection_id + acc_update.append(obj) + else: + acc_create.append( + TrafficLightAccessory( + pole = pole, + intersection_id = pole.intersection_id, + code = new_code, + name_fr = name, + name_nl = name, + model_id = accessory_model.pk, + serial_number = serial, + ) + ) + + # ── Bulk DB writes ──────────────────────────────────────────────────── + with transaction.atomic(): + TrafficLightLantern.objects.bulk_create( + lan_create, batch_size=BULK_BATCH_SIZE + ) + TrafficLightLantern.objects.bulk_update( + lan_update, fields=LANTERN_UPDATE_FIELDS, batch_size=BULK_BATCH_SIZE + ) + TrafficLightAccessory.objects.bulk_create( + acc_create, batch_size=BULK_BATCH_SIZE + ) + TrafficLightAccessory.objects.bulk_update( + acc_update, fields=ACCESSORY_UPDATE_FIELDS, batch_size=BULK_BATCH_SIZE + ) + + # ── Summary ─────────────────────────────────────────────────────────── + self.stdout.write( + self.style.SUCCESS( + f"\nLantern import complete:\n" + f" Created : {len(lan_create)}\n" + f" Updated : {len(lan_update)}\n" + f" Skipped (pole not found) : {lan_skip_pole}\n" + f" Skipped (model not found) : {lan_skip_model}\n" + f"\nAccessory import complete:\n" + f" Created : {len(acc_create)}\n" + f" Updated : {len(acc_update)}\n" + f" Skipped (pole not found) : {acc_skip_pole}\n" + f" Skipped (model not found) : {acc_skip_model}" + ) + ) diff --git a/streetup/assets/management/commands/import_tl_models_from_mobigis.py b/streetup/assets/management/commands/import_tl_models_from_mobigis.py new file mode 100644 index 0000000..1675472 --- /dev/null +++ b/streetup/assets/management/commands/import_tl_models_from_mobigis.py @@ -0,0 +1,144 @@ +""" +Management command to import TrafficLight model types from MobiGIS CSV exports. + +Handled files (located in scripts/data/tl_mobigis/): + - tlaccessorytype.csv → TrafficLightAccessoryModel + - tltrafficlighttype.csv → TrafficLightPoleModel + - tllanterntype.csv → TrafficLightLanternModel + +CSV format: semicolon-delimited, UTF-8 BOM, columns: id;code;descr_fr;descr_nl;active +Only rows with active='t' are imported. +""" + +import csv +import os + +from django.core.management.base import BaseCommand +from django.db import transaction + +from assets.models.trafficlights import ( + TrafficLightAccessoryModel, + TrafficLightPoleModel, + TrafficLightLanternModel, +) + +CSV_DIR = os.path.join( + os.path.dirname(__file__), # .../assets/management/commands/ + "..", "..", "..", "scripts", "data", "tl_mobigis", +) + + +def _clean_code(value: str) -> str: + """Return an empty string when the source value is the placeholder '0'.""" + return "" if value.strip() == "0" else value.strip() + + +def _clean_nl(value: str) -> str | None: + """Return None when the Dutch description is the placeholder '0'.""" + v = value.strip() + return None if v == "0" else v + + +def _read_csv(filename: str): + path = os.path.normpath(os.path.join(CSV_DIR, filename)) + rows = [] + with open(path, newline="", encoding="utf-8-sig") as f: + reader = csv.DictReader(f, delimiter=";") + for row in reader: + if row.get("active", "").strip() == "t": + rows.append(row) + return rows + + +class Command(BaseCommand): + help = ( + "Import TrafficLightAccessoryModel, TrafficLightPoleModel and " + "TrafficLightLanternModel from the MobiGIS CSV exports." + ) + + @transaction.atomic + def handle(self, *args, **options): + self._import_accessory_models() + self._import_pole_models() + self._import_lantern_models() + + # ------------------------------------------------------------------ + # TrafficLightAccessoryModel ← tlaccessorytype.csv + # ------------------------------------------------------------------ + def _import_accessory_models(self): + rows = _read_csv("tlaccessorytype.csv") + created = updated = 0 + for row in rows: + name_fr = row["descr_fr"].strip() + if not name_fr: + continue + code = _clean_code(row.get("code", "")) + name_nl = _clean_nl(row.get("descr_nl", "")) + _, was_created = TrafficLightAccessoryModel.objects.update_or_create( + name_fr=name_fr, + defaults={"code": code, "name_nl": name_nl}, + ) + if was_created: + created += 1 + else: + updated += 1 + self.stdout.write( + self.style.SUCCESS( + f"TrafficLightAccessoryModel: {created} created, {updated} updated." + ) + ) + + # ------------------------------------------------------------------ + # TrafficLightPoleModel ← tltrafficlighttype.csv + # ------------------------------------------------------------------ + def _import_pole_models(self): + rows = _read_csv("tltrafficlighttype.csv") + created = updated = 0 + for row in rows: + name_fr = row["descr_fr"].strip() + if not name_fr: + continue + code = _clean_code(row.get("code", "")) + name_nl = _clean_nl(row.get("descr_nl", "")) + _, was_created = TrafficLightPoleModel.objects.update_or_create( + name_fr=name_fr, + defaults={"code": code, "name_nl": name_nl}, + ) + if was_created: + created += 1 + else: + updated += 1 + self.stdout.write( + self.style.SUCCESS( + f"TrafficLightPoleModel: {created} created, {updated} updated." + ) + ) + + # ------------------------------------------------------------------ + # TrafficLightLanternModel ← tllanterntype.csv + # The CSV does not contain voltage data; voltage defaults to 0. + # Update manually after import if needed. + # ------------------------------------------------------------------ + def _import_lantern_models(self): + rows = _read_csv("tllanterntype.csv") + created = updated = 0 + for row in rows: + name_fr = row["descr_fr"].strip() + if not name_fr: + continue + code = _clean_code(row.get("code", "")) + name_nl = _clean_nl(row.get("descr_nl", "")) + _, was_created = TrafficLightLanternModel.objects.update_or_create( + name_fr=name_fr, + defaults={"code": code, "name_nl": name_nl, "voltage": 0}, + ) + if was_created: + created += 1 + else: + updated += 1 + self.stdout.write( + self.style.SUCCESS( + f"TrafficLightLanternModel: {created} created, {updated} updated " + "(voltage defaulted to 0 — update manually if needed)." + ) + ) diff --git a/streetup/assets/management/commands/import_tl_panels_from_mobigis.py b/streetup/assets/management/commands/import_tl_panels_from_mobigis.py new file mode 100644 index 0000000..a26487b --- /dev/null +++ b/streetup/assets/management/commands/import_tl_panels_from_mobigis.py @@ -0,0 +1,225 @@ +""" +Management command to import SignPanel objects from the MobiGIS CSV export, +linked to the existing TrafficLightPole records. + +Source files : scripts/data/tl_mobigis/tlpannels.csv + scripts/data/tl_mobigis/tlpanneltype.csv +Lookup file : scripts/data/tl_mobigis/tltrafficlights.csv + +Mapping +------- + tlpannels.trafficlight_id + → tltrafficlights.id (NOT trafficlight_id — same off-by-one trap) + → (tl_key, nr_trafficlight) + → TrafficLightPole in DB + → SignPanel.trafficlight_pole + +Panel type +---------- + tlpannels.pannel_type_id + → tlpanneltype.code e.g. "B5 Stop" + → first token ("B5") + → SignPanelType.objects.filter(code__iexact="B5") + +Code format +----------- + {tl_key}_{nr}_PAN{seq:02d} e.g. SWB03_A04_PAN01, SWB03_A04_PAN02, … + Sequential per pole, ordered by MobiGIS row id (integer sort, stable). + +Idempotency +----------- + Keyed on (trafficlight_pole_id, code). Running twice: 0 created, N updated. + +Performance +----------- + bulk_create + bulk_update (batch size 500). +""" + +import csv +import os +from collections import defaultdict + +from django.core.management.base import BaseCommand +from django.db import transaction + +from assets.models.sign import SignPanel, SignPanelType +from assets.models.trafficlights import TrafficLightPole + +BASE_DIR = os.path.normpath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "scripts", "data", "tl_mobigis", + ) +) + +BULK_BATCH_SIZE = 500 + +UPDATE_FIELDS = [ + "code", "name_fr", "name_nl", + "signpanel_type", "support_type", + "trafficlight_pole", +] + + +def _csv_path(filename): + return os.path.join(BASE_DIR, filename) + + +class Command(BaseCommand): + help = ( + "Import SignPanel objects from " + "scripts/data/tl_mobigis/tlpannels.csv, " + "linked to TrafficLightPole via tltrafficlights.csv" + ) + + def handle(self, *args, **options): + # ── Lookup: tlpanneltype id → {code, descr_fr, descr_nl} ──────────── + panneltype_lut = {} + with open(_csv_path("tlpanneltype.csv"), newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + panneltype_lut[(row.get("id") or "").strip()] = row + + # ── Lookup: tltrafficlights id → (tl_key, nr_trafficlight) ────────── + # IMPORTANT: keyed by the "id" column, not "trafficlight_id" + tl_lut = {} + with open(_csv_path("tltrafficlights.csv"), newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + row_id = (row.get("id") or "").strip() + tl_key = (row.get("tl_key") or "").strip() + nr = (row.get("nr_trafficlight") or "").strip() + if row_id and tl_key: + tl_lut[row_id] = (tl_key, nr) + + # ── DB: poles keyed by (intersection_code, pole_code) ──────────────── + poles = { + (p.intersection.code, p.code): p + for p in TrafficLightPole.objects.select_related("intersection").all() + } + self.stdout.write(f" Poles loaded : {len(poles)}") + + # ── DB: SignPanelType keyed by code (upper) ─────────────────────────── + # Panel type code from CSV is like "B5 Stop"; we match on the first + # whitespace-separated token (e.g. "B5") against SignPanelType.code. + sign_panel_types = { + pt.code.strip().upper(): pt + for pt in SignPanelType.objects.all() + } + self.stdout.write(f" SignPanelType loaded : {len(sign_panel_types)}") + + # ── DB: existing SignPanels keyed by (trafficlight_pole_id, code) ───── + existing_panels = { + (sp.trafficlight_pole_id, sp.code): sp + for sp in SignPanel.objects.filter(trafficlight_pole__isnull=False) + } + self.stdout.write(f" Existing panels loaded : {len(existing_panels)}") + + # ── Read CSV ────────────────────────────────────────────────────────── + with open(_csv_path("tlpannels.csv"), newline="", encoding="utf-8-sig") as f: + pan_rows = list(csv.DictReader(f, delimiter=";")) + self.stdout.write(f" Panel rows in CSV : {len(pan_rows)}") + + # ── Pre-pass: assign sequential codes per pole ──────────────────────── + groups = defaultdict(list) # (tl_key, raw_nr) → [mob_id, …] + for row in pan_rows: + tl_ref = (row.get("trafficlight_id") or "").strip() + mob_id = (row.get("id") or "").strip() + mapping = tl_lut.get(tl_ref) + if not mapping: + continue + tl_key, raw_nr = mapping + if (tl_key, f"{tl_key}_{raw_nr}") not in poles: + continue + groups[(tl_key, raw_nr)].append(mob_id) + + panel_new_codes = {} # mob_id → "SWB03_A04_PAN01" + for (tl_key, raw_nr), mob_ids in groups.items(): + sorted_ids = sorted(mob_ids, key=lambda x: int(x) if x.isdigit() else x) + for seq, mid in enumerate(sorted_ids, 1): + panel_new_codes[mid] = f"{tl_key}_{raw_nr}_PAN{seq:02d}" + + # ── Parse rows ──────────────────────────────────────────────────────── + to_create: list[SignPanel] = [] + to_update: list[SignPanel] = [] + skip_pole = skip_type = 0 + + for row in pan_rows: + tl_ref = (row.get("trafficlight_id") or "").strip() + mob_id = (row.get("id") or "").strip() + + new_code = panel_new_codes.get(mob_id) + if new_code is None: + skip_pole += 1 + continue + + mapping = tl_lut.get(tl_ref) + tl_key, raw_nr = mapping + pole = poles.get((tl_key, f"{tl_key}_{raw_nr}")) + if pole is None: + skip_pole += 1 + continue + + # Resolve panel type via prefix of tlpanneltype.code + type_row = panneltype_lut.get((row.get("pannel_type_id") or "").strip()) + sign_panel_type = None + if type_row: + raw_code = (type_row.get("code") or "").strip() + name_fr = (type_row.get("descr_fr") or raw_code).strip() + name_nl = (type_row.get("descr_nl") or raw_code).strip() + prefix = raw_code.split()[0].upper() if raw_code else "" + sign_panel_type = sign_panel_types.get(prefix) + else: + name_fr = new_code + name_nl = new_code + + if sign_panel_type is None: + skip_type += 1 + # Still create the panel, type will be null + + obj = existing_panels.get((pole.pk, new_code)) + if obj is not None: + obj.code = new_code + obj.name_fr = name_fr + obj.name_nl = name_nl + obj.signpanel_type = sign_panel_type + obj.support_type = "trafficlightpole" + obj.trafficlight_pole = pole + to_update.append(obj) + else: + to_create.append( + SignPanel( + code = new_code, + name_fr = name_fr, + name_nl = name_nl, + signpanel_type = sign_panel_type, + support_type = "trafficlightpole", + trafficlight_pole = pole, + ) + ) + + # ── Bulk DB operations ──────────────────────────────────────────────── + with transaction.atomic(): + created_count = 0 + for i in range(0, len(to_create), BULK_BATCH_SIZE): + batch = SignPanel.objects.bulk_create( + to_create[i : i + BULK_BATCH_SIZE], + batch_size=BULK_BATCH_SIZE, + ) + created_count += len(batch) + + updated_count = 0 + for i in range(0, len(to_update), BULK_BATCH_SIZE): + updated_count += SignPanel.objects.bulk_update( + to_update[i : i + BULK_BATCH_SIZE], + UPDATE_FIELDS, + batch_size=BULK_BATCH_SIZE, + ) + + self.stdout.write( + self.style.SUCCESS( + f"\nSignPanel import complete:\n" + f" Created : {created_count}\n" + f" Updated : {updated_count}\n" + f" Skipped (pole not found) : {skip_pole}\n" + f" Created without type (prefix no match): {skip_type}\n" + ) + ) diff --git a/streetup/assets/management/commands/import_tl_pole_photos_from_mobigis.py b/streetup/assets/management/commands/import_tl_pole_photos_from_mobigis.py new file mode 100644 index 0000000..800e438 --- /dev/null +++ b/streetup/assets/management/commands/import_tl_pole_photos_from_mobigis.py @@ -0,0 +1,207 @@ +""" +Management command to import photos for the TrafficLightPoles +that have an image in the MobiGIS CSV export. + +For each pole with an image field, the filename is extracted from the path and +the file is fetched from: + https://data.mobility.brussels/media/maintain/inventory/trafficlight/ + +A ManagedDocument + DocumentVersion are created and linked to the pole via a +DocumentAttachment. The DocumentTag "photo" is applied to every document. + +Re-running the command is safe: existing attachments (same document title + +same pole) are skipped. +""" + +import csv +import os +import urllib.request + +from django.contrib.contenttypes.models import ContentType +from django.core.files.base import ContentFile +from django.core.management.base import BaseCommand +from django.db import models, transaction + +from assets.models.trafficlights import TrafficLightIntersection, TrafficLightPole +from documents.models import ( + DocumentAttachment, + DocumentTag, + DocumentVersion, + ManagedDocument, +) + +BASE_DIR = os.path.normpath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "scripts", "data", "tl_mobigis", + ) +) +CSV_PATH = os.path.join(BASE_DIR, "tltrafficlights.csv") + +PHOTO_BASE_URL = "https://data.mobility.brussels/media/maintain/inventory/trafficlight/" +MAX_POLES = 10000 + + +class Command(BaseCommand): + help = ( + "Fetch and attach photos for the first 10000 TrafficLightPoles " + "that have an image in tltrafficlights.csv." + ) + + def handle(self, *args, **options): + # ── Get-or-create the "photo" DocumentTag ──────────────────────────── + photo_tag, tag_created = DocumentTag.objects.get_or_create( + slug="photo", + defaults={"name_fr": "photo", "name_nl": "foto", "order": 0}, + ) + if tag_created: + self.stdout.write(self.style.SUCCESS(' DocumentTag "photo" created.')) + else: + self.stdout.write(f' DocumentTag "photo" found (id={photo_tag.pk}).') + + # ── Pre-load poles keyed by (intersection_code, pole_code) ────────── + intersections = {i.code: i for i in TrafficLightIntersection.objects.all()} + poles_by_key = { + (p.intersection.code, p.code): p + for p in TrafficLightPole.objects.select_related("intersection").all() + } + pole_ct = ContentType.objects.get_for_model(TrafficLightPole) + + # ── Deduplication pass ─────────────────────────────────────────────── + # For each pole, group its DocumentAttachments by the filename stem + # stored in the related DocumentVersion.file field (stripped of path, + # extension, and any Django-added suffix like "_abc123"). + # When >1 attachment share the same stem keep only the most recent one + # (highest pk) and delete the others together with their documents. + self.stdout.write(" Running deduplication pass…") + dedup_deleted = 0 + all_pole_attachments = ( + DocumentAttachment.objects + .filter(content_type=pole_ct) + .select_related("document") + .prefetch_related("document__versions") + ) + # Group by (object_id, filename_stem) + seen: dict[tuple, int] = {} # (pole_pk, stem) → best attachment pk + duplicates: list[int] = [] # attachment pks to delete + for att in all_pole_attachments.order_by("pk"): + version = att.document.versions.order_by("pk").first() + if version is None: + continue + raw_name = os.path.basename(version.file.name) # foo_abc123.jpg + stem_full = os.path.splitext(raw_name)[0] # foo_abc123 + # Strip Django's "_" suffix (last "_" if the token + # looks like an 8-char alphanumeric hash). + parts = stem_full.rsplit("_", 1) + stem = parts[0] if len(parts) == 2 and len(parts[1]) <= 10 and parts[1].isalnum() else stem_full + key = (att.object_id, stem) + if key in seen: + duplicates.append(att.pk) # keep the first (lowest pk), drop later ones + else: + seen[key] = att.pk + + if duplicates: + # Delete in bulk: attachments, then orphaned documents + dup_attachments = DocumentAttachment.objects.filter(pk__in=duplicates) + doc_ids = list(dup_attachments.values_list("document_id", flat=True)) + dup_attachments.delete() + ManagedDocument.objects.filter(pk__in=doc_ids).delete() + dedup_deleted = len(duplicates) + self.stdout.write( + self.style.WARNING(f" Deduplication: removed {dedup_deleted} duplicate attachment(s).") + ) + else: + self.stdout.write(" Deduplication: no duplicates found.") + + # ── Read CSV and collect rows with images ──────────────────────────── + with open(CSV_PATH, newline="", encoding="utf-8-sig") as f: + rows = list(csv.DictReader(f, delimiter=";")) + + candidates = [ + r for r in rows + if (r.get("tl_key") or "").strip() + and (r.get("image") or "").strip() + ][:MAX_POLES] + + self.stdout.write(f" Candidates with photo: {len(candidates)}") + + created = updated = skipped = errors = 0 + + for row in candidates: + tl_key = row["tl_key"].strip() + pole_code = (row.get("nr_trafficlight") or "").strip() + # Pole codes are stored as "_" + full_pole_code = f"{tl_key}_{pole_code}" if pole_code else tl_key + image_path = row["image"].strip() + filename = os.path.basename(image_path) + url = PHOTO_BASE_URL + filename + + # ── Find the pole ───────────────────────────────────────────── + pole = poles_by_key.get((tl_key, full_pole_code)) + if pole is None: + self.stdout.write( + self.style.WARNING( + f" Pole not found: intersection={tl_key!r}, " + f"code={full_pole_code!r} — skipping." + ) + ) + skipped += 1 + continue + + # ── Idempotency: skip if attachment already exists ───────────── + # Match on document title (set by us) OR on the filename stem + # stored inside DocumentVersion.file (robust against Django's + # automatic "_" renaming of duplicate file names). + title = os.path.splitext(filename)[0] + already = DocumentAttachment.objects.filter( + content_type=pole_ct, + object_id=pole.pk, + ).filter( + models.Q(document__title=title) + | models.Q(document__versions__file__icontains=title) + ).exists() + if already: + updated += 1 + continue + + # ── Download the image ──────────────────────────────────────── + try: + with urllib.request.urlopen(url, timeout=15) as response: + image_data = response.read() + except Exception as exc: + self.stdout.write( + self.style.WARNING(f" Download failed {url}: {exc}") + ) + errors += 1 + continue + + # ── Persist document + attachment (committed immediately) ───── + with transaction.atomic(): + doc = ManagedDocument.objects.create(title=title) + doc.tags.add(photo_tag) + + version = DocumentVersion( + document=doc, + status=DocumentVersion.STATUS_APPROVED, + ) + version.file.save(filename, ContentFile(image_data), save=False) + version.save() # sets version_number, current_version, thumbnail + + DocumentAttachment.objects.create( + document=doc, + content_type=pole_ct, + object_id=pole.pk, + ) + + created += 1 + self.stdout.write(f" ✓ {tl_key}/{full_pole_code} → {filename}") + + self.stdout.write( + self.style.SUCCESS( + f"\nPhoto import complete:\n" + f" Created : {created}\n" + f" Already present: {updated}\n" + f" Skipped (pole not found): {skipped}\n" + f" Errors (download) : {errors}" + ) + ) diff --git a/streetup/assets/management/commands/import_tl_poles_from_mobigis.py b/streetup/assets/management/commands/import_tl_poles_from_mobigis.py new file mode 100644 index 0000000..a8b0ddb --- /dev/null +++ b/streetup/assets/management/commands/import_tl_poles_from_mobigis.py @@ -0,0 +1,239 @@ +""" +Management command to import TrafficLightPole objects from the MobiGIS CSV export. + +Source file : scripts/data/tl_mobigis/tltrafficlights.csv +Lookup files: tltrafficlighttype.csv, tlcolour.csv, tlobsolescence.csv + +Rules +----- +- Skip rows that have no tl_key (tl_key = intersection code). +- Geometry is in EPSG 31370 (Belgian Lambert 72); transform to EPSG 3812 for + the geom field, and derive lon/lat (WGS84) + geojson from the same point. +- Existing poles (same intersection + same code) are updated, not duplicated. +- name_fr and name_nl are set to the pole code. +- Photos are ignored for now. + +Performance +----------- +All DB work is done in two bulk operations (bulk_create + bulk_update) instead +of one update_or_create per row, reducing ~12 000 individual SQL round-trips to +a handful of queries. +""" + +import csv +import json +import os + +from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.contrib.gis.geos import GEOSGeometry +from django.core.management.base import BaseCommand +from django.db import transaction + +from assets.models.trafficlights import ( + TrafficLightIntersection, + TrafficLightPole, + TrafficLightPoleModel, +) + +BASE_DIR = os.path.normpath( + os.path.join( + os.path.dirname(__file__), # …/assets/management/commands/ + "..", "..", "..", "scripts", "data", "tl_mobigis", + ) +) + +BULK_BATCH_SIZE = 500 + +# Fields written during both create and update +UPDATE_FIELDS = [ + "code", "model_id", "geom", "lon", "lat", "geojson", + "arc_length", "extension", "colour", "obsolescence", "note", + "name_fr", "name_nl", +] + +OBSOLESCENCE_MAP = { + "13": "good", # Bon état général + "14": "medium", # Moyen + "15": "bad", # Dégradé + "16": "critical", # Dangereux +} + + +def _csv_path(filename): + return os.path.join(BASE_DIR, filename) + + +def _read_lookup(filename, key_col="id"): + """Return {id_string: row_dict} for a semicolon-delimited CSV.""" + result = {} + with open(_csv_path(filename), newline="", encoding="utf-8-sig") as f: + for row in csv.DictReader(f, delimiter=";"): + result[row[key_col].strip()] = row + return result + + +class Command(BaseCommand): + help = ( + "Import TrafficLightPole objects from " + "scripts/data/tl_mobigis/tltrafficlights.csv" + ) + + def handle(self, *args, **options): + # ── Lookup tables (CSV, no DB) ─────────────────────────────────────── + colour_lut = _read_lookup("tlcolour.csv") + type_lut = _read_lookup("tltrafficlighttype.csv") + + # ── DB lookups (3 queries total) ───────────────────────────────────── + intersections = {i.code: i for i in TrafficLightIntersection.objects.all()} + self.stdout.write(f" Intersections loaded : {len(intersections)}") + + all_models = list(TrafficLightPoleModel.objects.all()) + pole_models_by_name = {m.name_fr: m for m in all_models if m.name_fr} + pole_models_by_code = {m.code: m for m in all_models if m.code} + self.stdout.write(f" PoleModels loaded : {len(all_models)}") + + # Existing poles keyed by (intersection_id, code) → pole instance + existing_poles = { + (p.intersection_id, p.code): p + for p in TrafficLightPole.objects.all() + } + self.stdout.write(f" Existing poles loaded: {len(existing_poles)}") + + # ── Coordinate transforms (built once, reused for every row) ───────── + srs_31370 = SpatialReference(31370) + srs_3812 = SpatialReference(3812) + srs_4326 = SpatialReference(4326) + ct_to_3812 = CoordTransform(srs_31370, srs_3812) + ct_to_4326 = CoordTransform(srs_31370, srs_4326) + + # ── Parse CSV into two buckets ──────────────────────────────────────── + to_create: list[TrafficLightPole] = [] + to_update: list[TrafficLightPole] = [] + skipped_no_key = skipped_no_intersection = skipped_no_geom = skipped_no_model = 0 + + with open(_csv_path("tltrafficlights.csv"), newline="", encoding="utf-8-sig") as f: + rows = list(csv.DictReader(f, delimiter=";")) + + for row in rows: + tl_key = (row.get("tl_key") or "").strip() + if not tl_key: + skipped_no_key += 1 + continue + + intersection = intersections.get(tl_key) + if intersection is None: + skipped_no_intersection += 1 + self.stdout.write(self.style.WARNING(f" Intersection not found: {tl_key!r}")) + continue + + # ── Geometry ────────────────────────────────────────────────────── + wkt = (row.get("wkt_geom") or "").strip() + if not wkt: + skipped_no_geom += 1 + continue + try: + geom_31370 = GEOSGeometry(wkt, srid=31370) + geom_3812 = geom_31370.clone() + geom_3812.transform(ct_to_3812) + geom_wgs84 = geom_31370.clone() + geom_wgs84.transform(ct_to_4326) + lon = geom_wgs84.x + lat = geom_wgs84.y + geojson = json.dumps( + {"type": "Point", "coordinates": [round(lon, 8), round(lat, 8)]} + ) + except Exception as exc: + self.stdout.write( + self.style.WARNING(f" Geometry error row id={row.get('id')}: {exc}") + ) + skipped_no_geom += 1 + continue + + # ── Pole model ──────────────────────────────────────────────────── + type_id = (row.get("trafficlight_type_id") or "").strip() + pole_model = None + if type_id and type_id in type_lut: + name_fr = type_lut[type_id]["descr_fr"].strip() + pole_model = pole_models_by_name.get(name_fr) or pole_models_by_code.get( + type_lut[type_id]["code"].strip() + ) + if pole_model is None: + pole_model = pole_models_by_code.get("A") # fallback → "Autre" + if pole_model is None: + skipped_no_model += 1 + self.stdout.write( + self.style.WARNING( + f" No pole model for trafficlight_type_id={type_id!r} " + f"(row id={row.get('id')}); skipping." + ) + ) + continue + + # ── Scalar fields ───────────────────────────────────────────────── + nr = (row.get("nr_trafficlight") or "").strip() + new_code = f"{tl_key}_{nr}" # e.g. SUL21_A01 + extension = (row.get("extension") or "").strip().lower() == "t" + arc_raw = (row.get("length_arc") or "").strip() + arc_length = float(arc_raw) if arc_raw else None + colour_id = (row.get("colour_id") or "").strip() + colour = colour_lut[colour_id]["code"].strip() if colour_id in colour_lut else "" + obsolescence = OBSOLESCENCE_MAP.get((row.get("obsolescence_id") or "").strip(), "unknown") + note = (row.get("remark") or "").strip() or None + + # ── Route to create or update bucket ────────────────────────────── + # Dual lookup: new prefixed code (re-run) OR old raw code (first migration) + key_new = (intersection.pk, new_code) + key_old = (intersection.pk, nr) + pole = existing_poles.get(key_new) or existing_poles.get(key_old) + if pole is not None: + pole.code = new_code + pole.model_id = pole_model.pk + pole.geom = geom_3812 + pole.lon = lon + pole.lat = lat + pole.geojson = geojson + pole.arc_length = arc_length + pole.extension = extension + pole.colour = colour + pole.obsolescence = obsolescence + pole.note = note + pole.name_fr = nr + pole.name_nl = nr + to_update.append(pole) + else: + to_create.append( + TrafficLightPole( + intersection = intersection, + code = new_code, + name_fr = nr, + name_nl = nr, + model_id = pole_model.pk, + geom = geom_3812, + lon = lon, + lat = lat, + geojson = geojson, + arc_length = arc_length, + extension = extension, + colour = colour, + obsolescence = obsolescence, + note = note, + ) + ) + + # ── Bulk DB writes (2 queries for the happy path) ───────────────────── + with transaction.atomic(): + TrafficLightPole.objects.bulk_create(to_create, batch_size=BULK_BATCH_SIZE) + TrafficLightPole.objects.bulk_update(to_update, fields=UPDATE_FIELDS, batch_size=BULK_BATCH_SIZE) + + # ── Summary ─────────────────────────────────────────────────────────── + self.stdout.write( + self.style.SUCCESS( + f"\nTrafficLightPole import complete:\n" + f" Created : {len(to_create)}\n" + f" Updated : {len(to_update)}\n" + f" Skipped (no tl_key) : {skipped_no_key}\n" + f" Skipped (intersection missing): {skipped_no_intersection}\n" + f" Skipped (bad/missing geom) : {skipped_no_geom}\n" + f" Skipped (no pole model) : {skipped_no_model}" + ) + ) diff --git a/streetup/assets/management/commands/initialize_asset_history.py b/streetup/assets/management/commands/initialize_asset_history.py new file mode 100644 index 0000000..46386e2 --- /dev/null +++ b/streetup/assets/management/commands/initialize_asset_history.py @@ -0,0 +1,183 @@ +""" +Management command to initialize AssetHistory for existing assets in the database. +Creates a 'create' history entry for each asset using: +- installation_date if available +- 01/01/2000 as fallback + +Usage: python manage.py initialize_asset_history [--dry-run] [--batch-size=1000] +""" + +from django.core.management.base import BaseCommand +from django.contrib.contenttypes.models import ContentType +from django.utils import timezone +from django.db import transaction +from datetime import datetime +from assets.models import AssetHistory + + +class Command(BaseCommand): + help = 'Initialize AssetHistory for existing assets (creates __created__ entries)' + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be done without actually creating history entries' + ) + parser.add_argument( + '--batch-size', + type=int, + default=1000, + help='Number of history entries to create in each batch (default: 1000)' + ) + parser.add_argument( + '--app-label', + type=str, + default='assets', + help='App label to process (default: assets)' + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + batch_size = options['batch_size'] + app_label = options['app_label'] + + self.stdout.write(self.style.SUCCESS('=' * 70)) + self.stdout.write(self.style.SUCCESS('Asset History Initialization')) + self.stdout.write(self.style.SUCCESS('=' * 70)) + + if dry_run: + self.stdout.write(self.style.WARNING('\n[!] DRY RUN MODE - No changes will be made\n')) + + # Default creation date for assets without installation_date + default_date = timezone.make_aware(datetime(2000, 1, 1, 0, 0, 0)) + + # Get all asset models from the specified app + asset_content_types = ContentType.objects.filter(app_label=app_label) + + total_assets = 0 + total_created = 0 + total_skipped = 0 + + for content_type in asset_content_types: + model_class = content_type.model_class() + + # Skip if model doesn't exist or isn't an asset-like model + if not model_class or not hasattr(model_class, 'code'): + continue + + model_name = content_type.model + self.stdout.write(f'\nProcessing {content_type.app_label}.{model_name}...') + + # Get all assets of this type + assets = model_class.objects.all() + asset_count = assets.count() + + if asset_count == 0: + self.stdout.write(self.style.WARNING(f' No assets found')) + continue + + self.stdout.write(f' Found {asset_count} assets') + + # Get existing history entries for this model + existing_history = set( + AssetHistory.objects.filter( + content_type=content_type, + action_type='create' + ).values_list('object_id', flat=True) + ) + + # Prepare batch of history entries + history_entries = [] + processed = 0 + created = 0 + skipped = 0 + + for asset in assets.iterator(chunk_size=batch_size): + processed += 1 + + # Skip if already has creation history + if asset.id in existing_history: + skipped += 1 + continue + + # Determine creation date + if hasattr(asset, 'installation_date') and asset.installation_date: + # Combine date with time + dt = datetime.combine(asset.installation_date, datetime.min.time()) + # Make timezone-aware if not already + if timezone.is_aware(dt): + creation_date = dt + else: + creation_date = timezone.make_aware(dt) + else: + creation_date = default_date + + # Create creation history entry + history_entry = AssetHistory( + content_type=content_type, + object_id=asset.id, + timestamp=creation_date, + user=None, # System-generated + field_name='__created__', + old_value=None, + new_value=asset.code, + action_type='create' + ) + history_entries.append(history_entry) + + # Add initial status entry if asset has a status + if hasattr(asset, 'status') and asset.status: + status_entry = AssetHistory( + content_type=content_type, + object_id=asset.id, + timestamp=creation_date, + user=None, # System-generated + field_name='status', + old_value=None, + new_value=asset.status, + action_type='status_change' + ) + history_entries.append(status_entry) + + created += 1 + + # Batch insert when we reach batch_size + if len(history_entries) >= batch_size: + if not dry_run: + with transaction.atomic(): + AssetHistory.objects.bulk_create(history_entries) + history_entries = [] + self.stdout.write(f' ✓ Processed {processed}/{asset_count} assets...', ending='\r') + + # Insert remaining entries + if history_entries and not dry_run: + with transaction.atomic(): + AssetHistory.objects.bulk_create(history_entries) + + # Summary for this model + self.stdout.write(f' ✓ Processed {processed}/{asset_count} assets') + if created > 0: + self.stdout.write(self.style.SUCCESS(f' [+] Created {created} history entries')) + if skipped > 0: + self.stdout.write(self.style.WARNING(f' [-] Skipped {skipped} (already have history)')) + + total_assets += asset_count + total_created += created + total_skipped += skipped + + # Final summary + self.stdout.write('\n' + '=' * 70) + self.stdout.write(self.style.SUCCESS('Summary:')) + self.stdout.write('=' * 70) + self.stdout.write(f'Total assets processed: {total_assets}') + self.stdout.write(self.style.SUCCESS(f'History entries created: {total_created}')) + self.stdout.write(self.style.WARNING(f'Assets skipped (existing history): {total_skipped}')) + + if dry_run: + self.stdout.write('\n' + self.style.WARNING('[!] DRY RUN - No changes were made')) + self.stdout.write(self.style.WARNING(' Run without --dry-run to apply changes')) + else: + self.stdout.write('\n' + self.style.SUCCESS('[+] Asset history initialization complete!')) + + self.stdout.write('') diff --git a/streetup/assets/management/commands/transfer_prevention_radars.py b/streetup/assets/management/commands/transfer_prevention_radars.py new file mode 100644 index 0000000..038a2de --- /dev/null +++ b/streetup/assets/management/commands/transfer_prevention_radars.py @@ -0,0 +1,194 @@ +from django.core.management.base import BaseCommand +from django.db import transaction +from django.contrib.contenttypes.models import ContentType + +from assets.models import ( + ITSGeoAsset, + ITSGeoAssetContract, + ITSAssetModel, + TrafficLightIntersection, + TrafficLightRadarModel, + TrafficLightRadar, + TrafficLightContract, + AssetCategory +) +from common.models import Thematic +from interventions.models import ( + Intervention, + InterventionAsset, + InterventionLocation +) + +class Command(BaseCommand): + help = "Transfer prevention radars from the ITS thematic to the Traffic Lights thematic." + + def handle(self, *args, **options): + # 1. Retrieve all prevention radars in ITSGeoAsset + its_radars = ITSGeoAsset.objects.filter(code__startswith='RAD_PREV_') + total_radars = its_radars.count() + self.stdout.write(f"Found {total_radars} prevention radars starting with 'RAD_PREV_' in ITSGeoAsset.") + + if total_radars == 0: + self.stdout.write(self.style.WARNING("No prevention radars to transfer. Exiting.")) + return + + # 2. Get necessary content types and thematics + its_geo_asset_ct = ContentType.objects.get_for_model(ITSGeoAsset) + traffic_light_radar_ct = ContentType.objects.get_for_model(TrafficLightRadar) + traffic_light_intersection_ct = ContentType.objects.get_for_model(TrafficLightIntersection) + + try: + trafficlights_thematic = Thematic.objects.get(code='trafficlights') + except Thematic.DoesNotExist: + self.stdout.write(self.style.ERROR("Thematic 'trafficlights' not found. Exiting.")) + return + + try: + radar_category = AssetCategory.objects.get(code='TL_RADAR_PREVENTIF') + except AssetCategory.DoesNotExist: + try: + radar_category = AssetCategory.objects.get(id=20) + except AssetCategory.DoesNotExist: + radar_category = AssetCategory.objects.filter(name_fr='Radar Préventif').first() + if not radar_category: + self.stdout.write(self.style.ERROR("AssetCategory 'Radar Préventif' (code TL_RADAR_PREVENTIF) not found. Exiting.")) + return + + # We will keep track of models mapped, locations created/retrieved, radars created, interventions updated, contracts migrated + transferred_count = 0 + locations_created = 0 + models_mapped = 0 + interventions_updated = 0 + contracts_migrated = 0 + + # Run everything in a transaction block + with transaction.atomic(): + for old_radar in its_radars: + # A. Map the location (TrafficLightIntersection) + intersection_code = old_radar.code.replace('RAD_PREV_', '').strip() + intersection, created = TrafficLightIntersection.objects.get_or_create( + code=intersection_code, + defaults={ + 'name_fr': f"Radar {intersection_code}", + 'name_nl': f"Radar {intersection_code}", + 'lon': old_radar.lon, + 'lat': old_radar.lat, + 'geom': old_radar.geom, + 'geojson': old_radar.geojson, + 'status': 'active', + 'is_radar': True + } + ) + if created: + locations_created += 1 + elif not intersection.is_radar: + intersection.is_radar = True + intersection.save(update_fields=['is_radar']) + + # B. Map the model (TrafficLightRadarModel) + its_model = old_radar.model + tl_model, created_model = TrafficLightRadarModel.objects.get_or_create( + code=its_model.code, + defaults={ + 'name_fr': its_model.name_fr, + 'name_nl': its_model.name_nl, + 'description': its_model.description, + 'brand': its_model.brand, + 'category': radar_category + } + ) + if created_model: + models_mapped += 1 + + # C. Recreate/map the radar to TrafficLightRadar + new_radar = TrafficLightRadar.objects.create( + code=old_radar.code, + name_fr=old_radar.name_fr, + name_nl=old_radar.name_nl, + status=old_radar.status, + obsolescence=old_radar.obsolescence, + installation_date=old_radar.installation_date, + uninstallation_date=old_radar.uninstallation_date, + serial_number=old_radar.serial_number, + warranty_duration=old_radar.warranty_duration, + last_inspection_date=old_radar.last_inspection_date, + last_preventive_date=old_radar.last_preventive_date, + last_corrective_date=old_radar.last_corrective_date, + last_survey_date=old_radar.last_survey_date, + brand=old_radar.brand, + replaced_by=None, + marked_as_removed_date=old_radar.marked_as_removed_date, + marked_as_removed_by=old_radar.marked_as_removed_by, + note=old_radar.note, + validation_status=old_radar.validation_status, + updated_by=old_radar.updated_by, + lon=old_radar.lon, + lat=old_radar.lat, + geom=old_radar.geom, + geojson=old_radar.geojson, + model=tl_model, + intersection=intersection, + category=radar_category + ) + transferred_count += 1 + + # D. Migrate associated Interventions + # Find all InterventionAssets referencing this old radar + int_assets = InterventionAsset.objects.filter(content_type=its_geo_asset_ct, object_id=old_radar.id) + for int_asset in int_assets: + # Update generic foreign key pointing to the new radar + int_asset.content_type = traffic_light_radar_ct + int_asset.object_id = new_radar.id + int_asset.save() + + intervention = int_asset.intervention + + # Update thematic of intervention + intervention.thematic = trafficlights_thematic + + # Update asset category of intervention if it points to ITS Radars (ID 82) + if intervention.asset_category and intervention.asset_category.id == 82: + intervention.asset_category = radar_category + + # Update location_code field if empty + if not intervention.location_code: + intervention.location_code = intersection.code + + intervention.save() + + # Create InterventionLocation pointing to the TrafficLightIntersection + InterventionLocation.objects.get_or_create( + intervention=intervention, + content_type=traffic_light_intersection_ct, + object_id=intersection.id + ) + + interventions_updated += 1 + + # E. Migrate associated Contracts + # For each ITSGeoAssetContract linked to this old radar + old_contracts = ITSGeoAssetContract.objects.filter(asset=old_radar) + for old_contract_link in old_contracts: + # Create corresponding TrafficLightContract for the intersection + TrafficLightContract.objects.get_or_create( + intersection=intersection, + contract=old_contract_link.contract, + defaults={ + 'status': old_contract_link.status, + 'maintenance_type': 'other' + } + ) + contracts_migrated += 1 + # Clean up old contract link + old_contract_link.delete() + + # F. Delete old ITSGeoAsset radar + old_radar.delete() + + self.stdout.write(self.style.SUCCESS( + f"Successfully transferred {transferred_count}/{total_radars} prevention radars.\n" + f"Created {locations_created} TrafficLightIntersections.\n" + f"Mapped {models_mapped} TrafficLightRadarModels.\n" + f"Updated {interventions_updated} associated interventions/assets/locations.\n" + f"Migrated {contracts_migrated} contract links." + )) diff --git a/streetup/assets/management/commands/update_null_radars_geom.py b/streetup/assets/management/commands/update_null_radars_geom.py new file mode 100644 index 0000000..7e9db1b --- /dev/null +++ b/streetup/assets/management/commands/update_null_radars_geom.py @@ -0,0 +1,184 @@ +import json +import re +import ssl +import urllib.request +from django.contrib.gis.geos import Point +from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q +from django.utils import timezone + +from assets.models.its import ITSGeoAsset + +def clean_code(code): + if not code: + return '' + c = code.upper().strip() + c = c.replace('RAD_PREV_', '').replace('RAD_', '').replace('CAM_', '').replace('CNT_', '').replace('PMV_', '') + c = c.replace('GATSO', '').replace('GAT', '').strip() + c = re.sub(r'\s+', '', c) + c = re.sub(r'_[0-9]+$', '', c) + c = re.sub(r'-[0-9]+$', '', c) + if c.startswith('Y_'): + c = c[2:] + if c and c[0] in ('S', 'R', 'C'): + c = c[1:] + return c + +def get_keys_to_try(code): + cleaned = clean_code(code) + keys = [code, cleaned] + if cleaned and cleaned[0] in ('S', 'R', 'C'): + suffix = cleaned[1:] + keys.extend([suffix, 'S' + suffix, 'R' + suffix, 'C' + suffix]) + return list(dict.fromkeys(keys)) # remove duplicates keeping order + +def find_coords(code, coords_map): + # Try exact / core variations + for k in get_keys_to_try(code): + if k in coords_map: + return coords_map[k] + + # Try trailing '0' normalization + cleaned = clean_code(code) + if cleaned.endswith('0'): + stripped = cleaned[:-1] + for k in get_keys_to_try(stripped): + if k in coords_map: + return coords_map[k] + + # If WFS key in coords_map ends with '0' + for wfs_key, coords in coords_map.items(): + if wfs_key.endswith('0'): + stripped_wfs = wfs_key[:-1] + # Check if our code core matches stripped WFS key + for k in get_keys_to_try(code): + if k == stripped_wfs: + return coords + + return None + + +class Command(BaseCommand): + help = "Update speed cameras (ITS radars) objects where geometry is null using Brussels Mobility WFS GeoJSON speedcameras and traffic lights data." + + def handle(self, *args, **options): + # 1. Fetch speedcameras WFS + url_speed = ( + "https://data.mobility.brussels/geoserver/bm_security/wfs?" + "service=wfs&version=1.1.0&request=GetFeature&" + "typeName=bm_security:speedcameras&outputFormat=json&srsName=EPSG:4326" + ) + # 2. Fetch traffic lights WFS + url_tl = ( + "https://data.mobility.brussels/geoserver/bm_equipment/wfs?" + "service=wfs&version=1.1.0&request=GetFeature&" + "typeName=bm_equipment:traffic_lights&outputFormat=json&srsName=EPSG:4326" + ) + + context = ssl._create_unverified_context() + + # Fetch speed cameras + self.stdout.write(f"Fetching speed cameras from WFS: {url_speed}") + try: + req_speed = urllib.request.Request(url_speed, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req_speed, context=context) as response: + data_speed = json.loads(response.read().decode('utf-8')) + features_speed = data_speed.get('features', []) + self.stdout.write(f"Successfully fetched {len(features_speed)} speed cameras.") + except Exception as e: + self.stdout.write(self.style.ERROR(f"Failed to fetch speed cameras data: {e}")) + return + + # Fetch traffic lights + self.stdout.write(f"Fetching traffic lights from WFS: {url_tl}") + try: + req_tl = urllib.request.Request(url_tl, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req_tl, context=context) as response: + data_tl = json.loads(response.read().decode('utf-8')) + features_tl = data_tl.get('features', []) + self.stdout.write(f"Successfully fetched {len(features_tl)} traffic lights.") + except Exception as e: + self.stdout.write(self.style.ERROR(f"Failed to fetch traffic lights data: {e}")) + return + + # 3. Map coordinates + coords_map = {} + + # First map from speed cameras + for feature in features_speed: + props = feature.get('properties', {}) + radar_id = props.get('radar_id') or '' + fr_key = props.get('fr_key') or '' + geom = feature.get('geometry') + + if geom and geom.get('type') == 'Point': + coordinates = geom.get('coordinates') + if len(coordinates) == 2: + for val in [radar_id, fr_key]: + cleaned = clean_code(val) + if cleaned: + coords_map[cleaned] = coordinates + + # Second, map from traffic lights if not already mapped + for feature in features_tl: + props = feature.get('properties', {}) + key = props.get('key') or '' + geom = feature.get('geometry') + + if geom and geom.get('type') == 'Point': + coordinates = geom.get('coordinates') + if len(coordinates) == 2: + cleaned = clean_code(key) + if cleaned and cleaned not in coords_map: + coords_map[cleaned] = coordinates + + self.stdout.write(f"Mapped coordinates for {len(coords_map)} unique identifiers.") + + # 4. Setup coordinate transformation to EPSG:3812 (Belgian Lambert 72) + srs_3812 = SpatialReference(3812) + srs_4326 = SpatialReference(4326) + ct_to_3812 = CoordTransform(srs_4326, srs_3812) + + # 5. Find all ITSGeoAsset (Radars) where geom is null + null_radars = ITSGeoAsset.objects.filter( + Q(category__name_fr='Radars') | Q(code__startswith='RAD_'), + geom__isnull=True + ) + self.stdout.write(f"Found {null_radars.count()} radars with null geometry in the database.") + + updated_radars_count = 0 + + # We perform updates in a transaction block + with transaction.atomic(): + for radar in null_radars: + code = radar.code.strip() + coords = find_coords(code, coords_map) + + if coords: + lon, lat = coords + try: + # Create EPSG:4326 point and transform to EPSG:3812 + geom_wgs84 = Point(lon, lat, srid=4326) + geom_3812 = geom_wgs84.clone() + geom_3812.transform(ct_to_3812) + + geojson_str = json.dumps( + {"type": "Point", "coordinates": [round(lon, 8), round(lat, 8)]} + ) + + radar.geom = geom_3812 + radar.lon = lon + radar.lat = lat + radar.geojson = geojson_str + radar.save() + updated_radars_count += 1 + except Exception as e: + self.stdout.write( + self.style.WARNING(f"Error transforming coordinates for radar {code}: {e}") + ) + + self.stdout.write( + self.style.SUCCESS(f"Updated {updated_radars_count} radars from WFS speedcameras/traffic-lights data.") + ) diff --git a/streetup/assets/management/commands/update_null_traffic_lights_geom.py b/streetup/assets/management/commands/update_null_traffic_lights_geom.py new file mode 100644 index 0000000..62bf660 --- /dev/null +++ b/streetup/assets/management/commands/update_null_traffic_lights_geom.py @@ -0,0 +1,233 @@ +import json +import ssl +import urllib.request +from django.contrib.gis.geos import Point +from django.contrib.gis.gdal import CoordTransform, SpatialReference +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q +from django.utils import timezone + +from assets.models.trafficlights import ( + TrafficLightIntersection, + TrafficLightPole, + TrafficLightController, + TrafficLightControllerHardware, + TrafficLightDetector, + TrafficLightNetworkHardware, + TrafficLightVoltageHardware, + TrafficLightElectricalCabinet, + TrafficLightElectricalCabinetContent, + TrafficLightAccessory, +) + +def clean_code(code): + c = code.upper().strip() + if c.startswith('Y_'): + c = c[2:] + return c + +def get_keys_to_try(code): + cleaned = clean_code(code) + keys = [code, cleaned] + if cleaned and cleaned[0] in ('S', 'R', 'C'): + suffix = cleaned[1:] + keys.extend([suffix, 'S' + suffix, 'R' + suffix, 'C' + suffix]) + return list(dict.fromkeys(keys)) # remove duplicates keeping order + +def find_coords(code, coords_map): + # Try exact / core variations + for k in get_keys_to_try(code): + if k in coords_map: + return coords_map[k] + + # Try trailing '0' normalization + # If code ends with '0' + cleaned = clean_code(code) + if cleaned.endswith('0'): + stripped = cleaned[:-1] + for k in get_keys_to_try(stripped): + if k in coords_map: + return coords_map[k] + + # If WFS key in coords_map ends with '0' + for wfs_key, coords in coords_map.items(): + if wfs_key.endswith('0'): + stripped_wfs = wfs_key[:-1] + # Check if our code core matches stripped WFS key + for k in get_keys_to_try(code): + if k == stripped_wfs: + return coords + + return None + + +class Command(BaseCommand): + help = "Update TrafficLightIntersection and equipment objects where geometry is null using Brussels Mobility WFS GeoJSON data." + + def handle(self, *args, **options): + # 1. Fetch WFS GeoJSON + url = ( + "https://data.mobility.brussels/geoserver/bm_equipment/wfs?" + "service=wfs&version=1.1.0&request=GetFeature&" + "typeName=bm_equipment:traffic_lights&outputFormat=json&srsName=EPSG:4326" + ) + self.stdout.write(f"Fetching GeoJSON from: {url}") + + try: + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context) as response: + data = json.loads(response.read().decode('utf-8')) + except Exception as e: + self.stdout.write(self.style.ERROR(f"Failed to fetch GeoJSON data: {e}")) + return + + features = data.get('features', []) + self.stdout.write(f"Successfully fetched {len(features)} features.") + + # 2. Map coordinates by intersection code (key) + # Coordinates in GeoJSON are [lon, lat] (EPSG:4326) + coords_map = {} + for feature in features: + props = feature.get('properties', {}) + key = (props.get('key') or '').strip().upper() + geom = feature.get('geometry') + if key and geom and geom.get('type') == 'Point': + coordinates = geom.get('coordinates') + if len(coordinates) == 2: + coords_map[key] = coordinates + + self.stdout.write(f"Mapped coordinates for {len(coords_map)} intersections.") + + # 3. Setup coordinate transformation to EPSG:3812 (Belgian Lambert 72) + srs_3812 = SpatialReference(3812) + srs_4326 = SpatialReference(4326) + ct_to_3812 = CoordTransform(srs_4326, srs_3812) + + # 4. Find all TrafficLightIntersection where geom is null + null_intersections = TrafficLightIntersection.objects.filter(geom__isnull=True) + self.stdout.write(f"Found {null_intersections.count()} intersections with null geometry in the database.") + + updated_intersections_count = 0 + now = timezone.now() + + # We perform updates in a transaction block + with transaction.atomic(): + for intersection in null_intersections: + code = intersection.code.strip() + coords = find_coords(code, coords_map) + if coords: + lon, lat = coords + try: + # Create EPSG:4326 point and transform to EPSG:3812 + geom_wgs84 = Point(lon, lat, srid=4326) + geom_3812 = geom_wgs84.clone() + geom_3812.transform(ct_to_3812) + + geojson_str = json.dumps( + {"type": "Point", "coordinates": [round(lon, 8), round(lat, 8)]} + ) + + intersection.geom = geom_3812 + intersection.lon = lon + intersection.lat = lat + intersection.geojson = geojson_str + # updated_at will be set automatically by save() auto_now=True + intersection.save() + updated_intersections_count += 1 + except Exception as e: + self.stdout.write( + self.style.WARNING(f"Error transforming coordinates for {code}: {e}") + ) + + self.stdout.write( + self.style.SUCCESS(f"Updated {updated_intersections_count} intersections from WFS data.") + ) + + # 5. Update child equipment where geometry is null + self.stdout.write("Updating equipment with null geometry based on their parent intersections...") + + # Get all intersections that now have a valid geometry + valid_intersections = TrafficLightIntersection.objects.filter(geom__isnull=False) + + equipment_stats = { + "poles": 0, + "controllers": 0, + "hardwares": 0, + "detectors": 0, + "network_hardwares": 0, + "voltage_hardwares": 0, + "cabinets": 0, + "cabinet_contents": 0, + "accessories": 0, + } + + with transaction.atomic(): + for intersection in valid_intersections: + # Cache geometry fields + geom = intersection.geom + lon = intersection.lon + lat = intersection.lat + geojson = intersection.geojson + + # Update TrafficLightPole + poles = TrafficLightPole.objects.filter(intersection=intersection, geom__isnull=True) + if poles.exists(): + p_count = poles.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["poles"] += p_count + + # Update TrafficLightController + controllers = TrafficLightController.objects.filter(intersection=intersection, geom__isnull=True) + if controllers.exists(): + c_count = controllers.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["controllers"] += c_count + + # Update TrafficLightControllerHardware + hardwares = TrafficLightControllerHardware.objects.filter(controller__intersection=intersection, geom__isnull=True) + if hardwares.exists(): + h_count = hardwares.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["hardwares"] += h_count + + # Update TrafficLightDetector + detectors = TrafficLightDetector.objects.filter(intersection=intersection, geom__isnull=True) + if detectors.exists(): + d_count = detectors.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["detectors"] += d_count + + # Update TrafficLightNetworkHardware + network_hardwares = TrafficLightNetworkHardware.objects.filter(intersection=intersection, geom__isnull=True) + if network_hardwares.exists(): + nh_count = network_hardwares.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["network_hardwares"] += nh_count + + # Update TrafficLightVoltageHardware + voltage_hardwares = TrafficLightVoltageHardware.objects.filter(intersection=intersection, geom__isnull=True) + if voltage_hardwares.exists(): + vh_count = voltage_hardwares.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["voltage_hardwares"] += vh_count + + # Update TrafficLightElectricalCabinet + cabinets = TrafficLightElectricalCabinet.objects.filter(intersection=intersection, geom__isnull=True) + if cabinets.exists(): + cab_count = cabinets.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["cabinets"] += cab_count + + # Update TrafficLightElectricalCabinetContent + cabinet_contents = TrafficLightElectricalCabinetContent.objects.filter(cabinet__intersection=intersection, geom__isnull=True) + if cabinet_contents.exists(): + cc_count = cabinet_contents.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["cabinet_contents"] += cc_count + + # Update TrafficLightAccessory + accessories = TrafficLightAccessory.objects.filter( + Q(intersection=intersection) | Q(pole__intersection=intersection), + geom__isnull=True + ) + if accessories.exists(): + acc_count = accessories.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) + equipment_stats["accessories"] += acc_count + + self.stdout.write(self.style.SUCCESS("Equipment update complete. Summary of updated equipment:")) + for eq_type, count in equipment_stats.items(): + self.stdout.write(f" - {eq_type}: {count}") diff --git a/streetup/assets/migrations/0001_initial.py b/streetup/assets/migrations/0001_initial.py new file mode 100644 index 0000000..851742e --- /dev/null +++ b/streetup/assets/migrations/0001_initial.py @@ -0,0 +1,2311 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import assets.models.controlcenters +import assets.models.its +import assets.models.nature +import assets.models.roads +import assets.models.sign +import assets.models.structures +import common.storages +import django.contrib.gis.db.models.fields +import django.contrib.postgres.fields +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ArtworkFountain', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('artist', models.CharField(blank=True, max_length=200, null=True)), + ('fountain_type', models.CharField(blank=True, choices=[('monumental_artistic_fountain', 'Fontaine artistique monumentale'), ('artistic_fountain', 'Fontaine artistique'), ('monumental_fountain', 'Fontaine monumentale'), ('artistic_play_fountain', 'Fontaine ludique artistique'), ('play_fountain', 'Fontaine artistique'), ('monumental_play_fountain', 'Fontaine ludique monumentale')], max_length=100, null=True)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('location_fr', models.CharField(blank=True, max_length=500, null=True)), + ('location_nl', models.CharField(blank=True, max_length=500, null=True)), + ('description_fr', models.TextField(blank=True, null=True)), + ('description_nl', models.TextField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ArtworkFountainDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/artworks/fountains/', verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/artworks/fountains/')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ArtworkMonument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('artist', models.CharField(blank=True, max_length=200, null=True)), + ('artwork_type', models.CharField(blank=True, choices=[('bench', 'Banc public'), ('construction', 'Construction'), ('metal_structure', 'Structure métalique'), ('monument', 'Monument'), ('monumental_structure', 'Structure monumentale'), ('pole', 'Poteau'), ('sculpture', 'Sculpture'), ('statue', 'Statue'), ('structure', 'Structure')], max_length=100, null=True)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('location_fr', models.CharField(blank=True, max_length=500, null=True)), + ('location_nl', models.CharField(blank=True, max_length=500, null=True)), + ('water', models.BooleanField(blank=True, default=False, null=True)), + ('description_fr', models.TextField(blank=True, null=True)), + ('description_nl', models.TextField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ArtworkMonumentDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/artworks/fountains/', verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/artworks/fountains/')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ArtworkStreet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('road_code', models.CharField(blank=True, max_length=50, null=True)), + ('section_code', models.CharField(blank=True, max_length=50, null=True)), + ('street_level', models.CharField(blank=True, max_length=50, null=True)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AssetCategory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=200)), + ('name_fr', models.CharField(max_length=255)), + ('name_nl', models.CharField(max_length=255)), + ], + options={ + 'ordering': ['thematic', 'name_fr'], + }, + ), + migrations.CreateModel( + name='AssetHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField(verbose_name="ID de l'asset")), + ('timestamp', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date de modification')), + ('field_name', models.CharField(max_length=100, verbose_name='Champ modifié')), + ('old_value', models.TextField(blank=True, null=True, verbose_name='Ancienne valeur')), + ('new_value', models.TextField(blank=True, null=True, verbose_name='Nouvelle valeur')), + ('action_type', models.CharField(choices=[('create', 'Création'), ('update', 'Modification'), ('delete', 'Suppression'), ('status_change', 'Changement de statut')], default='update', max_length=50, verbose_name="Type d'action")), + ], + options={ + 'verbose_name': "Historique d'asset", + 'verbose_name_plural': "Historiques d'assets", + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='CleanGlassContainer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('glass_type', models.CharField(blank=True, choices=[('clear', 'Clear glass'), ('colored', 'Coloured glass'), ('mixed', 'Mixed')], max_length=50, null=True, verbose_name='Accepted glass type')), + ('capacity', models.FloatField(blank=True, null=True, verbose_name='Capacity (litres)')), + ('is_underground', models.BooleanField(default=False, verbose_name='Underground')), + ('nb_openings', models.IntegerField(blank=True, null=True, verbose_name='Number of openings')), + ('color', models.CharField(blank=True, max_length=100, null=True, verbose_name='Colour')), + ('material', models.CharField(blank=True, max_length=200, null=True, verbose_name='Material')), + ('emptying_frequency', models.CharField(blank=True, max_length=200, null=True, verbose_name='Emptying frequency')), + ('operator', models.CharField(blank=True, max_length=255, null=True, verbose_name='Operator / manager')), + ('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')), + ], + options={ + 'verbose_name': 'Glass container', + 'verbose_name_plural': 'Glass containers', + }, + ), + migrations.CreateModel( + name='CleanGlassContainerModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=100, null=True, verbose_name='Colour')), + ('ral', models.CharField(blank=True, max_length=20, null=True, verbose_name='RAL code')), + ('material', models.CharField(blank=True, max_length=200, null=True, verbose_name='Material')), + ('capacity', models.FloatField(blank=True, null=True, verbose_name='Capacity (litres)')), + ('is_underground', models.BooleanField(default=False, verbose_name='Underground')), + ('nb_openings', models.IntegerField(blank=True, null=True, verbose_name='Number of openings')), + ], + options={ + 'verbose_name': 'Glass container model', + 'verbose_name_plural': 'Glass container models', + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='CleanLitterBin', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('fixation_type', models.CharField(blank=True, choices=[('cemented', 'Cemented to ground'), ('bolted', 'Bolted to ground'), ('pole_mounted', 'Pole-mounted'), ('wall_mounted', 'Wall-mounted'), ('freestanding', 'Freestanding'), ('other', 'Other')], max_length=50, null=True, verbose_name='Fixation type')), + ('color', models.CharField(blank=True, max_length=100, null=True, verbose_name='Colour')), + ('material', models.CharField(blank=True, max_length=200, null=True, verbose_name='Material')), + ('volume', models.FloatField(blank=True, null=True, verbose_name='Volume (litres)')), + ('has_cover', models.BooleanField(default=False, verbose_name='Cover')), + ('has_ashtray', models.BooleanField(default=False, verbose_name='Built-in ashtray')), + ('has_extinguisher', models.BooleanField(default=False, verbose_name='Built-in extinguisher stub (éteignoir)')), + ('emptying_frequency', models.CharField(blank=True, max_length=200, null=True, verbose_name='Emptying frequency')), + ('collection_route', models.CharField(blank=True, max_length=200, null=True, verbose_name='Collection route (tournée)')), + ('external_id', models.IntegerField(blank=True, null=True, verbose_name='External ID (source system)')), + ('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')), + ], + options={ + 'verbose_name': 'Litter bin', + 'verbose_name_plural': 'Litter bins', + }, + ), + migrations.CreateModel( + name='CleanLitterBinModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=100, null=True, verbose_name='Colour')), + ('ral', models.CharField(blank=True, max_length=20, null=True, verbose_name='RAL code')), + ('material', models.CharField(blank=True, max_length=200, null=True, verbose_name='Material')), + ('volume', models.FloatField(blank=True, null=True, verbose_name='Volume (litres)')), + ('has_cover', models.BooleanField(default=False, verbose_name='Cover')), + ('has_ashtray', models.BooleanField(default=False, verbose_name='Built-in ashtray')), + ], + options={ + 'verbose_name': 'Litter bin model', + 'verbose_name_plural': 'Litter bin models', + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='CleanLitterBinObservation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('observation_datetime', models.DateTimeField(blank=True, null=True, verbose_name='Observation date/time')), + ('observer_note', models.TextField(blank=True, null=True, verbose_name='Observer notes')), + ('sol_pente_trop_forte', models.BooleanField(default=False, verbose_name='Slope too steep')), + ('sol_instable', models.BooleanField(default=False, verbose_name='Unstable ground')), + ('sol_reflechissant', models.BooleanField(default=False, verbose_name='Reflective surface')), + ('sol_glissant', models.BooleanField(default=False, verbose_name='Slippery surface')), + ('sol_fentes', models.BooleanField(default=False, verbose_name='Cracks in ground')), + ('sol_entrave', models.BooleanField(default=False, verbose_name='Ground obstacle')), + ('sol_dalles_podo', models.BooleanField(default=False, verbose_name='Tactile paving (dalles podotactiles)')), + ('sol_pente_transversale', models.FloatField(blank=True, null=True, verbose_name='Transversal slope (%)')), + ('sol_pente_longitudinale', models.FloatField(blank=True, null=True, verbose_name='Longitudinal slope (%)')), + ('sol_remarque', models.TextField(blank=True, null=True, verbose_name='Ground remarks')), + ('acc_type_quartier', models.CharField(blank=True, choices=[('importante_ponctuelle', 'Important – ponctuel'), ('importante_continue', 'Important – continu'), ('faible', 'Faible'), ('autre', 'Autre')], max_length=50, null=True, verbose_name='Neighbourhood type')), + ('acc_densite_flux_pieton', models.CharField(blank=True, choices=[('high', 'Élevée'), ('medium', 'Moyenne'), ('low', 'Faible')], max_length=50, null=True, verbose_name='Pedestrian flow density')), + ('acc_point_interet', models.JSONField(blank=True, default=list, verbose_name='Points of interest')), + ('acc_approche_2m', models.BooleanField(default=False, verbose_name='Clear 2 m approach')), + ('acc_voirie_a_traverser', models.BooleanField(default=False, verbose_name='Road to cross')), + ('acc_poubelle_isolee', models.BooleanField(default=False, verbose_name='Isolated bin')), + ('acc_gene_flux_routier', models.BooleanField(default=False, verbose_name='Obstructs road traffic')), + ('acc_gene_flux_pieton', models.BooleanField(default=False, verbose_name='Obstructs pedestrian flow')), + ('acc_chemin_logique', models.BooleanField(default=False, verbose_name='Logical path (itinéraire logique)')), + ('acc_contraste', models.BooleanField(default=False, verbose_name='Visual contrast')), + ('acc_indications_tri', models.BooleanField(default=False, verbose_name='Sorting indicators present')), + ('acc_agent_vehicule', models.CharField(blank=True, choices=[('normal', 'Normal'), ('difficult', 'Difficile'), ('impossible', 'Impossible')], max_length=20, null=True, verbose_name='Agent access – vehicle')), + ('acc_agent_pied', models.CharField(blank=True, choices=[('normal', 'Normal'), ('difficult', 'Difficile'), ('impossible', 'Impossible')], max_length=20, null=True, verbose_name='Agent access – on foot')), + ('acc_point_interet_autre', models.CharField(blank=True, max_length=255, null=True, verbose_name='Other point of interest')), + ('acc_pertinence_placement', models.JSONField(blank=True, default=list, verbose_name='Placement relevance codes')), + ('acc_entrave', models.JSONField(blank=True, default=list, verbose_name='Obstruction codes')), + ('acc_point_interet_remarque', models.TextField(blank=True, null=True, verbose_name='Point of interest remarks')), + ('acc_remarque', models.TextField(blank=True, null=True, verbose_name='Accessibility remarks')), + ], + options={ + 'verbose_name': 'Litter bin observation', + 'verbose_name_plural': 'Litter bin observations', + 'ordering': ['-observation_datetime'], + }, + ), + migrations.CreateModel( + name='CleanLitterBinPhoto', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('photo', models.ImageField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='clean/litter_bin_photos/', verbose_name='Photo')), + ('source_url', models.URLField(blank=True, max_length=500, null=True, verbose_name='Source URL (original)')), + ('order', models.PositiveSmallIntegerField(default=1, verbose_name='Order')), + ('taken_at', models.DateTimeField(blank=True, null=True, verbose_name='Taken at')), + ('caption', models.CharField(blank=True, max_length=255, null=True, verbose_name='Caption')), + ], + options={ + 'verbose_name': 'Litter bin photo', + 'verbose_name_plural': 'Litter bin photos', + 'ordering': ['litter_bin', 'order'], + }, + ), + migrations.CreateModel( + name='CleanLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('road_code', models.CharField(blank=True, max_length=50, null=True)), + ('section_code', models.CharField(blank=True, max_length=50, null=True)), + ('street_level', models.CharField(blank=True, max_length=50, null=True)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CleanLocationContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('clean', 'Nettoyage'), ('collection', 'Ramassage'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'Clean Location Contract', + 'verbose_name_plural': 'Clean Location Contracts', + }, + ), + migrations.CreateModel( + name='ComplementaryPoliceRegulation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('title', models.CharField(max_length=255)), + ('reference', models.CharField(blank=True, max_length=100, null=True)), + ('decision_date', models.DateField(blank=True, null=True)), + ('file', models.FileField(max_length=255, upload_to=assets.models.sign.upload_to_police_regulation, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, upload_to=assets.models.sign.upload_to_police_regulation)), + ], + options={ + 'ordering': ['-decision_date', 'reference', 'title'], + }, + ), + migrations.CreateModel( + name='ControlCenter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50, unique=True)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ], + options={ + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='ControlCenterAssetDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.controlcenters.upload_to_controlcenter, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.controlcenters.upload_to_controlcenter)), + ('document_type', models.CharField(choices=[('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ControlCenterAssetModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'verbose_name': 'Control Center Asset Model', + 'verbose_name_plural': 'Control Center Asset Models', + }, + ), + migrations.CreateModel( + name='ControlCenterContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('building', 'Bâtiment'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'Control Center Contract', + 'verbose_name_plural': 'Control Center Contracts', + }, + ), + migrations.CreateModel( + name='ControlCenterDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.controlcenters.upload_to_controlcenter, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.controlcenters.upload_to_controlcenter)), + ('document_type', models.CharField(choices=[('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ControlCenterGeoAsset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='ControlCenterGeoAssetContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('building', 'Bâtiment'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'Control Center Asset Contract', + 'verbose_name_plural': 'Control Center Asset Contracts', + }, + ), + migrations.CreateModel( + name='ControlCenterLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('is_main_location', models.BooleanField(default=False, verbose_name='Main Location')), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GreenSurfacesComment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('comment', models.TextField(blank=True, default='', verbose_name='Commentaire')), + ('status', models.CharField(choices=[('memo', 'Mémo'), ('attention', "Point d'attention"), ('todo', 'À faire'), ('ok', 'OK'), ('problem', 'Problème')], default='memo', max_length=20, verbose_name='Statut')), + ('latitude', models.FloatField(verbose_name='Latitude')), + ('longitude', models.FloatField(verbose_name='Longitude')), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812, verbose_name='Géométrie')), + ('address', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Adresse')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date de création')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Date de modification')), + ], + options={ + 'verbose_name': 'Commentaire (Espaces verts)', + 'verbose_name_plural': 'Commentaires (Espaces verts)', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='GreenSurfacesCommentPhoto', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('photo', models.ImageField(storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.nature.comment_photo_upload_path, verbose_name='Photo')), + ('heading', models.FloatField(blank=True, help_text='Direction de la boussole dans laquelle la photo a été prise (0-360°)', null=True, verbose_name='Direction (azimut en degrés)')), + ('order', models.PositiveIntegerField(default=0, help_text="Ordre d'affichage de la photo", verbose_name='Ordre')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date de création')), + ], + options={ + 'verbose_name': 'Photo de commentaire', + 'verbose_name_plural': 'Photos de commentaires', + 'ordering': ['order', 'created_at'], + }, + ), + migrations.CreateModel( + name='ITSAssetModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'verbose_name': 'ITS Asset Model', + 'verbose_name_plural': 'ITS Asset Models', + }, + ), + migrations.CreateModel( + name='ITSGeoAsset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='ITSGeoAssetContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'ITS Asset Contract', + 'verbose_name_plural': 'ITS Asset Contracts', + }, + ), + migrations.CreateModel( + name='ITSLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('is_main_location', models.BooleanField(default=False, verbose_name='Main Location')), + ], + options={ + 'verbose_name': 'ITS Location', + 'verbose_name_plural': 'ITS Locations', + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='ITSLocationContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'ITS Location Contract', + 'verbose_name_plural': 'ITS Location Contracts', + }, + ), + migrations.CreateModel( + name='ITSLocationDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.its.upload_to_its_location, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.its.upload_to_its_location)), + ('document_type', models.CharField(choices=[('as_built', 'As Built'), ('schema', 'Schéma'), ('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='NatureAssetModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'verbose_name': 'Nature Asset Model', + 'verbose_name_plural': 'Nature Asset Models', + }, + ), + migrations.CreateModel( + name='NatureBench', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('material', models.CharField(blank=True, max_length=200, null=True)), + ('length', models.FloatField(blank=True, null=True)), + ('width', models.FloatField(blank=True, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='NatureGreenSurface', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('surface_area', models.FloatField(blank=True, null=True)), + ('slope', models.FloatField(blank=True, null=True)), + ('weighting', models.FloatField(blank=True, null=True)), + ('density', models.FloatField(blank=True, null=True)), + ('mower_frenquency', models.IntegerField(blank=True, null=True)), + ('buffer', models.FloatField(blank=True, null=True)), + ('width', models.FloatField(blank=True, null=True)), + ('length', models.FloatField(blank=True, null=True)), + ('bulb_types', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('tulipes', 'Tulipes'), ('jacynthes', 'Jacynthes'), ('jonquilles', 'Jonquilles'), ('narcisses', 'Narcisses'), ('crocus', 'Crocus'), ('perce_neige', 'Perce-neige'), ('autres', 'Autres')], max_length=50), blank=True, default=list, size=None, verbose_name='Types de bulbes')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='NatureGreenSurfaceDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/nature/surfaces/', verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/nature/surfaces/')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='NatureHollowGreenSpace', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('surface_area', models.FloatField(blank=True, null=True)), + ('width', models.FloatField(blank=True, null=True)), + ('length', models.FloatField(blank=True, null=True)), + ('hgs_type', models.CharField(blank=True, choices=[('rain_garden', 'Jardin de pluie'), ('swale', 'Noue'), ('ditch', 'Fossé'), ('dry_basin', "Bassin sec/d'infiltration"), ('wet_basin', 'Bassin en eau')], max_length=50, null=True, verbose_name="Type d'EVC")), + ('coat_type', models.CharField(blank=True, choices=[('earth_stone_mixture', 'Mélange terre-pierre'), ('vegetal', 'Végétal'), ('grassed_structure', 'Structure enherbée'), ('non_aggregated_material', 'Matériau non agrégé'), ('mineral_filled_structure', 'Structure à remplissage minéral'), ('elevated_coating', 'Revêtement surélevé'), ('modular_porous', 'Modulaire poreux'), ('continuous_porous', 'Continu poreux'), ('impermeable', 'Impermeable')], max_length=50, null=True, verbose_name='Type de revêtement')), + ], + options={ + 'verbose_name': 'Espace vert creux', + 'verbose_name_plural': 'Espaces verts creux', + }, + ), + migrations.CreateModel( + name='NatureLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('zone', models.IntegerField(blank=True, null=True)), + ('is_highway', models.BooleanField(default=False)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='NatureRWIZ', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ], + options={ + 'verbose_name': 'Zone de gestion intégrée des eaux pluviales (ZIEP)', + 'verbose_name_plural': 'Zones de gestion intégrée des eaux pluviales (ZIEP)', + }, + ), + migrations.CreateModel( + name='NatureShrub', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ('depth', models.FloatField(blank=True, null=True)), + ('trimmed', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'Arbuste', + 'verbose_name_plural': 'Arbustes', + }, + ), + migrations.CreateModel( + name='NatureTree', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ('depth', models.FloatField(blank=True, null=True)), + ('protection_buffer', models.FloatField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PublicLightingElectricalCabinet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('ean_code', models.CharField(blank=True, max_length=50, null=True, verbose_name='Code EAN')), + ('power_supply', models.CharField(blank=True, max_length=100, null=True, verbose_name='Alimentation électrique')), + ('meter_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Numéro de compteur')), + ('number_of_circuits', models.IntegerField(blank=True, null=True, verbose_name='Nombre de circuits')), + ], + ), + migrations.CreateModel( + name='PublicLightingElectricalCabinetModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('width', models.FloatField(blank=True, null=True, verbose_name='Largeur (cm)')), + ('height', models.FloatField(blank=True, null=True, verbose_name='Hauteur (cm)')), + ('depth', models.FloatField(blank=True, null=True, verbose_name='Profondeur (cm)')), + ('ip_rating', models.CharField(blank=True, max_length=10, null=True, verbose_name='Indice de protection IP')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PublicLightingLightFixture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Luminaire', + 'verbose_name_plural': 'Luminaires', + }, + ), + migrations.CreateModel( + name='PublicLightingLightFixtureModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PublicLightingPole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='PublicLightingPoleModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=200, null=True)), + ('ral', models.CharField(blank=True, max_length=200, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PublicLightingStreet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PublicLightingStreetDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/publiclighting/', verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/publiclighting/')), + ('document_type', models.CharField(choices=[('as_built', 'As Built'), ('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadConcreteBlockSet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('address', models.CharField(blank=True, max_length=2048, null=True)), + ('main_function', models.CharField(blank=True, choices=[('protection', 'Protection Asset'), ('antiterrorist', 'Antiterroriste'), ('parking_neutralization', 'Neutralisation stationnement'), ('road_safety', 'Sécurité routière'), ('public_space', 'Espace public'), ('construction', 'Chantier'), ('unidentified', 'Non identifié')], max_length=200, null=True)), + ('secondary_functions', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('protection', 'Protection Asset'), ('antiterrorist', 'Antiterroriste'), ('parking_neutralization', 'Neutralisation stationnement'), ('road_safety', 'Sécurité routière'), ('public_space', 'Espace public'), ('construction', 'Chantier'), ('unidentified', 'Non identifié')], max_length=200), blank=True, null=True, size=None)), + ('number_of_blocks', models.IntegerField(blank=True, null=True)), + ('length', models.FloatField(blank=True, null=True)), + ('recommended_action', models.CharField(blank=True, choices=[('keep', 'Maintenir'), ('remove', 'Enlever'), ('replace', 'Remplacer')], max_length=2048, null=True)), + ('priority_level', models.IntegerField(blank=True, null=True)), + ('targeted_action_date', models.DateField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadDrain', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('soil_type', models.CharField(blank=True, choices=[('concrete', 'Béton'), ('asphalt', 'Asphalte'), ('stone', 'Pierre'), ('concrete_slabs', 'Dalles en béton'), ('concrete_paving', 'Pavés en béton'), ('stone_slabs', 'Dalles en pierre'), ('stone_paving', 'Pavés en pierre'), ('concrete_grass', 'Béton/Gazon'), ('gravel', 'Gravier'), ('grass', 'Gazon'), ('earth', 'Terre'), ('other', 'Autre'), ('na', 'N/A')], max_length=200, null=True)), + ('positionning_source', models.CharField(blank=True, max_length=2048, null=True)), + ('positionning_note', models.TextField(blank=True, null=True)), + ('positionning_date', models.DateField(blank=True, null=True)), + ('drain_location_type', models.CharField(blank=True, choices=[('waterway', "Filet d'eau"), ('sidewalk', 'Trottoir'), ('berme', 'Berme'), ('sidewalk_facade_to_road', 'Trottoir façade vers chaussée'), ('square', 'Place'), ('along_sidewalk', 'Le long du trottoir'), ('middle_road', 'Milieu chaussée'), ('other', 'Autre'), ('na', 'N/A')], max_length=200, null=True)), + ('nearby_vegetation', models.BooleanField(blank=True, null=True)), + ('possible_rooting', models.BooleanField(blank=True, null=True)), + ('is_defect', models.BooleanField(blank=True, null=True)), + ('defect_types', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('obstructed', 'Obstrué'), ('smell_cut_missing', 'Coupe-odeur manquant'), ('grate_broken', 'Grille cassée'), ('grate_missing', 'Grille manquante'), ('poor_water_flow', 'Mauvais écoulement'), ('other', 'Autre')], max_length=200), blank=True, null=True, size=None)), + ('defect_note', models.TextField(blank=True, null=True)), + ('related_image_type', models.CharField(blank=True, max_length=1000, null=True)), + ('altitude', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, dim=3, null=True, srid=3812)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadDrainModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadFurniture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Street Furniture', + 'verbose_name_plural': 'Street Furnitures', + }, + ), + migrations.CreateModel( + name='RoadFurnitureModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=200, null=True)), + ('ral', models.CharField(blank=True, max_length=200, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ('width', models.FloatField(blank=True, null=True)), + ('material', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadSidewalk', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('coating_type', models.CharField(blank=True, choices=[('concrete', 'Concrete'), ('paving_stones', 'Paving Stones'), ('tiles20', 'Tiles 20x20'), ('tiles30', 'Tiles 30x30'), ('tiles40', 'Tiles 40x40'), ('tiles50', 'Tiles 50x50'), ('tiles', 'Tiles'), ('asphalt', 'Asphalt'), ('dolomite', 'Dolomie'), ('gravel', 'Gravel'), ('dirt', 'Dirt'), ('other', 'Other')], max_length=200, null=True)), + ('quality_level', models.IntegerField(blank=True, choices=[(1, '1 - Très mauvais'), (2, '2 - Mauvais'), (3, '3 - Bon'), (4, '4 - Très bon'), (5, '5 - Excellent')], null=True)), + ('area', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ], + options={ + 'verbose_name': 'Sidewalk', + 'verbose_name_plural': 'Sidewalks', + }, + ), + migrations.CreateModel( + name='RoadSidewalkModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadStreet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('road_code', models.CharField(blank=True, max_length=50, null=True)), + ('section_code', models.CharField(blank=True, max_length=50, null=True)), + ('street_level', models.CharField(blank=True, max_length=50, null=True)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoadStreetDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.roads.upload_to_roadstreet, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.roads.upload_to_roadstreet)), + ('document_type', models.CharField(choices=[('as_built', 'As Built'), ('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Roadway', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('coating_type', models.CharField(blank=True, choices=[('asphalt', 'Asphalt'), ('concrete', 'Concrete'), ('cobblestone', 'Cobblestone'), ('gravel', 'Gravel'), ('dirt', 'Dirt'), ('other', 'Other')], max_length=200, null=True)), + ('quality_level', models.IntegerField(blank=True, choices=[(1, '1 - Très mauvais'), (2, '2 - Mauvais'), (3, '3 - Bon'), (4, '4 - Très bon'), (5, '5 - Excellent')], null=True)), + ('lanes', models.IntegerField(blank=True, null=True)), + ('area', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ], + options={ + 'verbose_name': 'Roadway', + 'verbose_name_plural': 'Roadways', + }, + ), + migrations.CreateModel( + name='RoadwayFunction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pedestrian_function', models.CharField(blank=True, choices=[('masterful', 'Magistrale'), ('plus', 'Plus'), ('confort', 'Confort'), ('other', 'Autre')], max_length=200, null=True)), + ('bicycle_function', models.CharField(blank=True, choices=[('masterful', 'Magistrale'), ('plus', 'Plus'), ('confort', 'Confort'), ('other', 'Autre')], max_length=200, null=True)), + ('public_transport_function', models.CharField(blank=True, choices=[('masterful', 'Magistrale'), ('plus', 'Plus'), ('confort', 'Confort'), ('other', 'Autre')], max_length=200, null=True)), + ('private_motorized_function', models.CharField(blank=True, choices=[('masterful', 'Magistrale'), ('plus', 'Plus'), ('confort', 'Confort'), ('other', 'Autre')], max_length=200, null=True)), + ], + options={ + 'verbose_name': 'Roadway Function', + 'verbose_name_plural': 'Roadway Functions', + }, + ), + migrations.CreateModel( + name='RoadwayModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('speed_limit', models.IntegerField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SignPanel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('vertical_order', models.IntegerField(blank=True, null=True)), + ('signpanel_text', models.CharField(blank=True, 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')], max_length=1000, null=True)), + ('signpanel_value', models.FloatField(blank=True, null=True)), + ('orientation', models.IntegerField(blank=True, help_text='Orientation en degrés (0-360)', null=True)), + ('support_type', models.CharField(blank=True, 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')], max_length=200, null=True)), + ('is_temporary', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'Panneau de signalisation', + 'verbose_name_plural': 'Panneaux de signalisation', + }, + ), + migrations.CreateModel( + name='SignPanelModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('shape', models.CharField(blank=True, max_length=200, null=True)), + ('color', models.CharField(blank=True, max_length=200, null=True)), + ('ral', models.CharField(blank=True, max_length=200, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ('width', models.FloatField(blank=True, null=True)), + ('diameter', models.FloatField(blank=True, null=True)), + ('material', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SignPanelType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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(blank=True, null=True)), + ('sgv_picture', models.ImageField(blank=True, null=True, upload_to='sign/signpanels_types/')), + ], + ), + migrations.CreateModel( + name='SignParkingSpot', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PolygonField(blank=True, null=True, srid=3812)), + ('spot_type', models.CharField(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')], default='other', max_length=50, verbose_name='Type de place')), + ('capacity', models.IntegerField(blank=True, help_text='Capacité de la place (ex: nombre de véhicules pouvant stationner)', null=True)), + ('is_temporary', models.BooleanField(default=False, help_text='Indique si la place est temporaire (ex: zone de chantier)')), + ('entity', models.CharField(blank=True, help_text='Entité responsable de la place (ex: société de parking)', max_length=255, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SignPole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('orientation', models.IntegerField(blank=True, default=90, help_text='Orientation en degrés (0-360)', null=True)), + ], + ), + migrations.CreateModel( + name='SignPoleModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('color', models.CharField(blank=True, max_length=200, null=True)), + ('ral', models.CharField(blank=True, max_length=200, null=True)), + ('height', models.FloatField(blank=True, null=True)), + ('diameter', models.FloatField(blank=True, null=True)), + ('material', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SignStreet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('road_code', models.CharField(blank=True, max_length=50, null=True)), + ('section_code', models.CharField(blank=True, max_length=50, null=True)), + ('street_level', models.CharField(blank=True, max_length=50, null=True)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SignStreetDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, upload_to=assets.models.sign.upload_to_signstreet, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, upload_to=assets.models.sign.upload_to_signstreet)), + ('document_type', models.CharField(choices=[('as_built', 'As Built'), ('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Structure', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50, unique=True)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('structure_type', models.CharField(blank=True, choices=[('tunnel', 'Tunnnel'), ('bridge', 'Pont'), ('viaduct', 'Viaduc'), ('overpass', 'Passerelle'), ('underpass', 'Passage souterrain'), ('gallery', 'Galerie technique'), ('atypical', 'Ouvrage atypique')], max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('linked_folder', models.CharField(blank=True, max_length=1024, null=True)), + ('structure_number', models.IntegerField(blank=True, null=True, verbose_name="Numéro de l'ouvrage")), + ], + options={ + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='StructureAssetModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'verbose_name': 'Structure Asset Model', + 'verbose_name_plural': 'Structure Asset Models', + }, + ), + migrations.CreateModel( + name='StructureContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('electromeca', 'Eléctromécanique'), ('civil_engineering', 'Génie civil'), ('urgent_cleaning', 'Nettoyage urgent'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'Structure Contract', + 'verbose_name_plural': 'Structure Contracts', + }, + ), + migrations.CreateModel( + name='StructureDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.structures.upload_to_structure, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.structures.upload_to_structure)), + ('document_type', models.CharField(choices=[('as_built', 'As Built'), ('cme', 'CME'), ('inspection_ce', 'Inspection G.C.'), ('inspection_em', 'Inspection E.M.'), ('pis', 'PIS'), ('other', 'Autre')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='StructureGeoAsset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='StructureLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('is_main_location', models.BooleanField(default=False, verbose_name='Main Location')), + ], + options={ + 'verbose_name': 'Structure Location', + 'verbose_name_plural': 'Structure Locations', + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='TrafficLightAccessory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('orientation', models.FloatField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Accessoire', + 'verbose_name_plural': 'Accessoires', + }, + ), + migrations.CreateModel( + name='TrafficLightAccessoryModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightCable', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.LineStringField(blank=True, null=True, srid=3812)), + ], + options={ + 'verbose_name': 'Câble', + 'verbose_name_plural': 'Câbles', + }, + ), + migrations.CreateModel( + name='TrafficLightCableModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('number_of_strands', models.IntegerField(verbose_name='Nombre de brins')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)), + ('maintenance_type', models.CharField(choices=[('infra', 'Infrastructure'), ('controller', 'Contrôleur de traffic'), ('other', 'Autre')], default='other', max_length=50)), + ], + options={ + 'verbose_name': 'Traffic Lights Contract', + 'verbose_name_plural': 'Traffic Lights Contracts', + }, + ), + migrations.CreateModel( + name='TrafficLightController', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('connected_to_control_center', models.BooleanField(default=False, verbose_name='Connecté à la centrale de contrôle')), + ('plan', models.CharField(blank=True, max_length=255, null=True, verbose_name='Plan')), + ('cabinet_type', models.CharField(blank=True, max_length=255, null=True, verbose_name='Type')), + ('power', models.CharField(blank=True, max_length=255, null=True, verbose_name='Puissance')), + ('firmware_version', models.CharField(blank=True, max_length=255, null=True, verbose_name='Version du firmware')), + ('rms', models.BooleanField(default=True, verbose_name='RMS')), + ('url', models.URLField(blank=True, null=True, verbose_name='URL')), + ], + options={ + 'verbose_name': 'Contrôleur', + 'verbose_name_plural': 'Contrôleurs', + }, + ), + migrations.CreateModel( + name='TrafficLightControllerHardware', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('hardware', models.CharField(blank=True, max_length=255, null=True, verbose_name='Hardware')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom du matériel')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du matériel')), + ], + options={ + 'verbose_name': 'Hardware contrôleur', + 'verbose_name_plural': 'Hardwares contrôleur', + }, + ), + migrations.CreateModel( + name='TrafficLightControllerLogFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets/traffic_lights/controller_logs/', verbose_name='Fichier de log')), + ('filedate', models.DateTimeField(blank=True, null=True, verbose_name='Date du log')), + ], + options={ + 'verbose_name': 'Fichier de log du contrôleur', + 'verbose_name_plural': 'Fichiers de log du contrôleur', + }, + ), + migrations.CreateModel( + name='TrafficLightControllerModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightDetector', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom du détecteur')), + ('hardware', models.CharField(blank=True, max_length=255, null=True, verbose_name='Hardware')), + ('phases', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, null=True, size=None, verbose_name='Phases associées')), + ('mentionned_on_plan', models.BooleanField(default=False, verbose_name='Mentionné sur le plan')), + ], + options={ + 'verbose_name': 'Détecteur', + 'verbose_name_plural': 'Détecteurs', + }, + ), + migrations.CreateModel( + name='TrafficLightDetectorModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightElectricalCabinet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('ean_code', models.CharField(blank=True, max_length=50, null=True, verbose_name='Code EAN')), + ('power_supply', models.CharField(blank=True, max_length=100, null=True, verbose_name='Alimentation électrique')), + ('meter_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Numéro de compteur')), + ('publicity', models.BooleanField(default=False, verbose_name='Publicité')), + ('key_type', models.CharField(blank=True, choices=[('bks', 'BKS'), ('bur16', 'BUR 16'), ('eu4', 'EU 4'), ('siemens', 'Siemens'), ('yale5', 'Yale 5'), ('58335', '58335'), ('no_key', 'Pas de clé'), ('other', 'Autre')], max_length=100, null=True, verbose_name='Type de clé')), + ('cabinet_type', models.CharField(blank=True, choices=[('traffic_lights', 'Feux de signalisation'), ('mixed', 'Mixte'), ('other', 'Autre')], default='traffic_lights', max_length=100, null=True, verbose_name="Type d'armoire")), + ('managed_by', models.CharField(blank=True, max_length=255, null=True, verbose_name='Géré par')), + ('meter_inside', models.BooleanField(default=False, verbose_name='Présence Compteur')), + ('differential_switch', models.BooleanField(default=False, verbose_name='Interrupteur différentiel')), + ('differential_switch_caliber', models.CharField(blank=True, max_length=50, null=True, verbose_name='Calibre du disjoncteur différentiel')), + ('differential_breaker', models.BooleanField(default=False, verbose_name='Disjoncteur différentiel')), + ('differential_breaker_caliber', models.CharField(blank=True, max_length=50, null=True, verbose_name='Calibre du disjoncteur')), + ('differential_caliber', models.CharField(blank=True, max_length=50, null=True, verbose_name='Calibre différentiel')), + ('departure_towards', models.CharField(blank=True, max_length=50, null=True, verbose_name='Départ vers')), + ('visual_conformity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Conformité visuelle')), + ('cleanliness', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propreté')), + ], + options={ + 'verbose_name': 'Armoire', + 'verbose_name_plural': 'Armoires', + }, + ), + migrations.CreateModel( + name='TrafficLightElectricalCabinetContent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('description', models.CharField(blank=True, max_length=255, null=True, verbose_name='Description du contenu')), + ('number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Numéro de référence')), + ('ean', models.CharField(blank=True, max_length=100, null=True, verbose_name='Code EAN')), + ], + options={ + 'verbose_name': "Contenu d'armoire", + 'verbose_name_plural': "Contenus d'armoires", + }, + ), + migrations.CreateModel( + name='TrafficLightElectricalCabinetContentModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightElectricalCabinetModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('width', models.FloatField(blank=True, null=True, verbose_name='Largeur (cm)')), + ('height', models.FloatField(blank=True, null=True, verbose_name='Hauteur (cm)')), + ('depth', models.FloatField(blank=True, null=True, verbose_name='Profondeur (cm)')), + ('ip_rating', models.CharField(blank=True, max_length=10, null=True, verbose_name='Indice de protection IP')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightIntersection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('voltage_type', models.CharField(blank=True, choices=[('10V', '10 Volts'), ('230V', '230 Volts'), ('LED', 'LED')], null=True)), + ('status', models.CharField(choices=[('active', 'Actif'), ('in_preparation', 'En préparation'), ('under_repair', 'En cours de réparation'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('inactive', 'Désactivé'), ('archived', 'Archivé')], default='active')), + ('begin_time', models.DateTimeField(blank=True, null=True)), + ('current_index', models.CharField(blank=True, max_length=100, null=True)), + ], + options={ + 'ordering': ['code'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightIntersectionDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/traffic_lights/', verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/thumbnails/traffic_lights/')), + ('document_type', models.CharField(choices=[('as_built', 'As Built'), ('cross_plan', 'Plan de croix'), ('other', 'Autre'), ('programming_file', 'Fichier de programmation'), ('connexion_plan', 'Plan de raccordement')], default='other', max_length=50, verbose_name='Type de document')), + ], + options={ + 'ordering': ['upload_date'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightLantern', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('orientation', models.FloatField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Lanterne', + 'verbose_name_plural': 'Lanternes', + }, + ), + migrations.CreateModel( + name='TrafficLightLanternModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('voltage', models.IntegerField(verbose_name='Tension (V)')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightNetworkHardware', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom du matériel')), + ('hardware', models.CharField(blank=True, max_length=255, null=True, verbose_name='Hardware')), + ('mentionned_on_plan', models.BooleanField(default=False, verbose_name='Mentionné sur le plan')), + ], + options={ + 'verbose_name': 'Network Hardware', + 'verbose_name_plural': 'Network Hardwares', + }, + ), + migrations.CreateModel( + name='TrafficLightPole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('arc_length', models.FloatField(blank=True, null=True, verbose_name="Longueur de l'arc (m)")), + ('extension', models.BooleanField(default=False, verbose_name='Extension')), + ('colour', models.CharField(blank=True, max_length=200, null=True, verbose_name='Couleur')), + ('ral', models.CharField(blank=True, max_length=200, null=True, verbose_name='RAL')), + ], + options={ + 'verbose_name': 'Poteau', + 'verbose_name_plural': 'Poteaux', + }, + ), + migrations.CreateModel( + name='TrafficLightPoleModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('colour', models.CharField(blank=True, max_length=200, null=True, verbose_name='Couleur')), + ('ral', models.CharField(blank=True, max_length=200, null=True, verbose_name='RAL')), + ('height', models.FloatField(blank=True, null=True, verbose_name='Hauteur (m)')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TrafficLightProgramming', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('commissioning_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de mise en service')), + ('uncommissioning_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de mise hors service')), + ('plan', models.CharField(blank=True, max_length=255, null=True, verbose_name='Plan')), + ('index', models.CharField(max_length=100, verbose_name='Index')), + ('info', models.TextField(blank=True, null=True, verbose_name='Informations')), + ], + options={ + 'ordering': ['-commissioning_date'], + }, + ), + migrations.CreateModel( + name='TrafficLightVoltageHardware', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nom du matériel')), + ('hardware', models.CharField(blank=True, max_length=255, null=True, verbose_name='Hardware')), + ('mentionned_on_plan', models.BooleanField(default=False, verbose_name='Mentionné sur le plan')), + ], + options={ + 'verbose_name': 'Voltage Hardware', + 'verbose_name_plural': 'Voltage Hardwares', + }, + ), + ] diff --git a/streetup/assets/migrations/0002_initial.py b/streetup/assets/migrations/0002_initial.py new file mode 100644 index 0000000..b473481 --- /dev/null +++ b/streetup/assets/migrations/0002_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('assets', '0001_initial'), + ('contracts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='artworkfountain', + name='maintenance_contract', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contracts.contract'), + ), + migrations.AddField( + model_name='artworkfountain', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='artworkfountain', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.artworkfountain', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='artworkfountain', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='artworkfountaindocument', + name='fountain', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.artworkfountain', verbose_name='Associated Fountain'), + ), + ] diff --git a/streetup/assets/migrations/0003_initial.py b/streetup/assets/migrations/0003_initial.py new file mode 100644 index 0000000..4d2f897 --- /dev/null +++ b/streetup/assets/migrations/0003_initial.py @@ -0,0 +1,762 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('assets', '0002_initial'), + ('common', '0002_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ('contracts', '0001_initial'), + ('documents', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='artworkfountaindocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='artworkfountaindocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='artworkmonument', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='artworkmonument', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.artworkmonument', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='artworkmonument', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='artworkmonumentdocument', + name='fountain', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.artworkmonument', verbose_name='Associated Monument'), + ), + migrations.AddField( + model_name='artworkmonumentdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='artworkmonumentdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='artworkstreet', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='artworks_streets', to='common.municipality'), + ), + migrations.AddField( + model_name='artworkstreet', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='artworkmonument', + name='street', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='monuments', to='assets.artworkstreet'), + ), + migrations.AddField( + model_name='artworkfountain', + name='street', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fountains', to='assets.artworkstreet'), + ), + migrations.AddField( + model_name='assetcategory', + name='parent', + field=models.ForeignKey(blank=True, help_text="Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne peut pas avoir d'enfants.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='assets.assetcategory', verbose_name='Catégorie parente'), + ), + migrations.AddField( + model_name='assetcategory', + name='thematic', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_categories', to='common.thematic'), + ), + migrations.AddField( + model_name='artworkmonument', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='artworkfountain', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='assethistory', + name='content_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset"), + ), + migrations.AddField( + model_name='assethistory', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_changes', to=settings.AUTH_USER_MODEL, verbose_name='Utilisateur'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.cleanglasscontainer', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='cleanglasscontainermodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.cleanglasscontainermodel', verbose_name='Model'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.cleanlitterbin', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='cleanlitterbinmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.cleanlitterbinmodel', verbose_name='Model'), + ), + migrations.AddField( + model_name='cleanlitterbinobservation', + name='litter_bin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='assets.cleanlitterbin', verbose_name='Litter bin'), + ), + migrations.AddField( + model_name='cleanlitterbinphoto', + name='litter_bin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='assets.cleanlitterbin', verbose_name='Litter bin'), + ), + migrations.AddField( + model_name='cleanlocation', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clean_locations', to='common.municipality'), + ), + migrations.AddField( + model_name='cleanlocation', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='litter_bins', to='assets.cleanlocation', verbose_name='Location'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='glass_containers', to='assets.cleanlocation', verbose_name='Location'), + ), + migrations.AddField( + model_name='cleanlocationcontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clean_location_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='cleanlocationcontract', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='assets.cleanlocation'), + ), + migrations.AddField( + model_name='complementarypoliceregulation', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='complementarypoliceregulation', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_complementary_police_regulations_assets', to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='controlcenterassetdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='controlcenterassetdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='controlcenterassetmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='controlcentercontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='controlcenters_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='controlcentercontract', + name='control_center', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='controlcenters_contracts', to='assets.controlcenter'), + ), + migrations.AddField( + model_name='controlcenterdocument', + name='control_center', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.controlcenter', verbose_name='Associated Location'), + ), + migrations.AddField( + model_name='controlcenterdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='controlcenterdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='control_center', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.controlcenter'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='controlcenters_assets', to='assets.controlcenterassetmodel'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.controlcentergeoasset', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='controlcenterassetdocument', + name='control_center_asset', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.controlcentergeoasset', verbose_name='Associated Location'), + ), + migrations.AddField( + model_name='controlcentergeoassetcontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='controlcenters_assets_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='controlcentergeoassetcontract', + name='control_center_asset', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='controlcenters_assets_contracts', to='assets.controlcentergeoasset'), + ), + migrations.AddField( + model_name='controlcenterlocation', + name='control_center', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='all_locations', to='assets.controlcenter'), + ), + migrations.AddField( + model_name='controlcenterlocation', + name='parent_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.controlcenterlocation'), + ), + migrations.AddField( + model_name='controlcenterlocation', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.controlcenterlocation'), + ), + migrations.AddField( + model_name='controlcenter', + name='main_location', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_control_center', to='assets.controlcenterlocation'), + ), + migrations.AddField( + model_name='greensurfacescomment', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='green_surfaces_comments', to=settings.AUTH_USER_MODEL, verbose_name='Créé par'), + ), + migrations.AddField( + model_name='greensurfacescommentphoto', + name='comment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='assets.greensurfacescomment', verbose_name='Commentaire'), + ), + migrations.AddField( + model_name='itsassetmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsgeoasset', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='itsgeoassetcontract', + name='asset', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='its_assets_contracts', to='assets.itsgeoasset'), + ), + migrations.AddField( + model_name='itsgeoassetcontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='its_assets_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='itslocation', + name='parent_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.itslocation'), + ), + migrations.AddField( + model_name='itslocation', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.itslocation'), + ), + migrations.AddField( + model_name='itslocationcontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='its_locations_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='itslocationcontract', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='its_locations_contracts', to='assets.itslocation'), + ), + migrations.AddField( + model_name='itslocationdocument', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.itslocation', verbose_name='Associated ITS Location'), + ), + migrations.AddField( + model_name='itslocationdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='itslocationdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='natureassetmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='naturebench', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='naturebench', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='naturebench', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_benches', to='assets.natureassetmodel'), + ), + migrations.AddField( + model_name='naturebench', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.naturebench', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='naturebench', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_green_surfaces', to='assets.natureassetmodel'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.naturegreensurface', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='naturegreensurfacedocument', + name='green_surface', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.naturegreensurface', verbose_name='Associated Surface'), + ), + migrations.AddField( + model_name='naturegreensurfacedocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='naturegreensurfacedocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='naturehollowgreenspace', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='naturehollowgreenspace', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='naturehollowgreenspace', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.naturehollowgreenspace', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='naturehollowgreenspace', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='naturelocation', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='naturelocation', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.municipality'), + ), + migrations.AddField( + model_name='naturelocation', + name='parent_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.naturelocation'), + ), + migrations.AddField( + model_name='naturelocation', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_green_surfaces', to='assets.naturelocation'), + ), + migrations.AddField( + model_name='naturebench', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_benches', to='assets.naturelocation'), + ), + migrations.AddField( + model_name='naturerwiz', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='naturehollowgreenspace', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hollow_green_spaces', to='assets.naturerwiz'), + ), + migrations.AddField( + model_name='natureshrub', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='natureshrub', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_shrubs', to='assets.naturelocation'), + ), + migrations.AddField( + model_name='natureshrub', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='natureshrub', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_shrubs', to='assets.natureassetmodel'), + ), + migrations.AddField( + model_name='natureshrub', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.natureshrub', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='natureshrub', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='naturetree', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='naturetree', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_trees', to='assets.naturelocation'), + ), + migrations.AddField( + model_name='naturetree', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='naturetree', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nature_trees', to='assets.natureassetmodel'), + ), + migrations.AddField( + model_name='naturetree', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.naturetree', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='naturetree', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publiclightingelectricalcabinet', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinetmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.publiclightingelectricalcabinetmodel'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publiclightinglightfixture', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='publiclightinglightfixturemodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.publiclightinglightfixturemodel'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publiclightingpole', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='light_fixtures', to='assets.publiclightingpole'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='lighting_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='its_assets', to='assets.publiclightingpole', verbose_name='Lighting Pole'), + ), + migrations.AddField( + model_name='publiclightingpolemodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.publiclightingpolemodel'), + ), + migrations.AddField( + model_name='publiclightingstreet', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publiclighting_streets', to='common.municipality'), + ), + migrations.AddField( + model_name='publiclightingstreet', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='poles', to='assets.publiclightingstreet'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='light_fixtures', to='assets.publiclightingstreet'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='electrical_cabinets', to='assets.publiclightingstreet'), + ), + migrations.AddField( + model_name='publiclightingstreetdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='publiclightingstreetdocument', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.publiclightingstreet', verbose_name='Associated Street'), + ), + migrations.AddField( + model_name='publiclightingstreetdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='roadconcreteblockset', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadconcreteblockset', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + ] diff --git a/streetup/assets/migrations/0004_initial.py b/streetup/assets/migrations/0004_initial.py new file mode 100644 index 0000000..a37de5d --- /dev/null +++ b/streetup/assets/migrations/0004_initial.py @@ -0,0 +1,1010 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('assets', '0003_initial'), + ('common', '0003_initial'), + ('contracts', '0001_initial'), + ('documents', '0001_initial'), + ('observations', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='roadconcreteblockset', + name='observation_source', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='concrete_block_sets', to='observations.observation'), + ), + migrations.AddField( + model_name='roadconcreteblockset', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.roadconcreteblockset', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='roadconcreteblockset', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='roaddrain', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roaddrain', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='roaddrain', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.roaddrain', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='roaddrain', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='roaddrainmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roaddrain', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.roaddrainmodel'), + ), + migrations.AddField( + model_name='roadfurniture', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadfurniture', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='roadfurniture', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.roadfurniture', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='roadfurniture', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='roadfurnituremodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadfurniture', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.roadfurnituremodel'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.roadsidewalk', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='roadsidewalkmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.roadsidewalkmodel'), + ), + migrations.AddField( + model_name='roadstreet', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='road_streets', to='common.municipality'), + ), + migrations.AddField( + model_name='roadstreet', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sidewalks', to='assets.roadstreet'), + ), + migrations.AddField( + model_name='roadfurniture', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='furnitures', to='assets.roadstreet'), + ), + migrations.AddField( + model_name='roaddrain', + name='street', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='drains', to='assets.roadstreet'), + ), + migrations.AddField( + model_name='roadstreetdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='roadstreetdocument', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.roadstreet', verbose_name='Associated Street'), + ), + migrations.AddField( + model_name='roadstreetdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='roadway', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadway', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='roadway', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.roadway', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='roadway', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='roadways', to='assets.roadstreet'), + ), + migrations.AddField( + model_name='roadway', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='roadwayfunction', + name='roadway', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='functions', to='assets.roadway'), + ), + migrations.AddField( + model_name='roadwaymodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='roadway', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.roadwaymodel'), + ), + migrations.AddField( + model_name='signpanel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='signpanel', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='signpanel', + name='publiclighting_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='signpanels_attached', to='assets.publiclightingpole'), + ), + migrations.AddField( + model_name='signpanel', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.signpanel', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='signpanel', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='complementarypoliceregulation', + name='panels', + field=models.ManyToManyField(blank=True, related_name='complementary_regulations', to='assets.signpanel'), + ), + migrations.AddField( + model_name='signpanelmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='signpanel', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.signpanelmodel'), + ), + migrations.AddField( + model_name='signpanel', + name='signpanel_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.signpaneltype'), + ), + migrations.AddField( + model_name='signparkingspot', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='signparkingspot', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='signparkingspot', + name='panels', + field=models.ManyToManyField(blank=True, related_name='parking_spots_attached', to='assets.signpanel'), + ), + migrations.AddField( + model_name='signparkingspot', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.signparkingspot', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='signparkingspot', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='signpole', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='signpole', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='signpole', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.signpole', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='signpole', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='signparkingspot', + name='poles', + field=models.ManyToManyField(blank=True, related_name='parking_spots_attached', to='assets.signpole'), + ), + migrations.AddField( + model_name='signpanel', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='signpanels', to='assets.signpole'), + ), + migrations.AddField( + model_name='signpolemodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='signpole', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.signpolemodel'), + ), + migrations.AddField( + model_name='signstreet', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sign_streets', to='common.municipality'), + ), + migrations.AddField( + model_name='signstreet', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='signpole', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='signpoles', to='assets.signstreet'), + ), + migrations.AddField( + model_name='signparkingspot', + name='street', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parking_spots', to='assets.signstreet'), + ), + migrations.AddField( + model_name='signpanel', + name='street', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='signpanels', to='assets.signstreet'), + ), + migrations.AddField( + model_name='signstreetdocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='signstreetdocument', + name='street', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.signstreet', verbose_name='Associated Street'), + ), + migrations.AddField( + model_name='signstreetdocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_sign_street_documents_assets', to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='structureassetmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='structurecontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structure_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='structurecontract', + name='structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='assets.structure'), + ), + migrations.AddField( + model_name='structuredocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='structuredocument', + name='structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.structure', verbose_name='Associated Structure'), + ), + migrations.AddField( + model_name='structuredocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.structureassetmodel'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='parent_asset', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.structuregeoasset'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.structuregeoasset', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='structure', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.structure'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='structurelocation', + name='parent_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.structurelocation'), + ), + migrations.AddField( + model_name='structurelocation', + name='structure', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='all_locations', to='assets.structure'), + ), + migrations.AddField( + model_name='structurelocation', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.structurelocation'), + ), + migrations.AddField( + model_name='structure', + name='main_location', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='main_structure', to='assets.structurelocation'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightaccessory', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightaccessorymodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightaccessorymodel'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightcable', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightcablemodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightcablemodel'), + ), + migrations.AddField( + model_name='trafficlightcontract', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trafficlight_contracts', to='contracts.contract'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='controller_maintenance_contract', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contracts.contract'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightcontroller', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightcontrollerhardware', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightcontrollerhardware', + name='controller', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hardwares', to='assets.trafficlightcontroller'), + ), + migrations.AddField( + model_name='trafficlightcontrollerhardware', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightcontrollerhardware', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightcontrollerhardware', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightcontrollerhardware', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightcontrollerlogfile', + name='controller', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs', to='assets.trafficlightcontroller'), + ), + migrations.AddField( + model_name='trafficlightcontrollermodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightcontrollermodel'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='controller', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='detectors', to='assets.trafficlightcontroller'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightdetector', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightdetectormodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightdetectormodel'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightelectricalcabinet', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='cabinet', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contents', to='assets.trafficlightelectricalcabinet'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightelectricalcabinetcontent', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontentmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.trafficlightelectricalcabinetcontentmodel'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.trafficlightelectricalcabinetmodel'), + ), + migrations.AddField( + model_name='trafficlightintersection', + name='master', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='slaves', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightintersection', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='trafficlights_intersections', to='common.municipality'), + ), + migrations.AddField( + model_name='trafficlightintersection', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='electrical_cabinets', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='detectors', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='controllers', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightcontract', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trafficlight_contracts', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cables', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='intersection', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='accessories', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightintersectiondocument', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.trafficlightintersection', verbose_name='Associated Intersection'), + ), + migrations.AddField( + model_name='trafficlightintersectiondocument', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='trafficlightintersectiondocument', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightlantern', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightlanternmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightlanternmodel'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='controller', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='network_hardwares', to='assets.trafficlightcontroller'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='network_hardwares', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightnetworkhardware', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='poles', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightpole', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='pole', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lanterns', to='assets.trafficlightpole'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='detectors', to='assets.trafficlightpole'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='accessories', to='assets.trafficlightpole'), + ), + 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='signpanels_attached', to='assets.trafficlightpole'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='traffic_light_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='light_fixtures', to='assets.trafficlightpole'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='trafficlights_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='its_assets', to='assets.trafficlightpole', verbose_name='Traffic Light Pole'), + ), + migrations.AddField( + model_name='trafficlightpolemodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightpolemodel'), + ), + migrations.AddField( + model_name='trafficlightprogramming', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightprogramming', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='programmations', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightprogramming', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightprogramming', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightprogramming', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightprogramming', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddField( + model_name='trafficlightintersectiondocument', + name='programming', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='assets.trafficlightprogramming', verbose_name='Programmation associée'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='controller', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='voltage_hardwares', to='assets.trafficlightcontroller'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='intersection', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='voltage_hardwares', to='assets.trafficlightintersection'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='marked_as_removed_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='replaced_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightvoltagehardware', verbose_name='Remplacé par'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'), + ), + migrations.AddIndex( + model_name='assethistory', + index=models.Index(fields=['content_type', 'object_id', '-timestamp'], name='assets_asse_content_d1c26b_idx'), + ), + migrations.AddIndex( + model_name='assethistory', + index=models.Index(fields=['-timestamp'], name='assets_asse_timesta_16981b_idx'), + ), + migrations.AddIndex( + model_name='assethistory', + index=models.Index(fields=['user', '-timestamp'], name='assets_asse_user_id_446ead_idx'), + ), + migrations.AddIndex( + model_name='assethistory', + index=models.Index(fields=['action_type', '-timestamp'], name='assets_asse_action__596a0e_idx'), + ), + migrations.AddIndex( + model_name='assethistory', + index=models.Index(fields=['field_name', '-timestamp'], name='assets_asse_field_n_bcedec_idx'), + ), + migrations.AddIndex( + model_name='assethistory', + index=models.Index(fields=['content_type', 'action_type'], name='assets_asse_content_ac1e8f_idx'), + ), + migrations.AddIndex( + model_name='cleanlitterbin', + index=models.Index(fields=['code'], name='assets_clea_code_74ad0c_idx'), + ), + migrations.AddIndex( + model_name='cleanglasscontainer', + index=models.Index(fields=['code'], name='assets_clea_code_0a414b_idx'), + ), + migrations.AlterUniqueTogether( + name='controlcentercontract', + unique_together={('control_center', 'contract')}, + ), + migrations.AlterUniqueTogether( + name='controlcentergeoassetcontract', + unique_together={('control_center_asset', 'contract')}, + ), + migrations.AddIndex( + model_name='controlcentergeoasset', + index=models.Index(fields=['code'], name='assets_cont_code_168e9a_idx'), + ), + migrations.AlterUniqueTogether( + name='itsgeoassetcontract', + unique_together={('asset', 'contract')}, + ), + migrations.AlterUniqueTogether( + name='itslocationcontract', + unique_together={('location', 'contract')}, + ), + migrations.AddIndex( + model_name='publiclightingpole', + index=models.Index(fields=['code'], name='assets_publ_code_e8da75_idx'), + ), + migrations.AddIndex( + model_name='publiclightingelectricalcabinet', + index=models.Index(fields=['code'], name='assets_publ_code_861920_idx'), + ), + migrations.AddIndex( + model_name='signpole', + index=models.Index(fields=['code'], name='assets_sign_code_530237_idx'), + ), + migrations.AlterUniqueTogether( + name='structurecontract', + unique_together={('structure', 'contract')}, + ), + migrations.AddIndex( + model_name='structuregeoasset', + index=models.Index(fields=['code'], name='assets_stru_code_abf387_idx'), + ), + migrations.AlterUniqueTogether( + name='trafficlightcontract', + unique_together={('intersection', 'contract')}, + ), + migrations.AddIndex( + model_name='signpanel', + index=models.Index(fields=['code'], name='assets_sign_code_be10b8_idx'), + ), + migrations.AddIndex( + model_name='publiclightinglightfixture', + index=models.Index(fields=['code'], name='assets_publ_code_49bdd9_idx'), + ), + migrations.AddIndex( + model_name='itsgeoasset', + index=models.Index(fields=['code'], name='assets_itsg_code_b86941_idx'), + ), + ] diff --git a/streetup/assets/migrations/0005_alter_artworkfountain_status_and_more.py b/streetup/assets/migrations/0005_alter_artworkfountain_status_and_more.py new file mode 100644 index 0000000..363e460 --- /dev/null +++ b/streetup/assets/migrations/0005_alter_artworkfountain_status_and_more.py @@ -0,0 +1,188 @@ +# Generated by Django 5.2.8 on 2026-05-23 05:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0004_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='artworkfountain', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='artworkmonument', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='cleanglasscontainer', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='cleanlitterbin', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='controlcentergeoasset', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='itsgeoasset', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='naturebench', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='naturegreensurface', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='naturehollowgreenspace', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='natureshrub', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='naturetree', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='publiclightingelectricalcabinet', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='publiclightinglightfixture', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='publiclightingpole', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='roadconcreteblockset', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='roaddrain', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='roadfurniture', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='roadsidewalk', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='roadway', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='signpanel', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='signparkingspot', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='signpole', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='structuregeoasset', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightaccessory', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightcable', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightcontroller', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightcontrollerhardware', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightdetector', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightelectricalcabinet', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightelectricalcabinetcontent', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightlantern', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightnetworkhardware', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightpole', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightprogramming', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + migrations.AlterField( + model_name='trafficlightvoltagehardware', + name='status', + field=models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50), + ), + ] diff --git a/streetup/assets/migrations/0006_naturegiepasset_naturegiepassetdocument.py b/streetup/assets/migrations/0006_naturegiepasset_naturegiepassetdocument.py new file mode 100644 index 0000000..b12456f --- /dev/null +++ b/streetup/assets/migrations/0006_naturegiepasset_naturegiepassetdocument.py @@ -0,0 +1,92 @@ +# Generated by Django 5.2.8 on 2026-05-27 09:44 + +import common.storages +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0005_alter_artworkfountain_status_and_more'), + ('documents', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='NatureGIEPAsset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=3812)), + ('external_id', models.IntegerField(blank=True, null=True, verbose_name='External ID')), + ('code_name', models.CharField(blank=True, choices=[('AV', 'Stormwater inlet (Avaloir)'), ('BS', 'Sequenced border (Bordure séquencée)'), ('BI', "Injection port (Bouche d'injection)"), ('CA', 'Non-return valve (Clapet anti-retour)'), ('ES', 'Sedimentation (Empierrement sédimentation)'), ('KC', 'Kerb cell'), ('PI', "Infiltration well (Puits d'infiltration)"), ('SU', 'Overflow (Surverse)'), ('VI', "Isolation valve (Vanne d'isolation)"), ('VR', 'Control valve (Vanne de régulation)'), ('GT', 'Sidewalk gargoyle (Gargouille de trottoir)'), ('FE', "Trickle of water (Filet d'eau)"), ('AR', 'Rail drain (Avaloir de rail)'), ('BA', 'Leveled edge (Bordure arasée)'), ('CI', 'Infiltration channel (Caniveau infiltrant)'), ('D', 'Drain'), ('TP', 'Overflowing (Trop plein)'), ('RES', 'Separate sewer system (Réseau égout séparatif)'), ('EVC', 'Hollow green space (Espace vert creux)'), ('OED', 'Decentralized underground structure (Ouvrage enterré décentralisé)'), ('OSI', 'Work on an impermeable surface (Ouvrage sur surface imperméable)'), ('TA', 'Alternative technique (Technique alternative)'), ('OA', 'Supplementary work (Ouvrage annexe)')], max_length=50, null=True, verbose_name='Code name')), + ('structure_type', models.CharField(blank=True, choices=[('avaloir', 'Stormwater inlet (Avaloir)'), ('bordure sequencee', 'Sequenced border (Bordure séquencée)'), ('bouche injection', "Injection port (Bouche d'injection)"), ('clapet anti-retour', 'Non-return valve (Clapet anti-retour)'), ('empierrement sedimentation', 'Sedimentation (Empierrement sédimentation)'), ('kerb cell', 'Kerb cell'), ('puits infiltration', "Infiltration well (Puits d'infiltration)"), ('surverse', 'Overflow (Surverse)'), ('vanne isolation', "Isolation valve (Vanne d'isolation)"), ('vanne regulation', 'Control valve (Vanne de régulation)'), ('gargouille de trottoir', 'Sidewalk gargoyle (Gargouille de trottoir)'), ('filet eau', "Trickle of water (Filet d'eau)"), ('avaloir de rail', 'Rail drain (Avaloir de rail)'), ('bordure arasee', 'Leveled edge (Bordure arasée)'), ('caniveau infiltrant', 'Infiltration channel (Caniveau infiltrant)'), ('drain', 'Drain'), ('trop plein', 'Overflowing (Trop plein)'), ('reseau egout separatif', 'Separate sewer system (Réseau égout séparatif)'), ('bassin sec/infiltration', 'Dry/infiltration basin (Bassin sec/infiltration)'), ('bassin en eau', 'Wet basin (Bassin en eau)'), ('fosse', 'Ditch (Fossé)'), ('jardin de pluie', 'Rain garden (Jardin de pluie)'), ('noue', 'Swale (Noue)'), ('massif drainant', 'Draining structure (Massif drainant)'), ('tranchee drainante', 'Draining trench (Tranchée drainante)'), ('toiture stockante', 'Retention roof (Toiture stockante)'), ('toiture verte', 'Green roof (Toiture verte)'), ('bassin orage', "Stormwater basin (Bassin d'orage)"), ('SAUL-vide', 'SAUL-empty (SAUL-vide)'), ('SAUL-terre', 'SAUL-earth (SAUL-terre)'), ('citerne', 'Cistern (Citerne)'), ('espace vert sur dalle', 'Green space on slab (Espace vert sur dalle)'), ('bande filtrante', 'Filter strip (Bande filtrante)'), ('collecteur en beton poreux', 'Porous concrete collector (Collecteur en béton poreux)')], max_length=100, null=True, verbose_name='Structure type')), + ('project_manager', models.CharField(blank=True, choices=[('DPV', 'DPV'), ('DITP', 'DITP'), ('DEN', 'DEN'), ('DPO', 'DPO'), ('Beliris', 'Beliris'), ('Commune', 'Commune')], max_length=50, null=True, verbose_name='Project manager')), + ('location_precision', models.CharField(blank=True, choices=[('precise', 'Precise (Précise)'), ('supposee', 'Assumed (Supposée)')], max_length=50, null=True, verbose_name='Location precision')), + ('construction_status', models.CharField(blank=True, choices=[('etude', 'Design (Étude)'), ('travaux', 'Construction (Travaux)'), ('reception provisoire', 'Provisional acceptance (Réception provisoire)'), ('reception definitive', 'Final acceptance (Réception définitive)')], max_length=100, null=True, verbose_name='Construction status')), + ('commissioning_date', models.DateField(blank=True, null=True, verbose_name='Commissioning date')), + ('condition', models.CharField(blank=True, choices=[('bon', 'Good (Bon)'), ('examen a programmer', 'Inspection to schedule (Examen à programmer)'), ('intervention curative necessaire', 'Curative action needed (Intervention curative nécessaire)'), ('refection totale necessaire', 'Total rehabilitation needed (Réfection totale nécessaire)'), ('travaux entretien a realiser', 'Maintenance works to perform (Travaux entretien à réaliser)'), ('travaux maintenance', 'Maintenance works (Travaux maintenance)'), ('travaux maintenance a programmer', 'Maintenance works to schedule (Travaux maintenance à programmer)')], max_length=100, null=True, verbose_name='Condition')), + ('administrator', models.CharField(blank=True, max_length=500, null=True, verbose_name='Administrator')), + ('monitoring', models.CharField(blank=True, max_length=500, null=True, verbose_name='Monitoring')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), + ('has_filter', models.BooleanField(blank=True, null=True, verbose_name='Has filter')), + ('sewer_connection', models.CharField(blank=True, choices=[('connecte', 'Connected (Connecté)'), ('non connecte', 'Not connected (Non connecté)')], max_length=50, null=True, verbose_name='Sewer connection')), + ('separate_network_connection', models.CharField(blank=True, choices=[('connecte', 'Connected (Connecté)'), ('non connecte', 'Not connected (Non connecté)')], max_length=50, null=True, verbose_name='Separate network connection')), + ('hydro_network_connection', models.CharField(blank=True, choices=[('connecte', 'Connected (Connecté)'), ('non connecte', 'Not connected (Non connecté)')], max_length=50, null=True, verbose_name='Hydro network connection')), + ('surface_area', models.FloatField(blank=True, null=True, verbose_name='Surface area (m²)')), + ('length', models.FloatField(blank=True, null=True, verbose_name='Length (m)')), + ('coating_category', models.CharField(blank=True, choices=[('Vegetal', 'Vegetal (Végétal)'), ('Structure enherbee', 'Grassed structure (Structure enherbée)'), ('Materiau non agrege', 'Unbound material (Matériau non agrégé)'), ('Structure a remplissage mineral', 'Mineral-filled structure (Structure à remplissage minéral)'), ('Revetement sureleve', 'Raised coating (Revêtement surélevé)'), ('Modulaire poreux', 'Porous modular (Modulaire poreux)'), ('Continu poreux', 'Porous continuous (Continu poreux)'), ('Impermeable', 'Impermeable (Imperméable)')], max_length=100, null=True, verbose_name='Coating category')), + ('coating_type', models.CharField(blank=True, choices=[('Pleine terre', 'Open soil (Pleine terre)'), ('Melange terre-pierre', 'Soil-stone mixture (Mélange terre-pierre)'), ('Paves a joints larges enherbes', 'Grassed wide-joint pavers (Pavés à joints larges enherbés)'), ('Dalles gazon en beton', 'Concrete grass grids (Dalles gazon en béton)'), ('Dalles alveolaires vegetalisees', 'Vegetated cellular tiles (Dalles alvéolaires végétalisées)'), ('Mulch vegetal', 'Woodchips/mulch (Mulch végétal)'), ('Graviers calcaires', 'Limestone gravel (Graviers calcaires)'), ('Graviers non calcaires', 'Non-limestone gravel (Graviers non calcaires)'), ('Terre battue', 'Dirt ground (Terre battue)'), ('Paves a joints larges engravillonnes', 'Gravel wide-joint pavers (Pavés à joints larges engravillonnés)'), ('Dalles alveolaires en gravier', 'Gravel cellular tiles (Dalles alvéolaires en gravier)'), ('Dalles en PEHD remplies de pave', 'HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)'), ('Grilles', 'Grates (Grilles)'), ('Platelages bois', 'Wooden decking (Platelages bois)'), ('Pave en beton poreux', 'Porous concrete pavers (Pavé en béton poreux)'), ('Dalle en beton poreux', 'Porous concrete slab (Dalle en béton poreux)'), ('Enrobes bitumineux poreux', 'Porous asphalt (Enrobés bitumineux poreux)'), ('Betons poreux', 'Porous concrete (Bétons poreux)'), ('Graviers lies a la resine', 'Resin-bound gravel (Graviers liés à la résine)'), ('Graviers concasses lies aux polymeres', 'Polymer-bound crushed gravel (Graviers concassés liés aux polymères)'), ('Revetements EPDM et ESBR', 'EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)'), ('Revetements en beton de ciment non permeables', 'Non-porous cement concrete coatings (Revêtements en béton de ciment non perméables)'), ('Revetements bitumineux non permeables', 'Non-porous asphalt coatings (Revêtements bitumineux non perméables)'), ('Dallages non permeables', 'Non-porous tiling (Dallages non perméables)'), ('Pavages non permeables', 'Non-porous paving (Pavages non perméables)'), ('Autres revetements non permeables', 'Other non-porous coatings (Autres revêtements non perméables)')], max_length=100, null=True, verbose_name='Coating type')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')), + ('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.naturegiepasset', verbose_name='Remplacé par')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')), + ], + options={ + 'verbose_name': 'GIEP Asset', + 'verbose_name_plural': 'GIEP Assets', + }, + ), + migrations.CreateModel( + name='NatureGIEPAssetDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/nature/giep/', verbose_name='File')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/nature/giep/')), + ('giep_asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.naturegiepasset', verbose_name='GIEP Asset')), + ('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')), + ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')), + ], + options={ + 'verbose_name': 'GIEP Asset Document', + 'verbose_name_plural': 'GIEP Asset Documents', + }, + ), + ] diff --git a/streetup/assets/migrations/0007_remove_naturegiepassetdocument_giep_asset_and_more.py b/streetup/assets/migrations/0007_remove_naturegiepassetdocument_giep_asset_and_more.py new file mode 100644 index 0000000..b59f290 --- /dev/null +++ b/streetup/assets/migrations/0007_remove_naturegiepassetdocument_giep_asset_and_more.py @@ -0,0 +1,111 @@ +# Generated by Django 5.2.8 on 2026-05-27 09:50 + +import common.storages +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0006_naturegiepasset_naturegiepassetdocument'), + ('documents', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='naturegiepassetdocument', + name='giep_asset', + ), + migrations.RemoveField( + model_name='naturegiepassetdocument', + name='managed_document', + ), + migrations.RemoveField( + model_name='naturegiepassetdocument', + name='uploaded_by', + ), + migrations.CreateModel( + name='NatureRWIAsset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=3812)), + ('external_id', models.IntegerField(blank=True, null=True, verbose_name='External ID')), + ('code_name', models.CharField(blank=True, choices=[('AV', 'Stormwater inlet (Avaloir)'), ('BS', 'Sequenced border (Bordure séquencée)'), ('BI', "Injection port (Bouche d'injection)"), ('CA', 'Non-return valve (Clapet anti-retour)'), ('ES', 'Sedimentation (Empierrement sédimentation)'), ('KC', 'Kerb cell'), ('PI', "Infiltration well (Puits d'infiltration)"), ('SU', 'Overflow (Surverse)'), ('VI', "Isolation valve (Vanne d'isolation)"), ('VR', 'Control valve (Vanne de régulation)'), ('GT', 'Sidewalk gargoyle (Gargouille de trottoir)'), ('FE', "Trickle of water (Filet d'eau)"), ('AR', 'Rail drain (Avaloir de rail)'), ('BA', 'Leveled edge (Bordure arasée)'), ('CI', 'Infiltration channel (Caniveau infiltrant)'), ('D', 'Drain'), ('TP', 'Overflowing (Trop plein)'), ('RES', 'Separate sewer system (Réseau égout séparatif)'), ('EVC', 'Hollow green space (Espace vert creux)'), ('OED', 'Decentralized underground structure (Ouvrage enterré décentralisé)'), ('OSI', 'Work on an impermeable surface (Ouvrage sur surface imperméable)'), ('TA', 'Alternative technique (Technique alternative)'), ('OA', 'Supplementary work (Ouvrage annexe)')], max_length=50, null=True, verbose_name='Code name')), + ('structure_type', models.CharField(blank=True, choices=[('avaloir', 'Stormwater inlet (Avaloir)'), ('bordure sequencee', 'Sequenced border (Bordure séquencée)'), ('bouche injection', "Injection port (Bouche d'injection)"), ('clapet anti-retour', 'Non-return valve (Clapet anti-retour)'), ('empierrement sedimentation', 'Sedimentation (Empierrement sédimentation)'), ('kerb cell', 'Kerb cell'), ('puits infiltration', "Infiltration well (Puits d'infiltration)"), ('surverse', 'Overflow (Surverse)'), ('vanne isolation', "Isolation valve (Vanne d'isolation)"), ('vanne regulation', 'Control valve (Vanne de régulation)'), ('gargouille de trottoir', 'Sidewalk gargoyle (Gargouille de trottoir)'), ('filet eau', "Trickle of water (Filet d'eau)"), ('avaloir de rail', 'Rail drain (Avaloir de rail)'), ('bordure arasee', 'Leveled edge (Bordure arasée)'), ('caniveau infiltrant', 'Infiltration channel (Caniveau infiltrant)'), ('drain', 'Drain'), ('trop plein', 'Overflowing (Trop plein)'), ('reseau egout separatif', 'Separate sewer system (Réseau égout séparatif)'), ('bassin sec/infiltration', 'Dry/infiltration basin (Bassin sec/infiltration)'), ('bassin en eau', 'Wet basin (Bassin en eau)'), ('fosse', 'Ditch (Fossé)'), ('jardin de pluie', 'Rain garden (Jardin de pluie)'), ('noue', 'Swale (Noue)'), ('massif drainant', 'Draining structure (Massif drainant)'), ('tranchee drainante', 'Draining trench (Tranchée drainante)'), ('toiture stockante', 'Retention roof (Toiture stockante)'), ('toiture verte', 'Green roof (Toiture verte)'), ('bassin orage', "Stormwater basin (Bassin d'orage)"), ('SAUL-vide', 'SAUL-empty (SAUL-vide)'), ('SAUL-terre', 'SAUL-earth (SAUL-terre)'), ('citerne', 'Cistern (Citerne)'), ('espace vert sur dalle', 'Green space on slab (Espace vert sur dalle)'), ('bande filtrante', 'Filter strip (Bande filtrante)'), ('collecteur en beton poreux', 'Porous concrete collector (Collecteur en béton poreux)')], max_length=100, null=True, verbose_name='Structure type')), + ('geometry_type', models.CharField(blank=True, choices=[('point', 'Point'), ('line', 'Line'), ('polygon', 'Polygon')], max_length=50, null=True, verbose_name='Geometry type')), + ('project_manager', models.CharField(blank=True, choices=[('DPV', 'DPV'), ('DITP', 'DITP'), ('DEN', 'DEN'), ('DPO', 'DPO'), ('Beliris', 'Beliris'), ('Commune', 'Commune')], max_length=50, null=True, verbose_name='Project manager')), + ('location_precision', models.CharField(blank=True, choices=[('precise', 'Precise (Précise)'), ('supposee', 'Assumed (Supposée)')], max_length=50, null=True, verbose_name='Location precision')), + ('construction_status', models.CharField(blank=True, choices=[('etude', 'Design (Étude)'), ('travaux', 'Construction (Travaux)'), ('reception provisoire', 'Provisional acceptance (Réception provisoire)'), ('reception definitive', 'Final acceptance (Réception définitive)')], max_length=100, null=True, verbose_name='Construction status')), + ('commissioning_date', models.DateField(blank=True, null=True, verbose_name='Commissioning date')), + ('condition', models.CharField(blank=True, choices=[('bon', 'Good (Bon)'), ('examen a programmer', 'Inspection to schedule (Examen à programmer)'), ('intervention curative necessaire', 'Curative action needed (Intervention curative nécessaire)'), ('refection totale necessaire', 'Total rehabilitation needed (Réfection totale nécessaire)'), ('travaux entretien a realiser', 'Maintenance works to perform (Travaux entretien à réaliser)'), ('travaux maintenance', 'Maintenance works (Travaux maintenance)'), ('travaux maintenance a programmer', 'Maintenance works to schedule (Travaux maintenance à programmer)')], max_length=100, null=True, verbose_name='Condition')), + ('administrator', models.CharField(blank=True, max_length=500, null=True, verbose_name='Administrator')), + ('monitoring', models.CharField(blank=True, max_length=500, null=True, verbose_name='Monitoring')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), + ('has_filter', models.BooleanField(blank=True, null=True, verbose_name='Has filter')), + ('sewer_connection', models.CharField(blank=True, choices=[('connecte', 'Connected (Connecté)'), ('non connecte', 'Not connected (Non connecté)')], max_length=50, null=True, verbose_name='Sewer connection')), + ('separate_network_connection', models.CharField(blank=True, choices=[('connecte', 'Connected (Connecté)'), ('non connecte', 'Not connected (Non connecté)')], max_length=50, null=True, verbose_name='Separate network connection')), + ('hydro_network_connection', models.CharField(blank=True, choices=[('connecte', 'Connected (Connecté)'), ('non connecte', 'Not connected (Non connecté)')], max_length=50, null=True, verbose_name='Hydro network connection')), + ('surface_area', models.FloatField(blank=True, null=True, verbose_name='Surface area (m²)')), + ('length', models.FloatField(blank=True, null=True, verbose_name='Length (m)')), + ('coating_category', models.CharField(blank=True, choices=[('Vegetal', 'Vegetal (Végétal)'), ('Structure enherbee', 'Grassed structure (Structure enherbée)'), ('Materiau non agrege', 'Unbound material (Matériau non agrégé)'), ('Structure a remplissage mineral', 'Mineral-filled structure (Structure à remplissage minéral)'), ('Revetement sureleve', 'Raised coating (Revêtement surélevé)'), ('Modulaire poreux', 'Porous modular (Modulaire poreux)'), ('Continu poreux', 'Porous continuous (Continu poreux)'), ('Impermeable', 'Impermeable (Imperméable)')], max_length=100, null=True, verbose_name='Coating category')), + ('coating_type', models.CharField(blank=True, choices=[('Pleine terre', 'Open soil (Pleine terre)'), ('Melange terre-pierre', 'Soil-stone mixture (Mélange terre-pierre)'), ('Paves a joints larges enherbes', 'Grassed wide-joint pavers (Pavés à joints larges enherbés)'), ('Dalles gazon en beton', 'Concrete grass grids (Dalles gazon en béton)'), ('Dalles alveolaires vegetalisees', 'Vegetated cellular tiles (Dalles alvéolaires végétalisées)'), ('Mulch vegetal', 'Woodchips/mulch (Mulch végétal)'), ('Graviers calcaires', 'Limestone gravel (Graviers calcaires)'), ('Graviers non calcaires', 'Non-limestone gravel (Graviers non calcaires)'), ('Terre battue', 'Dirt ground (Terre battue)'), ('Paves a joints larges engravillonnes', 'Gravel wide-joint pavers (Pavés à joints larges engravillonnés)'), ('Dalles alveolaires en gravier', 'Gravel cellular tiles (Dalles alvéolaires en gravier)'), ('Dalles en PEHD remplies de pave', 'HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)'), ('Grilles', 'Grates (Grilles)'), ('Platelages bois', 'Wooden decking (Platelages bois)'), ('Pave en beton poreux', 'Porous concrete pavers (Pavé en béton poreux)'), ('Dalle en beton poreux', 'Porous concrete slab (Dalle en béton poreux)'), ('Enrobes bitumineux poreux', 'Porous asphalt (Enrobés bitumineux poreux)'), ('Betons poreux', 'Porous concrete (Bétons poreux)'), ('Graviers lies a la resine', 'Resin-bound gravel (Graviers liés à la résine)'), ('Graviers concasses lies aux polymeres', 'Polymer-bound crushed gravel (Graviers concassés liés aux polymères)'), ('Revetements EPDM et ESBR', 'EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)'), ('Revetements en beton de ciment non permeables', 'Non-porous cement concrete coatings (Revêtements en béton de ciment non perméables)'), ('Revetements bitumineux non permeables', 'Non-porous asphalt coatings (Revêtements bitumineux non perméables)'), ('Dallages non permeables', 'Non-porous tiling (Dallages non perméables)'), ('Pavages non permeables', 'Non-porous paving (Pavages non perméables)'), ('Autres revetements non permeables', 'Other non-porous coatings (Autres revêtements non perméables)')], max_length=100, null=True, verbose_name='Coating type')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')), + ('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.naturerwiasset', verbose_name='Remplacé par')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')), + ], + options={ + 'verbose_name': 'RWI Asset', + 'verbose_name_plural': 'RWI Assets', + }, + ), + migrations.CreateModel( + name='NatureRWIAssetDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/nature/rwi/', verbose_name='File')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='assets_documents/nature/rwi/')), + ('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')), + ('rwi_asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.naturerwiasset', verbose_name='RWI Asset')), + ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')), + ], + options={ + 'verbose_name': 'RWI Asset Document', + 'verbose_name_plural': 'RWI Asset Documents', + }, + ), + migrations.DeleteModel( + name='NatureGIEPAsset', + ), + migrations.DeleteModel( + name='NatureGIEPAssetDocument', + ), + ] diff --git a/streetup/assets/migrations/0008_delete_naturehollowgreenspace.py b/streetup/assets/migrations/0008_delete_naturehollowgreenspace.py new file mode 100644 index 0000000..49c35d4 --- /dev/null +++ b/streetup/assets/migrations/0008_delete_naturehollowgreenspace.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.8 on 2026-05-27 12:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0007_remove_naturegiepassetdocument_giep_asset_and_more'), + ] + + operations = [ + migrations.DeleteModel( + name='NatureHollowGreenSpace', + ), + ] diff --git a/streetup/assets/migrations/0009_signpaneltype_default_diameter_and_more.py b/streetup/assets/migrations/0009_signpaneltype_default_diameter_and_more.py new file mode 100644 index 0000000..6435f8c --- /dev/null +++ b/streetup/assets/migrations/0009_signpaneltype_default_diameter_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.8 on 2026-05-30 13:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0008_delete_naturehollowgreenspace'), + ] + + operations = [ + migrations.AddField( + model_name='signpaneltype', + name='default_diameter', + field=models.FloatField(blank=True, null=True, verbose_name='Diamètre par défaut (cm)'), + ), + migrations.AddField( + model_name='signpaneltype', + name='default_height', + field=models.FloatField(blank=True, null=True, verbose_name='Hauteur par défaut (cm)'), + ), + migrations.AddField( + model_name='signpaneltype', + name='default_width', + field=models.FloatField(blank=True, null=True, verbose_name='Largeur par défaut (cm)'), + ), + ] diff --git a/streetup/assets/migrations/0010_trafficlightradarmodel_trafficlightradar.py b/streetup/assets/migrations/0010_trafficlightradarmodel_trafficlightradar.py new file mode 100644 index 0000000..236e146 --- /dev/null +++ b/streetup/assets/migrations/0010_trafficlightradarmodel_trafficlightradar.py @@ -0,0 +1,71 @@ +# Generated by Django 5.2.8 on 2026-06-22 08:31 + +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0009_signpaneltype_default_diameter_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='TrafficLightRadarModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ], + options={ + 'verbose_name': 'Traffic Light Radar Model', + 'verbose_name_plural': 'Traffic Light Radar Models', + }, + ), + migrations.CreateModel( + name='TrafficLightRadar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ('intersection', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='radars', to='assets.trafficlightintersection')), + ('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')), + ('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightradar', verbose_name='Remplacé par')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')), + ('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightradarmodel')), + ], + options={ + 'verbose_name': 'Radar', + 'verbose_name_plural': 'Radars', + }, + ), + ] diff --git a/streetup/assets/migrations/0011_create_radar_preventif_category.py b/streetup/assets/migrations/0011_create_radar_preventif_category.py new file mode 100644 index 0000000..cd776a5 --- /dev/null +++ b/streetup/assets/migrations/0011_create_radar_preventif_category.py @@ -0,0 +1,44 @@ +from django.db import migrations + +def create_radar_preventif_category(apps, schema_editor): + Thematic = apps.get_model('common', 'Thematic') + AssetCategory = apps.get_model('assets', 'AssetCategory') + + # Get trafficlights thematic + try: + tl_thematic = Thematic.objects.get(code='trafficlights') + except Thematic.DoesNotExist: + tl_thematic = Thematic.objects.create( + code='trafficlights', + name_fr='Feux de signalisation', + name_nl='Verkeerslichten' + ) + + # Get or create radar_preventif category + category, created = AssetCategory.objects.get_or_create( + code='TL_RADAR_PREVENTIF', + defaults={ + 'name_fr': 'Radar Préventif', + 'name_nl': 'Preventieve radar', + 'thematic': tl_thematic + } + ) + if not created and category.thematic != tl_thematic: + category.thematic = tl_thematic + category.save() + +def reverse_radar_preventif_category(apps, schema_editor): + AssetCategory = apps.get_model('assets', 'AssetCategory') + AssetCategory.objects.filter(code='TL_RADAR_PREVENTIF').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0010_trafficlightradarmodel_trafficlightradar'), + ('common', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_radar_preventif_category, reverse_radar_preventif_category), + ] diff --git a/streetup/assets/migrations/0012_trafficlightintersection_is_radar.py b/streetup/assets/migrations/0012_trafficlightintersection_is_radar.py new file mode 100644 index 0000000..269e1f7 --- /dev/null +++ b/streetup/assets/migrations/0012_trafficlightintersection_is_radar.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.8 on 2026-06-22 12:43 + +from django.db import migrations, models + + +def populate_is_radar(apps, schema_editor): + TrafficLightIntersection = apps.get_model('assets', 'TrafficLightIntersection') + TrafficLightRadar = apps.get_model('assets', 'TrafficLightRadar') + + # Intersections linked to at least one radar are radar intersections + radar_intersection_ids = TrafficLightRadar.objects.filter(intersection__isnull=False).values_list('intersection_id', flat=True).distinct() + TrafficLightIntersection.objects.filter(id__in=radar_intersection_ids).update(is_radar=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0011_create_radar_preventif_category'), + ] + + operations = [ + migrations.AddField( + model_name='trafficlightintersection', + name='is_radar', + field=models.BooleanField(default=False, verbose_name='Est un radar ?'), + ), + migrations.RunPython(populate_is_radar, migrations.RunPython.noop), + ] diff --git a/streetup/assets/migrations/0013_update_rwi_assets_codification.py b/streetup/assets/migrations/0013_update_rwi_assets_codification.py new file mode 100644 index 0000000..55ac786 --- /dev/null +++ b/streetup/assets/migrations/0013_update_rwi_assets_codification.py @@ -0,0 +1,169 @@ +from django.db import migrations + +def update_rwi_codes(apps, schema_editor): + NatureRWIAsset = apps.get_model('assets', 'NatureRWIAsset') + + # We try-catch the GuidedOperationData import because it's in another app, + # and we want to ensure robust execution. + try: + GuidedOperationData = apps.get_model('interventions', 'GuidedOperationData') + except LookupError: + GuidedOperationData = None + + # Mappings from structure type to acronym + mappings = { + 'avaloir': 'AV', + 'bordure sequencee': 'BS', + 'bouche injection': 'BI', + 'clapet anti-retour': 'CA', + 'empierrement sedimentation': 'ES', + 'kerb cell': 'KC', + 'puits infiltration': 'PI', + 'surverse': 'SU', + 'vanne isolation': 'VI', + 'vanne regulation': 'VR', + 'gargouille de trottoir': 'GT', + 'filet eau': 'FE', + 'avaloir de rail': 'AR', + 'bordure arasee': 'BA', + 'caniveau infiltrant': 'CI', + 'drain': 'D', + 'trop plein': 'TP', + 'reseau egout separatif': 'RES', + 'bassin sec/infiltration': 'OED', + 'bassin en eau': 'OED', + 'fosse': 'EVC', + 'jardin de pluie': 'EVC', + 'noue': 'EVC', + 'massif drainant': 'OED', + 'tranchee drainante': 'OED', + 'toiture stockante': 'OSI', + 'toiture verte': 'OSI', + 'bassin orage': 'OED', + 'SAUL-vide': 'OED', + 'SAUL-terre': 'TA', + 'citerne': 'OED', + 'espace vert sur dalle': 'EVC', + 'bande filtrante': 'EVC', + 'collecteur en beton poreux': 'OED', + 'chambre de visite': 'CV', + } + + code_updates = {} + + for asset in NatureRWIAsset.objects.all(): + old_code = asset.code + + # Calculate acronym + struct_type_lower = asset.structure_type.strip().lower() if asset.structure_type else None + + acronym = None + if asset.code_name: + acronym = asset.code_name.strip().upper() + elif struct_type_lower in mappings: + acronym = mappings[struct_type_lower] + elif asset.geometry_type: + acronym = asset.geometry_type.upper() + else: + acronym = 'UNK' + + fid = asset.external_id or asset.id + new_code = f"RWI_{acronym}_{fid:03d}" + + if old_code != new_code: + asset.code = new_code + if asset.name_fr == old_code: + asset.name_fr = new_code + if asset.name_nl == old_code: + asset.name_nl = new_code + asset.save(update_fields=['code', 'name_fr', 'name_nl']) + code_updates[old_code] = new_code + + # Update GuidedOperationData JSON references + if GuidedOperationData and code_updates: + for gd in GuidedOperationData.objects.all(): + modified = False + data = gd.data + if not isinstance(data, dict): + continue + + def replace_codes(obj): + nonlocal modified + if isinstance(obj, dict): + for k, v in list(obj.items()): + if k == 'rwi_asset_code' and v in code_updates: + obj[k] = code_updates[v] + modified = True + else: + replace_codes(v) + elif isinstance(obj, list): + for item in obj: + replace_codes(item) + + replace_codes(data) + if modified: + gd.data = data + gd.save(update_fields=['data']) + +def reverse_rwi_codes(apps, schema_editor): + NatureRWIAsset = apps.get_model('assets', 'NatureRWIAsset') + try: + GuidedOperationData = apps.get_model('interventions', 'GuidedOperationData') + except LookupError: + GuidedOperationData = None + + code_updates_reverse = {} + + for asset in NatureRWIAsset.objects.all(): + old_code = asset.code + + # Calculate old format: RWI_{geometry_type.upper()}_{external_id:03d} + geom_type_upper = asset.geometry_type.upper() if asset.geometry_type else 'POLYGON' + fid = asset.external_id or asset.id + original_code = f"RWI_{geom_type_upper}_{fid:03d}" + + if old_code != original_code: + asset.code = original_code + if asset.name_fr == old_code: + asset.name_fr = original_code + if asset.name_nl == old_code: + asset.name_nl = original_code + asset.save(update_fields=['code', 'name_fr', 'name_nl']) + code_updates_reverse[old_code] = original_code + + # Revert GuidedOperationData JSON references + if GuidedOperationData and code_updates_reverse: + for gd in GuidedOperationData.objects.all(): + modified = False + data = gd.data + if not isinstance(data, dict): + continue + + def replace_codes(obj): + nonlocal modified + if isinstance(obj, dict): + for k, v in list(obj.items()): + if k == 'rwi_asset_code' and v in code_updates_reverse: + obj[k] = code_updates_reverse[v] + modified = True + else: + replace_codes(v) + elif isinstance(obj, list): + for item in obj: + replace_codes(item) + + replace_codes(data) + if modified: + gd.data = data + gd.save(update_fields=['data']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0012_trafficlightintersection_is_radar'), + ] + + operations = [ + migrations.RunPython(update_rwi_codes, reverse_rwi_codes), + ] diff --git a/streetup/assets/migrations/0014_remove_nature_rwi_asset_location_precision.py b/streetup/assets/migrations/0014_remove_nature_rwi_asset_location_precision.py new file mode 100644 index 0000000..f4eed05 --- /dev/null +++ b/streetup/assets/migrations/0014_remove_nature_rwi_asset_location_precision.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.8 on 2026-06-25 12:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0013_update_rwi_assets_codification'), + ] + + operations = [ + migrations.RemoveField( + model_name='naturerwiasset', + name='location_precision', + ), + ] diff --git a/streetup/assets/migrations/0015_trafficlightradar_pole.py b/streetup/assets/migrations/0015_trafficlightradar_pole.py new file mode 100644 index 0000000..00b85ad --- /dev/null +++ b/streetup/assets/migrations/0015_trafficlightradar_pole.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.8 on 2026-06-26 12:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0014_remove_nature_rwi_asset_location_precision'), + ] + + operations = [ + migrations.AddField( + model_name='trafficlightradar', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.publiclightingpole'), + ), + ] diff --git a/streetup/assets/migrations/0016_trafficlightradar_publiclighting_pole_and_more.py b/streetup/assets/migrations/0016_trafficlightradar_publiclighting_pole_and_more.py new file mode 100644 index 0000000..f058390 --- /dev/null +++ b/streetup/assets/migrations/0016_trafficlightradar_publiclighting_pole_and_more.py @@ -0,0 +1,91 @@ +# Generated by Django 5.2.8 on 2026-06-26 12:41 + +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0015_trafficlightradar_pole'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='trafficlightradar', + name='publiclighting_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.publiclightingpole'), + ), + migrations.AddField( + model_name='trafficlightradar', + name='trafficlights_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.trafficlightpole'), + ), + migrations.CreateModel( + name='TrafficLightRadarPole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('geojson', models.TextField(blank=True, null=True)), + ('colour', models.CharField(blank=True, max_length=200, null=True, verbose_name='Couleur')), + ('ral', models.CharField(blank=True, max_length=200, null=True, verbose_name='RAL')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ('intersection', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='radar_poles', to='assets.trafficlightintersection')), + ('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')), + ('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.trafficlightradarpole', verbose_name='Remplacé par')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')), + ], + options={ + 'verbose_name': 'Radar Pole', + 'verbose_name_plural': 'Radar Poles', + }, + ), + migrations.AlterField( + model_name='trafficlightradar', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.trafficlightradarpole'), + ), + migrations.CreateModel( + name='TrafficLightRadarPoleModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='trafficlightradarpole', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.trafficlightradarpolemodel'), + ), + ] diff --git a/streetup/assets/migrations/0017_create_radar_pole_category.py b/streetup/assets/migrations/0017_create_radar_pole_category.py new file mode 100644 index 0000000..c6328c2 --- /dev/null +++ b/streetup/assets/migrations/0017_create_radar_pole_category.py @@ -0,0 +1,44 @@ +from django.db import migrations + +def create_radar_pole_category(apps, schema_editor): + Thematic = apps.get_model('common', 'Thematic') + AssetCategory = apps.get_model('assets', 'AssetCategory') + + # Get trafficlights thematic + try: + tl_thematic = Thematic.objects.get(code='trafficlights') + except Thematic.DoesNotExist: + tl_thematic = Thematic.objects.create( + code='trafficlights', + name_fr='Feux de signalisation', + name_nl='Verkeerslichten' + ) + + # Get or create radar_pole category + category, created = AssetCategory.objects.get_or_create( + code='TL_RADAR_POLE', + defaults={ + 'name_fr': 'Poteau radar', + 'name_nl': 'Radarpaal', + 'thematic': tl_thematic + } + ) + if not created and category.thematic != tl_thematic: + category.thematic = tl_thematic + category.save() + +def reverse_radar_pole_category(apps, schema_editor): + AssetCategory = apps.get_model('assets', 'AssetCategory') + AssetCategory.objects.filter(code='TL_RADAR_POLE').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0016_trafficlightradar_publiclighting_pole_and_more'), + ('common', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_radar_pole_category, reverse_radar_pole_category), + ] diff --git a/streetup/assets/migrations/0018_alter_publiclightingpole_options_and_more.py b/streetup/assets/migrations/0018_alter_publiclightingpole_options_and_more.py new file mode 100644 index 0000000..a01e226 --- /dev/null +++ b/streetup/assets/migrations/0018_alter_publiclightingpole_options_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.8 on 2026-06-26 16:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0017_create_radar_pole_category'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.AlterModelOptions( + name='publiclightingpole', + options={'verbose_name': 'Public lighting pole', 'verbose_name_plural': 'Public lighting poles'}, + ), + migrations.AlterModelOptions( + name='trafficlightpole', + options={'verbose_name': 'Traffic light pole', 'verbose_name_plural': 'Traffic light poles'}, + ), + migrations.AddField( + model_name='assetcategory', + name='allowed_models', + field=models.ManyToManyField(blank=True, help_text="Les types de modèles d'assets (Django) autorisés pour cette catégorie.", related_name='asset_categories', to='contenttypes.contenttype', verbose_name='Modèles Django autorisés'), + ), + ] diff --git a/streetup/assets/migrations/0019_populate_assetcategory_allowed_models.py b/streetup/assets/migrations/0019_populate_assetcategory_allowed_models.py new file mode 100644 index 0000000..73014a4 --- /dev/null +++ b/streetup/assets/migrations/0019_populate_assetcategory_allowed_models.py @@ -0,0 +1,70 @@ +from django.db import migrations + +def populate_allowed_models(apps, schema_editor): + AssetCategory = apps.get_model('assets', 'AssetCategory') + ContentType = apps.get_model('contenttypes', 'ContentType') + + # Mapping of category codes to Django model name strings (lowercase) + mapping = { + # Traffic lights + 'TL_POTEAU': ['trafficlightpole'], + 'TL_CABLE': ['trafficlightcable'], + 'TL_LANTERNE': ['trafficlightlantern'], + 'TL_REGELAAR': ['trafficlightcontroller'], + 'TL_ARMOIRE': ['trafficlightelectricalcabinet'], + 'TL_DETEC_BP': ['trafficlightdetector'], + 'TL_DETEC_RADAR': ['trafficlightdetector'], + 'TL_DETEC_VIDEO': ['trafficlightdetector'], + 'TL_DETEC_OTHER': ['trafficlightdetector'], + 'TL_BORNIER': ['trafficlightaccessory'], + 'TL_DIVERS': ['trafficlightaccessory'], + 'TL_RADAR_PREVENTIF': ['trafficlightradar'], + 'TL_RADAR_POLE': ['trafficlightradarpole'], + + # Public lighting + 'LI_poles': ['publiclightingpole'], + 'LI_lightfixtures': ['publiclightinglightfixture'], + 'LI_electrical_cabinets': ['publiclightingelectricalcabinet'], + + # Cleanliness + 'CL_BASKET': ['cleanlitterbin'], + 'CL_BUBBLE': ['cleanglasscontainer'], + + # Nature + 'NAT_GREEN_AREAS': ['naturegreensurface'], + 'NAT_TREES': ['naturetree'], + 'NAT_RWI': ['naturerwiasset'], + 'NAT_FURNITURES': ['naturebench', 'natureshrub'], + + # Artworks + 'ART_MONUMENT': ['artworkmonument'], + 'ART_FONTAIN': ['artworkfountain'], + } + + for category_code, model_names in mapping.items(): + try: + category = AssetCategory.objects.get(code=category_code) + for model_name in model_names: + try: + ct = ContentType.objects.get(app_label='assets', model=model_name) + category.allowed_models.add(ct) + except ContentType.DoesNotExist: + print(f"ContentType assets.{model_name} not found.") + except AssetCategory.DoesNotExist: + pass + +def clear_allowed_models(apps, schema_editor): + AssetCategory = apps.get_model('assets', 'AssetCategory') + for category in AssetCategory.objects.all(): + category.allowed_models.clear() + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0018_alter_publiclightingpole_options_and_more'), + ] + + operations = [ + migrations.RunPython(populate_allowed_models, clear_allowed_models), + ] diff --git a/streetup/assets/migrations/0020_alter_trafficlightradarpole_model.py b/streetup/assets/migrations/0020_alter_trafficlightradarpole_model.py new file mode 100644 index 0000000..c77ae52 --- /dev/null +++ b/streetup/assets/migrations/0020_alter_trafficlightradarpole_model.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.8 on 2026-06-27 07:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0019_populate_assetcategory_allowed_models'), + ] + + operations = [ + migrations.AlterField( + model_name='trafficlightradarpole', + name='model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.trafficlightradarpolemodel'), + ), + ] diff --git a/streetup/assets/migrations/0021_set_default_validation_status_for_active.py b/streetup/assets/migrations/0021_set_default_validation_status_for_active.py new file mode 100644 index 0000000..ce2054e --- /dev/null +++ b/streetup/assets/migrations/0021_set_default_validation_status_for_active.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.8 on 2026-07-03 09:16 + +from django.db import migrations + + +def set_default_validation_status(apps, schema_editor): + model_names = [ + 'trafficlightpole', 'trafficlightcable', 'trafficlightlantern', 'trafficlightcontroller', + 'trafficlightcontrollerhardware', 'trafficlightdetector', 'trafficlightnetworkhardware', + 'trafficlightvoltagehardware', 'trafficlightelectricalcabinet', 'trafficlightelectricalcabinetcontent', + 'trafficlightaccessory', 'trafficlightprogramming', 'trafficlightradarpole', 'trafficlightradar', + 'publiclightingpole', 'publiclightinglightfixture', 'publiclightingelectricalcabinet', 'artworkmonument', + 'artworkfountain', 'naturegreensurface', 'naturetree', 'natureshrub', 'naturebench', + 'naturerwiasset', 'structuregeoasset', 'controlcentergeoasset', 'signpole', 'signpanel', + 'signparkingspot', 'roadway', 'roadsidewalk', 'roadfurniture', 'roadconcreteblockset', + 'roaddrain', 'itsgeoasset', 'cleanlitterbin', 'cleanglasscontainer' + ] + for model_name in model_names: + try: + Model = apps.get_model('assets', model_name) + # Update all active assets with no validation status to 'validated' + Model.objects.filter(status='active', validation_status__isnull=True).update(validation_status='validated') + Model.objects.filter(status='active', validation_status='').update(validation_status='validated') + except LookupError: + pass + +def reverse_default_validation_status(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0020_alter_trafficlightradarpole_model'), + ] + + operations = [ + migrations.RunPython(set_default_validation_status, reverse_default_validation_status), + ] diff --git a/streetup/assets/migrations/0022_sync_trafficlights_documents.py b/streetup/assets/migrations/0022_sync_trafficlights_documents.py new file mode 100644 index 0000000..54d4fee --- /dev/null +++ b/streetup/assets/migrations/0022_sync_trafficlights_documents.py @@ -0,0 +1,48 @@ +# Generated by hand + +from django.db import migrations + +def sync_trafficlights_documents(apps, schema_editor): + from assets.models import TrafficLightIntersection as ActiveIntersection, TrafficLightIntersectionDocument as ActiveIntersectionDoc + from assets.views.trafficlights import ensure_intersection_document_folder, generate_dxf_overlay_managed_document + from documents.models import ensure_document_for_legacy + + for intersection in ActiveIntersection.objects.all(): + try: + ensure_intersection_document_folder(intersection) + except Exception as e: + print(f"Error ensuring document folder for intersection {intersection.id}: {e}") + + for doc in ActiveIntersectionDoc.objects.all(): + try: + import os + if not doc.file or not doc.file.name: + continue + + # If the file does not exist on disk, we skip it gracefully + if not os.path.exists(doc.file.path): + print(f"Migration: File not found on disk, skipping sync for {doc.file.name}") + continue + + # Sync it to the ManagedDocument system + ensure_document_for_legacy(doc) + + # If it's a DXF, generate the overlay + if doc.file.name.lower().endswith('.dxf') and doc.managed_document: + generate_dxf_overlay_managed_document(doc.managed_document, user=doc.uploaded_by) + except Exception as e: + print(f"Error syncing document during migration for {doc.file.name if doc.file else 'None'}: {e}") + +def reverse_sync_trafficlights_documents(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0021_set_default_validation_status_for_active'), + ('documents', '0003_manageddocument_georeference_coords_and_more'), + ] + + operations = [ + migrations.RunPython(sync_trafficlights_documents, reverse_sync_trafficlights_documents), + ] diff --git a/streetup/assets/migrations/0023_trafficlightradar_power_publiclighting_pole_and_more.py b/streetup/assets/migrations/0023_trafficlightradar_power_publiclighting_pole_and_more.py new file mode 100644 index 0000000..d602a0e --- /dev/null +++ b/streetup/assets/migrations/0023_trafficlightradar_power_publiclighting_pole_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.8 on 2026-07-10 09:13 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0022_sync_trafficlights_documents'), + ] + + operations = [ + migrations.AddField( + model_name='trafficlightradar', + name='power_publiclighting_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='powered_radars', to='assets.publiclightingpole', verbose_name='Public Lighting Pole (Power)'), + ), + migrations.AddField( + model_name='trafficlightradar', + name='power_trafficlights_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='powered_radars', to='assets.trafficlightpole', verbose_name='Traffic Light Pole (Power)'), + ), + migrations.AlterField( + model_name='trafficlightradar', + name='pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.trafficlightradarpole', verbose_name='Radar Pole (Support)'), + ), + migrations.AlterField( + model_name='trafficlightradar', + name='publiclighting_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.publiclightingpole', verbose_name='Public Lighting Pole (Support)'), + ), + migrations.AlterField( + model_name='trafficlightradar', + name='trafficlights_pole', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='radars', to='assets.trafficlightpole', verbose_name='Traffic Light Pole (Support)'), + ), + ] diff --git a/streetup/assets/migrations/__init__.py b/streetup/assets/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/assets/models/__init__.py b/streetup/assets/models/__init__.py new file mode 100644 index 0000000..35e2c7e --- /dev/null +++ b/streetup/assets/models/__init__.py @@ -0,0 +1,12 @@ +from .core import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel, AssetCategory, ASSET_STATUS_CHOICES +from .history import AssetHistory +from .trafficlights import TrafficLightIntersection, TrafficLightIntersectionDocument, TrafficLightPoleModel, TrafficLightPole, TrafficLightCableModel, TrafficLightCable, TrafficLightLanternModel, TrafficLightLantern, TrafficLightControllerModel, TrafficLightController, TrafficLightDetectorModel, TrafficLightDetector, TrafficLightContract, TrafficLightElectricalCabinetModel, TrafficLightElectricalCabinet, TrafficLightNetworkHardware, TrafficLightVoltageHardware, TrafficLightControllerHardware, TrafficLightProgramming, TrafficLightRadarModel, TrafficLightRadar, TrafficLightRadarPoleModel, TrafficLightRadarPole, TrafficLightControllerLogFile, TrafficLightElectricalCabinetContentModel, TrafficLightElectricalCabinetContent, TrafficLightAccessoryModel, TrafficLightAccessory +from .publiclighting import PublicLightingStreet, PublicLightingStreetDocument, PublicLightingPoleModel, PublicLightingPole, PublicLightingLightFixtureModel, PublicLightingLightFixture, PublicLightingElectricalCabinetModel, PublicLightingElectricalCabinet +from .artworks import ArtworkMonument, ArtworkFountain, ArtworkFountainDocument, ArtworkMonumentDocument, ArtworkStreet +from .nature import NatureLocation, NatureAssetModel, NatureGreenSurface, NatureGreenSurfaceDocument, NatureTree, NatureShrub, NatureBench, GreenSurfacesComment, GreenSurfacesCommentPhoto, NatureRWIZ, NatureRWIAsset, NatureRWIAssetDocument +from .structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel, StructureDocument, StructureContract +from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument +from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignPanel, SignPanelModel, SignPanelType, ComplementaryPoliceRegulation, SignParkingSpot +from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain +from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract +from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract \ No newline at end of file diff --git a/streetup/assets/models/artworks.py b/streetup/assets/models/artworks.py new file mode 100644 index 0000000..6028de7 --- /dev/null +++ b/streetup/assets/models/artworks.py @@ -0,0 +1,138 @@ +# --------------------- +# Artworks Models +# --------------------- + +from django.contrib.gis.db import models +from django.urls import reverse +from django.contrib.contenttypes.models import ContentType + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() + + + + +# Road Street +class ArtworkStreet(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="artworks_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): + monuments = self.monuments.filter(status='active') + fountains = self.fountains.filter(status='active') + + return { + "monuments": monuments, + "fountains": fountains, + } + + def get_all_assets(self): + monuments = self.monuments.all() + fountains = self.fountains.all() + + return { + "monuments": monuments, + "fountains": fountains, + } + + def get_direct_child_assets(self): + monuments = self.monuments.all() + fountains = self.fountains.all() + + return { + "monuments": monuments, + "fountains": fountains, + } + + def get_absolute_url(self): + return reverse("assets:artworks_streets_detail", kwargs={"street_id": self.id}) + + + + +MONUMENT_TYPE_CHOICES = { + 'bench': 'Banc public', + 'construction': 'Construction', + 'metal_structure': 'Structure métalique', + 'monument': 'Monument', + 'monumental_structure': 'Structure monumentale', + 'pole': 'Poteau', + 'sculpture': 'Sculpture', + 'statue': 'Statue', + 'structure': 'Structure', +} + +# Monument +class ArtworkMonument(AbstractGeoAsset): + artist = models.CharField(max_length=200,null=True,blank=True) + artwork_type = models.CharField(choices=MONUMENT_TYPE_CHOICES, max_length=100, null=True, blank=True) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + location_fr = models.CharField(max_length=500,null=True,blank=True) + location_nl = models.CharField(max_length=500,null=True,blank=True) + street = models.ForeignKey(ArtworkStreet, related_name="monuments", on_delete=models.SET_NULL, null=True, blank=True) + water = models.BooleanField(default=False,null=True,blank=True) + description_fr = models.TextField(null=True,blank=True) + description_nl = models.TextField(null=True,blank=True) + + def get_absolute_url(self): + # content_type = ContentType.objects.get_for_model(self.__class__) + return reverse( + 'assets:artworks_monuments_detail', + kwargs={ + # 'asset_model': content_type.model, + 'asset_id': self.id + } + ) + + + +FOUNTAIN_TYPE_CHOICES = { + 'monumental_artistic_fountain': 'Fontaine artistique monumentale', + 'artistic_fountain': 'Fontaine artistique', + 'monumental_fountain': 'Fontaine monumentale', + 'artistic_play_fountain': 'Fontaine ludique artistique', + 'play_fountain': 'Fontaine artistique', + 'monumental_play_fountain': 'Fontaine ludique monumentale', +} + +# Foutain +class ArtworkFountain(AbstractGeoAsset): + artist = models.CharField(max_length=200,null=True,blank=True) + fountain_type = models.CharField(choices=FOUNTAIN_TYPE_CHOICES, max_length=100, null=True, blank=True) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + maintenance_contract = models.ForeignKey("contracts.Contract", on_delete=models.SET_NULL, null=True, blank=True) + location_fr = models.CharField(max_length=500,null=True,blank=True) + location_nl = models.CharField(max_length=500,null=True,blank=True) + street = models.ForeignKey(ArtworkStreet, related_name="fountains", on_delete=models.SET_NULL, null=True, blank=True) + description_fr = models.TextField(null=True,blank=True) + description_nl = models.TextField(null=True,blank=True) + + def get_absolute_url(self): + # content_type = ContentType.objects.get_for_model(self.__class__) + return reverse( + 'assets:artworks_fountains_detail', + kwargs={ + # 'asset_model': content_type.model, + 'asset_id': self.id + } + ) + + +class ArtworkFountainDocument(Document): + fountain = models.ForeignKey(ArtworkFountain, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Fountain") + file = models.FileField(upload_to="assets_documents/artworks/fountains/", storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to="assets_documents/artworks/fountains/", storage=private_storage, null=True, blank=True, max_length=255) + + +class ArtworkMonumentDocument(Document): + fountain = models.ForeignKey(ArtworkMonument, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Monument") + file = models.FileField(upload_to="assets_documents/artworks/fountains/", storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to="assets_documents/artworks/fountains/", storage=private_storage, null=True, blank=True, max_length=255) \ No newline at end of file diff --git a/streetup/assets/models/clean.py b/streetup/assets/models/clean.py new file mode 100644 index 0000000..55fa904 --- /dev/null +++ b/streetup/assets/models/clean.py @@ -0,0 +1,382 @@ +# --------------------- +# Clean Models +# --------------------- + +from django.contrib.gis.db import models +from django.urls import reverse +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext_lazy as _ + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage + + +FIXATION_TYPE_CHOICES = ( + ('cemented', _('Cemented to ground')), + ('bolted', _('Bolted to ground')), + ('pole_mounted', _('Pole-mounted')), + ('wall_mounted', _('Wall-mounted')), + ('freestanding', _('Freestanding')), + ('other', _('Other')), +) + +GLASS_TYPE_CHOICES = ( + ('clear', _('Clear glass')), + ('colored', _('Coloured glass')), + ('mixed', _('Mixed')), +) + +private_storage = PrivateMediaStorage() + + + +# Clean Location +class CleanLocation(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="clean_locations") + 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 assets + litter_bins = self.litter_bins.filter(status='active') + glass_containers = self.glass_containers.filter(status='active') + + return { + "litter_bins": litter_bins, + "glass_containers": glass_containers, + } + + def get_all_assets(self): + # Retrieve associated assets + litter_bins = self.litter_bins.all() + glass_containers = self.glass_containers.all() + + return { + "litter_bins": litter_bins, + "glass_containers": glass_containers, + } + + def get_direct_child_assets(self): + # Retrieve associated assets + litter_bins = self.litter_bins.all() + glass_containers = self.glass_containers.all() + + return { + "litter_bins": litter_bins, + "glass_containers": glass_containers, + } + + def get_absolute_url(self): + return reverse("assets:clean_location_detail", kwargs={"location_id": self.id}) + + +# --------------------- +# Litter Bin +# --------------------- + +class CleanLitterBinModel(AbstractAssetModel): + """Reference model / template for a litter bin.""" + color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) + ral = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("RAL code")) + material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) + volume = models.FloatField(null=True, blank=True, verbose_name=_("Volume (litres)")) + has_cover = models.BooleanField(default=False, verbose_name=_("Cover")) + has_ashtray = models.BooleanField(default=False, verbose_name=_("Built-in ashtray")) + + class Meta: + verbose_name = _("Litter bin model") + verbose_name_plural = _("Litter bin models") + ordering = ["code"] + + def __str__(self): + return f"{self.code} - {self.get_name()}" + + +class CleanLitterBin(AbstractGeoAsset): + """Street litter bin / waste basket.""" + location = models.ForeignKey( + CleanLocation, + related_name="litter_bins", + on_delete=models.PROTECT, + verbose_name=_("Location"), + ) + model = models.ForeignKey( + CleanLitterBinModel, + on_delete=models.SET_NULL, + null=True, blank=True, + verbose_name=_("Model"), + ) + fixation_type = models.CharField( + max_length=50, + choices=FIXATION_TYPE_CHOICES, + null=True, blank=True, + verbose_name=_("Fixation type"), + ) + color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) + material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) + volume = models.FloatField(null=True, blank=True, verbose_name=_("Volume (litres)")) + has_cover = models.BooleanField(default=False, verbose_name=_("Cover")) + has_ashtray = models.BooleanField(default=False, verbose_name=_("Built-in ashtray")) + has_extinguisher = models.BooleanField(default=False, verbose_name=_("Built-in extinguisher stub (éteignoir)")) + emptying_frequency = models.CharField( + max_length=200, null=True, blank=True, + verbose_name=_("Emptying frequency"), + ) + collection_route = models.CharField( + max_length=200, null=True, blank=True, + verbose_name=_("Collection route (tournée)"), + ) + external_id = models.IntegerField( + null=True, blank=True, + verbose_name=_("External ID (source system)"), + ) + remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks")) + + class Meta: + verbose_name = _("Litter bin") + verbose_name_plural = _("Litter bins") + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse("assets:clean_assets_detail", kwargs={"asset_model": "litterbin", "asset_id": self.id}) + + +# --------------------- +# Glass Container +# --------------------- + +class CleanGlassContainerModel(AbstractAssetModel): + """Reference model / template for a glass recycling container.""" + color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) + ral = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("RAL code")) + material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) + capacity = models.FloatField(null=True, blank=True, verbose_name=_("Capacity (litres)")) + is_underground = models.BooleanField(default=False, verbose_name=_("Underground")) + nb_openings = models.IntegerField(null=True, blank=True, verbose_name=_("Number of openings")) + + class Meta: + verbose_name = _("Glass container model") + verbose_name_plural = _("Glass container models") + ordering = ["code"] + + def __str__(self): + return f"{self.code} - {self.get_name()}" + + +class CleanGlassContainer(AbstractGeoAsset): + """Glass recycling container (bottle bank).""" + location = models.ForeignKey( + CleanLocation, + related_name="glass_containers", + on_delete=models.PROTECT, + verbose_name=_("Location"), + ) + model = models.ForeignKey( + CleanGlassContainerModel, + on_delete=models.SET_NULL, + null=True, blank=True, + verbose_name=_("Model"), + ) + glass_type = models.CharField( + max_length=50, + choices=GLASS_TYPE_CHOICES, + null=True, blank=True, + verbose_name=_("Accepted glass type"), + ) + capacity = models.FloatField(null=True, blank=True, verbose_name=_("Capacity (litres)")) + is_underground = models.BooleanField(default=False, verbose_name=_("Underground")) + nb_openings = models.IntegerField(null=True, blank=True, verbose_name=_("Number of openings")) + color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) + material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) + emptying_frequency = models.CharField( + max_length=200, null=True, blank=True, + verbose_name=_("Emptying frequency"), + ) + operator = models.CharField( + max_length=255, null=True, blank=True, + verbose_name=_("Operator / manager"), + ) + remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks")) + + class Meta: + verbose_name = _("Glass container") + verbose_name_plural = _("Glass containers") + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse("assets:clean_assets_detail", kwargs={"asset_model": "glasscontainer", "asset_id": self.id}) + + +class CleanLocationContract(models.Model): + location = models.ForeignKey(CleanLocation, on_delete=models.CASCADE, related_name="contracts") + contract = models.ForeignKey('contracts.Contract', on_delete=models.CASCADE, related_name="clean_location_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=[('clean', 'Nettoyage'), ('collection', 'Ramassage'), ('other', 'Autre')], default='other') + + class Meta: + verbose_name = "Clean Location Contract" + verbose_name_plural = "Clean Location Contracts" + + +# --------------------- +# Litter Bin Photo +# --------------------- + +class CleanLitterBinPhoto(models.Model): + """Photo associated with a litter bin.""" + litter_bin = models.ForeignKey( + CleanLitterBin, + on_delete=models.CASCADE, + related_name="photos", + verbose_name=_("Litter bin"), + ) + photo = models.ImageField( + upload_to="clean/litter_bin_photos/", + null=True, blank=True, + verbose_name=_("Photo"), + storage=private_storage, + ) + source_url = models.URLField( + max_length=500, null=True, blank=True, + verbose_name=_("Source URL (original)"), + ) + order = models.PositiveSmallIntegerField(default=1, verbose_name=_("Order")) + taken_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Taken at")) + caption = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Caption")) + + class Meta: + verbose_name = _("Litter bin photo") + verbose_name_plural = _("Litter bin photos") + ordering = ["litter_bin", "order"] + + def __str__(self): + return f"Photo {self.order} – {self.litter_bin}" + + +# --------------------- +# Litter Bin Observation (sol + accessibilité) +# --------------------- + +NEIGHBOURHOOD_TYPE_CHOICES = ( + ('importante_ponctuelle', _('Important – ponctuel')), + ('importante_continue', _('Important – continu')), + ('faible', _('Faible')), + ('autre', _('Autre')), +) + +PEDESTRIAN_DENSITY_CHOICES = ( + ('high', _('Élevée')), + ('medium', _('Moyenne')), + ('low', _('Faible')), +) + +AGENT_ACCESS_CHOICES = ( + ('normal', _('Normal')), + ('difficult', _('Difficile')), + ('impossible', _('Impossible')), +) + +POINT_OF_INTEREST_CHOICES = ( + ('bus_stop', _('Arrêt Bus/Tram')), + ('park', _('Parc')), + ('school', _('École')), + ('commerce', _('Commerce')), + ('monument', _('Monument')), + ('pedestrian_crossing', _('Passage piéton')), + ('place', _('Place')), + ('none', _('Aucun')), + ('other', _('Autre')), +) + + +class CleanLitterBinObservation(models.Model): + """Field observation for a litter bin: ground conditions and accessibility.""" + litter_bin = models.ForeignKey( + CleanLitterBin, + on_delete=models.CASCADE, + related_name="observations", + verbose_name=_("Litter bin"), + ) + observation_datetime = models.DateTimeField(null=True, blank=True, verbose_name=_("Observation date/time")) + observer_note = models.TextField(null=True, blank=True, verbose_name=_("Observer notes")) + + # --- Ground / sol --- + sol_pente_trop_forte = models.BooleanField(default=False, verbose_name=_("Slope too steep")) + sol_instable = models.BooleanField(default=False, verbose_name=_("Unstable ground")) + sol_reflechissant = models.BooleanField(default=False, verbose_name=_("Reflective surface")) + sol_glissant = models.BooleanField(default=False, verbose_name=_("Slippery surface")) + sol_fentes = models.BooleanField(default=False, verbose_name=_("Cracks in ground")) + sol_entrave = models.BooleanField(default=False, verbose_name=_("Ground obstacle")) + sol_dalles_podo = models.BooleanField(default=False, verbose_name=_("Tactile paving (dalles podotactiles)")) + sol_pente_transversale = models.FloatField(null=True, blank=True, verbose_name=_("Transversal slope (%)")) + sol_pente_longitudinale = models.FloatField(null=True, blank=True, verbose_name=_("Longitudinal slope (%)")) + sol_remarque = models.TextField(null=True, blank=True, verbose_name=_("Ground remarks")) + + # --- Accessibility / accessibilité --- + acc_type_quartier = models.CharField( + max_length=50, + choices=NEIGHBOURHOOD_TYPE_CHOICES, + null=True, blank=True, + verbose_name=_("Neighbourhood type"), + ) + acc_densite_flux_pieton = models.CharField( + max_length=50, + choices=PEDESTRIAN_DENSITY_CHOICES, + null=True, blank=True, + verbose_name=_("Pedestrian flow density"), + ) + # Stored as a JSON list of strings (e.g. ["bus_stop", "park"]) + acc_point_interet = models.JSONField( + default=list, blank=True, + verbose_name=_("Points of interest"), + ) + acc_approche_2m = models.BooleanField(default=False, verbose_name=_("Clear 2 m approach")) + acc_voirie_a_traverser = models.BooleanField(default=False, verbose_name=_("Road to cross")) + acc_poubelle_isolee = models.BooleanField(default=False, verbose_name=_("Isolated bin")) + acc_gene_flux_routier = models.BooleanField(default=False, verbose_name=_("Obstructs road traffic")) + acc_gene_flux_pieton = models.BooleanField(default=False, verbose_name=_("Obstructs pedestrian flow")) + acc_chemin_logique = models.BooleanField(default=False, verbose_name=_("Logical path (itinéraire logique)")) + acc_contraste = models.BooleanField(default=False, verbose_name=_("Visual contrast")) + acc_indications_tri = models.BooleanField(default=False, verbose_name=_("Sorting indicators present")) + acc_agent_vehicule = models.CharField( + max_length=20, + choices=AGENT_ACCESS_CHOICES, + null=True, blank=True, + verbose_name=_("Agent access – vehicle"), + ) + acc_agent_pied = models.CharField( + max_length=20, + choices=AGENT_ACCESS_CHOICES, + null=True, blank=True, + verbose_name=_("Agent access – on foot"), + ) + acc_point_interet_autre = models.CharField( + max_length=255, null=True, blank=True, + verbose_name=_("Other point of interest"), + ) + # Integer relevance/obstruction codes stored as JSON list (e.g. [1, 3]) + acc_pertinence_placement = models.JSONField( + default=list, blank=True, + verbose_name=_("Placement relevance codes"), + ) + acc_entrave = models.JSONField( + default=list, blank=True, + verbose_name=_("Obstruction codes"), + ) + acc_point_interet_remarque = models.TextField(null=True, blank=True, verbose_name=_("Point of interest remarks")) + acc_remarque = models.TextField(null=True, blank=True, verbose_name=_("Accessibility remarks")) + + class Meta: + verbose_name = _("Litter bin observation") + verbose_name_plural = _("Litter bin observations") + ordering = ["-observation_datetime"] + + def __str__(self): + return f"Observation {self.observation_datetime} – {self.litter_bin}" \ No newline at end of file diff --git a/streetup/assets/models/controlcenters.py b/streetup/assets/models/controlcenters.py new file mode 100644 index 0000000..92c2cd4 --- /dev/null +++ b/streetup/assets/models/controlcenters.py @@ -0,0 +1,169 @@ +# ---------------------- +# Control Centers Models +# ---------------------- + +from django.contrib.gis.db import models +from django.urls import reverse +from django.utils.translation import get_language +from django.core.exceptions import ValidationError +from django.utils.text import slugify + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Document +from common.storages import PrivateMediaStorage + +import os + +private_storage = PrivateMediaStorage() + + + +# Control Center Location +class ControlCenterLocation(AbstractLocation): + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations") + is_main_location = models.BooleanField(default=False, verbose_name="Main Location") + control_center = models.ForeignKey('ControlCenter', on_delete=models.SET_NULL, null=True, blank=True, related_name="all_locations") + + def get_active_assets(self): + def collect_assets(location): + assets = list(location.assets.filter(status='active')) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_all_assets(self): + def collect_assets(location): + assets = list(location.assets.all()) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_direct_child_assets(self): + return self.assets.all() + + def clean(self): + if self.is_main_location and ControlCenterLocation.objects.filter(control_center=self.control_center, is_main_location=True).exclude(pk=self.pk).exists(): + raise ValidationError("Ce centre de contrôle a déjà une localisation principale.") + + def get_absolute_url(self): + return reverse( + 'assets:controlcenters_locations_detail', + kwargs={ + # 'asset_model': content_type.model, + 'location_id': self.id + } + ) + + +class ControlCenter(models.Model): + code = models.CharField(max_length=50, unique=True) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + main_location = models.OneToOneField(ControlCenterLocation, on_delete=models.SET_NULL, null=True, blank=True, related_name="main_control_center") + description = models.TextField(null=True, blank=True) + + def get_name(self): + lang = get_language() + name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr + return name + + def get_active_assets(self): + return self.assets.filter(status='active') + + def get_all_assets(self): + return self.assets.all() + + def get_all_locations(self): + return self.all_locations.all() + + def get_main_location(self): + return self.main_location + + def __str__(self): + return self.code + " - " + self.get_name() + + class Meta: + ordering = ["code"] + + + +class ControlCenterAssetModel(AbstractAssetModel): + class Meta: + verbose_name = "Control Center Asset Model" + verbose_name_plural = "Control Center Asset Models" + +class ControlCenterGeoAsset(AbstractGeoAsset): + model = models.ForeignKey(ControlCenterAssetModel, on_delete=models.PROTECT, null=True, blank=True, related_name="controlcenters_assets") + location = models.ForeignKey(ControlCenterLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True) + control_center = models.ForeignKey(ControlCenter, on_delete=models.SET_NULL, related_name="assets", null=True, blank=True) + + class Meta: + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:controlcenters_assets_detail', kwargs={'asset_model': 'controlcentergeoasset', 'asset_id':self.id}) + + + + +class ControlCenterContract(models.Model): + control_center = models.ForeignKey(ControlCenter, on_delete=models.CASCADE, related_name="controlcenters_contracts") + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="controlcenters_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=[('building', 'Bâtiment'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other') + + def __str__(self): + return f"{self.control_center.code} - {self.contract.contract_number}" + + class Meta: + verbose_name = "Control Center Contract" + verbose_name_plural = "Control Center Contracts" + unique_together = ('control_center', 'contract') + + +class ControlCenterGeoAssetContract(models.Model): + control_center_asset = models.ForeignKey(ControlCenterGeoAsset, on_delete=models.CASCADE, related_name="controlcenters_assets_contracts") + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="controlcenters_assets_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=[('building', 'Bâtiment'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other') + + def __str__(self): + return f"{self.control_center_asset.code} - {self.contract.contract_number}" + + class Meta: + verbose_name = "Control Center Asset Contract" + verbose_name_plural = "Control Center Asset Contracts" + unique_together = ('control_center_asset', 'contract') + + + + +def upload_to_controlcenter(instance, filename): + code = slugify(instance.control_center.code or "unknown") # Fallback si code est None + return os.path.join("assets_documents", "controlcenters", code, filename) + +TU_DOCUMENT_TYPE_CHOICES = ( + ('other', 'Autre'), +) + +# Control Center Location Document +class ControlCenterDocument(Document): + control_center = models.ForeignKey(ControlCenter, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Location") + file = models.FileField(upload_to=upload_to_controlcenter, storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to=upload_to_controlcenter, storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=TU_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") + + +# Control Center Asset Document +class ControlCenterAssetDocument(Document): + control_center_asset = models.ForeignKey(ControlCenterGeoAsset, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Location") + file = models.FileField(upload_to=upload_to_controlcenter, storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to=upload_to_controlcenter, storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=TU_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") \ No newline at end of file diff --git a/streetup/assets/models/core.py b/streetup/assets/models/core.py new file mode 100644 index 0000000..4df1ca2 --- /dev/null +++ b/streetup/assets/models/core.py @@ -0,0 +1,208 @@ +from django.contrib.gis.db import models +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import get_language, gettext_lazy as _ + +from common.models import Thematic + +# Abstract base model for locations +class AbstractLocation(models.Model): + code = models.CharField(max_length=50) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + lon = models.FloatField(null=True,blank=True) + lat = models.FloatField(null=True,blank=True) + geom = models.PolygonField(srid=3812, null=True,blank=True) + geojson = models.TextField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + updated_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_location_updated_by', verbose_name="Mis à jour par") + + 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 self.code + " - " + self.get_name() + + class Meta: + abstract = True + ordering = ["code"] + + + +# Category of assets +class AssetCategory(models.Model): + thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True, related_name="asset_categories") + parent = models.ForeignKey( + 'self', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='children', + verbose_name=_("Catégorie parente"), + help_text=_("Laisser vide pour une catégorie de premier niveau. Une sous-catégorie ne peut pas avoir d'enfants."), + ) + code = models.CharField(max_length=200) + name_fr = models.CharField(max_length=255) + name_nl = models.CharField(max_length=255) + allowed_models = models.ManyToManyField( + ContentType, + blank=True, + related_name="asset_categories", + verbose_name=_("Modèles Django autorisés"), + help_text=_("Les types de modèles d'assets (Django) autorisés pour cette catégorie.") + ) + + def get_name(self): + lang = get_language() + return getattr(self, f"name_{lang}", self.name_fr) # Fallback to name_fr + + def __str__(self): + return f"{self.get_name()}" + + @property + def is_leaf(self): + """Retourne True si cette catégorie n'a pas d'enfants (feuille de l'arbre).""" + return not self.children.exists() + + def get_children(self): + """Retourne le queryset des sous-catégories directes.""" + return self.children.all() + + def get_category_ids_with_children(self): + """Retourne la liste des ids : cette catégorie + toutes ses sous-catégories.""" + ids = [self.pk] + ids.extend(self.children.values_list('pk', flat=True)) + return ids + + class Meta: + ordering = ["thematic", "name_fr"] + + +# Abstract base model for "models of assets" +class AbstractAssetModel(models.Model): + code = models.CharField(max_length=50) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + description = models.TextField(null=True,blank=True) + brand = models.CharField(max_length=500, null=True,blank=True) + category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True) + + 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 self.code + " - " + self.get_name() + + class Meta: + abstract = True + + +ASSET_STATUS_CHOICES = { + 'to_be_approved': _('À approuver'), + 'planned': _('Planifié'), + 'active': _('Actif'), + 'damaged': _('Endommagé'), + 'to_replace': _('A remplacer'), + 'voluntarily_out_of_service': _('Mis hors service volontairement'), + 'removed': _('Enlevé'), + 'archived': _('Archivé'), +} + +ASSET_OBSOLESCENCE_CHOICES = { + 'good': _('Bon état'), + 'medium': _('Moyen'), + 'bad': _('Dégradé'), + 'critical': _('Critique'), + 'unknown': _('Inconnu'), +} + +ASSET_VALIDATION_STATUS_CHOICES = { + 'to_be_validated': _('À valider'), + 'validated': _('Validé'), + 'to_be_completed': _('À compléter'), + 'rejected': _('Rejeté'), +} + +# Abstract base model for assets +class AbstractAsset(models.Model): + code = models.CharField(max_length=50) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + status = models.CharField(choices=ASSET_STATUS_CHOICES, default='active', max_length=50) + obsolescence = models.CharField(choices=ASSET_OBSOLESCENCE_CHOICES, default='unknown', null=True, blank=True) + installation_date = models.DateTimeField(null=True,blank=True) + uninstallation_date = models.DateTimeField(null=True,blank=True) + serial_number = models.CharField(max_length=200,null=True,blank=True) + warranty_duration = models.IntegerField(null=True,blank=True, verbose_name="Duration of the warranty (months)") + last_inspection_date = models.DateTimeField(null=True,blank=True) + last_preventive_date = models.DateTimeField(null=True,blank=True) + last_corrective_date = models.DateTimeField(null=True,blank=True) + last_survey_date = models.DateTimeField(null=True,blank=True) + category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True) + brand = models.CharField(max_length=500, null=True,blank=True) + replaced_by = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='replaces', verbose_name="Remplacé par") + marked_as_removed_date = models.DateTimeField(null=True, blank=True, verbose_name="Date de marquage comme retiré") + marked_as_removed_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_marked_as_removed', verbose_name="Marqué comme retiré par") + note = models.TextField(null=True, blank=True, verbose_name="Note") + validation_status = models.CharField(max_length=50, choices=ASSET_VALIDATION_STATUS_CHOICES, null=True, blank=True, verbose_name="Statut de validation") + updated_at = models.DateTimeField(auto_now=True) + updated_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_updated_by', verbose_name="Mis à jour par") + + def get_replacement_chain(self): + """Get the complete replacement chain starting from this asset.""" + chain = [self] + current = self + seen_ids = {self.id} + + while current.replaced_by: + if current.replaced_by.id in seen_ids: # Avoid infinite loops + break + chain.append(current.replaced_by) + seen_ids.add(current.replaced_by.id) + current = current.replaced_by + + return chain + + def get_original_asset(self): + """Get the first asset in the replacement chain.""" + original = self + seen_ids = {self.id} + + # Try to find an asset that replaces this one + replacer = self.replaces.first() + while replacer: + if replacer.id in seen_ids: # Avoid infinite loops + break + original = replacer + seen_ids.add(replacer.id) + replacer = replacer.replaces.first() + + return original + + 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 self.code + " - " + self.get_name() + + class Meta: + abstract = True + + +# Abstract base model for geo assets +class AbstractGeoAsset(AbstractAsset): + lon = models.FloatField(null=True,blank=True) + lat = models.FloatField(null=True,blank=True) + geom = models.PointField(srid=3812, null=True,blank=True) + geojson = models.TextField(null=True, blank=True) + + class Meta: + abstract = True + + + diff --git a/streetup/assets/models/history.py b/streetup/assets/models/history.py new file mode 100644 index 0000000..fe12e3e --- /dev/null +++ b/streetup/assets/models/history.py @@ -0,0 +1,347 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from datetime import datetime, timedelta + + +class AssetHistoryQuerySet(models.QuerySet): + """Custom QuerySet for AssetHistory with filtering helpers.""" + + def for_asset(self, asset): + """Filter history for a specific asset.""" + content_type = ContentType.objects.get_for_model(asset) + return self.filter(content_type=content_type, object_id=asset.pk) + + def before_date(self, date): + """Filter history before a specific date.""" + if isinstance(date, str): + date = datetime.fromisoformat(date) + return self.filter(timestamp__lte=date) + + def after_date(self, date): + """Filter history after a specific date.""" + if isinstance(date, str): + date = datetime.fromisoformat(date) + return self.filter(timestamp__gte=date) + + def between_dates(self, start_date, end_date): + """Filter history between two dates.""" + return self.after_date(start_date).before_date(end_date) + + def status_changes(self): + """Filter only status change entries.""" + return self.filter(action_type='status_change') + + def creations(self): + """Filter only creation entries.""" + return self.filter(action_type='create') + + def deletions(self): + """Filter only deletion entries.""" + return self.filter(action_type='delete') + + +class AssetHistoryManager(models.Manager): + """Custom manager for AssetHistory.""" + + def get_queryset(self): + return AssetHistoryQuerySet(self.model, using=self._db) + + def for_asset(self, asset): + return self.get_queryset().for_asset(asset) + + def get_content_type_for_model(self, model_name, app_label=None): + """ + Get ContentType for a model, handling cases where multiple apps have the same model name. + Returns the ContentType or raises ContentType.DoesNotExist if not found. + If multiple ContentTypes exist and no app_label specified, returns the first one. + """ + if app_label: + return ContentType.objects.get(app_label=app_label, model=model_name.lower()) + + content_types = list(ContentType.objects.filter(model=model_name.lower())) + if not content_types: + raise ContentType.DoesNotExist(f"Model '{model_name}' not found") + + # If multiple, return the first one (usually 'assets' if it exists) + # User can specify app_label to disambiguate + return content_types[0] + + def get_asset_status_at_date(self, asset, date): + """ + Get the status of an asset at a specific date. + Returns the status value or None if asset didn't exist or was deleted. + """ + if isinstance(date, str): + date = datetime.fromisoformat(date) + + content_type = ContentType.objects.get_for_model(asset) + + # Check if asset was created by the date + creation = self.filter( + content_type=content_type, + object_id=asset.pk, + action_type='create', + timestamp__lte=date + ).first() + + if not creation: + return None # Asset didn't exist yet + + # Check if asset was deleted by the date + deletion = self.filter( + content_type=content_type, + object_id=asset.pk, + action_type='delete', + timestamp__lte=date + ).first() + + if deletion: + return None # Asset was deleted + + # Get the last status change before the date + last_status_change = self.filter( + content_type=content_type, + object_id=asset.pk, + field_name='status', + timestamp__lte=date + ).order_by('-timestamp').first() + + if last_status_change: + return last_status_change.new_value + + # If no status change, try to get the current status from the asset itself + # This should not happen in normal cases, but it's a fallback + try: + return asset.status + except: + return None + + def get_assets_active_at_date(self, asset_model_class, date): + """ + Get all assets of a given type that were active (status='active') at a specific date. + """ + if isinstance(date, str): + date = datetime.fromisoformat(date) + + content_type = ContentType.objects.get_for_model(asset_model_class) + + # Get all created assets by that date + created_by_date = self.filter( + content_type=content_type, + action_type='create', + timestamp__lte=date + ).values_list('object_id', flat=True).distinct() + + if not created_by_date: + return [] + + # Get deleted assets by that date + deleted_by_date = self.filter( + content_type=content_type, + action_type='delete', + timestamp__lte=date + ).values_list('object_id', flat=True).distinct() + + # Get asset IDs with last status != 'active' before the date + non_active_ids = [] + for asset_id in created_by_date: + if asset_id not in deleted_by_date: + last_status = self.filter( + content_type=content_type, + object_id=asset_id, + field_name='status', + timestamp__lte=date + ).order_by('-timestamp').first() + + if last_status and last_status.new_value != 'active': + non_active_ids.append(asset_id) + + # Return active assets (created, not deleted, status='active') + active_ids = [aid for aid in created_by_date if aid not in deleted_by_date and aid not in non_active_ids] + + return asset_model_class.objects.filter(pk__in=active_ids) + + +class AssetHistory(models.Model): + """ + Model to track the history of changes to assets. + Uses ContentType to work with any asset type (generic asset tracking). + + This model stores: + - Who made the change (user) + - When the change was made (timestamp) + - What changed (field_name, old_value, new_value) + - Which asset was modified (generic relation) + + This design is efficient for querying history across all assets for timeline views. + """ + + # Generic relation to any asset + content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + verbose_name=_("Type d'asset") + ) + object_id = models.PositiveIntegerField( + verbose_name=_("ID de l'asset") + ) + asset = GenericForeignKey('content_type', 'object_id') + + # Tracking information + user = models.ForeignKey( + 'auth.User', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='asset_changes', + verbose_name=_("Utilisateur") + ) + timestamp = models.DateTimeField( + default=timezone.now, + db_index=True, + verbose_name=_("Date de modification") + ) + + # Change details + field_name = models.CharField( + max_length=100, + verbose_name=_("Champ modifié") + ) + old_value = models.TextField( + null=True, + blank=True, + verbose_name=_("Ancienne valeur") + ) + new_value = models.TextField( + null=True, + blank=True, + verbose_name=_("Nouvelle valeur") + ) + + # Optional: action type for special events + action_type = models.CharField( + max_length=50, + default='update', + choices=[ + ('create', _('Création')), + ('update', _('Modification')), + ('delete', _('Suppression')), + ('status_change', _('Changement de statut')), + ], + verbose_name=_("Type d'action") + ) + + objects = AssetHistoryManager() + + class Meta: + verbose_name = _("Historique d'asset") + verbose_name_plural = _("Historiques d'assets") + ordering = ['-timestamp'] + indexes = [ + models.Index(fields=['content_type', 'object_id', '-timestamp']), + models.Index(fields=['-timestamp']), + models.Index(fields=['user', '-timestamp']), + models.Index(fields=['action_type', '-timestamp']), + models.Index(fields=['field_name', '-timestamp']), + models.Index(fields=['content_type', 'action_type']), + ] + + def __str__(self): + return f"{self.get_action_type_display()} - {self.field_name} - {self.timestamp}" + + def get_display_old_value(self): + """Get a human-readable display of the old value""" + return self._format_value(self.old_value, self.field_name) + + def get_display_new_value(self): + """Get a human-readable display of the new value""" + return self._format_value(self.new_value, self.field_name) + + def _format_value(self, value, field_name): + """Format a value for display based on field type""" + if value is None or value == '' or value == 'None': + return _("(vide)") + + # Special handling for common field types + if field_name in ['installation_date', 'uninstallation_date', 'last_inspection_date', + 'last_preventive_date', 'last_corrective_date', 'last_survey_date', + 'marked_as_removed_date']: + try: + from django.utils.dateparse import parse_datetime + dt = parse_datetime(value) + if dt: + from django.utils.formats import date_format + return date_format(dt, "SHORT_DATETIME_FORMAT") + except: + pass + + # For status field, try to get the display value + if field_name == 'status': + from .core import ASSET_STATUS_CHOICES + status_dict = dict(ASSET_STATUS_CHOICES) + return str(status_dict.get(value, value)) + + # For Foreign Key fields, try to get the object's code + if field_name in ['location', 'structure', 'model', 'category', 'replaced_by']: + try: + # Value is stored as the object's PK (ID) + pk_value = int(value) + + # Get the asset to determine the correct model class + if self.asset: + related_object = None + + # Try to get the related object from the asset + if hasattr(self.asset, field_name): + related_field = getattr(self.asset.__class__, field_name, None) + if related_field and hasattr(related_field, 'field'): + related_model = related_field.field.related_model + try: + related_object = related_model.objects.get(pk=pk_value) + except: + pass + + # Format the display + if related_object: + if hasattr(related_object, 'code'): + return str(related_object.code) + elif hasattr(related_object, 'name_fr'): + return str(related_object.name_fr) + elif hasattr(related_object, 'name'): + return str(related_object.name) + else: + return str(related_object) + except (ValueError, TypeError, AttributeError): + pass + + return str(value) + + def get_field_display_name(self): + """Get a human-readable field name""" + field_names = { + 'code': _('Code'), + 'name_fr': _('Nom (FR)'), + 'name_nl': _('Nom (NL)'), + 'status': _('Statut'), + 'installation_date': _("Date d'installation"), + 'uninstallation_date': _('Date de désinstallation'), + 'serial_number': _('Numéro de série'), + 'warranty_duration': _('Durée de garantie'), + 'last_inspection_date': _('Dernière inspection'), + 'last_preventive_date': _('Dernier entretien préventif'), + 'last_corrective_date': _('Dernier entretien correctif'), + 'last_survey_date': _('Dernier relevé'), + 'category': _('Catégorie'), + 'brand': _('Marque'), + 'model': _('Modèle'), + 'location': _('Localisation'), + 'structure': _('Structure'), + 'lon': _('Longitude'), + 'lat': _('Latitude'), + 'geom': _('Géométrie'), + 'geojson': _('GeoJSON'), + } + return field_names.get(self.field_name, self.field_name) diff --git a/streetup/assets/models/its.py b/streetup/assets/models/its.py new file mode 100644 index 0000000..daa9c7b --- /dev/null +++ b/streetup/assets/models/its.py @@ -0,0 +1,132 @@ +# ---------------------- +# ITS 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 + +import os + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() + + + +# ITS locations +class ITSLocation(AbstractLocation): + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations") + is_main_location = models.BooleanField(default=False, verbose_name="Main Location") + + def get_active_assets(self): + def collect_assets(location): + assets = list(location.assets.filter(status='active')) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_all_assets(self): + def collect_assets(location): + assets = list(location.assets.all()) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_direct_child_assets(self): + return self.assets.all() + + + def get_absolute_url(self): + return reverse( + 'assets:its_locations_detail', + kwargs={ + 'location_id': self.id + } + ) + + + class Meta: + verbose_name = "ITS Location" + verbose_name_plural = "ITS Locations" + ordering = ["code"] + + + +def upload_to_its_location(instance, filename): + location = instance.location + code = slugify(location.code or "unknown") # Fallback si code est None + return os.path.join("assets_documents", "its_locations", code, filename) + +ITS_LOC_DOCUMENT_TYPE_CHOICES = ( + ('as_built', 'As Built'), + ('schema', "Schéma"), + ('other', 'Autre'), +) + +# ITS Location Document +class ITSLocationDocument(Document): + location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated ITS Location") + file = models.FileField(upload_to=upload_to_its_location, storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to=upload_to_its_location, storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=ITS_LOC_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") + + +class ITSAssetModel(AbstractAssetModel): + class Meta: + verbose_name = "ITS Asset Model" + verbose_name_plural = "ITS Asset Models" + + +class ITSGeoAsset(AbstractGeoAsset): + model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT) + location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True) + trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole") + lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole") + + class Meta: + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id}) + + + +class ITSLocationContract(models.Model): + location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts") + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="its_locations_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other') + + def __str__(self): + return f"{self.location.code} - {self.contract.contract_number}" + + class Meta: + verbose_name = "ITS Location Contract" + verbose_name_plural = "ITS Location Contracts" + unique_together = ('location', 'contract') + + +class ITSGeoAssetContract(models.Model): + asset = models.ForeignKey(ITSGeoAsset, on_delete=models.CASCADE, related_name="its_assets_contracts") + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="its_assets_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other') + + def __str__(self): + return f"{self.asset.code} - {self.contract.contract_number}" + + class Meta: + verbose_name = "ITS Asset Contract" + verbose_name_plural = "ITS Asset Contracts" + unique_together = ('asset', 'contract') \ No newline at end of file diff --git a/streetup/assets/models/nature.py b/streetup/assets/models/nature.py new file mode 100644 index 0000000..5d2ef66 --- /dev/null +++ b/streetup/assets/models/nature.py @@ -0,0 +1,501 @@ +from django.contrib.gis.db import models +from django.contrib.postgres.fields import ArrayField +from django.urls import reverse +from django.conf import settings +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from datetime import datetime + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel, AssetCategory +from common.models import Document +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() + + + +class NatureLocation(AbstractLocation): + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations") + zone = models.IntegerField(null=True, blank=True) + is_highway = models.BooleanField(default=False) + category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True) + municipality = models.ForeignKey('common.Municipality', on_delete=models.SET_NULL, null=True, blank=True) + + def _get_direct_assets(self, active_only=False): + related_sets = ( + self.nature_green_surfaces, + self.nature_trees, + self.nature_shrubs, + self.nature_benches, + ) + + assets = [] + for related_manager in related_sets: + queryset = related_manager.filter(status='active') if active_only else related_manager.all() + assets.extend(list(queryset)) + return assets + + def get_active_assets(self): + def collect_assets(location): + assets = location._get_direct_assets(active_only=True) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_all_assets(self): + def collect_assets(location): + assets = location._get_direct_assets(active_only=False) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_direct_child_assets(self): + return self._get_direct_assets(active_only=False) + + def get_absolute_url(self): + return reverse( + 'assets:nature_locations_detail', + kwargs={ + # 'asset_model': content_type.model, + 'location_id': self.id + } + ) + + + +class NatureAssetModel(AbstractAssetModel): + color = models.CharField(max_length=200, null=True, blank=True) + class Meta: + verbose_name = "Nature Asset Model" + verbose_name_plural = "Nature Asset Models" + + + + +BULB_TYPE_CHOICES = [ + ('tulipes', _('Tulipes')), + ('jacynthes', _('Jacynthes')), + ('jonquilles', _('Jonquilles')), + ('narcisses', _('Narcisses')), + ('crocus', _('Crocus')), + ('perce_neige', _('Perce-neige')), + ('autres', _('Autres')), +] + +class NatureGreenSurface(AbstractGeoAsset): + geom = models.MultiPolygonField(srid=3812, null=True, blank=True) + model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_green_surfaces") + location = models.ForeignKey(NatureLocation, related_name="nature_green_surfaces", on_delete=models.SET_NULL, null=True, blank=True) + surface_area = models.FloatField(null=True, blank=True) + slope = models.FloatField(null=True, blank=True) + weighting = models.FloatField(null=True, blank=True) + density = models.FloatField(null=True, blank=True) + mower_frenquency = models.IntegerField(null=True, blank=True) + buffer = models.FloatField(null=True, blank=True) + width = models.FloatField(null=True, blank=True) + length = models.FloatField(null=True, blank=True) + bulb_types = ArrayField( + models.CharField(max_length=50, choices=BULB_TYPE_CHOICES), + blank=True, + default=list, + verbose_name=_("Types de bulbes") + ) + + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturegreensurface', 'asset_id':self.id}) + + +class NatureGreenSurfaceDocument(Document): + green_surface = models.ForeignKey(NatureGreenSurface, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Surface") + file = models.FileField(upload_to="assets_documents/nature/surfaces/", storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to="assets_documents/nature/surfaces/", storage=private_storage, null=True, blank=True, max_length=255) + + +class NatureTree(AbstractGeoAsset): + model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_trees") + location = models.ForeignKey(NatureLocation, related_name="nature_trees", on_delete=models.SET_NULL, null=True, blank=True) + height = models.FloatField(null=True, blank=True) + depth = models.FloatField(null=True, blank=True) + protection_buffer = models.FloatField(null=True, blank=True) + + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturetree', 'asset_id':self.id}) + + +# Arbustes +class NatureShrub(AbstractGeoAsset): + model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_shrubs") + location = models.ForeignKey(NatureLocation, related_name="nature_shrubs", on_delete=models.SET_NULL, null=True, blank=True) + height = models.FloatField(null=True, blank=True) + depth = models.FloatField(null=True, blank=True) + trimmed = models.BooleanField(default=False) + + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'natureshrub', 'asset_id':self.id}) + + class Meta: + verbose_name = "Arbuste" + verbose_name_plural = "Arbustes" + + +# Bancs +class NatureBench(AbstractGeoAsset): + model = models.ForeignKey(NatureAssetModel, on_delete=models.SET_NULL, null=True, blank=True, related_name="nature_benches") + location = models.ForeignKey(NatureLocation, related_name="nature_benches", on_delete=models.SET_NULL, null=True, blank=True) + material = models.CharField(max_length=200, null=True, blank=True) + length = models.FloatField(null=True, blank=True) + width = models.FloatField(null=True, blank=True) + height = models.FloatField(null=True, blank=True) + + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturebench', 'asset_id':self.id}) + + + +# ============================================================================ +# Geolocated Comments (independent from assets) +# ============================================================================ + +class GreenSurfacesComment(models.Model): + """ + Commentaire géolocalisé indépendant pour l'édition des espaces verts. + Permet aux utilisateurs d'ajouter des remarques géolocalisées avec photos, + sans être lié à un asset spécifique. + """ + STATUS_CHOICES = [ + ('memo', 'Mémo'), + ('attention', 'Point d\'attention'), + ('todo', 'À faire'), + ('ok', 'OK'), + ('problem', 'Problème'), + ] + + comment = models.TextField(verbose_name="Commentaire", blank=True, default="") + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='memo', + verbose_name="Statut" + ) + latitude = models.FloatField(verbose_name="Latitude") + longitude = models.FloatField(verbose_name="Longitude") + geom = models.PointField(srid=3812, null=True, blank=True, verbose_name="Géométrie") + address = models.CharField(max_length=1024, null=True, blank=True, verbose_name="Adresse") + + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + related_name='green_surfaces_comments', + null=True, + blank=True, + verbose_name="Créé par" + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Date de création") + updated_at = models.DateTimeField(auto_now=True, verbose_name="Date de modification") + + class Meta: + verbose_name = "Commentaire (Espaces verts)" + verbose_name_plural = "Commentaires (Espaces verts)" + ordering = ['-created_at'] + + def __str__(self): + return f"Commentaire de {self.created_by} - {self.created_at.strftime('%d/%m/%Y %H:%M')}" + + +def comment_photo_upload_path(instance, filename): + """Chemin de stockage pour les photos de commentaires""" + now = timezone.now() + year = now.strftime("%Y") + month = now.strftime("%m") + return f"green_surfaces/comments/photos/{year}/{month}/{filename}" + + +class GreenSurfacesCommentPhoto(models.Model): + """ + Photo associée à un commentaire géolocalisé. + Conserve la direction (azimut) dans laquelle la photo a été prise. + """ + comment = models.ForeignKey( + GreenSurfacesComment, + related_name='photos', + on_delete=models.CASCADE, + verbose_name="Commentaire" + ) + photo = models.ImageField( + upload_to=comment_photo_upload_path, + storage=private_storage, + verbose_name="Photo" + ) + heading = models.FloatField( + null=True, + blank=True, + verbose_name="Direction (azimut en degrés)", + help_text="Direction de la boussole dans laquelle la photo a été prise (0-360°)" + ) + order = models.PositiveIntegerField( + default=0, + verbose_name="Ordre", + help_text="Ordre d'affichage de la photo" + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Date de création") + + class Meta: + verbose_name = "Photo de commentaire" + verbose_name_plural = "Photos de commentaires" + ordering = ['order', 'created_at'] + + def __str__(self): + direction = f" (direction: {self.heading}°)" if self.heading is not None else "" + return f"Photo {self.order + 1} du commentaire #{self.comment.id}{direction}" + + + + + +class NatureRWIZ(AbstractLocation): + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + + def get_active_assets(self): + if not self.geom: + return NatureRWIAsset.objects.none() + return NatureRWIAsset.objects.filter(geom__intersects=self.geom, status='active') + + def get_all_assets(self): + if not self.geom: + return NatureRWIAsset.objects.none() + return NatureRWIAsset.objects.filter(geom__intersects=self.geom) + + def get_absolute_url(self): + return reverse( + 'assets:zirw_detail', + kwargs={ + 'location_id': self.id + } + ) + + class Meta: + verbose_name = _("Zone de gestion intégrée des eaux pluviales (ZIEP)") + verbose_name_plural = _("Zones de gestion intégrée des eaux pluviales (ZIEP)") + + + + + + +CODE_NAME_CHOICES = [ + ('AV', _('Stormwater inlet (Avaloir)')), + ('BS', _('Sequenced border (Bordure séquencée)')), + ('BI', _('Injection port (Bouche d\'injection)')), + ('CA', _('Non-return valve (Clapet anti-retour)')), + ('ES', _('Sedimentation (Empierrement sédimentation)')), + ('KC', _('Kerb cell')), + ('PI', _('Infiltration well (Puits d\'infiltration)')), + ('SU', _('Overflow (Surverse)')), + ('VI', _('Isolation valve (Vanne d\'isolation)')), + ('VR', _('Control valve (Vanne de régulation)')), + ('GT', _('Sidewalk gargoyle (Gargouille de trottoir)')), + ('FE', _('Trickle of water (Filet d\'eau)')), + ('AR', _('Rail drain (Avaloir de rail)')), + ('BA', _('Leveled edge (Bordure arasée)')), + ('CI', _('Infiltration channel (Caniveau infiltrant)')), + ('D', _('Drain')), + ('TP', _('Overflowing (Trop plein)')), + ('RES', _('Separate sewer system (Réseau égout séparatif)')), + ('EVC', _('Hollow green space (Espace vert creux)')), + ('OED', _('Decentralized underground structure (Ouvrage enterré décentralisé)')), + ('OSI', _('Work on an impermeable surface (Ouvrage sur surface imperméable)')), + ('TA', _('Alternative technique (Technique alternative)')), + ('OA', _('Supplementary work (Ouvrage annexe)')), +] + +STRUCTURE_TYPE_CHOICES = [ + # Points + ('avaloir', _('Stormwater inlet (Avaloir)')), + ('bordure sequencee', _('Sequenced border (Bordure séquencée)')), + ('bouche injection', _('Injection port (Bouche d\'injection)')), + ('clapet anti-retour', _('Non-return valve (Clapet anti-retour)')), + ('empierrement sedimentation', _('Sedimentation (Empierrement sédimentation)')), + ('kerb cell', _('Kerb cell')), + ('puits infiltration', _('Infiltration well (Puits d\'infiltration)')), + ('surverse', _('Overflow (Surverse)')), + ('vanne isolation', _('Isolation valve (Vanne d\'isolation)')), + ('vanne regulation', _('Control valve (Vanne de régulation)')), + # Lines + ('gargouille de trottoir', _('Sidewalk gargoyle (Gargouille de trottoir)')), + ('filet eau', _('Trickle of water (Filet d\'eau)')), + ('avaloir de rail', _('Rail drain (Avaloir de rail)')), + ('bordure arasee', _('Leveled edge (Bordure arasée)')), + ('caniveau infiltrant', _('Infiltration channel (Caniveau infiltrant)')), + ('drain', _('Drain')), + ('trop plein', _('Overflowing (Trop plein)')), + ('reseau egout separatif', _('Separate sewer system (Réseau égout séparatif)')), + # Polygons + ('bassin sec/infiltration', _('Dry/infiltration basin (Bassin sec/infiltration)')), + ('bassin en eau', _('Wet basin (Bassin en eau)')), + ('fosse', _('Ditch (Fossé)')), + ('jardin de pluie', _('Rain garden (Jardin de pluie)')), + ('noue', _('Swale (Noue)')), + ('massif drainant', _('Draining structure (Massif drainant)')), + ('tranchee drainante', _('Draining trench (Tranchée drainante)')), + ('toiture stockante', _('Retention roof (Toiture stockante)')), + ('toiture verte', _('Green roof (Toiture verte)')), + ('bassin orage', _('Stormwater basin (Bassin d\'orage)')), + ('SAUL-vide', _('SAUL-empty (SAUL-vide)')), + ('SAUL-terre', _('SAUL-earth (SAUL-terre)')), + ('citerne', _('Cistern (Citerne)')), + ('espace vert sur dalle', _('Green space on slab (Espace vert sur dalle)')), + ('bande filtrante', _('Filter strip (Bande filtrante)')), + ('collecteur en beton poreux', _('Porous concrete collector (Collecteur en béton poreux)')), +] + +PROJECT_MANAGER_CHOICES = [ + ('DPV', 'DPV'), + ('DITP', 'DITP'), + ('DEN', 'DEN'), + ('DPO', 'DPO'), + ('Beliris', 'Beliris'), + ('Commune', 'Commune'), +] + + + +CONSTRUCTION_STATUS_CHOICES = [ + ('etude', _('Design (Étude)')), + ('travaux', _('Construction (Travaux)')), + ('reception provisoire', _('Provisional acceptance (Réception provisoire)')), + ('reception definitive', _('Final acceptance (Réception définitive)')), +] + +CONDITION_CHOICES = [ + ('bon', _('Good (Bon)')), + ('examen a programmer', _('Inspection to schedule (Examen à programmer)')), + ('intervention curative necessaire', _('Curative action needed (Intervention curative nécessaire)')), + ('refection totale necessaire', _('Total rehabilitation needed (Réfection totale nécessaire)')), + ('travaux entretien a realiser', _('Maintenance works to perform (Travaux entretien à réaliser)')), + ('travaux maintenance', _('Maintenance works (Travaux maintenance)')), + ('travaux maintenance a programmer', _('Maintenance works to schedule (Travaux maintenance à programmer)')), +] + +CONNECTION_CHOICES = [ + ('connecte', _('Connected (Connecté)')), + ('non connecte', _('Not connected (Non connecté)')), +] + +COATING_CATEGORY_CHOICES = [ + ('Vegetal', _('Vegetal (Végétal)')), + ('Structure enherbee', _('Grassed structure (Structure enherbée)')), + ('Materiau non agrege', _('Unbound material (Matériau non agrégé)')), + ('Structure a remplissage mineral', _('Mineral-filled structure (Structure à remplissage minéral)')), + ('Revetement sureleve', _('Raised coating (Revêtement surélevé)')), + ('Modulaire poreux', _('Porous modular (Modulaire poreux)')), + ('Continu poreux', _('Porous continuous (Continu poreux)')), + ('Impermeable', _('Impermeable (Imperméable)')), +] + +COATING_TYPE_CHOICES = [ + ('Pleine terre', _('Open soil (Pleine terre)')), + ('Melange terre-pierre', _('Soil-stone mixture (Mélange terre-pierre)')), + ('Paves a joints larges enherbes', _('Grassed wide-joint pavers (Pavés à joints larges enherbés)')), + ('Dalles gazon en beton', _('Concrete grass grids (Dalles gazon en béton)')), + ('Dalles alveolaires vegetalisees', _('Vegetated cellular tiles (Dalles alvéolaires végétalisées)')), + ('Mulch vegetal', _('Woodchips/mulch (Mulch végétal)')), + ('Graviers calcaires', _('Limestone gravel (Graviers calcaires)')), + ('Graviers non calcaires', _('Non-limestone gravel (Graviers non calcaires)')), + ('Terre battue', _('Dirt ground (Terre battue)')), + ('Paves a joints larges engravillonnes', _('Gravel wide-joint pavers (Pavés à joints larges engravillonnés)')), + ('Dalles alveolaires en gravier', _('Gravel cellular tiles (Dalles alvéolaires en gravier)')), + ('Dalles en PEHD remplies de pave', _('HDPE tiles filled with pavers (Dalles en PEHD remplies de pavé)')), + ('Grilles', _('Grates (Grilles)')), + ('Platelages bois', _('Wooden decking (Platelages bois)')), + ('Pave en beton poreux', _('Porous concrete pavers (Pavé en béton poreux)')), + ('Dalle en beton poreux', _('Porous concrete slab (Dalle en béton poreux)')), + ('Enrobes bitumineux poreux', _('Porous asphalt (Enrobés bitumineux poreux)')), + ('Betons poreux', _('Porous concrete (Bétons poreux)')), + ('Graviers lies a la resine', _('Resin-bound gravel (Graviers liés à la résine)')), + ('Graviers concasses lies aux polymeres', _('Polymer-bound crushed gravel (Graviers concassés liés aux polymères)')), + ('Revetements EPDM et ESBR', _('EPDM and SBR rubber coatings (Revêtements EPDM et ESBR)')), + ('Revetements en beton de ciment non permeables', _('Non-porous cement concrete coatings (Revêtements en béton de ciment non perméables)')), + ('Revetements bitumineux non permeables', _('Non-porous asphalt coatings (Revêtements bitumineux non perméables)')), + ('Dallages non permeables', _('Non-porous tiling (Dallages non perméables)')), + ('Pavages non permeables', _('Non-porous paving (Pavages non perméables)')), + ('Autres revetements non permeables', _('Other non-porous coatings (Autres revêtements non perméables)')), +] + + +GEOMETRY_TYPE_CHOICES = [ + ('point', _('Point')), + ('line', _('Line')), + ('polygon', _('Polygon')), +] + + +class NatureRWIAsset(AbstractGeoAsset): + geom = models.GeometryField(srid=3812, null=True, blank=True) + + external_id = models.IntegerField(null=True, blank=True, verbose_name=_("External ID")) + code_name = models.CharField(max_length=50, choices=CODE_NAME_CHOICES, null=True, blank=True, verbose_name=_("Code name")) + structure_type = models.CharField(max_length=100, choices=STRUCTURE_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Structure type")) + geometry_type = models.CharField(max_length=50, choices=GEOMETRY_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Geometry type")) + project_manager = models.CharField(max_length=50, choices=PROJECT_MANAGER_CHOICES, null=True, blank=True, verbose_name=_("Project manager")) + + construction_status = models.CharField(max_length=100, choices=CONSTRUCTION_STATUS_CHOICES, null=True, blank=True, verbose_name=_("Construction status")) + commissioning_date = models.DateField(null=True, blank=True, verbose_name=_("Commissioning date")) + condition = models.CharField(max_length=100, choices=CONDITION_CHOICES, null=True, blank=True, verbose_name=_("Condition")) + administrator = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Administrator")) + monitoring = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Monitoring")) + comment = models.TextField(null=True, blank=True, verbose_name=_("Comment")) + has_filter = models.BooleanField(null=True, blank=True, verbose_name=_("Has filter")) + + sewer_connection = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True, blank=True, verbose_name=_("Sewer connection")) + separate_network_connection = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True, blank=True, verbose_name=_("Separate network connection")) + hydro_network_connection = models.CharField(max_length=50, choices=CONNECTION_CHOICES, null=True, blank=True, verbose_name=_("Hydro network connection")) + + surface_area = models.FloatField(null=True, blank=True, verbose_name=_("Surface area (m²)")) + length = models.FloatField(null=True, blank=True, verbose_name=_("Length (m)")) + + coating_category = models.CharField(max_length=100, choices=COATING_CATEGORY_CHOICES, null=True, blank=True, verbose_name=_("Coating category")) + coating_type = models.CharField(max_length=100, choices=COATING_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Coating type")) + + indexes = [ + models.Index(fields=["code"]), + ] + + class Meta: + verbose_name = _("RWI Asset") + verbose_name_plural = _("RWI Assets") + + def get_absolute_url(self): + return reverse('assets:water_assets_detail', kwargs={'asset_model': 'naturerwiasset', 'asset_id': self.id}) + + +class NatureRWIAssetDocument(Document): + rwi_asset = models.ForeignKey(NatureRWIAsset, on_delete=models.CASCADE, related_name="documents", verbose_name=_("RWI Asset")) + file = models.FileField(upload_to="assets_documents/nature/rwi/", storage=private_storage, verbose_name=_("File"), max_length=255) + thumbnail = models.ImageField(upload_to="assets_documents/nature/rwi/", storage=private_storage, null=True, blank=True, max_length=255) + + class Meta: + verbose_name = _("RWI Asset Document") + verbose_name_plural = _("RWI Asset Documents") + + diff --git a/streetup/assets/models/publiclighting.py b/streetup/assets/models/publiclighting.py new file mode 100644 index 0000000..98fcddc --- /dev/null +++ b/streetup/assets/models/publiclighting.py @@ -0,0 +1,143 @@ +# ---------------------- +# Public Lighting Models +# ---------------------- + +from django.contrib.gis.db import models +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage +from assets.models.trafficlights import TrafficLightPole + +private_storage = PrivateMediaStorage() + + + +# Public Lighting Street +class PublicLightingStreet(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="publiclighting_streets") + + def get_active_assets(self): + # Retrieve associated poles + poles = self.poles.filter(status='active') + light_fixtures = self.light_fixtures.filter(status='active') + + return { + "poles": poles, + "light_fixtures": light_fixtures, + } + + def get_all_assets(self): + # Retrieve associated poles + poles = self.poles.all() + light_fixtures = self.light_fixtures.all() + + return { + "poles": poles, + "light_fixtures": light_fixtures, + } + + def get_direct_child_assets(self): + # Retrieve associated poles + poles = self.poles.all() + light_fixtures = self.light_fixtures.all() + + return { + "poles": poles, + "light_fixtures": light_fixtures, + } + + def get_absolute_url(self): + return reverse("assets:publiclighting_streets_detail", kwargs={"street_id": self.id}) + + + +TL_DOCUMENT_TYPE_CHOICES = ( + ('as_built', 'As Built'), + ('other', 'Autre'), +) +class PublicLightingStreetDocument(Document): + street = models.ForeignKey(PublicLightingStreet, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Street") + file = models.FileField(upload_to="assets_documents/publiclighting/", storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to="assets_documents/publiclighting/", storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=TL_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") + + + +# Public Lighting Pole Model +class PublicLightingPoleModel(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) + +# Public Lighting Pole +class PublicLightingPole(AbstractGeoAsset): + model = models.ForeignKey(PublicLightingPoleModel, on_delete=models.PROTECT) + street = models.ForeignKey(PublicLightingStreet, related_name="poles", on_delete=models.PROTECT) + + class Meta: + indexes = [ + models.Index(fields=["code"]), + ] + verbose_name = _("Public lighting pole") + verbose_name_plural = _("Public lighting poles") + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + for radar in self.radars.all(): + radar.save() + + def get_absolute_url(self): + return reverse('assets:publiclighting_assets_detail', kwargs={'asset_model': 'publiclightingpole', 'asset_id':self.id}) + + +# Public Lighting Light Fixture Model (Luminaire) +class PublicLightingLightFixtureModel(AbstractAssetModel): + color = models.CharField(max_length=200, null=True,blank=True) + +# Public Lighting Fixture (Luminaire) +class PublicLightingLightFixture(AbstractGeoAsset): + model = models.ForeignKey(PublicLightingLightFixtureModel, on_delete=models.PROTECT, null=True, blank=True) + pole = models.ForeignKey(PublicLightingPole, related_name="light_fixtures", on_delete=models.SET_NULL, null=True, blank=True) + traffic_light_pole = models.ForeignKey(TrafficLightPole, related_name="light_fixtures", on_delete=models.SET_NULL, null=True, blank=True) + street = models.ForeignKey(PublicLightingStreet, related_name="light_fixtures", on_delete=models.PROTECT) + + class Meta: + indexes = [ + models.Index(fields=["code"]), + ] + + verbose_name = _("Luminaire") + verbose_name_plural = _("Luminaires") + + def get_absolute_url(self): + return reverse('assets:publiclighting_assets_detail', kwargs={'asset_model': 'publiclightinglightfixture', 'asset_id':self.id}) + + +# Public Lighting Electrical Cabinet Model +class PublicLightingElectricalCabinetModel(AbstractAssetModel): + width = models.FloatField(null=True, blank=True, verbose_name="Largeur (cm)") + height = models.FloatField(null=True, blank=True, verbose_name="Hauteur (cm)") + depth = models.FloatField(null=True, blank=True, verbose_name="Profondeur (cm)") + ip_rating = models.CharField(max_length=10, null=True, blank=True, verbose_name="Indice de protection IP") + + +# Public Lighting Electrical Cabinet +class PublicLightingElectricalCabinet(AbstractGeoAsset): + model = models.ForeignKey(PublicLightingElectricalCabinetModel, on_delete=models.PROTECT) + street = models.ForeignKey(PublicLightingStreet, related_name="electrical_cabinets", on_delete=models.PROTECT) + ean_code = models.CharField(max_length=50, null=True, blank=True, verbose_name="Code EAN") + power_supply = models.CharField(max_length=100, null=True, blank=True, verbose_name="Alimentation électrique") + meter_number = models.CharField(max_length=100, null=True, blank=True, verbose_name="Numéro de compteur") + number_of_circuits = models.IntegerField(null=True, blank=True, verbose_name="Nombre de circuits") + + class Meta: + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:publiclighting_assets_detail', kwargs={'asset_model': 'publiclightingelectricalcabinet', 'asset_id':self.id}) \ No newline at end of file diff --git a/streetup/assets/models/roads.py b/streetup/assets/models/roads.py new file mode 100644 index 0000000..04d5fdf --- /dev/null +++ b/streetup/assets/models/roads.py @@ -0,0 +1,330 @@ +# ---------------------- +# Roads Models +# ---------------------- + +from django.contrib.gis.db import models +from django.contrib.postgres.fields import ArrayField +from django.urls import reverse +from django.utils.text import slugify +from django.utils.translation import get_language +from django.utils.translation import gettext_lazy as _ + +import os + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage + +from observations.models import Observation + +private_storage = PrivateMediaStorage() + + + +# Road Street +class RoadStreet(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="road_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): + roadways = self.roadways.filter(status='active') + sidewalks = self.sidewalks.filter(status='active') + furnitures = self.furnitures.filter(status='active') + + return { + "roadways": roadways, + "sidewalks": sidewalks, + "furnitures": furnitures, + } + + def get_all_assets(self): + roadways = self.roadways.all() + sidewalks = self.sidewalks.all() + furnitures = self.furnitures.all() + + return { + "roadways": roadways, + "sidewalks": sidewalks, + "furnitures": furnitures, + } + + def get_direct_child_assets(self): + roadways = self.roadways.all() + sidewalks = self.sidewalks.all() + furnitures = self.furnitures.all() + + return { + "roadways": roadways, + "sidewalks": sidewalks, + "furnitures": furnitures, + } + + def get_absolute_url(self): + return reverse("assets:roads_streets_detail", kwargs={"street_id": self.id}) + + +def upload_to_roadstreet(instance, filename): + location = instance.street + code = slugify(location.code or "unknown") # Fallback si code est None + return os.path.join("assets_documents", "roads", code, filename) + +ROAD_DOCUMENT_TYPE_CHOICES = ( + ('as_built', 'As Built'), + ('other', 'Autre'), +) +class RoadStreetDocument(Document): + street = models.ForeignKey(RoadStreet, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Street") + file = models.FileField(upload_to=upload_to_roadstreet, storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to=upload_to_roadstreet, storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=ROAD_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") + + + + +QUALITY_LEVEL_CHOICES = ( + (1, '1 - Très mauvais'), + (2, '2 - Mauvais'), + (3, '3 - Bon'), + (4, '4 - Très bon'), + (5, '5 - Excellent'), +) + + + +# Roadway Model +class RoadwayModel(AbstractAssetModel): + speed_limit = models.IntegerField(null=True,blank=True) + + +ROAD_COATING_TYPE_CHOICES = ( + ('asphalt', 'Asphalt'), + ('concrete', 'Concrete'), + ('cobblestone', 'Cobblestone'), + ('gravel', 'Gravel'), + ('dirt', 'Dirt'), + ('other', 'Other'), +) + + +# Roadway +class Roadway(AbstractGeoAsset): + coating_type = models.CharField(max_length=200, null=True,blank=True, choices=ROAD_COATING_TYPE_CHOICES) + model = models.ForeignKey(RoadwayModel, on_delete=models.SET_NULL, null=True, blank=True) + street = models.ForeignKey(RoadStreet, related_name="roadways", on_delete=models.PROTECT) + quality_level = models.IntegerField(null=True, blank=True, choices=QUALITY_LEVEL_CHOICES) + lanes = models.IntegerField(null=True, blank=True) + area = models.FloatField(null=True, blank=True) + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + + class Meta: + verbose_name = "Roadway" + verbose_name_plural = "Roadways" + + def get_absolute_url(self): + return reverse( + "assets:roads_assets_detail", + kwargs={"asset_model": self._meta.model_name, "asset_id": self.id}, + ) + + + +# Sidewalk Model +class RoadSidewalkModel(AbstractAssetModel): + pass + + +SW_COATING_TYPE_CHOICES = ( + ('concrete', 'Concrete'), + ('paving_stones', 'Paving Stones'), + ('tiles20', 'Tiles 20x20'), + ('tiles30', 'Tiles 30x30'), + ('tiles40', 'Tiles 40x40'), + ('tiles50', 'Tiles 50x50'), + ('tiles', 'Tiles'), + ('asphalt', 'Asphalt'), + ('dolomite','Dolomie'), + ('gravel', 'Gravel'), + ('dirt', 'Dirt'), + ('other', 'Other'), +) + +# Sidewalk +class RoadSidewalk(AbstractGeoAsset): + coating_type = models.CharField(max_length=200, null=True,blank=True, choices=SW_COATING_TYPE_CHOICES) + model = models.ForeignKey(RoadSidewalkModel, on_delete=models.SET_NULL, null=True, blank=True) + street = models.ForeignKey(RoadStreet, related_name="sidewalks", on_delete=models.PROTECT) + quality_level = models.IntegerField(null=True, blank=True, choices=QUALITY_LEVEL_CHOICES) + area = models.FloatField(null=True, blank=True) + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + + class Meta: + verbose_name = "Sidewalk" + verbose_name_plural = "Sidewalks" + + def get_absolute_url(self): + return reverse( + "assets:roads_assets_detail", + kwargs={"asset_model": self._meta.model_name, "asset_id": self.id}, + ) + + +# Street Furniture Model +class RoadFurnitureModel(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) + width = models.FloatField(null=True,blank=True) + material = models.CharField(max_length=200, null=True,blank=True) + + +# Street Furniture +class RoadFurniture(AbstractGeoAsset): + model = models.ForeignKey(RoadFurnitureModel, on_delete=models.SET_NULL, null=True, blank=True) + street = models.ForeignKey(RoadStreet, related_name="furnitures", on_delete=models.PROTECT) + + class Meta: + verbose_name = "Street Furniture" + verbose_name_plural = "Street Furnitures" + + def get_absolute_url(self): + return reverse( + "assets:roads_assets_detail", + kwargs={"asset_model": self._meta.model_name, "asset_id": self.id}, + ) + + + + +ROAD_FUNCTION_CHOICES = ( + ('masterful', 'Magistrale'), + ('plus', 'Plus'), + ('confort', 'Confort'), + ('other', 'Autre'), +) + +# Roadway Functions +class RoadwayFunction(models.Model): + roadway = models.ForeignKey(Roadway, on_delete=models.CASCADE, related_name="functions") + pedestrian_function = models.CharField(max_length=200, null=True, blank=True, choices=ROAD_FUNCTION_CHOICES) + bicycle_function = models.CharField(max_length=200, null=True, blank=True, choices=ROAD_FUNCTION_CHOICES) + public_transport_function = models.CharField(max_length=200, null=True, blank=True, choices=ROAD_FUNCTION_CHOICES) + private_motorized_function = models.CharField(max_length=200, null=True, blank=True, choices=ROAD_FUNCTION_CHOICES) + + class Meta: + verbose_name = "Roadway Function" + verbose_name_plural = "Roadway Functions" + + + + +CONCRETE_BLOCK_FUNCTION_CHOICES = ( + ('protection', _('Protection Asset')), + ('antiterrorist', _('Antiterroriste')), + ('parking_neutralization', _('Neutralisation stationnement')), + ('road_safety', _('Sécurité routière')), + ('public_space', _('Espace public')), + ('construction', _('Chantier')), + ('unidentified', _('Non identifié')), +) + +CONCRETE_BLOCK_ACTION_CHOICES = ( + ('keep', _('Maintenir')), + ('remove', _('Enlever')), + ('replace', _('Remplacer')), +) + +# Road Concrete blocks Set +class RoadConcreteBlockSet(AbstractGeoAsset): + address = models.CharField(max_length=2048, null=True, blank=True) + main_function = models.CharField(max_length=200, null=True, blank=True, choices=CONCRETE_BLOCK_FUNCTION_CHOICES) + secondary_functions = ArrayField(models.CharField(max_length=200, choices=CONCRETE_BLOCK_FUNCTION_CHOICES), null=True, blank=True) + number_of_blocks = models.IntegerField(null=True, blank=True) + length = models.FloatField(null=True, blank=True) + recommended_action = models.CharField(max_length=2048, null=True, blank=True, choices=CONCRETE_BLOCK_ACTION_CHOICES) + priority_level = models.IntegerField(null=True, blank=True) + targeted_action_date = models.DateField(null=True, blank=True) + observation_source = models.ForeignKey(Observation, on_delete=models.SET_NULL, null=True, blank=True, related_name="concrete_block_sets") + + def get_absolute_url(self): + return reverse( + 'assets:roads_assets_detail', + kwargs={'asset_model': self._meta.model_name, 'asset_id': self.id}, + ) + + def __str__(self): + return self.address if self.address else _("Road Concrete Block Set") + + + + +DRAIN_SOIL_TYPE_CHOICES = ( + ('concrete', _('Béton')), + ('asphalt', _('Asphalte')), + ('stone', _('Pierre')), + ('concrete_slabs', _('Dalles en béton')), + ('concrete_paving', _('Pavés en béton')), + ('stone_slabs', _('Dalles en pierre')), + ('stone_paving', _('Pavés en pierre')), + ('concrete_grass', _('Béton/Gazon')), + ('gravel', _('Gravier')), + ('grass', _('Gazon')), + ('earth', _('Terre')), + ('other', _('Autre')), + ('na', _('N/A')), +) + +DRAIN_LOCATION_TYPE_CHOICES = ( + ('waterway', _("Filet d'eau")), + ('sidewalk', _('Trottoir')), + ('berme', _('Berme')), + ('sidewalk_facade_to_road', _('Trottoir façade vers chaussée')), + ('square', _('Place')), + ('along_sidewalk', _('Le long du trottoir')), + ('middle_road', _('Milieu chaussée')), + ('other', _('Autre')), + ('na', _('N/A')), +) + +DRAIN_DEFECT_TYPE_CHOICES = ( + ('obstructed', _('Obstrué')), + ('smell_cut_missing', _('Coupe-odeur manquant')), + ('grate_broken', _('Grille cassée')), + ('grate_missing', _('Grille manquante')), + ('poor_water_flow', _('Mauvais écoulement')), + ('other', _('Autre')), +) + + +# Road Drain Model +class RoadDrainModel(AbstractAssetModel): + pass + +# Road Drain +class RoadDrain(AbstractGeoAsset): + street = models.ForeignKey(RoadStreet, related_name="drains", on_delete=models.SET_NULL, null=True, blank=True) + model = models.ForeignKey(RoadDrainModel, on_delete=models.SET_NULL, null=True, blank=True) + soil_type = models.CharField(max_length=200, null=True, blank=True, choices=DRAIN_SOIL_TYPE_CHOICES) + positionning_source = models.CharField(max_length=2048, null=True, blank=True) + positionning_note = models.TextField(null=True, blank=True) + positionning_date = models.DateField(null=True, blank=True) + drain_location_type = models.CharField(max_length=200, null=True, blank=True, choices=DRAIN_LOCATION_TYPE_CHOICES) + nearby_vegetation = models.BooleanField(null=True, blank=True) + possible_rooting = models.BooleanField(null=True, blank=True) + is_defect = models.BooleanField(null=True, blank=True) + defect_types = ArrayField(models.CharField(max_length=200, choices=DRAIN_DEFECT_TYPE_CHOICES), null=True, blank=True) + defect_note = models.TextField(null=True, blank=True) + related_image_type = models.CharField(max_length=1000, null=True, blank=True) + altitude = models.FloatField(null=True, blank=True) + geom = models.PointField(srid=3812, null=True, blank=True, dim=3) + + def get_absolute_url(self): + return reverse( + 'assets:roads_assets_detail', + kwargs={'asset_model': self._meta.model_name, 'asset_id': self.id}, + ) + + def __str__(self): + return self.code if self.code else _("Road Drain") \ No newline at end of file diff --git a/streetup/assets/models/sign.py b/streetup/assets/models/sign.py new file mode 100644 index 0000000..f47c86c --- /dev/null +++ b/streetup/assets/models/sign.py @@ -0,0 +1,271 @@ +# ---------------------- +# 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 ''}" \ No newline at end of file diff --git a/streetup/assets/models/structures.py b/streetup/assets/models/structures.py new file mode 100644 index 0000000..981abdb --- /dev/null +++ b/streetup/assets/models/structures.py @@ -0,0 +1,180 @@ +# ----------------------------- +# Engineering Structures 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.core.exceptions import ValidationError + +import os + + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() + + + +STRUCTURE_TYPE_CHOICES = ( + ('tunnel', 'Tunnnel'), + ('bridge', 'Pont'), + ('viaduct', 'Viaduc'), + ('overpass', 'Passerelle'), + ('underpass', 'Passage souterrain'), + ('gallery', 'Galerie technique'), + ('atypical', 'Ouvrage atypique'), +) + + +# Structure locations +class StructureLocation(AbstractLocation): + geom = models.MultiPolygonField(srid=3812, null=True,blank=True) + parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations") + is_main_location = models.BooleanField(default=False, verbose_name="Main Location") + structure = models.ForeignKey('Structure', on_delete=models.SET_NULL, null=True, blank=True, related_name="all_locations") + + def get_active_assets(self): + def collect_assets(location): + assets = list(location.assets.filter(status='active')) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_all_assets(self): + def collect_assets(location): + assets = list(location.assets.all()) + for child in location.child_locations.all(): + assets += collect_assets(child) + return assets + + return collect_assets(self) + + def get_direct_child_assets(self): + return self.assets.all() + + def clean(self): + if self.is_main_location and StructureLocation.objects.filter(structure=self.structure, is_main_location=True).exclude(pk=self.pk).exists(): + raise ValidationError("Cette structure a déjà une localisation principale.") + + def get_absolute_url(self): + return reverse( + 'assets:structures_locations_detail', + kwargs={ + # 'asset_model': content_type.model, + 'location_id': self.id + } + ) + + + class Meta: + verbose_name = "Structure Location" + verbose_name_plural = "Structure Locations" + ordering = ["code"] + + +class Structure(models.Model): + code = models.CharField(max_length=50, unique=True) + name_fr = models.CharField(max_length=200,null=True,blank=True) + name_nl = models.CharField(max_length=200,null=True,blank=True) + structure_type = models.CharField(max_length=200, choices=STRUCTURE_TYPE_CHOICES, null=True, blank=True) + main_location = models.OneToOneField(StructureLocation, on_delete=models.SET_NULL, null=True, blank=True, related_name="main_structure") + description = models.TextField(null=True, blank=True) + linked_folder = models.CharField(max_length=1024, null=True, blank=True) + structure_number = models.IntegerField(null=True, blank=True, verbose_name="Numéro de l'ouvrage") + + def get_name(self): + lang = get_language() + name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr + return name + + def get_active_assets(self): + return self.assets.filter(status='active') + + def get_all_assets(self): + return self.assets.all() + + + def get_all_locations(self): + return self.all_locations.all() + + def get_main_location(self): + return self.main_location + + def __str__(self): + return self.code + " - " + self.get_name() + + class Meta: + ordering = ["code"] + + +def upload_to_structure(instance, filename): + location = instance.structure + code = slugify(location.code or "unknown") # Fallback si code est None + return os.path.join("assets_documents", "structures", code, filename) + +TU_DOCUMENT_TYPE_CHOICES = ( + ('as_built', 'As Built'), + ('cme', "CME"), + ('inspection_ce', 'Inspection G.C.'), + ('inspection_em', 'Inspection E.M.'), + ('pis', 'PIS'), + ('other', 'Autre'), +) + +# Structure Location Document +class StructureDocument(Document): + structure = models.ForeignKey(Structure, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Structure") + file = models.FileField(upload_to=upload_to_structure, storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to=upload_to_structure, storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=TU_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") + + +SC_MAINTENANCE_TYPE_CHOICES = ( + ('electromeca', 'Eléctromécanique'), + ('civil_engineering', 'Génie civil'), + ('urgent_cleaning', 'Nettoyage urgent'), + ('other', 'Autre'), +) + +class StructureContract(models.Model): + structure = models.ForeignKey(Structure, on_delete=models.CASCADE, related_name="contracts") + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="structure_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=SC_MAINTENANCE_TYPE_CHOICES, default='other') + + def __str__(self): + return f"{self.structure.code} - {self.contract.contract_number}" + + class Meta: + verbose_name = "Structure Contract" + verbose_name_plural = "Structure Contracts" + unique_together = ('structure', 'contract') + + +class StructureAssetModel(AbstractAssetModel): + class Meta: + verbose_name = "Structure Asset Model" + verbose_name_plural = "Structure Asset Models" + + +class StructureGeoAsset(AbstractGeoAsset): + model = models.ForeignKey(StructureAssetModel, on_delete=models.PROTECT) + location = models.ForeignKey(StructureLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True) + structure = models.ForeignKey(Structure, on_delete=models.SET_NULL, related_name="assets", null=True, blank=True) + parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets") + + class Meta: + indexes = [ + models.Index(fields=["code"]), + ] + + def get_absolute_url(self): + return reverse('assets:structures_assets_detail', kwargs={'asset_model': 'structuregeoasset', 'asset_id':self.id}) + + diff --git a/streetup/assets/models/trafficlights.py b/streetup/assets/models/trafficlights.py new file mode 100644 index 0000000..9ca2326 --- /dev/null +++ b/streetup/assets/models/trafficlights.py @@ -0,0 +1,632 @@ +# --------------------- +# Traffic Lights Models +# --------------------- + +from django.contrib.gis.db import models +from django.urls import reverse +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext_lazy as _ +from django.contrib.postgres.fields import ArrayField + +from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel +from common.models import Municipality, Document +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() + +TL_INTERSECTION_STATUS_CHOICES = { + 'active': _('Actif'), + 'in_preparation': _('En préparation'), + 'under_repair': _('En cours de réparation'), + 'voluntarily_out_of_service': _('Mis hors service volontairement'), + 'inactive': _('Désactivé'), + 'archived': _('Archivé'), +} + +TL_VOLTAGE_CHOICES = { + '10V': '10 Volts', + '230V': '230 Volts', + 'LED': 'LED', +} + + +class TrafficLightAbstractAsset(AbstractAsset): + def get_absolute_url(self): + content_type = ContentType.objects.get_for_model(self.__class__) + return reverse( + 'assets:trafficlights_assets_detail', + kwargs={ + 'asset_model': content_type.model, + 'asset_id': self.id + } + ) + + class Meta: + abstract = True + + +# Traffic Light Abstract Geo Asset +class TrafficLightAbstractGeoAsset(AbstractGeoAsset): + def get_absolute_url(self): + content_type = ContentType.objects.get_for_model(self.__class__) + return reverse( + 'assets:trafficlights_assets_detail', + kwargs={ + 'asset_model': content_type.model, + 'asset_id': self.id + } + ) + + class Meta: + abstract = True + + +# Traffic Light Intersection (crossroads, ...) +class TrafficLightIntersection(AbstractLocation): + geom = models.PointField(srid=3812, null=True,blank=True) + voltage_type = models.CharField(choices=TL_VOLTAGE_CHOICES, null=True,blank=True) + status = models.CharField(choices=TL_INTERSECTION_STATUS_CHOICES, default='active') + begin_time = models.DateTimeField(null=True,blank=True) + master = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, related_name="slaves") + municipality = models.ForeignKey(Municipality, on_delete=models.SET_NULL, null=True, blank=True, related_name="trafficlights_intersections") + current_index = models.CharField(max_length=100, null=True, blank=True) + is_radar = models.BooleanField(default=False, verbose_name=_("Est un radar ?")) + + def get_active_assets(self): + # Retrieve associated poles + poles = list(self.poles.filter(status='active')) + radar_poles = list(self.radar_poles.filter(status='active')) + all_poles = poles + radar_poles + all_poles.sort(key=lambda p: p.code) + + # Retrieve associated cables + cables = self.cables.filter(status='active').order_by('code') + + # Retrieve associated lanterns via poles + lanterns = TrafficLightLantern.objects.filter(pole__intersection=self).filter(status='active').order_by('code') + + # Retrieve associated controllers + controllers = self.controllers.filter(status='active').order_by('code') + + # Retrieve associated detectors + detectors = self.detectors.filter(status='active').order_by('code') + + # Retrieve associated electrical cabinets + electrical_cabinets = self.electrical_cabinets.filter(status='active').order_by('code') + + # Retrieve associated radars + radars = self.radars.filter(status='active').order_by('code') + + return { + "poles": all_poles, + "cables": cables, + "lanterns": lanterns, + "controllers": controllers, + "detectors": detectors, + "electrical_cabinets": electrical_cabinets, + "radars": radars, + } + + def get_all_assets(self): + # Retrieve associated poles + poles = list(self.poles.all()) + radar_poles = list(self.radar_poles.all()) + all_poles = poles + radar_poles + all_poles.sort(key=lambda p: p.code) + + # Retrieve associated cables + cables = self.cables.all().order_by('code') + + # Retrieve associated lanterns via poles + lanterns = TrafficLightLantern.objects.filter(pole__intersection=self).all().order_by('code') + + # Retrieve associated controllers + controllers = self.controllers.all().order_by('code') + + # Retrieve associated detectors + detectors = self.detectors.all().order_by('code') + + # Retrieve associated electrical cabinets + electrical_cabinets = self.electrical_cabinets.all().order_by('code') + + # Retrieve associated radars + radars = self.radars.all().order_by('code') + + return { + "poles": all_poles, + "cables": cables, + "lanterns": lanterns, + "controllers": controllers, + "detectors": detectors, + "electrical_cabinets": electrical_cabinets, + "radars": radars, + } + + def get_direct_child_assets(self): + # Retrieve associated poles + poles = list(self.poles.all()) + radar_poles = list(self.radar_poles.all()) + all_poles = poles + radar_poles + all_poles.sort(key=lambda p: p.code) + + # Retrieve associated cables + cables = self.cables.all().order_by('code') + + # Retrieve associated lanterns via poles + lanterns = TrafficLightLantern.objects.filter(pole__intersection=self).all().order_by('code') + + # Retrieve associated controllers + controllers = self.controllers.all().order_by('code') + + # Retrieve associated detectors + detectors = self.detectors.all().order_by('code') + + # Retrieve associated electrical cabinets + electrical_cabinets = self.electrical_cabinets.all().order_by('code') + + # Retrieve associated radars + radars = self.radars.all().order_by('code') + + return { + "poles": all_poles, + "cables": cables, + "lanterns": lanterns, + "controllers": controllers, + "detectors": detectors, + "electrical_cabinets": electrical_cabinets, + "radars": radars, + } + + def get_controller_contract(self): + """ + Get the maintenance contract of the most recently installed active controller. + """ + latest_controller = self.controllers.filter(status='active').order_by('-installation_date').first() + return latest_controller.controller_maintenance_contract if latest_controller else None + + @property + def is_connected_to_control_center(self): + """ + Is the most recently installed active controller connected to the control center. + """ + latest_controller = self.controllers.filter(status='active').order_by('-installation_date').first() + return latest_controller.connected_to_control_center if latest_controller else None + + def get_absolute_url(self): + return reverse("assets:trafficlights_intersections_detail", kwargs={"intersection_id": self.id}) + + + +TL_DOCUMENT_TYPE_CHOICES = ( + ('as_built', 'As Built'), + ('cross_plan', _('Plan de croix')), + ('other', _('Autre')), + ('programming_file', _('Fichier de programmation')), + ('connexion_plan', _('Plan de raccordement')), +) +class TrafficLightIntersectionDocument(Document): + DOCUMENT_MANAGEMENT_SYNC = True + + intersection = models.ForeignKey(TrafficLightIntersection, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Intersection") + programming = models.ForeignKey("TrafficLightProgramming", on_delete=models.SET_NULL, null=True, blank=True, related_name="documents", verbose_name=_("Programmation associée")) + file = models.FileField(upload_to="assets_documents/traffic_lights/", storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to="assets_documents/thumbnails/traffic_lights/", storage=private_storage, null=True, blank=True, max_length=255) + document_type = models.CharField(max_length=50, choices=TL_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document") + + def save(self, *args, **kwargs): + from assets.views.trafficlights import ensure_intersection_document_folder, generate_dxf_overlay_managed_document + + # Determine if it's a new or changed file + is_dxf = self.file and self.file.name.lower().endswith('.dxf') + + # Ensure the intersection folder exists + folder = ensure_intersection_document_folder(self.intersection) + + # Call the parent save method + super().save(*args, **kwargs) + + # Link the managed document to the intersection folder so it appears in the documents tree + if self.managed_document and folder: + self.managed_document.folders.add(folder) + + # If it's a DXF, generate/update overlay in document management + if is_dxf and self.managed_document and not getattr(self, 'skip_overlay_generation', False): + try: + generate_dxf_overlay_managed_document(self.managed_document, user=self.uploaded_by) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error generating DXF overlay ManagedDocument: {e}") + + + +class TrafficLightContract(models.Model): + intersection = models.ForeignKey(TrafficLightIntersection, on_delete=models.CASCADE, related_name="trafficlight_contracts") + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="trafficlight_contracts") + status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') + maintenance_type = models.CharField(max_length=50, choices=[('infra', 'Infrastructure'), ('controller', 'Contrôleur de traffic'), ('other', 'Autre')], default='other') + + def __str__(self): + return f"{self.intersection.code} - {self.contract.contract_number}" + + class Meta: + verbose_name = "Traffic Lights Contract" + verbose_name_plural = "Traffic Lights Contracts" + unique_together = ('intersection', 'contract') + + + + +# Traffic Light Pole Model +class TrafficLightPoleModel(AbstractAssetModel): + colour = models.CharField(max_length=200, null=True,blank=True, verbose_name=_("Couleur")) + ral = models.CharField(max_length=200, null=True,blank=True, verbose_name=_("RAL")) + height = models.FloatField(null=True,blank=True, verbose_name=_("Hauteur (m)")) + +# Traffic Light Pole +class TrafficLightPole(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightPoleModel, on_delete=models.PROTECT) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="poles", on_delete=models.PROTECT) + geom = models.PointField(srid=3812, null=True,blank=True) + arc_length = models.FloatField(null=True, blank=True, verbose_name=_("Longueur de l'arc (m)")) + extension = models.BooleanField(default=False, verbose_name=_("Extension")) + colour = models.CharField(max_length=200, null=True,blank=True, verbose_name=_("Couleur")) + ral = models.CharField(max_length=200, null=True,blank=True, verbose_name=_("RAL")) + + @property + def sorted_lanterns(self): + return self.lanterns.order_by('code') + + @property + def sorted_accessories(self): + return self.accessories.order_by('code') + + @property + def sorted_radars(self): + return self.radars.order_by('code') + + @property + def sorted_detectors(self): + return self.detectors.order_by('code') + + @property + def sorted_signpanels(self): + return self.signpanels_attached.order_by('code') + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + for radar in self.radars.all(): + radar.save() + + class Meta: + verbose_name = _("Traffic light pole") + verbose_name_plural = _("Traffic light poles") + + +# Traffic Light Cable Model +class TrafficLightCableModel(AbstractAssetModel): + number_of_strands = models.IntegerField(verbose_name=_("Nombre de brins")) + +# Traffic Light Cable +class TrafficLightCable(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightCableModel, on_delete=models.PROTECT) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="cables", on_delete=models.PROTECT) + geom = models.LineStringField(srid=3812, null=True,blank=True) + + class Meta: + verbose_name = _("Câble") + verbose_name_plural = _("Câbles") + + +# Traffic Light Lantern Model +class TrafficLightLanternModel(AbstractAssetModel): + voltage = models.IntegerField(verbose_name=_("Tension (V)")) + +# Traffic Light Lantern +class TrafficLightLantern(TrafficLightAbstractAsset): + model = models.ForeignKey(TrafficLightLanternModel, on_delete=models.PROTECT) + pole = models.ForeignKey(TrafficLightPole, related_name="lanterns", on_delete=models.PROTECT) + orientation = models.FloatField(null=True, blank=True) + + class Meta: + verbose_name = _("Lanterne") + verbose_name_plural = _("Lanternes") + + +# Traffic Light Controller Model +class TrafficLightControllerModel(AbstractAssetModel): + pass + +# Traffic Light Controller +class TrafficLightController(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightControllerModel, on_delete=models.PROTECT) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="controllers", on_delete=models.PROTECT) + connected_to_control_center = models.BooleanField(default=False, verbose_name=_("Connecté à la centrale de contrôle")) + controller_maintenance_contract = models.ForeignKey("contracts.Contract", on_delete=models.SET_NULL, null=True, blank=True) + plan = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Plan")) + cabinet_type = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Type")) + power = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Puissance")) + firmware_version = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Version du firmware")) + rms = models.BooleanField(default=True, verbose_name=_("RMS")) + url = models.URLField(null=True, blank=True, verbose_name=_("URL")) + + @property + def sorted_hardwares(self): + return self.hardwares.order_by('code') + + @property + def sorted_detectors(self): + return self.detectors.order_by('code') + + @property + def sorted_network_hardwares(self): + return self.network_hardwares.order_by('code') + + @property + def sorted_voltage_hardwares(self): + return self.voltage_hardwares.order_by('code') + + class Meta: + verbose_name = _("Contrôleur") + verbose_name_plural = _("Contrôleurs") + + +# Traffic Light Controller Hardware +class TrafficLightControllerHardware(TrafficLightAbstractGeoAsset): + controller = models.ForeignKey(TrafficLightController, related_name="hardwares", on_delete=models.CASCADE) + hardware = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Hardware")) + name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Nom du matériel")) + description = models.TextField(null=True, blank=True, verbose_name=_("Description du matériel")) + + class Meta: + verbose_name = _("Hardware contrôleur") + verbose_name_plural = _("Hardwares contrôleur") + + +# Traffic Light Controller Log File +class TrafficLightControllerLogFile(models.Model): + controller = models.ForeignKey(TrafficLightController, related_name="logs", on_delete=models.SET_NULL, null=True, blank=True) + file = models.FileField(upload_to="assets/traffic_lights/controller_logs/", storage=private_storage, verbose_name="Fichier de log", max_length=255) + filedate = models.DateTimeField(verbose_name=_("Date du log"), null=True, blank=True) + + class Meta: + verbose_name = _("Fichier de log du contrôleur") + verbose_name_plural = _("Fichiers de log du contrôleur") + + +# Traffic Light Detector Model +class TrafficLightDetectorModel(AbstractAssetModel): + pass + +# Traffic Light Detector +class TrafficLightDetector(TrafficLightAbstractGeoAsset): + name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Nom du détecteur")) + hardware = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Hardware")) + model = models.ForeignKey(TrafficLightDetectorModel, on_delete=models.PROTECT) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="detectors", on_delete=models.PROTECT) + controller = models.ForeignKey(TrafficLightController, related_name="detectors", on_delete=models.PROTECT, null=True, blank=True) + pole = models.ForeignKey(TrafficLightPole, related_name="detectors", on_delete=models.PROTECT, null=True, blank=True) + phases = ArrayField(models.CharField(max_length=50), null=True, blank=True, verbose_name=_("Phases associées")) + mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan")) + + class Meta: + verbose_name = _("Détecteur") + verbose_name_plural = _("Détecteurs") + + +# Traffic Light Network Hardware +class TrafficLightNetworkHardware(TrafficLightAbstractGeoAsset): + name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Nom du matériel")) + hardware = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Hardware")) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="network_hardwares", on_delete=models.PROTECT) + controller = models.ForeignKey(TrafficLightController, related_name="network_hardwares", on_delete=models.PROTECT, null=True, blank=True) + mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan")) + + def __str__(self): + return self.name or f"Matériel {self.id}" + + class Meta: + verbose_name = _("Network Hardware") + verbose_name_plural = _("Network Hardwares") + + +# Traffic Light Voltage Hardware +class TrafficLightVoltageHardware(TrafficLightAbstractGeoAsset): + name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Nom du matériel")) + hardware = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Hardware")) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="voltage_hardwares", on_delete=models.PROTECT) + controller = models.ForeignKey(TrafficLightController, related_name="voltage_hardwares", on_delete=models.PROTECT, null=True, blank=True) + mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan")) + + def __str__(self): + return self.name or f"Matériel {self.id}" + + class Meta: + verbose_name = _("Voltage Hardware") + verbose_name_plural = _("Voltage Hardwares") + + + +# Traffic Light Electrical Cabinet Model +class TrafficLightElectricalCabinetModel(AbstractAssetModel): + width = models.FloatField(null=True, blank=True, verbose_name=_("Largeur (cm)")) + height = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur (cm)")) + depth = models.FloatField(null=True, blank=True, verbose_name=_("Profondeur (cm)")) + ip_rating = models.CharField(max_length=10, null=True, blank=True, verbose_name=_("Indice de protection IP")) + + +CABINET_TYPE_CHOICES = ( + ('traffic_lights', 'Feux de signalisation'), + ('mixed', 'Mixte'), + ('other', 'Autre'), +) + +KEY_TYPE_CHOICES = ( + ('bks', 'BKS'), + ('bur16', 'BUR 16'), + ('eu4', 'EU 4'), + ('siemens', 'Siemens'), + ('yale5', 'Yale 5'), + ('58335', '58335'), + ('no_key', 'Pas de clé'), + ('other', 'Autre'), +) + +# Traffic Light Electrical Cabinet +class TrafficLightElectricalCabinet(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightElectricalCabinetModel, on_delete=models.SET_NULL, null=True, blank=True) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="electrical_cabinets", on_delete=models.PROTECT) + geom = models.PointField(srid=3812, null=True, blank=True) + ean_code = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Code EAN")) + power_supply = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Alimentation électrique")) + meter_number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Numéro de compteur")) + publicity = models.BooleanField(default=False, verbose_name=_("Publicité")) + key_type = models.CharField(max_length=100, null=True, blank=True, choices=KEY_TYPE_CHOICES, verbose_name=_("Type de clé")) + cabinet_type = models.CharField(max_length=100, null=True, blank=True, choices=CABINET_TYPE_CHOICES, verbose_name=_("Type d'armoire"), default='traffic_lights') + managed_by = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Géré par")) + meter_inside = models.BooleanField(default=False, verbose_name=_("Présence Compteur")) + differential_switch = models.BooleanField(default=False, verbose_name=_("Interrupteur différentiel")) + differential_switch_caliber = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Calibre du disjoncteur différentiel")) + differential_breaker = models.BooleanField(default=False, verbose_name=_("Disjoncteur différentiel")) + differential_breaker_caliber = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Calibre du disjoncteur")) + differential_caliber = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Calibre différentiel")) + departure_towards = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Départ vers")) + visual_conformity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Conformité visuelle")) + cleanliness = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Propreté")) + + class Meta: + verbose_name = _("Armoire") + verbose_name_plural = _("Armoires") + + +class TrafficLightElectricalCabinetContentModel(AbstractAssetModel): + pass + +class TrafficLightElectricalCabinetContent(TrafficLightAbstractGeoAsset): + cabinet = models.ForeignKey(TrafficLightElectricalCabinet, related_name="contents", on_delete=models.CASCADE) + model = models.ForeignKey(TrafficLightElectricalCabinetContentModel, on_delete=models.SET_NULL, null=True, blank=True) + description = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Description du contenu")) + number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Numéro de référence")) + ean = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Code EAN")) + + class Meta: + verbose_name = _("Contenu d'armoire") + verbose_name_plural = _("Contenus d'armoires") + + +# Traffic Light Accessory Model +class TrafficLightAccessoryModel(AbstractAssetModel): + pass + +# Traffic Light Accessory +class TrafficLightAccessory(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightAccessoryModel, on_delete=models.PROTECT, null=True, blank=True) + pole = models.ForeignKey(TrafficLightPole, related_name="accessories", on_delete=models.PROTECT, null=True, blank=True) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="accessories", on_delete=models.PROTECT, null=True, blank=True) + orientation = models.FloatField(null=True, blank=True) + + class Meta: + verbose_name = _("Accessoire") + verbose_name_plural = _("Accessoires") + + + +# Treffic Light Programmation +class TrafficLightProgramming(TrafficLightAbstractAsset): + intersection = models.ForeignKey(TrafficLightIntersection, on_delete=models.CASCADE, related_name="programmations") + commissioning_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de mise en service")) + uncommissioning_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de mise hors service")) + plan = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Plan")) + index = models.CharField(max_length=100, verbose_name=_("Index")) + info = models.TextField(null=True, blank=True, verbose_name=_("Informations")) + + def __str__(self): + return f"{self.index} - {self.intersection.code}" + + class Meta: + ordering = ['-commissioning_date'] + + +# Traffic Light Radar Pole Model +class TrafficLightRadarPoleModel(AbstractAssetModel): + pass + +# Traffic Light Radar Pole +class TrafficLightRadarPole(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightRadarPoleModel, on_delete=models.SET_NULL, null=True, blank=True) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="radar_poles", on_delete=models.PROTECT) + colour = models.CharField(max_length=200, null=True,blank=True, verbose_name=_("Couleur")) + ral = models.CharField(max_length=200, null=True,blank=True, verbose_name=_("RAL")) + + @property + def sorted_lanterns(self): + return [] + + @property + def sorted_accessories(self): + return [] + + @property + def sorted_radars(self): + return self.radars.order_by('code') + + @property + def sorted_signpanels(self): + return [] + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + for radar in self.radars.all(): + radar.save() + + class Meta: + verbose_name = _("Radar Pole") + verbose_name_plural = _("Radar Poles") + +# Traffic Light Radar Model +class TrafficLightRadarModel(AbstractAssetModel): + class Meta: + verbose_name = _("Traffic Light Radar Model") + verbose_name_plural = _("Traffic Light Radar Models") + +# Traffic Light Radar +class TrafficLightRadar(TrafficLightAbstractGeoAsset): + model = models.ForeignKey(TrafficLightRadarModel, on_delete=models.PROTECT) + intersection = models.ForeignKey(TrafficLightIntersection, related_name="radars", on_delete=models.PROTECT) + + # Physical Supports + pole = models.ForeignKey(TrafficLightRadarPole, verbose_name=_("Radar Pole (Support)"), related_name="radars", on_delete=models.SET_NULL, null=True, blank=True) + publiclighting_pole = models.ForeignKey('assets.PublicLightingPole', verbose_name=_("Public Lighting Pole (Support)"), related_name="radars", on_delete=models.SET_NULL, null=True, blank=True) + trafficlights_pole = models.ForeignKey(TrafficLightPole, verbose_name=_("Traffic Light Pole (Support)"), related_name="radars", on_delete=models.SET_NULL, null=True, blank=True) + + # Electrical Connections / Power Sources + power_publiclighting_pole = models.ForeignKey('assets.PublicLightingPole', verbose_name=_("Public Lighting Pole (Power)"), related_name="powered_radars", on_delete=models.SET_NULL, null=True, blank=True) + power_trafficlights_pole = models.ForeignKey(TrafficLightPole, verbose_name=_("Traffic Light Pole (Power)"), related_name="powered_radars", on_delete=models.SET_NULL, null=True, blank=True) + + class Meta: + verbose_name = _("Radar") + verbose_name_plural = _("Radars") + + def clean(self): + super().clean() + support = self.pole or self.publiclighting_pole or self.trafficlights_pole + if support and support.geom: + self.geom = support.geom + if hasattr(support, 'lon'): + self.lon = support.lon + if hasattr(support, 'lat'): + self.lat = support.lat + if hasattr(support, 'geojson'): + self.geojson = support.geojson + + def save(self, *args, **kwargs): + support = self.pole or self.publiclighting_pole or self.trafficlights_pole + if support and support.geom: + self.geom = support.geom + if hasattr(support, 'lon'): + self.lon = support.lon + if hasattr(support, 'lat'): + self.lat = support.lat + if hasattr(support, 'geojson'): + self.geojson = support.geojson + super().save(*args, **kwargs) \ No newline at end of file diff --git a/streetup/assets/permissions.py b/streetup/assets/permissions.py new file mode 100644 index 0000000..44ab4f7 --- /dev/null +++ b/streetup/assets/permissions.py @@ -0,0 +1,558 @@ +# -------------------- +# Asset Permissions +# -------------------- + +from functools import wraps +from django.shortcuts import get_object_or_404 +from django.core.exceptions import PermissionDenied +from django.http import HttpResponseForbidden, JsonResponse +from django.utils.translation import gettext as _ + +from common.models import UserConfig, UserThematics, UserAssetAccess, UserAssetTypeAccess, AssetTypePermissionConfig + + +# Fields editable by role for assets +ASSET_EDITABLE_FIELDS_BY_ROLE = { + 'admin': [ + 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'serial_number', 'installation_date', 'brand', 'warranty_duration', + 'last_inspection_date', 'geom', 'lon', 'lat', + 'location_id', 'intersection_id', + ], + 'manager': [ + 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'serial_number', 'installation_date', 'brand', 'warranty_duration', + 'last_inspection_date', 'geom', 'lon', 'lat', + 'location_id', 'intersection_id', + ], + 'controller': [ + 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'serial_number', 'installation_date', 'brand', 'warranty_duration', + 'last_inspection_date', 'geom', 'lon', 'lat', + 'location_id', 'intersection_id', + ], + 'external_manager': [ + 'name_fr', 'name_nl', 'category', 'brand', 'serial_number' + ], + 'technician': ['name_fr', 'name_nl', 'category', 'brand', 'serial_number'], + 'operator': [], + 'observer': [], + 'viewer': [], + 'editor': [ + 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'serial_number', 'installation_date', 'brand', 'warranty_duration', + 'last_inspection_date', 'geom', 'lon', 'lat', + 'location_id', 'intersection_id', + ], +} + +# Actions allowed by role for assets +ASSET_ACTIONS_BY_ROLE = { + 'admin': ['edit', 'create', 'archive', 'replace', 'bulk_archive'], + 'manager': ['edit', 'create', 'archive', 'replace', 'bulk_archive'], + 'controller': ['edit', 'create', 'archive', 'replace', 'bulk_archive'], + 'external_manager': ['edit', 'create', 'replace'], + 'operator': [], + 'technician': ['edit', 'create', 'replace'], + 'observer': [], + 'viewer': [], + 'editor': ['edit', 'create', 'archive', 'replace'], +} + + +def get_user_asset_thematic_access(user, thematic): + """ + Check if user has access to view/edit assets for a given thematic. + Returns a tuple (can_view, can_edit). + """ + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return False, False + + try: + user_thematic = UserThematics.objects.get( + user_config=user_config, + thematic=thematic + ) + return user_thematic.can_view_assets, user_thematic.can_edit_assets + except UserThematics.DoesNotExist: + return False, False + + +def _check_instance_permission(user, obj, permission_field, thematic_fallback_fn=None): + """ + Shared logic for per-instance permission checks (assets and locations). + + Priority order (mirrors contract permission logic): + 1. Admin role → always True. + 2. Explicit UserAssetAccess row exists → use its value (True or False, no fallback). + 3. No row exists: + a. AssetTypePermissionConfig.requires_explicit_permissions=True → False. + b. Otherwise → call thematic_fallback_fn() (defaults to False if None). + + Args: + user: Django User instance. + obj: Asset or Location model instance. + permission_field: 'can_view', 'can_edit', or 'can_delete'. + thematic_fallback_fn: callable() → bool; used when no explicit row exists + and the type does not require explicit permissions. + """ + from django.contrib.contenttypes.models import ContentType + + if not user or not user.is_authenticated: + return False + + # Step 1: admin bypass + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return False + + if user_config.roles.filter(name='admin').exists(): + return True + + # Step 2: explicit per-instance row + ct = ContentType.objects.get_for_model(obj) + try: + access = UserAssetAccess.objects.get( + user_config=user_config, + content_type=ct, + object_id=obj.pk, + ) + return getattr(access, permission_field, False) + except UserAssetAccess.DoesNotExist: + pass + + # Step 3: per-user type-level access (applies to all instances of this content type) + try: + type_access = UserAssetTypeAccess.objects.get( + user_config=user_config, + content_type=ct, + ) + return getattr(type_access, permission_field, False) + except UserAssetTypeAccess.DoesNotExist: + pass + + # Step 4a: type-level restriction + try: + type_config = AssetTypePermissionConfig.objects.get(content_type=ct) + if type_config.requires_explicit_permissions: + return False + except AssetTypePermissionConfig.DoesNotExist: + pass + + # Step 4b: thematic fallback + if thematic_fallback_fn is not None: + return thematic_fallback_fn() + return False + + +def can_view_asset(user, asset): + """Check if user can view a specific asset.""" + def _fallback(): + thematic = _get_asset_thematic(asset) + if not thematic: + return False + can_view, _ = get_user_asset_thematic_access(user, thematic) + return can_view + + return _check_instance_permission(user, asset, 'can_view', _fallback) + + +def can_edit_asset(user, asset): + """Check if user can edit a specific asset.""" + def _fallback(): + thematic = _get_asset_thematic(asset) + if not thematic: + return False + _, can_edit = get_user_asset_thematic_access(user, thematic) + return can_edit + + return _check_instance_permission(user, asset, 'can_edit', _fallback) + + +def can_delete_asset(user, asset): + """Check if user can delete (archive/remove) a specific asset.""" + def _fallback(): + thematic = _get_asset_thematic(asset) + if not thematic: + return False + _, can_edit = get_user_asset_thematic_access(user, thematic) + # By default, delete follows edit rights at the thematic level + return can_edit + + return _check_instance_permission(user, asset, 'can_delete', _fallback) + + +def can_validate_asset(user, asset): + """Check if user has permission to validate a specific asset.""" + if not user or not user.is_authenticated: + return False + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return False + + if user_config.roles.filter(name='admin').exists(): + return True + + thematic = _get_asset_thematic(asset) + if not thematic: + return False + + try: + ut = UserThematics.objects.get(user_config=user_config, thematic=thematic) + return ut.can_validate_assets + except UserThematics.DoesNotExist: + return False + + +def can_view_location(user, location): + """Check if user can view a specific location.""" + def _fallback(): + thematic = _get_location_thematic(location) + if not thematic: + return False + can_view, _ = get_user_asset_thematic_access(user, thematic) + return can_view + + return _check_instance_permission(user, location, 'can_view', _fallback) + + +def can_edit_location(user, location): + """Check if user can edit a specific location.""" + def _fallback(): + thematic = _get_location_thematic(location) + if not thematic: + return False + try: + user_config = UserConfig.objects.get(user=user) + ut = UserThematics.objects.get(user_config=user_config, thematic=thematic) + return ut.can_edit_locations + except Exception: + return False + + return _check_instance_permission(user, location, 'can_edit', _fallback) + + +def can_delete_location(user, location): + """Check if user can delete a specific location.""" + def _fallback(): + thematic = _get_location_thematic(location) + if not thematic: + return False + try: + user_config = UserConfig.objects.get(user=user) + ut = UserThematics.objects.get(user_config=user_config, thematic=thematic) + return ut.can_edit_locations + except Exception: + return False + + return _check_instance_permission(user, location, 'can_delete', _fallback) + + +def _get_asset_thematic(asset): + """Get the thematic for an asset.""" + from common.models import Thematic + + # Try from category + if hasattr(asset, 'category') and asset.category: + return asset.category.thematic + + # Try from model's category + if hasattr(asset, 'model') and asset.model and hasattr(asset.model, 'category') and asset.model.category: + return asset.model.category.thematic + + # Fallback: deduce thematic from asset class name + class_name = asset.__class__.__name__.lower() + thematic_mappings = { + 'naturerwiasset': 'water', + 'trafficlight': 'trafficlights', + 'publiclighting': 'publiclighting', + 'road': 'roads', + 'artwork': 'artworks', + 'monument': 'artworks', + 'fountain': 'artworks', + 'structure': 'structures', + 'nature': 'nature', + 'green': 'nature', + 'sign': 'sign', + 'controlcenter': 'controlcenters', + 'its': 'its', + } + + for prefix, thematic_code in thematic_mappings.items(): + if class_name.startswith(prefix): + try: + return Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + pass + + return None + + +def _get_location_thematic(location): + """Deduce the thematic for a location instance.""" + from common.models import Thematic + + # RoadStreet, NatureLocation, etc. have a `thematic` attribute via their linked assets/categories, + # but locations are usually tied to a thematic via class name. + class_name = location.__class__.__name__.lower() + thematic_mappings = { + 'naturerwiz': 'water', + 'roadstreet': 'roads', + 'trafficlightintersection': 'trafficlights', + 'naturelocation': 'nature', + 'structurelocation': 'structures', + 'controlcenter': 'controlcenters', + 'itslocation': 'its', + 'cleanlocation': 'clean', + 'publiclightingstreet': 'publiclighting', + 'signstreet': 'sign', + } + thematic_code = thematic_mappings.get(class_name) + if not thematic_code: + for prefix, code in thematic_mappings.items(): + if class_name.startswith(prefix[:5]): + thematic_code = code + break + if not thematic_code: + return None + try: + return Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + return None + + +def get_allowed_update_fields_for_asset(user, asset): + """ + Get the list of fields the user is allowed to update for a specific asset. + + Priority: + 1. If a UserAssetAccess row with non-empty editable_fields exists, that list is returned + as-is (completely replaces role-based logic). + 2. Otherwise uses the existing role-based logic (ASSET_EDITABLE_FIELDS_BY_ROLE). + """ + if not can_edit_asset(user, asset): + return [] + + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return [] + + # Check for per-instance editable_fields override + from django.contrib.contenttypes.models import ContentType + ct = ContentType.objects.get_for_model(asset) + try: + access = UserAssetAccess.objects.get( + user_config=user_config, + content_type=ct, + object_id=asset.pk, + ) + if access.editable_fields: # non-null, non-empty list → use it + return list(access.editable_fields) + except UserAssetAccess.DoesNotExist: + pass + + # Check for per-type editable_fields override (applies to all instances) + try: + type_access = UserAssetTypeAccess.objects.get( + user_config=user_config, + content_type=ct, + ) + if type_access.editable_fields: # non-null, non-empty list → use it + return list(type_access.editable_fields) + except UserAssetTypeAccess.DoesNotExist: + pass + + # Fall back to role-based logic + allowed_fields = set() + for role in user_config.roles.all(): + role_fields = ASSET_EDITABLE_FIELDS_BY_ROLE.get(role.name, []) + allowed_fields.update(role_fields) + + # Check validation permission + if can_validate_asset(user, asset): + allowed_fields.add('validation_status') + + # Also add asset-specific fields (not in base models) when the user already has some edit rights + if allowed_fields and asset is not None: + from django.db.models import ForeignKey, OneToOneField, ManyToManyField, AutoField + from django.contrib.gis.db.models import GeometryField + from assets.models import AbstractAsset, AbstractGeoAsset + + excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField) + base_field_names = {f.name for f in AbstractAsset._meta.fields} + base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields} + base_field_names.add('geojson') + + for field in type(asset)._meta.fields: + if field.name in base_field_names: + continue + if isinstance(field, excluded_types): + continue + allowed_fields.add(field.name) + + return list(allowed_fields) + + +def get_visible_fields_for_asset(user, asset): + """ + Get the list of fields visible to the user for a specific asset. + + Priority (mirrors get_allowed_update_fields_for_asset): + 1. UserAssetAccess.visible_fields (non-empty) → return it. + 2. UserAssetTypeAccess.visible_fields (non-empty) → return it. + 3. No restriction defined → return None (all fields visible). + """ + if not can_view_asset(user, asset): + return [] + + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return None + + from django.contrib.contenttypes.models import ContentType + ct = ContentType.objects.get_for_model(asset) + + # Check per-instance override + try: + access = UserAssetAccess.objects.get( + user_config=user_config, + content_type=ct, + object_id=asset.pk, + ) + if access.visible_fields: + return list(access.visible_fields) + except UserAssetAccess.DoesNotExist: + pass + + # Check per-type override + try: + type_access = UserAssetTypeAccess.objects.get( + user_config=user_config, + content_type=ct, + ) + if type_access.visible_fields: + return list(type_access.visible_fields) + except UserAssetTypeAccess.DoesNotExist: + pass + + # No restriction → all fields visible + return None + + +def get_allowed_actions_for_asset(user, asset): + """ + Get the list of actions the user is allowed to perform on assets. + """ + if not can_edit_asset(user, asset): + return [] + + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return [] + + allowed_actions = set() + for role in user_config.roles.all(): + role_actions = ASSET_ACTIONS_BY_ROLE.get(role.name, []) + allowed_actions.update(role_actions) + + return list(allowed_actions) + + +def get_allowed_actions_for_thematic(user, thematic): + """ + Get the list of actions the user is allowed to perform for a thematic. + """ + can_view, can_edit = get_user_asset_thematic_access(user, thematic) + if not can_edit: + return [] + + try: + user_config = UserConfig.objects.get(user=user) + except UserConfig.DoesNotExist: + return [] + + allowed_actions = set() + for role in user_config.roles.all(): + role_actions = ASSET_ACTIONS_BY_ROLE.get(role.name, []) + allowed_actions.update(role_actions) + + return list(allowed_actions) + + +def asset_edit_permission_required(view_func): + """ + Decorator that checks if user has permission to edit assets. + Expects asset_id and asset_model as URL parameters. + """ + @wraps(view_func) + def wrapper(request, *args, **kwargs): + from django.contrib.contenttypes.models import ContentType + + asset_model = kwargs.get('asset_model') + asset_id = kwargs.get('asset_id') + + if not asset_model or not asset_id: + return HttpResponseForbidden(_("Missing asset information")) + + try: + content_type = ContentType.objects.get(model=asset_model.lower()) + model_class = content_type.model_class() + asset = get_object_or_404(model_class, pk=asset_id) + except ContentType.DoesNotExist: + return HttpResponseForbidden(_("Invalid asset type")) + + if not can_edit_asset(request.user, asset): + return HttpResponseForbidden(_("You do not have permission to edit this asset")) + + return view_func(request, *args, **kwargs) + + return wrapper + + +def can_edit_location_for_thematic(user, thematic_code): + """ + Check if the user has permission to create/edit/delete locations for a given thematic. + Relies on UserThematics.can_edit_locations. + """ + if not user or not user.is_authenticated: + return False + try: + user_config = UserConfig.objects.get(user=user) + from common.models import Thematic + thematic = Thematic.objects.get(code=thematic_code) + user_thematic = UserThematics.objects.get(user_config=user_config, thematic=thematic) + return user_thematic.can_edit_locations + except Exception: + return False + + +def location_asset_edit_permission_required(thematic_code): + """ + Decorator that checks if user has permission to edit assets for a thematic. + Used for location-based asset operations. + """ + def decorator(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + from common.models import Thematic + + try: + thematic = Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + return HttpResponseForbidden(_("Invalid thematic")) + + can_view, can_edit = get_user_asset_thematic_access(request.user, thematic) + if not can_edit: + return HttpResponseForbidden(_("You do not have permission to edit assets for this thematic")) + + return view_func(request, *args, **kwargs) + + return wrapper + return decorator diff --git a/streetup/assets/signals.py b/streetup/assets/signals.py new file mode 100644 index 0000000..b24f09d --- /dev/null +++ b/streetup/assets/signals.py @@ -0,0 +1,227 @@ +""" +Signals for tracking asset modifications. +This module automatically captures changes to assets and stores them in AssetHistory. +""" + +from django.db.models.signals import pre_save, post_save, pre_delete +from django.dispatch import receiver +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.geos import GEOSGeometry +from .models import AssetHistory +from .models.core import AbstractAsset +import json + + +# Small numeric tolerance to ignore insignificant float/geometry differences +NUMERIC_EPSILON = 1e-7 + + +def _serialize_value(value): + """Serialize a value to string for storage""" + if value is None: + return None + + # Handle ForeignKey objects + if hasattr(value, 'pk'): + return str(value.pk) + + # Handle datetime objects + if hasattr(value, 'isoformat'): + return value.isoformat() + + # Handle geometry/GeoJSON + if hasattr(value, 'geojson'): + return value.geojson + + return str(value) + + +def _get_tracked_fields(): + """Return list of fields to track for changes""" + return [ + 'code', + 'name_fr', + 'name_nl', + 'status', + 'installation_date', + 'uninstallation_date', + 'serial_number', + 'warranty_duration', + 'last_inspection_date', + 'last_preventive_date', + 'last_corrective_date', + 'last_survey_date', + 'category', + 'brand', + 'model', + 'location', + 'structure', + 'lon', + 'lat', + 'geom', + 'geojson', + 'replaced_by', + 'marked_as_removed_date', + 'marked_as_removed_by', + ] + + +def _values_are_equivalent(old_value, new_value, field_name): + """Return True if values are effectively equal (tolerance for floats/geometry).""" + # Exact equality or both None/empty + if old_value is None and new_value is None: + return True + if old_value is None or new_value is None: + return False + if old_value == new_value: + return True + + # Floats (lon/lat or other float fields) + if isinstance(old_value, (float, int)) and isinstance(new_value, (float, int)): + return abs(old_value - new_value) < NUMERIC_EPSILON + + # Geometry equality with tolerance + if hasattr(old_value, 'geom_type') and hasattr(new_value, 'geom_type'): + try: + return old_value.equals_exact(new_value, NUMERIC_EPSILON) or old_value.equals(new_value) + except Exception: + # Fallback to WKT comparison if equals_exact fails + return old_value.wkt == new_value.wkt + + # GeoJSON string comparison (try geometry equivalence) + if field_name == 'geojson' and isinstance(old_value, str) and isinstance(new_value, str): + try: + old_geom = GEOSGeometry(old_value) + new_geom = GEOSGeometry(new_value) + return old_geom.equals_exact(new_geom, NUMERIC_EPSILON) or old_geom.equals(new_geom) + except Exception: + return old_value == new_value + + return False + + +def _should_track_asset(instance): + """Check if an asset should be tracked""" + # Check if it's an AbstractAsset subclass + if not isinstance(instance, AbstractAsset): + return False + + # Don't track abstract models + if instance._meta.abstract: + return False + + return True + + +@receiver(pre_save) +def capture_asset_changes(sender, instance, **kwargs): + """ + Capture changes before save. + Store the old state in a temporary attribute for post_save processing. + """ + if not _should_track_asset(instance): + return + + # Only track if instance already exists (not a new creation) + if instance.pk: + try: + old_instance = sender.objects.get(pk=instance.pk) + instance._old_instance = old_instance + except sender.DoesNotExist: + instance._old_instance = None + else: + instance._old_instance = None + + +@receiver(post_save) +def save_asset_changes(sender, instance, created, **kwargs): + """ + Save changes to AssetHistory after save. + """ + if not _should_track_asset(instance): + return + + # Get the user from the request (if available) + # This should be set in the view/API when updating + user = getattr(instance, '_history_user', None) + + if created: + # Track creation + content_type = ContentType.objects.get_for_model(instance) + AssetHistory.objects.create( + content_type=content_type, + object_id=instance.pk, + user=user, + field_name='__created__', + old_value=None, + new_value=_serialize_value(instance.code), + action_type='create' + ) + else: + # Track modifications + old_instance = getattr(instance, '_old_instance', None) + if not old_instance: + return + + content_type = ContentType.objects.get_for_model(instance) + tracked_fields = _get_tracked_fields() + + for field_name in tracked_fields: + # Check if field exists on this model + if not hasattr(instance, field_name): + continue + + old_value = getattr(old_instance, field_name, None) + new_value = getattr(instance, field_name, None) + + # Skip if values are effectively the same (tolerance for floats/geometry) + if _values_are_equivalent(old_value, new_value, field_name): + continue + + # Serialize for comparison + old_serialized = _serialize_value(old_value) + new_serialized = _serialize_value(new_value) + + # Only track if value changed + if old_serialized != new_serialized: + # Determine action type + action_type = 'update' + if field_name == 'status': + action_type = 'status_change' + + AssetHistory.objects.create( + content_type=content_type, + object_id=instance.pk, + user=user, + field_name=field_name, + old_value=old_serialized, + new_value=new_serialized, + action_type=action_type + ) + + # Clean up temporary attribute + if hasattr(instance, '_old_instance'): + delattr(instance, '_old_instance') + + +@receiver(pre_delete) +def track_asset_deletion(sender, instance, **kwargs): + """ + Track asset deletion. + """ + if not _should_track_asset(instance): + return + + # Get the user from the request (if available) + user = getattr(instance, '_history_user', None) + + content_type = ContentType.objects.get_for_model(instance) + AssetHistory.objects.create( + content_type=content_type, + object_id=instance.pk, + user=user, + field_name='__deleted__', + old_value=_serialize_value(instance.code), + new_value=None, + action_type='delete' + ) diff --git a/streetup/assets/static/assets/artwork_fountain_icon.svg b/streetup/assets/static/assets/artwork_fountain_icon.svg new file mode 100644 index 0000000..4842085 --- /dev/null +++ b/streetup/assets/static/assets/artwork_fountain_icon.svg @@ -0,0 +1,25 @@ + diff --git a/streetup/assets/static/assets/artwork_statue_icon.svg b/streetup/assets/static/assets/artwork_statue_icon.svg new file mode 100644 index 0000000..cc472bd --- /dev/null +++ b/streetup/assets/static/assets/artwork_statue_icon.svg @@ -0,0 +1,28 @@ + diff --git a/streetup/assets/static/assets/artworks_detail.js b/streetup/assets/static/assets/artworks_detail.js new file mode 100644 index 0000000..4a9666a --- /dev/null +++ b/streetup/assets/static/assets/artworks_detail.js @@ -0,0 +1,54 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 17, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const pointData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [feature_lon, feature_lat] + }, + properties: { + name: '...' + } + } + ] + }; + + // Add the circle as a GeoJSON source + map.addSource('artwork', { + type: 'geojson', + data: pointData + }); + + map.addLayer({ + 'id': 'artwork', + 'type': 'circle', + 'source': 'artwork', + 'paint': { + 'circle-radius': { + 'base': 1.75, + 'stops': [ + [12, 2], + [22, 300] + ] + }, + 'circle-color': '#0094ff', + 'circle-opacity': 0.7 + } + }); +}); + diff --git a/streetup/assets/static/assets/asset_add_modal.js b/streetup/assets/static/assets/asset_add_modal.js new file mode 100644 index 0000000..d21cdd8 --- /dev/null +++ b/streetup/assets/static/assets/asset_add_modal.js @@ -0,0 +1,634 @@ +/** + * Asset Add Modal - JavaScript handler + * Handles the creation of new assets from modal forms + */ +(function() { + 'use strict'; + + // Initialize all add asset modals on the page + document.addEventListener('DOMContentLoaded', function() { + initializeAddAssetModals(); + }); + + function initializeAddAssetModals() { + const forms = document.querySelectorAll('.add-asset-form'); + forms.forEach(form => { + const modalId = form.closest('.modal').id; + initializeForm(form, modalId); + }); + } + + const categoriesCache = {}; + + function initializeForm(form, modalId) { + const thematicCode = form.dataset.thematic; + const categorySelect = form.querySelector('.asset-category-select'); + const modelSelect = form.querySelector('.asset-model-select'); + const typeSelect = form.querySelector('.asset-type-select'); + const typeContainer = form.querySelector(`#${modalId}TypeContainer`); + const modelRequiredStar = form.querySelector(`#${modalId}ModelRequiredStar`); + const alertContainer = form.querySelector(`#${modalId}Alert`); + const submitBtn = form.querySelector(`#${modalId}Submit`); + + // Fetch and cache categories for this thematic + if (thematicCode && !categoriesCache[thematicCode]) { + fetch(`/assets/${thematicCode}/categories/`) + .then(response => response.json()) + .then(data => { + if (data.categories) { + categoriesCache[thematicCode] = data.categories; + } + }) + .catch(error => { + console.error('Error prefetching categories:', error); + }); + } + + async function onCategoryChange() { + const categoryId = categorySelect.value; + + // Clear & disable model select + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + modelSelect.required = false; + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + + if (!categoryId) { + if (typeContainer) typeContainer.classList.add('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + typeSelect.value = ''; + typeSelect.required = false; + } + return; + } + + // Ensure categories are loaded in cache + if (!categoriesCache[thematicCode]) { + try { + const response = await fetch(`/assets/${thematicCode}/categories/`); + const data = await response.json(); + if (data.categories) { + categoriesCache[thematicCode] = data.categories; + } + } catch (error) { + console.error('Error fetching categories:', error); + } + } + + const categories = categoriesCache[thematicCode] || []; + const categoryData = categories.find(c => c.id == categoryId); + + if (categoryData && categoryData.allowed_models && categoryData.allowed_models.length > 1) { + // Show Type selection + if (typeContainer) typeContainer.classList.remove('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + categoryData.allowed_models.forEach(model => { + const option = document.createElement('option'); + option.value = model.model_name; + option.textContent = model.verbose_name; + option.dataset.isModelRequired = model.is_model_required; + typeSelect.appendChild(option); + }); + typeSelect.value = ''; + typeSelect.required = true; + } + } else { + // Hide Type selection + if (typeContainer) typeContainer.classList.add('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + typeSelect.value = ''; + typeSelect.required = false; + } + + const singleModel = (categoryData && categoryData.allowed_models && categoryData.allowed_models.length === 1) + ? categoryData.allowed_models[0] + : null; + + const selectedType = singleModel ? singleModel.model_name : null; + const isModelRequired = singleModel ? singleModel.is_model_required : false; + + // Load models for this category + loadModelsForCategory(thematicCode, categoryId, modelSelect, selectedType); + + // Set model template requirement + if (isModelRequired) { + if (modelRequiredStar) modelRequiredStar.style.display = ''; + modelSelect.required = true; + } else { + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + modelSelect.required = false; + } + } + } + + // Handle category change + if (categorySelect && modelSelect) { + categorySelect.addEventListener('change', onCategoryChange); + } + + // Handle Type selection change + if (typeSelect) { + typeSelect.addEventListener('change', function() { + const selectedType = this.value; + const categoryId = categorySelect ? categorySelect.value : null; + + // Clear & disable model select + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + modelSelect.required = false; + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + + if (!categoryId || !selectedType) { + return; + } + + // Find the allowed model metadata to check if template is required + const categories = categoriesCache[thematicCode] || []; + const categoryData = categories.find(c => c.id == categoryId); + const allowedModel = categoryData && categoryData.allowed_models + ? categoryData.allowed_models.find(m => m.model_name === selectedType) + : null; + const isModelRequired = allowedModel ? allowedModel.is_model_required : false; + + // Load models filtered by the selected type + loadModelsForCategory(thematicCode, categoryId, modelSelect, selectedType); + + // Set model template requirement + if (isModelRequired) { + if (modelRequiredStar) modelRequiredStar.style.display = ''; + modelSelect.required = true; + } else { + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + modelSelect.required = false; + } + }); + } + + // Handle form submission + form.addEventListener('submit', function(e) { + e.preventDefault(); + submitNewAsset(form, modalId, alertContainer, submitBtn); + }); + + // Reset form when modal is hidden + const modal = document.getElementById(modalId); + if (modal) { + modal.addEventListener('hidden.bs.modal', function() { + resetForm(form, modelSelect, alertContainer); + }); + } + } + + function loadModelsForCategory(thematicCode, categoryId, modelSelect, selectedType = null) { + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + + if (!categoryId) { + modelSelect.innerHTML = ''; + return; + } + + const url = `/assets/${thematicCode}/category/${categoryId}/models/`; + + fetch(url) + .then(response => response.json()) + .then(data => { + modelSelect.innerHTML = ''; + + if (data.models && data.models.length > 0) { + let filteredModels = data.models; + if (selectedType) { + filteredModels = data.models.filter(model => { + const mType = model.model_type ? model.model_type.toLowerCase() : ''; + const sType = selectedType.toLowerCase(); + const mTypeClean = mType.replace('model', '').replace('geo', ''); + const sTypeClean = sType.replace('model', '').replace('geo', ''); + return mTypeClean === sTypeClean; + }); + } + + if (filteredModels.length > 0) { + filteredModels.forEach(model => { + const option = document.createElement('option'); + option.value = model.id; + option.dataset.modelType = model.model_type; + option.textContent = `${model.name}`; + modelSelect.appendChild(option); + }); + modelSelect.disabled = false; + } else { + modelSelect.innerHTML = ''; + } + } else { + modelSelect.innerHTML = ''; + } + }) + .catch(error => { + console.error('Error loading models:', error); + modelSelect.innerHTML = ''; + }); + } + + function submitNewAsset(form, modalId, alertContainer, submitBtn) { + // Disable submit button + const originalBtnText = submitBtn.innerHTML; + submitBtn.disabled = true; + submitBtn.innerHTML = '' + gettext('Création...'); + + // Clear previous alerts + alertContainer.classList.add('d-none'); + alertContainer.classList.remove('alert-success', 'alert-danger'); + + // Gather form data + const formData = new FormData(form); + const data = {}; + formData.forEach((value, key) => { + data[key] = value; + }); + + // Resolve asset_type + let assetType = null; + const typeSelect = form.querySelector('.asset-type-select'); + if (typeSelect && typeSelect.value) { + assetType = typeSelect.value; + } else { + const modelSelect = form.querySelector('.asset-model-select'); + const selectedOption = modelSelect ? modelSelect.options[modelSelect.selectedIndex] : null; + if (selectedOption && selectedOption.dataset.modelType) { + let mType = selectedOption.dataset.modelType.toLowerCase(); + if (mType.includes('assetmodel') && !mType.includes('geo')) { + assetType = mType.replace('assetmodel', 'geoasset'); + } else { + assetType = mType.replace('model', ''); + } + } + } + + const categorySelect = form.querySelector('.asset-category-select'); + const categoryId = categorySelect ? categorySelect.value : null; + const thematicCode = form.dataset.thematic; + + if (!assetType && categoryId && thematicCode) { + const catData = (categoriesCache[thematicCode] || []).find(c => c.id == categoryId); + if (catData && catData.allowed_models && catData.allowed_models.length === 1) { + assetType = catData.allowed_models[0].model_name; + } + } + + if (!assetType) { + assetType = getAssetTypeForThematic(thematicCode); + } + + data.asset_type = assetType; + + const csrfToken = form.querySelector('[name=csrfmiddlewaretoken]').value; + const interventionId = form.dataset.interventionId; + + fetch('/assets/api/create-asset/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + submitBtn.disabled = false; + submitBtn.innerHTML = originalBtnText; + + if (result.success) { + alertContainer.className = 'alert alert-success'; + alertContainer.innerHTML = '' + result.message; + alertContainer.classList.remove('d-none'); + + // Si un asset a été créé et nécessite la sélection d'un template + if (result.needs_template_selection && result.asset_id) { + checkAssetTemplates(interventionId, data.asset_type, result.asset_id, modalId, csrfToken); + } else { + // Redirection normale après un court délai + setTimeout(() => { + performRedirect(modalId, interventionId, result); + }, 1000); + } + } else { + alertContainer.className = 'alert alert-danger'; + alertContainer.innerHTML = '' + (result.error || gettext('Erreur lors de la création')); + alertContainer.classList.remove('d-none'); + } + }) + .catch(error => { + console.error('Error:', error); + submitBtn.disabled = false; + submitBtn.innerHTML = originalBtnText; + + alertContainer.className = 'alert alert-danger'; + alertContainer.innerHTML = '' + gettext('Erreur de connexion'); + alertContainer.classList.remove('d-none'); + }); + } + + function checkAssetTemplates(interventionId, assetType, assetId, modalId, csrfToken) { + // Vérifier si l'asset a des templates correspondants + fetch(`/interventions/${interventionId}/check-asset-templates/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ + asset_type: assetType, + asset_id: assetId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success && data.has_templates && data.available_templates && data.available_templates.length > 0) { + // Afficher le modal de sélection de template + showTemplateSelectionModal( + interventionId, + assetType, + assetId, + data.available_templates, + modalId, + csrfToken, + data.message + ); + } else { + // Pas de templates disponibles, rediriger normalement + setTimeout(() => { + performRedirect(modalId, interventionId, {intervention_linked: true}); + }, 1000); + } + }) + .catch(error => { + console.error('Error checking templates:', error); + // En cas d'erreur, rediriger quand même + setTimeout(() => { + performRedirect(modalId, interventionId, {intervention_linked: true}); + }, 1000); + }); + } + + function showTemplateSelectionModal(interventionId, assetType, assetId, templates, originalModalId, csrfToken, customMessage) { + // Fermer le modal de création d'asset + const originalModal = document.getElementById(originalModalId); + if (originalModal) { + const bsModal = bootstrap.Modal.getInstance(originalModal); + if (bsModal) { + bsModal.hide(); + } + } + + // Créer un modal pour la sélection du template + const modalHtml = ` + + `; + + // Ajouter le modal au DOM + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + const selectTemplateModal = document.getElementById('selectTemplateModal'); + const templateCheckboxes = selectTemplateModal.querySelectorAll('.template-checkbox'); + const createBtn = document.getElementById('createOperationsBtn'); + const skipBtn = document.getElementById('skipTemplateBtn'); + const templateAlert = document.getElementById('templateAlert'); + const selectAllBtn = document.getElementById('selectAllTemplates'); + const deselectAllBtn = document.getElementById('deselectAllTemplates'); + + // Fonction pour vérifier si au moins un template est sélectionné + function updateCreateButtonState() { + const anyChecked = Array.from(templateCheckboxes).some(cb => cb.checked); + createBtn.disabled = !anyChecked; + } + + // Activer le bouton de création quand au moins un template est sélectionné + templateCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', updateCreateButtonState); + }); + + // Bouton "Tout sélectionner" + selectAllBtn.addEventListener('click', function() { + templateCheckboxes.forEach(cb => cb.checked = true); + updateCreateButtonState(); + }); + + // Bouton "Tout désélectionner" + deselectAllBtn.addEventListener('click', function() { + templateCheckboxes.forEach(cb => cb.checked = false); + updateCreateButtonState(); + }); + + // Gérer le clic sur "Ignorer et continuer" + skipBtn.addEventListener('click', function() { + const bsModal = bootstrap.Modal.getInstance(selectTemplateModal); + bsModal.hide(); + performRedirect(originalModalId, interventionId, {intervention_linked: true}); + }); + + // Gérer le clic sur "Créer les opérations" + createBtn.addEventListener('click', function() { + const selectedTemplateIds = Array.from(templateCheckboxes) + .filter(cb => cb.checked) + .map(cb => parseInt(cb.value)); + + if (selectedTemplateIds.length === 0) return; + + createBtn.disabled = true; + const originalBtnText = createBtn.innerHTML; + createBtn.innerHTML = '' + gettext('Création en cours...'); + + // Appeler l'API pour créer les opérations + fetch(`/interventions/${interventionId}/create-operations-from-template/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ + asset_type: assetType, + asset_id: assetId, + template_ids: selectedTemplateIds + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + templateAlert.className = 'alert alert-success'; + templateAlert.innerHTML = '' + data.message; + templateAlert.classList.remove('d-none'); + + // Fermer le modal et rediriger après un court délai + setTimeout(() => { + const bsModal = bootstrap.Modal.getInstance(selectTemplateModal); + bsModal.hide(); + performRedirect(originalModalId, interventionId, {intervention_linked: true}); + }, 1500); + } else { + createBtn.disabled = false; + createBtn.innerHTML = originalBtnText; + templateAlert.className = 'alert alert-danger'; + templateAlert.innerHTML = '' + (data.error || gettext('Erreur lors de la création')); + templateAlert.classList.remove('d-none'); + } + }) + .catch(error => { + console.error('Error:', error); + createBtn.disabled = false; + createBtn.innerHTML = originalBtnText; + templateAlert.className = 'alert alert-danger'; + templateAlert.innerHTML = '' + gettext('Erreur de connexion'); + templateAlert.classList.remove('d-none'); + }); + }); + + // Nettoyer le modal du DOM après fermeture + selectTemplateModal.addEventListener('hidden.bs.modal', function() { + modalContainer.remove(); + }); + + // Afficher le modal + const bsModal = new bootstrap.Modal(selectTemplateModal); + bsModal.show(); + } + + function performRedirect(modalId, interventionId, result) { + const modalEl = document.getElementById(modalId); + const modal = bootstrap.Modal.getInstance(modalEl); + if (modal) { + modal.hide(); + } + + // Determine redirect URL + let redirectUrl = null; + + // Check if we're on a mobile view + const isMobileView = window.location.pathname.startsWith('/mobile/') || + document.body.classList.contains('mobile-view') || + document.querySelector('body')?.getAttribute('data-mobile-view') === 'true'; + + if (interventionId && result.intervention_linked) { + // If we're in mobile view and there's an intervention, redirect to mobile intervention view + if (isMobileView) { + redirectUrl = `/mobile/interventions/${interventionId}/`; + } else { + redirectUrl = `/interventions/${interventionId}/`; + } + } else if (result.redirect_url) { + redirectUrl = result.redirect_url; + } + + // Reload or redirect + if (redirectUrl) { + window.location.href = redirectUrl; + } else { + window.location.reload(); + } + } + + function getAssetTypeForThematic(thematicCode) { + const mapping = { + 'structures': 'structuregeoasset', + 'trafficlights': 'trafficlightgeoasset', + 'publiclighting': 'publiclightinggeoasset', + 'nature': 'naturegeoasset', + 'artworks': 'artworksgeoasset', + 'controlcenters': 'controlcentergeoasset', + 'its': 'itsgeoasset', + 'sign': 'signgeoasset', + 'roads': 'roadgeoasset' + }; + return mapping[thematicCode] || 'geoasset'; + } + + function resetForm(form, modelSelect, alertContainer) { + form.reset(); + + const modalId = form.closest('.modal').id; + const typeContainer = form.querySelector(`#${modalId}TypeContainer`); + const typeSelect = form.querySelector('.asset-type-select'); + const modelRequiredStar = form.querySelector(`#${modalId}ModelRequiredStar`); + + if (typeContainer) typeContainer.classList.add('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + typeSelect.value = ''; + typeSelect.required = false; + } + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + + if (modelSelect) { + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + modelSelect.required = false; + } + alertContainer.classList.add('d-none'); + } + + // Helper function for translations (fallback if gettext is not available) + function gettext(text) { + if (typeof window.gettext === 'function') { + return window.gettext(text); + } + return text; + } + + // Expose for external use + window.AssetAddModal = { + initialize: initializeAddAssetModals, + loadModelsForCategory: loadModelsForCategory + }; + +})(); diff --git a/streetup/assets/static/assets/asset_cms_edit_mode.css b/streetup/assets/static/assets/asset_cms_edit_mode.css new file mode 100644 index 0000000..2bfe75a --- /dev/null +++ b/streetup/assets/static/assets/asset_cms_edit_mode.css @@ -0,0 +1,234 @@ +/* CMS Edit Mode Styles for Asset Detail */ + +/* Edit Bar */ +#asset-cms-edit-bar { + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +/* Editable Fields */ +.asset-editable-field { + transition: all 0.2s ease; + position: relative; + margin-bottom: 1px; +} + +.asset-editable-field.editing { + transition: all 0.2s ease; + animation: asset-highlight-pulse 0.5s ease-in-out; +} + +@keyframes asset-highlight-pulse { + 0% { + background-color: transparent; + } + 50% { + background-color: #ffe69c; + } + 100% { + background-color: #fff3cd; + } +} + +.asset-editable-field .field-display, +.asset-editable-field .field-edit { + min-height: 24px; +} + +.asset-editable-field label { + font-weight: 600; + font-size: 0.875rem; + margin-bottom: 4px; + display: block; + letter-spacing: 0.5px; +} + +.asset-editable-field .field-value { + font-size: 1rem; + display: block; + padding: 4px 0; + color: #333; +} + +.asset-editable-field .field-edit input, +.asset-editable-field .field-edit select { + width: 100%; + font-size: 0.95rem; +} + +/* Disabled fields styling */ +.asset-editable-field[data-can-edit="false"] .field-value { + color: #6c757d; +} + +/* Loading state for save button */ +#asset-cms-save-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Alert messages positioning */ +.asset-cms-alert { + position: fixed; + top: 60px; + left: 50%; + transform: translateX(-50%); + z-index: 1060; + min-width: 300px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Asset Actions */ +.asset-action-btn { + transition: all 0.2s ease; +} + +.asset-action-btn:hover { + transform: translateY(-1px); +} + +/* Confirmation Modal Styles */ +.asset-confirm-modal .modal-content { + border-radius: 8px; +} + +.asset-confirm-modal .modal-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +/* Replace Asset Form */ +.replace-asset-form .form-check { + padding: 12px; + background-color: #f8f9fa; + border-radius: 4px; + margin-bottom: 8px; +} + +.replace-asset-form .form-check:hover { + background-color: #e9ecef; +} + +/* Bulk Archive Warning */ +.bulk-archive-warning { + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: 4px; + padding: 12px; +} + +.bulk-archive-warning i { + color: #856404; +} + +/* Geometry Editor */ +.geometry-edit-container { + border: 2px dashed #007bff; + border-radius: 8px; + padding: 8px; + background-color: #e7f3ff; +} + +.geometry-edit-container.active { + border-color: #28a745; + background-color: #d4edda; +} + +.geometry-edit-btn { + position: absolute; + top: 8px; + right: 8px; + z-index: 1000; +} + +/* Map Edit Mode */ +#asset-map.edit-mode { + cursor: crosshair; +} + +#asset-map.edit-mode .maplibregl-canvas { + cursor: crosshair; +} + +/* Category/Model Selector */ +.asset-type-selector { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.asset-type-card { + flex: 1 1 calc(50% - 8px); + min-width: 200px; + padding: 16px; + border: 2px solid #dee2e6; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.asset-type-card:hover { + border-color: #007bff; + background-color: #f0f7ff; +} + +.asset-type-card.selected { + border-color: #007bff; + background-color: #e7f3ff; +} + +.asset-type-card .asset-type-icon { + font-size: 2rem; + margin-bottom: 8px; + color: #6c757d; +} + +.asset-type-card.selected .asset-type-icon { + color: #007bff; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + #asset-cms-edit-bar { + font-size: 0.875rem; + padding: 0.5rem !important; + } + + #asset-cms-edit-bar button { + font-size: 0.75rem; + padding: 0.25rem 0.5rem !important; + } + + .asset-editable-field label { + font-size: 0.75rem; + } + + .asset-editable-field .field-value { + font-size: 0.875rem; + } + + .asset-type-card { + flex: 1 1 100%; + } +} + +/* Orthophoto Toggle Button */ +#ortho-toggle-btn { + background: #fff; + border: none; + color: #333; + transition: all 0.2s ease; +} + +#ortho-toggle-btn:hover { + background: #f4f4f4; +} + +#ortho-toggle-btn.active { + background: #007bff; + color: #fff; +} + +#ortho-toggle-btn.active:hover { + background: #0056b3; +} diff --git a/streetup/assets/static/assets/asset_cms_edit_mode.js b/streetup/assets/static/assets/asset_cms_edit_mode.js new file mode 100644 index 0000000..791c3de --- /dev/null +++ b/streetup/assets/static/assets/asset_cms_edit_mode.js @@ -0,0 +1,981 @@ +// CMS-style inline editing for asset detail page +(function() { + 'use strict'; + + const editBtn = document.getElementById('asset-cms-edit-btn'); + const saveBtn = document.getElementById('asset-cms-save-btn'); + const cancelBtn = document.getElementById('asset-cms-cancel-btn'); + const editableFields = document.querySelectorAll('.asset-editable-field'); + + let isEditMode = false; + let originalValues = {}; + + // Initialize if edit button exists (user has edit permission) + if (editBtn) { + editBtn.addEventListener('click', enterEditMode); + } + if (saveBtn) { + saveBtn.addEventListener('click', saveChanges); + } + if (cancelBtn) { + cancelBtn.addEventListener('click', cancelChanges); + } + + // Initialize action buttons + initializeActionButtons(); + + function enterEditMode() { + isEditMode = true; + + // Add class to body + document.body.classList.add('asset-cms-edit-mode'); + + // Emit event for other components + document.dispatchEvent(new CustomEvent('asset-cms-edit-mode-enter')); + + // Store original values + originalValues = {}; + editableFields.forEach(field => { + const canEdit = field.dataset.canEdit === 'true'; + if (!canEdit) return; + + const fieldName = field.dataset.field; + const displayDiv = field.querySelector('.field-display'); + const editDiv = field.querySelector('.field-edit'); + const input = editDiv ? editDiv.querySelector('input, select, textarea') : null; + + if (input) { + originalValues[fieldName] = input.value; + + // Show edit mode + if (displayDiv) displayDiv.classList.add('d-none'); + if (editDiv) editDiv.classList.remove('d-none'); + + // Add edit styling + field.classList.add('editing'); + field.style.backgroundColor = '#fffdf7ff'; + field.style.padding = '8px'; + field.style.borderRadius = '4px'; + field.style.border = '2px solid #f8e8b6'; + } + }); + + // Update button visibility + editBtn.classList.add('d-none'); + saveBtn.classList.remove('d-none'); + cancelBtn.classList.remove('d-none'); + + // Hide normal action buttons (Archive, Replace) + document.querySelectorAll('.asset-normal-action').forEach(btn => { + btn.classList.add('d-none'); + }); + + // Show edit-mode-only elements (like change model button) + document.querySelectorAll('[data-edit-mode-only="true"]').forEach(element => { + element.classList.remove('d-none'); + }); + + // Enable map editing if available + if (typeof window.enableMapEditing === 'function') { + window.enableMapEditing(); + } + } + + function cancelChanges() { + isEditMode = false; + + // Remove class from body + document.body.classList.remove('asset-cms-edit-mode'); + + // Emit event + document.dispatchEvent(new CustomEvent('asset-cms-edit-mode-exit')); + + // Restore original values + editableFields.forEach(field => { + const canEdit = field.dataset.canEdit === 'true'; + if (!canEdit) return; + + const fieldName = field.dataset.field; + const displayDiv = field.querySelector('.field-display'); + const editDiv = field.querySelector('.field-edit'); + const input = editDiv ? editDiv.querySelector('input, select, textarea') : null; + + if (input && originalValues[fieldName] !== undefined) { + input.value = originalValues[fieldName]; + + // Hide edit mode + if (displayDiv) displayDiv.classList.remove('d-none'); + if (editDiv) editDiv.classList.add('d-none'); + + // Remove edit styling + field.classList.remove('editing'); + field.style.backgroundColor = ''; + field.style.padding = ''; + field.style.borderRadius = ''; + field.style.border = ''; + } + }); + + // Update button visibility + editBtn.classList.remove('d-none'); + saveBtn.classList.add('d-none'); + cancelBtn.classList.add('d-none'); + + // Show normal action buttons again (Archive, Replace) + document.querySelectorAll('.asset-normal-action').forEach(btn => { + btn.classList.remove('d-none'); + }); + + // Hide edit-mode-only elements + document.querySelectorAll('[data-edit-mode-only="true"]').forEach(element => { + element.classList.add('d-none'); + }); + + // Disable map editing if available + if (typeof window.disableMapEditing === 'function') { + window.disableMapEditing(); + } + + originalValues = {}; + } + + async function saveChanges() { + // Collect changed values + const changes = {}; + let hasChanges = false; + + editableFields.forEach(field => { + const canEdit = field.dataset.canEdit === 'true'; + if (!canEdit) return; + + const fieldName = field.dataset.field; + const editDiv = field.querySelector('.field-edit'); + const input = editDiv ? editDiv.querySelector('input, select, textarea') : null; + + if (input) { + const newValue = input.value; + const oldValue = originalValues[fieldName]; + + if (newValue !== oldValue) { + changes[fieldName] = newValue; + hasChanges = true; + } + } + }); + + // Check for geometry changes if available + if (typeof window.getGeometryChanges === 'function') { + const geomChanges = window.getGeometryChanges(); + if (geomChanges) { + changes.geom = geomChanges; + hasChanges = true; + } + } + + // Show loading state + saveBtn.disabled = true; + saveBtn.innerHTML = ' Enregistrement...'; + + try { + // Run pre-save hooks registered by other components (e.g. linked observations) + if (Array.isArray(window.assetPreSaveHooks)) { + for (const hook of window.assetPreSaveHooks) { + await hook(); + } + } + + if (!hasChanges) { + // No asset-field changes but hooks may have saved related data → reload + window.location.reload(); + return; + } + + // Get asset info from page + const assetModel = document.getElementById('asset-detail-container').dataset.assetModel; + const assetId = document.getElementById('asset-detail-container').dataset.assetId; + + if (!assetModel || !assetId) { + throw new Error('Asset information not found'); + } + + // Send update request + const response = await fetch(`/assets/${assetModel}/${assetId}/update/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify(changes) + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || result.errors?.join(', ') || 'Erreur lors de la sauvegarde'); + } + + // Exit edit mode and reload + isEditMode = false; + window.location.reload(); + + } catch (error) { + console.error('Save error:', error); + showErrorMessage(error.message || 'Erreur lors de la sauvegarde'); + + // Restore button state + saveBtn.disabled = false; + saveBtn.innerHTML = ' Sauvegarder'; + } + } + + function exitEditMode() { + isEditMode = false; + + // Remove class from body + document.body.classList.remove('asset-cms-edit-mode'); + + // Emit event + document.dispatchEvent(new CustomEvent('asset-cms-edit-mode-exit')); + + editableFields.forEach(field => { + const canEdit = field.dataset.canEdit === 'true'; + if (!canEdit) return; + + const displayDiv = field.querySelector('.field-display'); + const editDiv = field.querySelector('.field-edit'); + + // Hide edit mode + if (displayDiv) displayDiv.classList.remove('d-none'); + if (editDiv) editDiv.classList.add('d-none'); + + // Remove edit styling + field.classList.remove('editing'); + field.style.backgroundColor = ''; + field.style.padding = ''; + field.style.borderRadius = ''; + field.style.border = ''; + }); + + // Update button visibility + editBtn.classList.remove('d-none'); + saveBtn.classList.add('d-none'); + cancelBtn.classList.add('d-none'); + + // Show normal action buttons again (Archive, Replace) + document.querySelectorAll('.asset-normal-action').forEach(btn => { + btn.classList.remove('d-none'); + }); + + // Hide edit-mode-only elements + document.querySelectorAll('[data-edit-mode-only="true"]').forEach(element => { + element.classList.add('d-none'); + }); + + // Restore button state + saveBtn.disabled = false; + saveBtn.innerHTML = ' Sauvegarder'; + + // Disable map editing if available + if (typeof window.disableMapEditing === 'function') { + window.disableMapEditing(); + } + + originalValues = {}; + } + + function initializeActionButtons() { + // Archive button + const archiveBtn = document.getElementById('asset-archive-btn'); + if (archiveBtn) { + archiveBtn.addEventListener('click', handleArchive); + } + + // Replace button + const replaceBtn = document.getElementById('asset-replace-btn'); + if (replaceBtn) { + replaceBtn.addEventListener('click', handleReplace); + } + + // Add asset button + const addAssetBtn = document.getElementById('add-asset-btn'); + if (addAssetBtn) { + addAssetBtn.addEventListener('click', handleAddAsset); + } + + // Bulk archive button + const bulkArchiveBtn = document.getElementById('bulk-archive-btn'); + if (bulkArchiveBtn) { + bulkArchiveBtn.addEventListener('click', handleBulkArchive); + } + } + + async function handleArchive() { + const assetModel = document.getElementById('asset-detail-container').dataset.assetModel; + const assetId = document.getElementById('asset-detail-container').dataset.assetId; + + if (!confirm('Êtes-vous sûr de vouloir archiver cet asset ?')) { + return; + } + + try { + const response = await fetch(`/assets/${assetModel}/${assetId}/archive/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + } + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || 'Erreur lors de l\'archivage'); + } + + showSuccessMessage(result.message || 'Asset archivé avec succès'); + setTimeout(() => window.location.reload(), 1500); + + } catch (error) { + console.error('Archive error:', error); + showErrorMessage(error.message); + } + } + + async function handleReplace() { + // Show replace modal + const modal = new bootstrap.Modal(document.getElementById('replaceAssetModal')); + modal.show(); + } + + async function handleAddAsset() { + // Show add asset modal + const modal = new bootstrap.Modal(document.getElementById('addAssetModal')); + modal.show(); + } + + async function handleBulkArchive() { + const locationContainer = document.getElementById('location-detail-container'); + if (!locationContainer) return; + + const thematicCode = locationContainer.dataset.thematicCode; + const locationType = locationContainer.dataset.locationType; + const locationId = locationContainer.dataset.locationId; + + if (!confirm('Êtes-vous sûr de vouloir archiver TOUS les assets de cette localisation ? Cette action est irréversible.')) { + return; + } + + try { + const response = await fetch(`/assets/${thematicCode}/${locationType}/${locationId}/bulk-archive/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + } + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || 'Erreur lors de l\'archivage en masse'); + } + + showSuccessMessage(result.message || 'Assets archivés avec succès'); + setTimeout(() => window.location.reload(), 1500); + + } catch (error) { + console.error('Bulk archive error:', error); + showErrorMessage(error.message); + } + } + + // Expose functions for replace and add asset modals + window.confirmReplaceAsset = async function(event, form) { + // Prevent form submission + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const container = document.getElementById('asset-detail-container'); + const assetModel = container.dataset.assetModel; + const assetId = container.dataset.assetId; + const thematicCode = container.dataset.thematicCode; + + const formData = new FormData(form); + const newData = {}; + + // Add serial number if provided + const serialNumber = formData.get('serial_number'); + if (serialNumber && serialNumber.trim()) { + newData.serial_number = serialNumber.trim(); + } + + // Add brand if provided + const brand = formData.get('brand'); + if (brand && brand.trim()) { + newData.brand = brand.trim(); + } + + const data = { + new_model_id: formData.get('new_model_id') || null, + new_data: newData + }; + + try { + const response = await fetch(`/assets/${assetModel}/${assetId}/replace/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || 'Erreur lors du remplacement'); + } + + showSuccessMessage(result.message || 'Asset remplacé avec succès'); + + // Redirect to new asset (use thematic code in URL if available) + setTimeout(() => { + if (thematicCode) { + window.location.href = `/assets/${thematicCode}/${assetModel}/${result.new_asset_id}/`; + } else { + window.location.href = `/assets/${assetModel}/${result.new_asset_id}/`; + } + }, 1500); + + } catch (error) { + console.error('Replace error:', error); + showErrorMessage(error.message); + } + + return false; + }; + + // Initialize replace form event listener + const replaceForm = document.querySelector('.replace-asset-form'); + if (replaceForm) { + replaceForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.confirmReplaceAsset(e, this); + }); + } + + // Initialize add asset form event listener + const addAssetForm = document.getElementById('addAssetForm'); + if (addAssetForm) { + addAssetForm.addEventListener('submit', function(e) { + e.preventDefault(); + window.confirmAddAsset(e, this); + }); + + const locationContainer = document.getElementById('location-detail-container'); + if (locationContainer) { + const thematicCode = locationContainer.dataset.thematicCode; + const categorySelect = document.getElementById('addAssetCategory'); + const modelSelect = document.getElementById('addAssetModel'); + const typeSelect = document.getElementById('addAssetType'); + const typeContainer = document.getElementById('addAssetTypeContainer'); + const modelRequiredStar = document.getElementById('addAssetModelRequiredStar'); + + const categoriesCache = {}; + + const addAssetModal = document.getElementById('addAssetModal'); + if (addAssetModal) { + addAssetModal.addEventListener('show.bs.modal', function() { + window.loadAssetCategories(thematicCode, categorySelect); + }); + + addAssetModal.addEventListener('hidden.bs.modal', function() { + addAssetForm.reset(); + if (typeContainer) typeContainer.classList.add('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + typeSelect.value = ''; + typeSelect.required = false; + } + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + if (modelSelect) { + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + modelSelect.required = false; + } + }); + } + + async function onCategoryChange() { + const categoryId = categorySelect.value; + + // Clear & disable model select + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + modelSelect.required = false; + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + + if (!categoryId) { + if (typeContainer) typeContainer.classList.add('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + typeSelect.value = ''; + typeSelect.required = false; + } + return; + } + + // Ensure categories are loaded in cache + if (!categoriesCache[thematicCode]) { + try { + const response = await fetch(`/assets/${thematicCode}/categories/`); + const data = await response.json(); + if (data.categories) { + categoriesCache[thematicCode] = data.categories; + } + } catch (error) { + console.error('Error fetching categories:', error); + } + } + + const categories = categoriesCache[thematicCode] || []; + const categoryData = categories.find(c => c.id == categoryId); + + if (categoryData && categoryData.allowed_models && categoryData.allowed_models.length > 1) { + // Show Type selection + if (typeContainer) typeContainer.classList.remove('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + categoryData.allowed_models.forEach(model => { + const option = document.createElement('option'); + option.value = model.model_name; + option.textContent = model.verbose_name; + option.dataset.isModelRequired = model.is_model_required; + typeSelect.appendChild(option); + }); + typeSelect.value = ''; + typeSelect.required = true; + } + } else { + // Hide Type selection + if (typeContainer) typeContainer.classList.add('d-none'); + if (typeSelect) { + typeSelect.innerHTML = ''; + typeSelect.value = ''; + typeSelect.required = false; + } + + const singleModel = (categoryData && categoryData.allowed_models && categoryData.allowed_models.length === 1) + ? categoryData.allowed_models[0] + : null; + + const selectedType = singleModel ? singleModel.model_name : null; + const isModelRequired = singleModel ? singleModel.is_model_required : false; + + // Automatically set the typeSelect value to simplify retrieval + if (typeSelect && selectedType) { + typeSelect.value = selectedType; + } + + // Load models for this category + window.loadAssetModels(thematicCode, categoryId, modelSelect, selectedType); + + // Set model template requirement + if (isModelRequired) { + if (modelRequiredStar) modelRequiredStar.style.display = ''; + modelSelect.required = true; + } else { + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + modelSelect.required = false; + } + } + } + + if (categorySelect && modelSelect) { + categorySelect.addEventListener('change', onCategoryChange); + } + + if (typeSelect) { + typeSelect.addEventListener('change', function() { + const selectedType = this.value; + const categoryId = categorySelect ? categorySelect.value : null; + + // Clear & disable model select + modelSelect.innerHTML = ''; + modelSelect.disabled = true; + modelSelect.required = false; + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + + if (!categoryId || !selectedType) { + return; + } + + // Find the allowed model metadata to check if template is required + const categories = categoriesCache[thematicCode] || []; + const categoryData = categories.find(c => c.id == categoryId); + const allowedModel = categoryData && categoryData.allowed_models + ? categoryData.allowed_models.find(m => m.model_name === selectedType) + : null; + const isModelRequired = allowedModel ? allowedModel.is_model_required : false; + + // Load models filtered by the selected type + window.loadAssetModels(thematicCode, categoryId, modelSelect, selectedType); + + // Set model template requirement + if (isModelRequired) { + if (modelRequiredStar) modelRequiredStar.style.display = ''; + modelSelect.required = true; + } else { + if (modelRequiredStar) modelRequiredStar.style.display = 'none'; + modelSelect.required = false; + } + }); + } + } + } + + // Clean up modal backdrop on modal hidden event + function cleanupModalBackdrop() { + // Remove any leftover backdrops + document.querySelectorAll('.modal-backdrop').forEach(el => el.remove()); + // Remove modal-open class from body + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + } + + // Add cleanup listener to all modals + document.querySelectorAll('.modal').forEach(modal => { + modal.addEventListener('hidden.bs.modal', cleanupModalBackdrop); + }); + + window.confirmAddAsset = async function(event, form) { + // Prevent form submission + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const locationContainer = document.getElementById('location-detail-container'); + if (!locationContainer) return false; + + const thematicCode = locationContainer.dataset.thematicCode; + const locationType = locationContainer.dataset.locationType; + const locationId = locationContainer.dataset.locationId; + + const formData = new FormData(form); + + // Get asset type from selected type or model option's dataset + let assetType = null; + const typeSelect = document.getElementById('addAssetType'); + if (typeSelect && typeSelect.value) { + assetType = typeSelect.value; + } else { + const modelSelect = document.getElementById('addAssetModel'); + const selectedOption = modelSelect ? modelSelect.options[modelSelect.selectedIndex] : null; + if (selectedOption && selectedOption.dataset.modelType) { + let mType = selectedOption.dataset.modelType.toLowerCase(); + if (mType.includes('assetmodel') && !mType.includes('geo')) { + assetType = mType.replace('assetmodel', 'geoasset'); + } else { + assetType = mType.replace('model', ''); + } + } + } + + if (!assetType && !formData.get('category_id')) { + showErrorMessage("Type d'asset ou catégorie requis."); + return false; + } + + const data = { + asset_type: assetType, + model_id: formData.get('model_id') || null, + category_id: formData.get('category_id') || null, + data: {} + }; + + try { + const response = await fetch(`/assets/${thematicCode}/${locationType}/${locationId}/create-asset/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || 'Erreur lors de la création'); + } + + // Close the modal + const modal = bootstrap.Modal.getInstance(document.getElementById('addAssetModal')); + if (modal) { + modal.hide(); + } + + showSuccessMessage(result.message || 'Asset créé avec succès'); + + // Redirect to new asset + setTimeout(() => { + if (result.asset_id && result.asset_type) { + window.location.href = `/assets/${thematicCode}/${result.asset_type}/${result.asset_id}/`; + } else if (result.asset_id && assetType) { + window.location.href = `/assets/${thematicCode}/${assetType}/${result.asset_id}/`; + } else { + window.location.reload(); + } + }, 1500); + + } catch (error) { + console.error('Create error:', error); + showErrorMessage(error.message); + } + + return false; + }; + + // Category and model loading for add asset modal + window.loadAssetCategories = async function(thematicCode, selectElement) { + try { + const response = await fetch(`/assets/${thematicCode}/categories/`); + const result = await response.json(); + + selectElement.innerHTML = ''; + result.categories.forEach(cat => { + const option = document.createElement('option'); + option.value = cat.id; + option.textContent = `${cat.code} - ${cat.name}`; + selectElement.appendChild(option); + }); + } catch (error) { + console.error('Error loading categories:', error); + } + }; + + window.loadAssetModels = async function(thematicCode, categoryId, selectElement, selectedType = null) { + try { + const response = await fetch(`/assets/${thematicCode}/category/${categoryId}/models/`); + const result = await response.json(); + + selectElement.innerHTML = ''; + + let filteredModels = result.models || []; + if (selectedType) { + filteredModels = filteredModels.filter(model => { + const mType = model.model_type ? model.model_type.toLowerCase() : ''; + const sType = selectedType.toLowerCase(); + const mTypeClean = mType.replace('model', '').replace('geo', ''); + const sTypeClean = sType.replace('model', '').replace('geo', ''); + return mTypeClean === sTypeClean; + }); + } + + filteredModels.forEach(model => { + const option = document.createElement('option'); + option.value = model.id; + option.dataset.modelType = model.model_type; + option.textContent = `${model.name}`; + selectElement.appendChild(option); + }); + + if (filteredModels.length > 0) { + selectElement.disabled = false; + } else { + selectElement.innerHTML = ''; + selectElement.disabled = true; + } + } catch (error) { + console.error('Error loading models:', error); + } + }; + + function showSuccessMessage(message) { + const alert = document.createElement('div'); + alert.className = 'alert alert-success alert-dismissible fade show asset-cms-alert'; + alert.innerHTML = ` + ${message} + + `; + document.body.appendChild(alert); + + setTimeout(() => { + alert.remove(); + }, 5000); + } + + function showErrorMessage(message) { + const alert = document.createElement('div'); + alert.className = 'alert alert-danger alert-dismissible fade show asset-cms-alert'; + alert.innerHTML = ` + ${message} + + `; + document.body.appendChild(alert); + + setTimeout(() => { + alert.remove(); + }, 5000); + } + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + // Warn before leaving page if in edit mode + window.addEventListener('beforeunload', (e) => { + if (isEditMode) { + e.preventDefault(); + e.returnValue = ''; + return ''; + } + }); + + // Model change functionality + initializeModelChange(); + + function initializeModelChange() { + const changeModelForm = document.getElementById('changeModelForm'); + const categorySelect = document.getElementById('changeModelCategory'); + const modelSelect = document.getElementById('changeModelSelect'); + + if (!changeModelForm) return; + + // Filter models by category + if (categorySelect && modelSelect) { + // Keep a master list of all model options + const allModelOptions = Array.from(modelSelect.options).map(opt => ({ + value: opt.value, + text: opt.textContent, + category: opt.dataset.category + })); + + function updateModelOptions() { + const selectedCategory = categorySelect.value; + const currentValue = modelSelect.value; + + // Clear current options + modelSelect.innerHTML = ''; + + let currentValueIsVisible = false; + + // Filter and add options + allModelOptions.forEach(optData => { + // Skip or keep placeholder if needed (placeholder is the option with empty value) + if (!optData.value) { + const opt = document.createElement('option'); + opt.value = optData.value; + opt.textContent = optData.text; + modelSelect.appendChild(opt); + return; + } + + const optCategory = optData.category; + if (!selectedCategory || optCategory === selectedCategory) { + const opt = document.createElement('option'); + opt.value = optData.value; + opt.textContent = optData.text; + opt.dataset.category = optData.category; + if (optData.value === currentValue) { + opt.selected = true; + currentValueIsVisible = true; + } + modelSelect.appendChild(opt); + } + }); + + // Reset selection if current selection is not visible (i.e. not in the filtered options) + if (!currentValueIsVisible && currentValue !== '') { + modelSelect.value = ''; + } + } + + categorySelect.addEventListener('change', updateModelOptions); + + // Run initially to filter the list on load based on pre-selected category + updateModelOptions(); + } + + // Handle form submission + changeModelForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const container = document.getElementById('asset-detail-container'); + const assetModel = container ? container.dataset.assetModel : null; + const assetId = container ? container.dataset.assetId : null; + const alertDiv = document.getElementById('changeModelAlert'); + const submitBtn = document.getElementById('changeModelSubmit'); + + if (!assetModel || !assetId || !modelSelect.value) { + alertDiv.className = 'alert alert-danger'; + alertDiv.textContent = 'Veuillez sélectionner un modèle.'; + alertDiv.classList.remove('d-none'); + return; + } + + // Disable submit button + const originalBtnText = submitBtn.innerHTML; + submitBtn.disabled = true; + submitBtn.innerHTML = ' Enregistrement...'; + + // Send update request + const updateUrl = `/assets/${assetModel}/${assetId}/update/`; + + fetch(updateUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ + 'model': modelSelect.value + }) + }) + .then(response => response.json()) + .then(result => { + submitBtn.disabled = false; + submitBtn.innerHTML = originalBtnText; + + if (result.success) { + alertDiv.className = 'alert alert-success'; + alertDiv.innerHTML = ' Modèle mis à jour avec succès'; + alertDiv.classList.remove('d-none'); + + // Reload page after short delay + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + alertDiv.className = 'alert alert-danger'; + alertDiv.textContent = result.error || 'Erreur lors de la mise à jour'; + alertDiv.classList.remove('d-none'); + } + }) + .catch(error => { + console.error('Error:', error); + submitBtn.disabled = false; + submitBtn.innerHTML = originalBtnText; + + alertDiv.className = 'alert alert-danger'; + alertDiv.textContent = 'Erreur de connexion'; + alertDiv.classList.remove('d-none'); + }); + }); + } +})(); diff --git a/streetup/assets/static/assets/asset_generic_map.js b/streetup/assets/static/assets/asset_generic_map.js new file mode 100644 index 0000000..a7ced9a --- /dev/null +++ b/streetup/assets/static/assets/asset_generic_map.js @@ -0,0 +1,766 @@ +// Asset Generic Detail Map - MapLibre with geometry display and editing support +// This module initializes a map for the generic asset detail view + +(function() { + 'use strict'; + + const MAPTILER_STYLE = window.MAP_STYLE_LIGHT; + const ORTHO_WMTS_URL = 'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'; + const URBIS_WMS_URL = 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'; + + let map = null; + let assetGeometry = null; + + document.addEventListener('DOMContentLoaded', function() { + initializeMap(); + }); + + function initializeMap() { + const mapContainer = document.getElementById('asset-map'); + if (!mapContainer) return; + + const container = document.getElementById('asset-detail-container'); + if (!container) return; + + // Get geometry from data attribute + const geometryJson = container.dataset.assetGeometry; + if (geometryJson) { + try { + assetGeometry = JSON.parse(geometryJson); + } catch (e) { + console.warn('Failed to parse asset geometry:', e); + } + } + + // Fallback to lon/lat if no geometry + if (!assetGeometry) { + const lonElement = document.querySelector('.asset-editable-field[data-field="lon"] .field-value'); + const latElement = document.querySelector('.asset-editable-field[data-field="lat"] .field-value'); + + if (lonElement && latElement) { + const lon = parseFloat(lonElement.textContent.trim()); + const lat = parseFloat(latElement.textContent.trim()); + + if (!isNaN(lon) && !isNaN(lat)) { + assetGeometry = { + type: 'Point', + coordinates: [lon, lat] + }; + } + } + } + + // Calculate center and bounds (use Brussels center as default if no geometry) + const center = assetGeometry ? calculateCenter(assetGeometry) : [4.3517, 50.8503]; // Brussels center + const bounds = assetGeometry ? calculateBounds(assetGeometry) : null; + + // Initialize MapLibre map + map = new maplibregl.Map({ + container: 'asset-map', + style: MAPTILER_STYLE, + center: center, + zoom: assetGeometry ? 17 : 12, // Zoom out if no geometry + pitch: 0, + antialias: true + }); + + // Make map available globally for geometry editing + window.map = map; + + // Add navigation controls + map.addControl(new maplibregl.NavigationControl(), 'top-left'); + + // Add layer switcher (Urbis default, Vlaanderen alternative) + addOrthoLayerSwitcher(); + + map.on('load', function() { + + // Add asset geometry if available + if (assetGeometry) { + addAssetGeometry(assetGeometry); + + // Fit to bounds if available + if (bounds) { + map.fitBounds(bounds, { + padding: 50, + maxZoom: 18 + }); + } + } + + // Add sibling assets from the same intersection (trafficlights only) + const thematicCode = container.dataset.thematicCode; + if (thematicCode === 'trafficlights') { + addIntersectionAssetsLayer(container); + addConnectionLinesLayer(); + } + }); + } + + function calculateCenter(geometry) { + if (!geometry) return [4.3517, 50.8503]; // Default Brussels center + + switch (geometry.type) { + case 'Point': + return geometry.coordinates; + + case 'LineString': + // Return midpoint of the line + const lineCoords = geometry.coordinates; + const midIndex = Math.floor(lineCoords.length / 2); + return lineCoords[midIndex] || lineCoords[0]; + + case 'Polygon': + // Return centroid of the polygon + const polyCoords = geometry.coordinates[0]; + if (!polyCoords || polyCoords.length === 0) return [4.3517, 50.8503]; + + let sumLon = 0, sumLat = 0; + polyCoords.forEach(coord => { + sumLon += coord[0]; + sumLat += coord[1]; + }); + return [sumLon / polyCoords.length, sumLat / polyCoords.length]; + + case 'MultiPoint': + const firstPoint = geometry.coordinates[0]; + return firstPoint || [4.3517, 50.8503]; + + case 'MultiLineString': + const firstLine = geometry.coordinates[0]; + if (firstLine && firstLine.length > 0) { + return firstLine[Math.floor(firstLine.length / 2)]; + } + return [4.3517, 50.8503]; + + case 'MultiPolygon': + const firstPoly = geometry.coordinates[0]; + if (firstPoly && firstPoly[0] && firstPoly[0].length > 0) { + let sumLon = 0, sumLat = 0; + firstPoly[0].forEach(coord => { + sumLon += coord[0]; + sumLat += coord[1]; + }); + return [sumLon / firstPoly[0].length, sumLat / firstPoly[0].length]; + } + return [4.3517, 50.8503]; + + default: + return [4.3517, 50.8503]; + } + } + + function calculateBounds(geometry) { + if (!geometry) return null; + + let allCoords = []; + + function extractCoords(coords) { + if (Array.isArray(coords[0])) { + coords.forEach(extractCoords); + } else { + allCoords.push(coords); + } + } + + extractCoords(geometry.coordinates); + + if (allCoords.length === 0) return null; + + let minLon = Infinity, maxLon = -Infinity; + let minLat = Infinity, maxLat = -Infinity; + + allCoords.forEach(coord => { + minLon = Math.min(minLon, coord[0]); + maxLon = Math.max(maxLon, coord[0]); + minLat = Math.min(minLat, coord[1]); + maxLat = Math.max(maxLat, coord[1]); + }); + + // Add padding for single point + if (minLon === maxLon && minLat === maxLat) { + const offset = 0.001; + return [[minLon - offset, minLat - offset], [maxLon + offset, maxLat + offset]]; + } + + return [[minLon, minLat], [maxLon, maxLat]]; + } + + function addAssetGeometry(geometry) { + if (!map || !geometry) return; + + const geojson = { + type: 'Feature', + geometry: geometry, + properties: {} + }; + + // Add source + map.addSource('asset-geometry', { + type: 'geojson', + data: geojson + }); + + // Add layers based on geometry type + switch (geometry.type) { + case 'Point': + case 'MultiPoint': + addPointLayer(); + break; + + case 'LineString': + case 'MultiLineString': + addLineLayer(); + break; + + case 'Polygon': + case 'MultiPolygon': + addPolygonLayer(); + break; + + default: + // Try to add all layer types + addPolygonLayer(); + addLineLayer(); + addPointLayer(); + } + } + + function addPointLayer() { + // Outer glow ring (yellow highlight for current asset) + map.addLayer({ + id: 'asset-point-outer', + type: 'circle', + source: 'asset-geometry', + filter: ['==', ['geometry-type'], 'Point'], + paint: { + 'circle-radius': 14, + 'circle-color': '#FFD700', + 'circle-opacity': 0.5, + 'circle-stroke-width': 0 + } + }); + + // Middle ring (white border) + map.addLayer({ + id: 'asset-point-middle', + type: 'circle', + source: 'asset-geometry', + filter: ['==', ['geometry-type'], 'Point'], + paint: { + 'circle-radius': 11, + 'circle-color': '#ffffff', + 'circle-stroke-width': 0 + } + }); + + // Inner circle (orange fill, blue stroke) + map.addLayer({ + id: 'asset-point-inner', + type: 'circle', + source: 'asset-geometry', + filter: ['==', ['geometry-type'], 'Point'], + paint: { + 'circle-radius': 8, + 'circle-color': '#FF6B00', + 'circle-stroke-width': 2, + 'circle-stroke-color': '#0056b3' + } + }); + } + + function addLineLayer() { + // Line outline (white border) + map.addLayer({ + id: 'asset-line-outline', + type: 'line', + source: 'asset-geometry', + filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'MultiLineString']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#ffffff', + 'line-width': 3.5 + } + }); + + // Line fill (blue) + map.addLayer({ + id: 'asset-line-fill', + type: 'line', + source: 'asset-geometry', + filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'MultiLineString']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#007bff', + 'line-width': 2 + } + }); + } + + function addPolygonLayer() { + // Polygon fill + map.addLayer({ + id: 'asset-polygon-fill', + type: 'fill', + source: 'asset-geometry', + filter: ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], + paint: { + 'fill-color': '#007bff', + 'fill-opacity': 0.3 + } + }); + + // Polygon outline + map.addLayer({ + id: 'asset-polygon-outline', + type: 'line', + source: 'asset-geometry', + filter: ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#007bff', + 'line-width': 2 + } + }); + } + + // Add sibling assets from the same intersection as semi-transparent markers + function addIntersectionAssetsLayer(container) { + const el = document.getElementById('intersection-assets-geojson'); + if (!el) return; + + let intersectionGeoJSON; + try { + intersectionGeoJSON = JSON.parse(el.textContent); + } catch (e) { + return; + } + + const tlIconUrl = container.dataset.tlIconUrl; + const simplePoleIconUrl = container.dataset.simplePoleIconUrl; + const cabinetIconUrl = container.dataset.cabinetIconUrl; + const radarIconUrl = container.dataset.radarIconUrl; + + // Load image at full opacity onto a fixed-size canvas. + // Opacity is controlled via MapLibre's icon-opacity paint property. + function loadImageToCanvas(url, size) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = size; canvas.height = size; + const ctx = canvas.getContext('2d'); + const scale = Math.min(size / img.width, size / img.height); + const w = img.width * scale, h = img.height * scale; + ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); + resolve(ctx.getImageData(0, 0, size, size)); + }; + img.onerror = reject; + img.src = url; + }); + } + + function registerBootstrapGlyphSdfIcon(map, iconName, codepoint, size = 32, color = '#333333') { + if (map.hasImage(iconName)) return Promise.resolve(); + + return document.fonts.ready.then(() => { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, size, size); + ctx.font = `${Math.round(size * 0.85)}px "bootstrap-icons"`; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(String.fromCodePoint(parseInt(codepoint, 16)), size / 2, size / 2); + const imageData = ctx.getImageData(0, 0, size, size); + if (!map.hasImage(iconName)) { + map.addImage(iconName, imageData); + } + }); + } + + const iconSize = ['interpolate', ['linear'], ['zoom'], 8, 0.1, 14, 0.5, 22, 1]; + + const promises = [ + tlIconUrl ? loadImageToCanvas(tlIconUrl, 64) : Promise.resolve(null), + simplePoleIconUrl ? loadImageToCanvas(simplePoleIconUrl, 64) : Promise.resolve(null), + cabinetIconUrl ? loadImageToCanvas(cabinetIconUrl, 32) : Promise.resolve(null), + radarIconUrl ? loadImageToCanvas(radarIconUrl, 64) : Promise.resolve(null), + registerBootstrapGlyphSdfIcon(map, 'icon-trafficlightsequipment', 'f1d5', 32, '#331110') + ]; + + Promise.all(promises).then(([tlImg, poleImg, cabinetImg, radarImg]) => { + if (tlImg && !map.hasImage('tl-icon')) map.addImage('tl-icon', tlImg); + if (poleImg && !map.hasImage('pole-icon')) map.addImage('pole-icon', poleImg); + if (cabinetImg && !map.hasImage('cabinet-icon')) map.addImage('cabinet-icon', cabinetImg); + if (radarImg && !map.hasImage('radar-icon')) map.addImage('radar-icon', radarImg); + + // ── Sibling assets source ────────────────────────────────────────── + const hasSiblings = intersectionGeoJSON && intersectionGeoJSON.features && intersectionGeoJSON.features.length > 0; + if (hasSiblings && !map.getSource('intersection-assets')) { + map.addSource('intersection-assets', { type: 'geojson', data: intersectionGeoJSON }); + } + + if (hasSiblings) { + // Sibling poles (semi-transparent) + if ((tlImg || poleImg) && !map.getLayer('sibling-poles')) { + map.addLayer({ + id: 'sibling-poles', + type: 'symbol', + source: 'intersection-assets', + filter: ['match', ['get', 'asset_type'], ['trafficlightpole', 'trafficlightradarpole'], true, false], + layout: { + 'icon-image': [ + 'match', + ['get', 'asset_type'], + 'trafficlightradarpole', 'pole-icon', + 'tl-icon' + ], + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'code'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 10, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-optional': true, + }, + paint: { + 'icon-opacity': 0.55, + 'text-color': '#555555', + 'text-halo-color': 'rgba(255,255,255,0.8)', + 'text-halo-width': 1.5, + 'text-opacity': 0.7, + }, + }); + } + + // Sibling electrical cabinets (semi-transparent) + if (cabinetImg && !map.getLayer('sibling-cabinets')) { + map.addLayer({ + id: 'sibling-cabinets', + type: 'symbol', + source: 'intersection-assets', + filter: ['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'], + layout: { + 'icon-image': 'cabinet-icon', + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'code'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 10, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-optional': true, + }, + paint: { + 'icon-opacity': 0.55, + 'text-color': '#555555', + 'text-halo-color': 'rgba(255,255,255,0.8)', + 'text-halo-width': 1.5, + 'text-opacity': 0.7, + }, + }); + } + + // Sibling radars (semi-transparent) + if (radarImg && !map.getLayer('sibling-radars')) { + map.addLayer({ + id: 'sibling-radars', + type: 'symbol', + source: 'intersection-assets', + filter: ['==', ['get', 'asset_type'], 'trafficlightradar'], + layout: { + 'icon-image': 'radar-icon', + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'icon-offset': [ + 'case', + ['coalesce', ['get', 'has_support'], false], ['literal', [0, -25]], + ['literal', [0, 0]] + ], + 'text-field': ['get', 'code'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 10, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-optional': true, + }, + paint: { + 'icon-opacity': 0.55, + 'text-color': '#555555', + 'text-halo-color': 'rgba(255,255,255,0.8)', + 'text-halo-width': 1.5, + 'text-opacity': 0.7, + }, + }); + } + + // Sibling generic equipment (semi-transparent) + if (!map.getLayer('sibling-equipment')) { + map.addLayer({ + id: 'sibling-equipment', + type: 'symbol', + source: 'intersection-assets', + filter: ['match', ['get', 'asset_type'], ['trafficlightcontroller', 'trafficlightdetector', 'trafficlightcontrollerhardware', 'trafficlightnetworkhardware', 'trafficlightvoltagehardware'], true, false], + layout: { + 'icon-image': 'icon-trafficlightsequipment', + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'code'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 10, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-optional': true, + }, + paint: { + 'icon-opacity': 0.55, + 'text-color': '#555555', + 'text-halo-color': 'rgba(255,255,255,0.8)', + 'text-halo-width': 1.5, + 'text-opacity': 0.7, + }, + }); + } + + // Sibling cables (line geometry) + if (!map.getLayer('sibling-cables')) { + map.addLayer({ + id: 'sibling-cables', + type: 'line', + source: 'intersection-assets', + filter: ['==', ['get', 'asset_type'], 'trafficlightcable'], + layout: { 'line-cap': 'round', 'line-join': 'round' }, + paint: { + 'line-color': ['coalesce', ['get', 'color'], '#e53935'], + 'line-width': 2, + 'line-opacity': 0.7, + 'line-dasharray': [2.5, 2] + }, + }); + } + + // Click to navigate to sibling assets + ['sibling-poles', 'sibling-cabinets', 'sibling-radars', 'sibling-equipment', 'sibling-cables'].forEach(layerId => { + if (!map.getLayer(layerId)) return; + map.on('click', layerId, (e) => { + if (e.features.length > 0) { + const url = e.features[0].properties.detail_url; + if (url) window.location.href = url; + } + }); + map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; }); + map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; }); + }); + } + + // ── Current asset icon (full opacity, on top of highlight rings) ── + const assetModel = container.dataset.assetModel; + const hasSupport = container.dataset.hasSupport === 'true'; + const currentIconImage = + assetModel === 'trafficlightpole' && tlImg ? 'tl-icon' : + assetModel === 'trafficlightradarpole' && poleImg ? 'pole-icon' : + assetModel === 'trafficlightelectricalcabinet' && cabinetImg ? 'cabinet-icon' : + assetModel === 'trafficlightradar' && radarImg ? 'radar-icon' : + ['trafficlightcontroller', 'trafficlightdetector', 'trafficlightcontrollerhardware', 'trafficlightnetworkhardware', 'trafficlightvoltagehardware'].includes(assetModel) ? 'icon-trafficlightsequipment' : + null; + + if (currentIconImage && !map.getLayer('current-asset-icon')) { + map.addLayer({ + id: 'current-asset-icon', + type: 'symbol', + source: 'asset-geometry', + filter: ['==', ['geometry-type'], 'Point'], + layout: { + 'icon-image': currentIconImage, + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'icon-offset': (assetModel === 'trafficlightradar' && hasSupport) ? [0, -25] : [0, 0] + }, + paint: { + 'icon-opacity': 1.0, + }, + }); + } + + // Bring highlight rings and icon to the top so they render above all other layers + bringAssetLayersToTop(); + + }).catch(e => console.warn('Failed to load asset icons:', e)); + } + + function addConnectionLinesLayer() { + const el = document.getElementById('connection-lines-geojson'); + if (!el) return; + + let connectionGeoJSON; + try { + connectionGeoJSON = JSON.parse(el.textContent); + } catch (e) { + return; + } + + if (connectionGeoJSON && connectionGeoJSON.features && connectionGeoJSON.features.length > 0) { + if (!map.getSource('connection-lines')) { + map.addSource('connection-lines', { + type: 'geojson', + data: connectionGeoJSON + }); + } + + if (!map.getLayer('connection-lines-layer')) { + map.addLayer({ + id: 'connection-lines-layer', + type: 'line', + source: 'connection-lines', + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#007bff', + 'line-width': 2, + 'line-opacity': 0.8, + 'line-dasharray': [2, 2] + } + }); + } + } + } + + // Add orthophoto WMTS source + // All layers that must always sit above the orthophoto + const ASSET_LAYER_ORDER = [ + 'connection-lines-layer', + 'sibling-cables', 'sibling-poles', 'sibling-cabinets', 'sibling-radars', 'sibling-equipment', + 'asset-polygon-fill', 'asset-polygon-outline', + 'asset-line-outline', 'asset-line-fill', + 'asset-point-outer', 'asset-point-middle', 'asset-point-inner', + 'current-asset-icon', + ]; + + function bringAssetLayersToTop() { + ASSET_LAYER_ORDER.forEach(id => { + if (map.getLayer(id)) map.moveLayer(id); + }); + } + + function addOrthoLayerSwitcher() { + if (typeof LayerSwitcherControl === 'undefined') return; + + const layerSwitcher = new LayerSwitcherControl({ + layers: [ + { + id: 'ortho', + name: 'Orthophotos', + icon: 'satellite', + group: 'ortho', + layer: { + type: 'raster', + paint: { 'raster-opacity': 1 } + }, + sublayers: [ + { + id: 'urbis', + name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)', + default: true, + source: { + type: 'raster', + tiles: [window.MAP_ORTHO_WMS_URL || URBIS_WMS_URL], + tileSize: 256, + attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS' + } + }, + { + id: 'vlaanderen', + name: 'Vlaanderen 2023', + source: { + type: 'raster', + tiles: [ORTHO_WMTS_URL], + tileSize: 256, + attribution: '© Digitaal Vlaanderen - Orthofoto 2023' + } + } + ] + } + ] + }); + + map.addControl(layerSwitcher, 'top-left'); + + // The LayerSwitcherControl appends sublayer selectors to document.body with + // position:fixed and hardcoded coordinates designed for fullscreen maps. + // We move each selector into #asset-map (position:relative) and switch to + // position:absolute so it stays anchored to the map div. + const mapEl = document.getElementById('asset-map'); + if (mapEl) { + // Wait one tick for the control to finish rendering + setTimeout(() => { + document.querySelectorAll('.layer-switcher-sublayer-selector').forEach(sel => { + // Only move selectors that are still attached to body + if (sel.parentNode === document.body) { + mapEl.appendChild(sel); + sel.style.position = 'absolute'; + sel.style.left = '46px'; + sel.style.top = '10px'; + sel.style.zIndex = '10'; + } + }); + }, 0); + } + + // After any ortho interaction, put asset layers back on top + map.getContainer().addEventListener('click', (e) => { + if (e.target.closest('.layer-switcher-btn') || e.target.closest('.layer-switcher-sublayer-btn')) { + setTimeout(bringAssetLayersToTop, 150); + } + }, true); + } + + // Function to refresh the map with new geometry (called after editing) + window.refreshAssetMap = function(newGeometry) { + if (!map) return; + + // Remove existing source and layers + const layerIds = ['asset-point-outer', 'asset-point-middle', 'asset-point-inner', + 'asset-line-outline', 'asset-line-fill', + 'asset-polygon-fill', 'asset-polygon-outline', + 'current-asset-icon']; + + layerIds.forEach(id => { + if (map.getLayer(id)) { + map.removeLayer(id); + } + }); + + if (map.getSource('asset-geometry')) { + map.removeSource('asset-geometry'); + } + + // Add new geometry + if (newGeometry) { + addAssetGeometry(newGeometry); + + const bounds = calculateBounds(newGeometry); + if (bounds) { + map.fitBounds(bounds, { + padding: 50, + maxZoom: 18 + }); + } + } + }; + +})(); diff --git a/streetup/assets/static/assets/asset_geometry_edit.js b/streetup/assets/static/assets/asset_geometry_edit.js new file mode 100644 index 0000000..46f97d7 --- /dev/null +++ b/streetup/assets/static/assets/asset_geometry_edit.js @@ -0,0 +1,560 @@ +// Asset Geometry Editing via MapLibre with MapboxDraw +// This module provides functionality to edit asset geometry (Point, LineString, Polygon) directly on the map + +(function() { + 'use strict'; + + let mapInstance = null; + let drawInstance = null; + let isEditingGeometry = false; + let originalGeometry = null; + let editedGeometry = null; + let currentGeometryType = 'Point'; // 'Point', 'LineString', or 'Polygon' + let editMarker = null; // For simple point editing fallback + let drawControlAdded = false; + + // MapboxDraw styles for asset editing + const drawStyles = [ + // Polygon fill + { + id: 'gl-draw-polygon-fill', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon']], + paint: { + 'fill-color': ['case', ['==', ['get', 'active'], 'true'], '#007bff', '#3388ff'], + 'fill-opacity': ['case', ['==', ['get', 'active'], 'true'], 0.3, 0.2] + } + }, + // Lines (for LineString and Polygon outlines) + { + id: 'gl-draw-lines', + type: 'line', + filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': ['case', ['==', ['get', 'active'], 'true'], '#007bff', '#3388ff'], + 'line-width': ['case', ['==', ['get', 'active'], 'true'], 3, 2] + } + }, + // Point (outer circle - white border) + { + id: 'gl-draw-point-outer', + type: 'circle', + filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 8, 6], + 'circle-color': 'white' + } + }, + // Point (inner circle - colored) + { + id: 'gl-draw-point-inner', + type: 'circle', + filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 6, 4], + 'circle-color': ['case', ['==', ['get', 'active'], 'true'], '#007bff', '#3388ff'] + } + }, + // Vertex points (outer) + { + id: 'gl-draw-vertex-outer', + type: 'circle', + filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['!=', 'mode', 'simple_select']], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 7, 5], + 'circle-color': 'white' + } + }, + // Vertex points (inner) + { + id: 'gl-draw-vertex-inner', + type: 'circle', + filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['!=', 'mode', 'simple_select']], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 5, 3], + 'circle-color': '#007bff' + } + }, + // Midpoints + { + id: 'gl-draw-midpoint', + type: 'circle', + filter: ['all', ['==', 'meta', 'midpoint']], + paint: { + 'circle-radius': 4, + 'circle-color': '#007bff' + } + } + ]; + + // Initialize when map is ready + document.addEventListener('DOMContentLoaded', function() { + // Wait for map to be available + const checkMap = setInterval(function() { + if (window.map) { + mapInstance = window.map; + clearInterval(checkMap); + initializeGeometryEditing(); + } + }, 100); + }); + + function initializeGeometryEditing() { + // Listen for edit mode events + document.addEventListener('asset-cms-edit-mode-enter', enableMapEditing); + document.addEventListener('asset-cms-edit-mode-exit', disableMapEditing); + } + + function initializeMapboxDraw() { + if (!window.MapboxDraw) { + console.warn('MapboxDraw is not available. Falling back to simple point editing.'); + return false; + } + + // Set MapboxDraw constants for MapLibre compatibility + MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas'; + MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl'; + MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-'; + MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group'; + MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib'; + + // Determine which controls to show based on geometry type + const controls = { + point: currentGeometryType === 'Point', + line_string: currentGeometryType === 'LineString', + polygon: currentGeometryType === 'Polygon', + trash: true, + combine_features: false, + uncombine_features: false + }; + + drawInstance = new MapboxDraw({ + displayControlsDefault: false, + controls: controls, + styles: drawStyles + }); + + return true; + } + + // Export function to enable editing + window.enableMapEditing = function() { + if (!mapInstance) return; + + // Ensure map is loaded before enabling editing + if (!mapInstance.loaded()) { + mapInstance.on('load', function() { + window.enableMapEditing(); + }); + return; + } + + isEditingGeometry = true; + + // Hide original read-only asset layers to avoid duplicate drawing + const layersToHide = [ + 'asset-point-outer', 'asset-point-middle', 'asset-point-inner', + 'asset-line-outline', 'asset-line-fill', + 'asset-polygon-fill', 'asset-polygon-outline' + ]; + layersToHide.forEach(layerId => { + try { + if (mapInstance.getLayer(layerId)) { + mapInstance.setLayoutProperty(layerId, 'visibility', 'none'); + } + } catch (err) { + console.warn(`Failed to hide layer ${layerId}:`, err); + } + }); + + // Get current asset geometry type from page data + const container = document.getElementById('asset-detail-container'); + if (!container) return; + + // Store original geometry + originalGeometry = getAssetGeometryFromPage(); + currentGeometryType = originalGeometry?.type || 'Point'; + + // Show edit instructions + showEditInstructions(); + + // Try to use MapboxDraw for all geometry types + if (initializeMapboxDraw() && drawInstance) { + try { + // Add the draw control + if (!drawControlAdded) { + mapInstance.addControl(drawInstance, 'top-right'); + drawControlAdded = true; + } + + // Load existing geometry into draw + if (originalGeometry) { + const feature = { + type: 'Feature', + geometry: originalGeometry, + properties: {} + }; + drawInstance.add(feature); + + // Select the feature for editing + // Use simple_select for points, direct_select for lines/polygons + const features = drawInstance.getAll(); + if (features.features.length > 0) { + const featureId = features.features[0].id; + if (currentGeometryType === 'Point') { + // For points, use simple_select mode + drawInstance.changeMode('simple_select', { + featureIds: [featureId] + }); + } else { + // For lines and polygons, use direct_select mode + drawInstance.changeMode('direct_select', { + featureId: featureId + }); + } + } + } else { + // No existing geometry: enter draw mode so the user can click to place + if (currentGeometryType === 'Point') { + drawInstance.changeMode('draw_point'); + } else if (currentGeometryType === 'LineString') { + drawInstance.changeMode('draw_line_string'); + } else if (currentGeometryType === 'Polygon') { + drawInstance.changeMode('draw_polygon'); + } + } + + // Listen for draw events + mapInstance.on('draw.create', handleDrawCreate); + mapInstance.on('draw.update', handleDrawUpdate); + mapInstance.on('draw.delete', handleDrawDelete); + + // Cursor management for draw mode + mapInstance.on('mousemove', handleDrawMouseMove); + } catch (e) { + console.warn('MapboxDraw error, falling back to simple point editing:', e); + // Cleanup any partial initialization + if (drawControlAdded && drawInstance) { + try { + mapInstance.removeControl(drawInstance); + } catch (removeError) { + // Ignore removal errors + } + drawControlAdded = false; + drawInstance = null; + } + // Fallback to simple point editing + enableSimplePointEditing(); + } + } else { + // Fallback to simple point editing + enableSimplePointEditing(); + } + }; + + // Export function to disable editing + window.disableMapEditing = function() { + if (!mapInstance) return; + + isEditingGeometry = false; + + // Restore visibility of original read-only asset layers + const layersToShow = [ + 'asset-point-outer', 'asset-point-middle', 'asset-point-inner', + 'asset-line-outline', 'asset-line-fill', + 'asset-polygon-fill', 'asset-polygon-outline' + ]; + layersToShow.forEach(layerId => { + try { + if (mapInstance.getLayer(layerId)) { + mapInstance.setLayoutProperty(layerId, 'visibility', 'visible'); + } + } catch (err) { + console.warn(`Failed to show layer ${layerId}:`, err); + } + }); + + // Remove draw control and listeners + if (drawInstance && drawControlAdded) { + mapInstance.off('draw.create', handleDrawCreate); + mapInstance.off('draw.update', handleDrawUpdate); + mapInstance.off('draw.delete', handleDrawDelete); + mapInstance.off('mousemove', handleDrawMouseMove); + + // Remove all drawn features + drawInstance.deleteAll(); + + // Remove the control + mapInstance.removeControl(drawInstance); + drawControlAdded = false; + drawInstance = null; + } + + // Cleanup simple point editing + disableSimplePointEditing(); + + // Reset geometry if not saved + if (!editedGeometry) { + editedGeometry = null; + } + + // Hide edit instructions + hideEditInstructions(); + + // Reset cursor + mapInstance.getCanvas().style.cursor = ''; + }; + + // Export function to get geometry changes + window.getGeometryChanges = function() { + // Get the latest geometry from draw if available + if (drawInstance) { + const features = drawInstance.getAll(); + if (features.features.length > 0) { + return features.features[0].geometry; + } + } + return editedGeometry; + }; + + function handleDrawCreate(e) { + const features = drawInstance.getAll().features; + + // Keep only the latest feature (delete old ones) + if (features.length > 1) { + const keepId = e.features[0].id; + const idsToDelete = features + .filter(f => f.id !== keepId) + .map(f => f.id); + drawInstance.delete(idsToDelete); + } + + editedGeometry = e.features[0].geometry; + updateCoordinateDisplay(editedGeometry); + showGeometryUpdateConfirmation(); + } + + function handleDrawUpdate(e) { + editedGeometry = e.features[0].geometry; + updateCoordinateDisplay(editedGeometry); + showGeometryUpdateConfirmation(); + } + + function handleDrawDelete(e) { + editedGeometry = null; + updateCoordinateDisplay(null); + } + + function handleDrawMouseMove(e) { + if (!drawInstance) return; + + const canvas = mapInstance.getCanvas(); + + // Get draw layers + const allLayers = mapInstance.getStyle().layers; + const drawLayerPrefixes = ['gl-draw-polygon-fill', 'gl-draw-vertex-outer', 'gl-draw-midpoint', 'gl-draw-point']; + const existingLayers = allLayers + .filter(layer => drawLayerPrefixes.some(prefix => layer.id.startsWith(prefix))) + .map(layer => layer.id); + + if (existingLayers.length === 0) return; + + const features = mapInstance.queryRenderedFeatures(e.point, { layers: existingLayers }); + + if (features.length > 0) { + const feature = features[0]; + if (feature.properties.meta === 'vertex' || feature.properties.meta === 'midpoint') { + canvas.style.cursor = 'grab'; + } else if (feature.properties.active === 'true') { + canvas.style.cursor = 'move'; + } else { + canvas.style.cursor = 'pointer'; + } + } else { + const currentMode = drawInstance.getMode(); + if (currentMode === 'draw_polygon' || currentMode === 'draw_line_string' || currentMode === 'draw_point') { + canvas.style.cursor = 'crosshair'; + } else { + canvas.style.cursor = ''; + } + } + } + + // --- Simple Point Editing (fallback) --- + function enableSimplePointEditing() { + mapInstance.getCanvas().style.cursor = 'crosshair'; + document.getElementById('asset-map')?.classList.add('edit-mode'); + mapInstance.on('click', handleSimpleMapClick); + + // Create draggable marker for existing point + if (originalGeometry && originalGeometry.type === 'Point') { + createDraggableMarker(originalGeometry.coordinates[0], originalGeometry.coordinates[1]); + } + } + + function disableSimplePointEditing() { + mapInstance.off('click', handleSimpleMapClick); + document.getElementById('asset-map')?.classList.remove('edit-mode'); + + if (editMarker) { + editMarker.remove(); + editMarker = null; + } + } + + function handleSimpleMapClick(e) { + if (!isEditingGeometry) return; + + const lngLat = e.lngLat; + editedGeometry = { + type: 'Point', + coordinates: [lngLat.lng, lngLat.lat] + }; + + createDraggableMarker(lngLat.lng, lngLat.lat); + updateCoordinateDisplay(editedGeometry); + showGeometryUpdateConfirmation(); + } + + function createDraggableMarker(lng, lat) { + if (editMarker) { + editMarker.setLngLat([lng, lat]); + } else { + const el = document.createElement('div'); + el.className = 'edit-marker'; + el.style.cssText = 'width: 20px; height: 20px; background-color: #007bff; border-radius: 50%; border: 3px solid #ffffff; box-shadow: 0 2px 6px rgba(0,0,0,0.3); cursor: move;'; + + editMarker = new maplibregl.Marker({ + element: el, + draggable: true + }) + .setLngLat([lng, lat]) + .addTo(mapInstance); + + editMarker.on('dragend', function() { + const lngLat = editMarker.getLngLat(); + editedGeometry = { + type: 'Point', + coordinates: [lngLat.lng, lngLat.lat] + }; + updateCoordinateDisplay(editedGeometry); + }); + } + + mapInstance.flyTo({ + center: [lng, lat], + zoom: mapInstance.getZoom() + }); + } + + // --- Helper Functions --- + function getAssetGeometryFromPage() { + // Try to get geometry from geojson data attribute + const container = document.getElementById('asset-detail-container'); + if (container && container.dataset.assetGeometry) { + try { + return JSON.parse(container.dataset.assetGeometry); + } catch (e) { + console.warn('Failed to parse asset geometry:', e); + } + } + + // Fallback: try to get from lon/lat inputs + const lonInput = document.querySelector('.asset-editable-field[data-field="lon"] input'); + const latInput = document.querySelector('.asset-editable-field[data-field="lat"] input'); + + if (lonInput && latInput && lonInput.value && latInput.value) { + return { + type: 'Point', + coordinates: [parseFloat(lonInput.value), parseFloat(latInput.value)] + }; + } + + return null; + } + + function updateCoordinateDisplay(geometry) { + if (!geometry) return; + + // Update lon/lat inputs for Point geometries + if (geometry.type === 'Point') { + const lonInput = document.querySelector('.asset-editable-field[data-field="lon"] .field-edit input'); + const latInput = document.querySelector('.asset-editable-field[data-field="lat"] .field-edit input'); + + if (lonInput) lonInput.value = geometry.coordinates[0].toFixed(6); + if (latInput) latInput.value = geometry.coordinates[1].toFixed(6); + } + + // Update geometry display for LineString and Polygon + const geomDisplay = document.getElementById('geometry-display'); + if (geomDisplay) { + if (geometry.type === 'LineString') { + geomDisplay.textContent = `Ligne: ${geometry.coordinates.length} points`; + } else if (geometry.type === 'Polygon') { + const pointCount = geometry.coordinates[0] ? geometry.coordinates[0].length : 0; + geomDisplay.textContent = `Polygone: ${pointCount} points`; + } + } + } + + function showEditInstructions() { + let instructions = document.getElementById('geometry-edit-instructions'); + if (instructions) instructions.remove(); + + instructions = document.createElement('div'); + instructions.id = 'geometry-edit-instructions'; + instructions.className = 'alert alert-info alert-dismissible position-absolute'; + instructions.style.cssText = 'top: 10px; left: 10px; right: 50px; z-index: 1000; font-size: 0.875rem;'; + + let message = ''; + switch (currentGeometryType) { + case 'Point': + message = originalGeometry + ? 'Cliquez sur la carte pour déplacer le point, ou faites glisser le marqueur.' + : 'Cliquez sur la carte pour placer le point.'; + break; + case 'LineString': + message = 'Cliquez sur les sommets pour les déplacer. Utilisez le bouton ligne pour dessiner une nouvelle géométrie.'; + break; + case 'Polygon': + message = 'Cliquez sur les sommets pour les déplacer. Utilisez le bouton polygone pour dessiner une nouvelle géométrie.'; + break; + default: + message = 'Utilisez les outils de dessin pour modifier la géométrie.'; + } + + instructions.innerHTML = ` + + ${message} + + `; + + const mapContainer = document.getElementById('asset-map') || document.getElementById('map'); + if (mapContainer) { + mapContainer.style.position = 'relative'; + mapContainer.appendChild(instructions); + } + } + + function hideEditInstructions() { + const instructions = document.getElementById('geometry-edit-instructions'); + if (instructions) { + instructions.remove(); + } + } + + function showGeometryUpdateConfirmation() { + const msg = document.createElement('div'); + msg.className = 'alert alert-success position-fixed'; + msg.style.cssText = 'bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 1060;'; + msg.innerHTML = 'Géométrie mise à jour'; + document.body.appendChild(msg); + + setTimeout(() => msg.remove(), 2000); + } + +})(); diff --git a/streetup/assets/static/assets/asset_location_edit.js b/streetup/assets/static/assets/asset_location_edit.js new file mode 100644 index 0000000..c5a64e1 --- /dev/null +++ b/streetup/assets/static/assets/asset_location_edit.js @@ -0,0 +1,192 @@ +/** + * Asset Location Edit - JavaScript handler + * Handles the changing of asset location in edit mode + */ +(function() { + 'use strict'; + + const changeLocationBtn = document.getElementById('changeLocationBtn'); + const changeLocationSelect = document.getElementById('changeLocationSelect'); + const changeLocationSearch = document.getElementById('changeLocationSearch'); + const changeLocationSearchHelp = document.getElementById('changeLocationSearchHelp'); + const confirmChangeLocationBtn = document.getElementById('confirmChangeLocationBtn'); + const changeLocationAlert = document.getElementById('changeLocationAlert'); + let searchDebounceTimer = null; + + if (!changeLocationBtn) return; // Skip if location editing not available + + // Initialize location selection when modal is shown + const changeLocationModal = document.getElementById('changeLocationModal'); + if (changeLocationModal) { + changeLocationModal.addEventListener('show.bs.modal', function() { + if (changeLocationSearch) changeLocationSearch.value = ''; + setSearchHelp(''); + loadLocationsForAsset(''); + }); + } + + if (changeLocationSearch) { + changeLocationSearch.addEventListener('input', function() { + const query = this.value.trim(); + clearTimeout(searchDebounceTimer); + + searchDebounceTimer = setTimeout(() => { + if (query.length === 0 || query.length >= 2) { + loadLocationsForAsset(query); + } else { + setSearchHelp(gettext('Saisissez au moins 2 caractères pour rechercher.')); + } + }, 250); + }); + } + + // Handle location change confirmation + if (confirmChangeLocationBtn) { + confirmChangeLocationBtn.addEventListener('click', function() { + handleLocationChange(); + }); + } + + function loadLocationsForAsset(query) { + const assetId = changeLocationSelect.dataset.assetId; + const thematicCode = changeLocationSelect.dataset.thematic; + + if (!assetId || !thematicCode) { + console.error('Missing asset ID or thematic code'); + return; + } + + changeLocationSelect.innerHTML = ''; + changeLocationSelect.disabled = true; + + // Fetch available locations based on thematic + const params = new URLSearchParams(); + params.set('limit', '120'); + if (query) params.set('q', query); + const url = `/assets/${thematicCode}/asset/${assetId}/available-locations/?${params.toString()}`; + + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + changeLocationSelect.innerHTML = ''; + + if (data.locations && data.locations.length > 0) { + data.locations.forEach(location => { + const option = document.createElement('option'); + option.value = location.id; + option.textContent = `${location.code} - ${location.name}`; + changeLocationSelect.appendChild(option); + }); + changeLocationSelect.disabled = false; + if (data.has_more) { + setSearchHelp(gettext('Résultats limités. Affinez la recherche pour trouver plus facilement.')); + } else { + setSearchHelp(''); + } + } else { + changeLocationSelect.innerHTML = ''; + if (query) { + setSearchHelp(gettext('Aucun résultat pour cette recherche.')); + } else { + setSearchHelp(''); + } + } + }) + .catch(error => { + console.error('Error loading locations:', error); + changeLocationSelect.innerHTML = ''; + setSearchHelp(''); + showAlert('danger', gettext('Erreur lors du chargement des localisations')); + }); + } + + function setSearchHelp(message) { + if (!changeLocationSearchHelp) return; + changeLocationSearchHelp.textContent = message || ''; + } + + function handleLocationChange() { + const locationId = changeLocationSelect.value; + + if (!locationId) { + showAlert('warning', gettext('Veuillez sélectionner une localisation')); + return; + } + + // Disable button + confirmChangeLocationBtn.disabled = true; + const originalText = confirmChangeLocationBtn.innerHTML; + confirmChangeLocationBtn.innerHTML = '' + gettext('Mise à jour...'); + + // Get asset info from page + const assetContainer = document.getElementById('asset-detail-container'); + const assetModel = assetContainer.dataset.assetModel; + const assetId = assetContainer.dataset.assetId; + + // Send update request + fetch(`/assets/${assetModel}/${assetId}/update/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ + location_id: locationId, + intersection_id: locationId // For compatibility with different asset types + }) + }) + .then(response => response.json()) + .then(result => { + confirmChangeLocationBtn.disabled = false; + confirmChangeLocationBtn.innerHTML = originalText; + + if (result.success) { + showAlert('success', gettext('Localisation mise à jour')); + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + showAlert('danger', result.error || gettext('Erreur lors de la mise à jour')); + } + }) + .catch(error => { + console.error('Error:', error); + confirmChangeLocationBtn.disabled = false; + confirmChangeLocationBtn.innerHTML = originalText; + showAlert('danger', gettext('Erreur lors de la mise à jour')); + }); + } + + function showAlert(type, message) { + if (!changeLocationAlert) return; + + changeLocationAlert.className = `alert alert-${type}`; + changeLocationAlert.innerHTML = message; + changeLocationAlert.classList.remove('d-none'); + + // Auto-hide after 5 seconds + setTimeout(() => { + changeLocationAlert.classList.add('d-none'); + }, 5000); + } + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +})(); diff --git a/streetup/assets/static/assets/assets_index.css b/streetup/assets/static/assets/assets_index.css new file mode 100644 index 0000000..8c8ffc7 --- /dev/null +++ b/streetup/assets/static/assets/assets_index.css @@ -0,0 +1,18 @@ +#intersections-table-container, #locations-table-container, #assets-table-container { + display: none; +} + +.intervention-btn { + color: var(--brand-color); + margin-right: 4px; + } + +.intersection-btn-toggle{ + color: var(--brand-color); +} + +.separator-top { + border-top: 1px solid var(--brand-color); + margin-top: 2rem; /* optionnel */ + padding-top: 2rem; /* optionnel */ +} \ No newline at end of file diff --git a/streetup/assets/static/assets/assets_index.js b/streetup/assets/static/assets/assets_index.js new file mode 100644 index 0000000..8aa951b --- /dev/null +++ b/streetup/assets/static/assets/assets_index.js @@ -0,0 +1,1508 @@ +var visible_locations = [] +var visible_assets = [] +var filter_geom = '' + +var locations_geojson = {}; +const locationsgeojsonloader = new GeojsonLoader(); + +if (!window.locations_outline_layer) { + locations_outline_layer = { + id: 'locations-outline', + type: 'line', + source: 'locations', + paint: { + 'line-color': [ + 'case', + ['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b', + '#00000000' // Transparent si pas highlighté + ], + 'line-width': [ + 'case', + ['boolean', ['feature-state', 'highlighted'], false], 1, + 0 + ] + } + }; +} +handleGeojsonMapLoading(init=true, locationsgeojsonloader, 'locations', [locations_layer_def,locations_outline_layer]); + +// Reset table if error occurs when loading GeoJSON +locationsgeojsonloader.addEventListener("geojsonError", event => { + loadDataIntoLocationsTable([]); +}); + +// Load data into table when GeoJSON is loaded +locationsgeojsonloader.addEventListener("geojsonLoaded", event => { + var geojson_data = event.detail; + loadDataIntoLocationsTable(geojson_data.features); + locations_geojson = geojson_data; +}); + + +var assets_geojson = {}; +const assetsgeojsonloader = new GeojsonLoader(); + +handleGeojsonMapLoading(init=true, assetsgeojsonloader, 'assets', assets_layers_defs); + + +assetsgeojsonloader.addEventListener("geojsonError", event => { + loadDataIntoAssetsTable([]); +}); + +assetsgeojsonloader.addEventListener("geojsonLoaded", event => { + var geojson_data = event.detail; + loadDataIntoAssetsTable(geojson_data.features); + assets_geojson = geojson_data; +}); + +// Load Map +maploader.loadMap("map"); + +// Drawing selection tool +MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas'; +MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl'; +MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-'; +MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group'; +MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib'; + +const drawstyles=[{id:"gl-draw-polygon-fill",type:"fill",filter:["all",["==","$type","Polygon"],],paint:{"fill-color":["case",["==",["get","active"],"true"],"orange","blue",],"fill-opacity":["case",["==",["get","active"],"true"],0.1,0,]}},{id:"gl-draw-lines",type:"line",filter:["any",["==","$type","LineString"],["==","$type","Polygon"],],layout:{"line-cap":"round","line-join":"round"},paint:{"line-color":["case",["==",["get","active"],"true"],"orange","blue",],"line-dasharray":["literal",[.2,2]],"line-width":2}},{id:"gl-draw-point-outer",type:"circle",filter:["all",["==","$type","Point"],["==","meta","feature"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],7,5,],"circle-color":"white"}},{id:"gl-draw-point-inner",type:"circle",filter:["all",["==","$type","Point"],["==","meta","feature"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],5,3,],"circle-color":["case",["==",["get","active"],"true"],"orange","blue",]}},{id:"gl-draw-vertex-outer",type:"circle",filter:["all",["==","$type","Point"],["==","meta","vertex"],["!=","mode","simple_select"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],7,5,],"circle-color":"white"}},{id:"gl-draw-vertex-inner",type:"circle",filter:["all",["==","$type","Point"],["==","meta","vertex"],["!=","mode","simple_select"],],paint:{"circle-radius":["case",["==",["get","active"],"true"],5,3,],"circle-color":"orange"}},{id:"gl-draw-midpoint",type:"circle",filter:["all",["==","meta","midpoint"],],paint:{"circle-radius":3,"circle-color":"orange"}},]; + +const draw = new MapboxDraw({ + displayControlsDefault: false, + controls: { + polygon: true, + trash: true + }, + styles: drawstyles +}); + +map.on('load', () => { + map.addControl(draw); + + map.boxZoom.enable(); + + // Initial state + updateAutoZoneVisibility(); + onMoveEndAuto(); // if already zoomed in on load, run once + + // Gestion du curseur pour les vertices et midpoints + map.on('mousemove', (e) => { + const canvas = map.getCanvas(); + + // Récupérer les layers draw en utilisant des préfixes (car ils ont des suffixes .cold/.hot) + const allLayers = map.getStyle().layers; + const drawLayerPrefixes = ['gl-draw-polygon-fill', 'gl-draw-vertex-outer', 'gl-draw-midpoint']; + const existingLayers = allLayers + .filter(layer => drawLayerPrefixes.some(prefix => layer.id.startsWith(prefix))) + .map(layer => layer.id); + + if (existingLayers.length === 0) return; + + const features = map.queryRenderedFeatures(e.point, { + layers: existingLayers + }); + + // Variable pour savoir si on a appliqué un curseur draw + let drawCursorApplied = false; + + if (features.length > 0) { + const feature = features[0]; + // Si c'est un vertex ou midpoint + if (feature.properties.meta === 'vertex' || feature.properties.meta === 'midpoint') { + canvas.style.cursor = 'grab'; + drawCursorApplied = true; + } + // Si c'est le polygone actif en mode direct_select + else if (feature.properties.active === 'true') { + canvas.style.cursor = 'move'; + drawCursorApplied = true; + } + // Si c'est un polygone non activé (sélectionnable) + else if (feature.properties.active === 'false') { + canvas.style.cursor = 'pointer'; + drawCursorApplied = true; + } + } + + // Réinitialiser le curseur si on ne survole plus un élément draw + // et qu'on n'est pas en mode dessin + if (!drawCursorApplied) { + const currentMode = draw.getMode(); + if (currentMode === 'draw_polygon') { + canvas.style.cursor = 'crosshair'; + } else if (currentMode === 'simple_select' || currentMode === 'direct_select') { + // Ne pas interférer avec les curseurs des autres couches + // Réinitialiser uniquement si aucun autre événement ne gère le curseur + const otherFeatures = map.queryRenderedFeatures(e.point); + const hasOtherFeatures = otherFeatures.some(f => + !existingLayers.includes(f.layer.id) + ); + if (!hasOtherFeatures) { + canvas.style.cursor = ''; + } + } + } + }); + + // Curseur pendant le drag d'un vertex + map.on('mousedown', (e) => { + const allLayers = map.getStyle().layers; + const drawLayerPrefixes = ['gl-draw-vertex-outer', 'gl-draw-midpoint']; + const existingLayers = allLayers + .filter(layer => drawLayerPrefixes.some(prefix => layer.id.startsWith(prefix))) + .map(layer => layer.id); + + if (existingLayers.length === 0) return; + + const features = map.queryRenderedFeatures(e.point, { + layers: existingLayers + }); + + if (features.length > 0) { + map.getCanvas().style.cursor = 'grabbing'; + } + }); + + map.on('mouseup', () => { + const currentMode = draw.getMode(); + if (currentMode === 'direct_select') { + map.getCanvas().style.cursor = 'move'; + } + }); +}); + +map.on('draw.create', (e) => { + const features = draw.getAll().features; + if (features.length > 1) { + // Supprime tous les autres sauf le dernier + const keep = e.features[0].id; + const idsToDelete = features + .filter(f => f.id !== keep) + .map(f => f.id); + draw.delete(idsToDelete); + } + // Réinitialiser le curseur après la création + map.getCanvas().style.cursor = ''; +}); + +map.on('dblclick', (e) => { + e.preventDefault(); + + // Parfois le dragPan est désactivé à tort, on le réactive manuellement + if (!map.dragPan.isEnabled()) { + map.dragPan.enable(); + } +}); + + +map.on('draw.modechange', (e) => { + const safeModes = ['simple_select', 'direct_select']; + + if (safeModes.includes(e.mode) && !map.boxZoom.isEnabled()) { + map.boxZoom.enable(); + } + + // Changer le curseur en fonction du mode + const canvas = map.getCanvas(); + if (e.mode === 'draw_polygon') { + canvas.style.cursor = 'crosshair'; + } else if (e.mode === 'direct_select') { + canvas.style.cursor = 'move'; + } else { + canvas.style.cursor = ''; + } +}); + + + +map.on('styledata', () => { + if (map.getLayer('assets') && map.getLayer('locations')) { + map.moveLayer('locations', 'assets'); + } +}); + + +// Récupération des IDs des layers 'assets' +const assetLayerIds = assets_layers_defs.map(l => l.id); + + +// Map layers events +//registerLayerMouseEnter(map, 'locations', popup, 'code', 14); +registerLayerMouseMove(map, 'locations', popup, 'code', 14); +registerLayerMouseLeave(map, 'locations', popup); + +assetLayerIds.forEach(id => { + registerLayerMouseMove(map, id, popup, 'name', 17); + registerLayerMouseLeave(map, id, popup); +}); + + +var locationshighlightState = { previousId: null }; // Track the previously highlighted feature +map.on('click', 'locations', (e) => { + const featureId = e.features[0].id; + const currentZoom = map.getZoom(); + if (currentZoom > 13) { + // Activer l'onglet "locations" pour afficher le tableau + const locationsTab = document.getElementById('locations-table-tab'); + if (locationsTab) { + const tab = new bootstrap.Tab(locationsTab); + tab.show(); + } + + // Highlight the corresponding row in the Bootstrap table + highlightRow('#locations-table', 'id', featureId); + + // Update feature highlight on the map + highlightMapFeature(map, 'locations', featureId, locationshighlightState); + + // Load assets GeoJSON for the selected location + // const locationId = featureId; + // const streetAssetsGeojsonUrl = `${assetsgeojsonUrl}?location=${locationId}`; + // assetsgeojsonloader.loadGeojson(streetAssetsGeojsonUrl); + // handleGeojsonMapLoading(init=false, assetsgeojsonloader, 'assets', [assets_layer_def]); + } +}); + +var assetshighlightState = { previousId: null }; // Track the previously highlighted feature + + + +// Handler unique pour tous les layers "assets" +function onAssetClick(e) { + const f = e.features && e.features[0]; + if (!f) return; + + // Id robuste (si jamais .id n’est pas présent) + const featureId = + f.id ?? + f.properties?.id ?? + f.properties?.pk ?? + f.properties?.objectid; + + if (featureId == null) return; + + if (map.getZoom() > 13) { + // Activer l'onglet "assets" pour afficher le tableau + const assetsTab = document.getElementById('assets-table-tab'); + if (assetsTab) { + const tab = new bootstrap.Tab(assetsTab); + tab.show(); + } + + // surbrillance dans le tableau + highlightRow('#assets-table', 'id', featureId); + + // surbrillance sur la carte + highlightMapFeature(map, 'assets', featureId, assetshighlightState); + } +} + +// Enregistrement du handler sur chaque layer + curseur +assetLayerIds.forEach(id => { + map.on('click', id, onAssetClick); + map.on('mouseenter', id, () => (map.getCanvas().style.cursor = 'pointer')); + map.on('mouseleave', id, () => (map.getCanvas().style.cursor = '')); +}); + + + + + + +// === config & state === +const AUTO_ZOOM_THRESHOLD = 15; +const autoZoneWrapper = document.getElementById('auto-zone-wrapper'); +const autoZoneCheck = document.getElementById('autoZoneCheck'); +let autoZoneEnabled = true; +let lastBboxKey = null; + +// mémoriser si on était au-dessus du seuil +let prevIsHighZoom = map.getZoom() >= AUTO_ZOOM_THRESHOLD; + + +function debounce(fn, wait=300) { + let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); }; +} + +// Make a deterministic key for current bbox (rounded to 6 decimals) +function bboxKey(bounds) { + const sw = bounds.getSouthWest(), ne = bounds.getNorthEast(); + const r = n => n.toFixed(6); + return `${r(sw.lng)},${r(sw.lat)}|${r(ne.lng)},${r(ne.lat)}`; +} + +function bboxFeatureFromBounds(bounds) { + const sw = bounds.getSouthWest(); + const ne = bounds.getNorthEast(); + const nw = { lng: sw.lng, lat: ne.lat }; + const se = { lng: ne.lng, lat: sw.lat }; + return { + type: "Feature", + properties: { source: "viewbbox" }, + geometry: { type: "Polygon", coordinates: [[ + [sw.lng, sw.lat],[se.lng, se.lat],[ne.lng, ne.lat],[nw.lng, nw.lat],[sw.lng, sw.lat] + ]]} + }; +} + +function updateAutoZoneVisibility() { + const isHigh = map.getZoom() >= AUTO_ZOOM_THRESHOLD; + + // Affiche/masque la checkbox + if (isHigh) { + autoZoneWrapper.classList.remove('d-none'); + } else { + autoZoneWrapper.classList.add('d-none'); + } + + // ⬇️ NEW: si on vient de passer de ≥ seuil à < seuil → reset ONCE + if (prevIsHighZoom && !isHigh) { + lastBboxKey = null; // invalide la clé de bbox + resetMapFilter(); // appelle ton reset exactement une fois sur la transition + } + + prevIsHighZoom = isHigh; // maj l'état pour la prochaine fois +} + +const triggerAutoZoneFilter = () => { + const geomFeature = bboxFeatureFromBounds(map.getBounds()); + updateSelection(geomFeature); // your existing function +}; + +// Single place that triggers the filter +const onMoveEndAuto = debounce(() => { + if (!autoZoneEnabled) return; + if (map.getZoom() < AUTO_ZOOM_THRESHOLD) return; + + const key = bboxKey(map.getBounds()); + if (key !== lastBboxKey) { + lastBboxKey = key; + triggerAutoZoneFilter(); + } +}, 250); + +// Wire events +map.on('zoomend', updateAutoZoneVisibility); // visibility only +map.on('moveend', onMoveEndAuto); + +autoZoneCheck.addEventListener('change', (e) => { + autoZoneEnabled = e.target.checked; + lastBboxKey = null; // ensure a fresh trigger on next move + if (autoZoneEnabled && map.getZoom() >= AUTO_ZOOM_THRESHOLD) { + onMoveEndAuto(); // schedule one run + } + if (!autoZoneEnabled) { + resetMapFilter(); + } +}); + +document.addEventListener("DOMContentLoaded", () => { + const autoZoneCheck = document.getElementById("autoZoneCheck"); + if (autoZoneCheck) { + autoZoneCheck.checked = true; + } +}); + + + + +// Charger les données initiales +let params = new URLSearchParams(window.location.search).toString(); +locationsgeojsonloader.loadGeojson(locationsgeojsonUrl+`?${params}`); +assetsgeojsonloader.loadGeojson(assetsgeojsonUrl+`?${params}`); + + + +function actionsFormatter(value, row, index) { + if (selected_locations_set.has(row.id)) { + icon_select = 'bi-check-square-fill'; + } else { + icon_select = 'bi-square'; + } + + resp = ``; + + if (row.lon) { + resp = resp + + ``; + } + + return resp; +} + +function codeFormatterLocations(value, row, index) { + if (row.detail_url) { + return `${value}`; + } + return value; +} + + +function actionsFormatterAssets(value, row, index) { + + const key = assetKey(row.asset_type, row.id); + const isSelected = selected_assets_set.has(key); + const btnClass = 'btn-light'; + const iconClass = isSelected ? 'bi-check-square' : 'bi-square'; + const selectedValue = isSelected ? 'true' : 'false'; + + resp = ` + `; + + if (row.lon) { + resp = resp + + ``; + } + + return resp; +} + +function codeFormatterAssets(value, row, index) { + if (row.detail_url) { + return `${value}`; + } + return value; +} + + + + +// Loads features into the locations table +function loadDataIntoLocationsTable(features) { + const tableData = features.map((feature) => { + const row = { + actions: '', // Colonne pour les actions (boutons, icônes, etc.) + id: feature.id, + code: feature.properties.code, + lon: feature.properties.lon, + lat: feature.properties.lat + }; + + // Ajoute dynamiquement les autre propriétés + locations_property_fields.forEach(field => { + row[field] = feature.properties[field]; + }); + + row.detail_url = feature.properties.detail_url || null; + + return row; + }); + + $('#locations-spinner').hide(); + $('#locations-table-container').show(); + $('#locations-table').bootstrapTable('load', tableData); +} + +// Loads features into the assets table +function loadDataIntoAssetsTable(features) { + const tableData = features.map((feature) => { + const row = { + actions: '', // Colonne pour les actions (boutons, icônes, etc.) + id: feature.id, + asset_type: feature.properties.asset_type, + code: feature.properties.code, + status: '', + lon: feature.properties.lon, + lat: feature.properties.lat + }; + + // Ajoute dynamiquement les autre propriétés + assets_property_fields.forEach(field => { + row[field] = feature.properties[field]; + }); + + row.detail_url = feature.properties.detail_url || null; + + return row; + }); + + $('#assets-spinner').hide(); + $('#assets-table-container').show(); + $('#assets-table').bootstrapTable('load', tableData); +} + + + +function redirectToThematic() { + const select = document.getElementById('theme-select'); + const selectedValue = select.value; + + if (selectedValue) { + // Rediriger vers l'URL sélectionnée + window.location.href = selectedValue; + } +} + + +$( document ).ready(function() { + + // Apply the correct width to the dropdown-menu + const parent_filter_dropdown = $('#col-table'); + const parentWidth = parent_filter_dropdown.width() - 20; + // $('#filter-dropdown-wrapper').find('.dropdown-menu').width(`${parentWidth}px`); + + function calculateTableHeight() { + const windowHeight = $(window).height(); // Total window height + const offsetTop = 285; // Y position of the table + const footerHeight = $('.footer').outerHeight() || 0; + return windowHeight - offsetTop - footerHeight - 20; + } + + $('#locations-table').bootstrapTable({ + columns: [ + { field: 'actions', title: '', formatter:'actionsFormatter', class: 'text-nowrap', width: 90 }, + { field: 'code', title: gettext('Code'), class: 'text-nowrap', formatter: 'codeFormatterLocations' }, + ...locations_columns // Colonnes spécifiques + ], + height: calculateTableHeight(), // Apply computed height + onClickRow: function (row, $element) { + // Remove highlight from all previously highlighted cells + $('#locations-table').find('tbody td').removeClass('rowhighlight'); + // Apply the highlight class to all `` cells in the row + $element.find('td').addClass('rowhighlight'); + + // Update feature highlight on the map + highlightMapFeature(map, 'locations', row.id, locationshighlightState); + + // Load assets GeoJSON for the selected location + // const locationId = row.id; + // const locationAssetsGeojsonUrl = `${assetsgeojsonUrl}?location=${locationId}`; + // assetsgeojsonloader.loadGeojson(locationAssetsGeojsonUrl); + // handleGeojsonMapLoading(init=false, assetsgeojsonloader, 'assets', [assets_layer_def]); + } + }); + + $('#assets-table').bootstrapTable({ + columns: [ + { field: 'actions', title: '', formatter:'actionsFormatterAssets', width: 90 }, + { field: 'code', title: gettext('Code'), formatter: 'codeFormatterAssets' }, + ...assets_columns, // Colonnes spécifiques + { field: 'status', title: gettext('Statut'), align: 'center' } + + ], + height: calculateTableHeight(), // Apply computed height + onClickRow: function (row, $element) { + // Remove highlight from all previously highlighted cells + $('#assets-table').find('tbody td').removeClass('rowhighlight'); + // Apply the highlight class to all `` cells in the row + $element.find('td').addClass('rowhighlight'); + + // Update feature highlight on the map + highlightMapFeature(map, 'assets', row.id, assetshighlightState); + } + }); + + + $('#theme-select').on('change', function () { + redirectToThematic(); + }); + + // reset view when tab is clicked (in case of window resize) + $('#locations-table-tab').on('click', function () { + $('#locations-table').bootstrapTable('resetView'); + }); + $('#assets-table-tab').on('click', function () { + $('#assets-table').bootstrapTable('resetView'); + }); + + // Apply filters + document.getElementById('apply-filters').addEventListener('click', function(e) { + + // const form = document.getElementById('filter-form'); + // const formData = new FormData(form); + // const params = new URLSearchParams(); + // for (const [key, value] of formData.entries()) { + // if (value.trim() !== '') { // Vérifie que le champ n'est pas vide + // params.append(key, value); + // } + // } + + // // Update URL + // const newUrl = `${window.location.pathname}?${params.toString()}`; + // window.history.pushState({}, '', newUrl); + + // Load filtered data + // handleGeojsonMapLoading(init=false, locationsgeojsonloader, 'locations', [locations_layer_def]); + // locationsgeojsonloader.loadGeojson(locationsgeojsonUrl+`?${params.toString()}`); + updateLocations(); + + // Trouver le dropdown parent et le refermer + const dropdownEl = this.closest('.dropdown'); + const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]')); + if (dropdown) { + dropdown.hide(); // ✅ ferme proprement le dropdown + } + }); + + // Reset filters + document.getElementById('reset-filters').addEventListener('click', function(e) { + const form = document.getElementById('filter-form'); + + //form.reset(); // Reset form fields + // Parcourir tous les champs du formulaire et les réinitialiser + form.querySelectorAll('input, select, textarea').forEach(field => { + if (field.tagName.toLowerCase() === 'select') { + field.selectedIndex = -1; // aucune option sélectionnée + } else if (field.type === 'checkbox' || field.type === 'radio') { + field.checked = false; // décocher + } else { + field.value = ''; // vider le texte / nombre / date + } + }); + + // // Remove URL parameters + // window.history.pushState({}, '', window.location.pathname); + + // Reload all data without filters + // handleGeojsonMapLoading(init=false, locationsgeojsonloader, 'locations', [locations_layer_def]); + // locationsgeojsonloader.loadGeojson(locationsgeojsonUrl); + updateLocations(); + + // Trouver le dropdown parent et le refermer + const dropdownEl = this.closest('.dropdown'); + const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]')); + if (dropdown) { + dropdown.hide(); // ✅ ferme proprement le dropdown + } + }); + + + // Apply assets filters + document.getElementById('apply-assets-filters').addEventListener('click', function(e) { + // Load filtered data + updateAssets(); + + // Trouver le dropdown parent et le refermer + const dropdownEl = this.closest('.dropdown'); + const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]')); + if (dropdown) { + dropdown.hide(); // ✅ ferme proprement le dropdown + } + }); + + // Reset filters + document.getElementById('reset-assets-filters').addEventListener('click', function(e) { + const form = document.getElementById('assets-filter-form'); + + // form.reset(); // Reset form fields + // Parcourir tous les champs du formulaire et les réinitialiser + form.querySelectorAll('input, select, textarea').forEach(field => { + if (field.tagName.toLowerCase() === 'select') { + field.selectedIndex = -1; // aucune option sélectionnée + } else if (field.type === 'checkbox' || field.type === 'radio') { + field.checked = false; // décocher + } else { + field.value = ''; // vider le texte / nombre / date + } + }); + + updateAssets(); + + // Trouver le dropdown parent et le refermer + const dropdownEl = this.closest('.dropdown'); + const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]')); + if (dropdown) { + dropdown.hide(); // ✅ ferme proprement le dropdown + } + }); + + + // Filter tables by map bounds + // $("#filterzone").on( "click", function(e) { + // e.preventDefault(); + // filterTableByMapBounds(map, locations_geojson, loadDataIntoLocationsTable); + // filterTableByMapBounds(map, assets_geojson, loadDataIntoAssetsTable); + // }); + + + // Register zoom action to the tables + registerZoomAction('#locations-table', '.zoom-btn', map, 17); + registerZoomAction('#assets-table', '.zoom-btn', map, 18); + + // Register location selection action to table locations + registerSelectLocationAction('#locations-table', '.location-btn-toggle'); +}); + + +function registerSelectLocationAction(tableId, buttonClass) { + $(tableId).on('click', buttonClass, function () { + const $button = $(this); + const locationId = $(this).data('id'); // Retrieve the ID from the button's data attribute + + if (locationId) { + toggleSelectedLocation(locationId, $button); + } + }); +} + +function toggleSelectedLocation(locationId, $button) { + if (selected_locations_set.has(locationId)) { + selected_locations_set.delete(locationId); + $button + .find('i').removeClass('bi-check-square-fill').addClass('bi-square'); + } else { + selected_locations_set.add(locationId); + $button + .find('i').removeClass('bi-square').addClass('bi-check-square-fill'); + } + updateSelectedLocationsInputAndStatus(); + + if (preventive_draft_id !== null && preventive_draft_id !== 'None' && preventive_draft_id !== '' && preventive_element_type == 'locations') { + // not necessary to search assets if it concerns a selection for preventive interventions + } else { + updateAssets(); + } +} + +function updateSelectedLocationsInputAndStatus() { + const previnputField = document.getElementById("input-preventive-selected-locations-ids"); + const statusBar = document.getElementById("locations-selection-status"); + const ids = Array.from(selected_locations_set); + if (previnputField) previnputField.value = ids.join(","); + statusBar.innerText = interpolate( + ngettext('%(count)s localisation sélectionnée', + '%(count)s localisations sélectionnées', + ids.length), + { count: ids.length }, + true + ); +} + +function updateSelectedAssetsInputAndStatus() { + const statusBar = document.getElementById("assets-selection-status"); + const assets = Array.from(selected_assets_set); + statusBar.innerText = interpolate( + ngettext('%(count)s asset sélectionné', + '%(count)s assets sélectionnés', + assets.length), + { count: assets.length }, + true + ); +} + +// Selection of locations +document.addEventListener("DOMContentLoaded", function () { + + document.getElementById("locations-select-all").addEventListener("click", () => { + const allRows = $('#locations-table').bootstrapTable('getData'); + allRows.forEach(row => { + selected_locations_set.add(parseInt(row.id)); + const btn = document.querySelector(`.location-btn-toggle[data-id="${row.id}"]`); + if (btn) { + $(btn).find('i').removeClass('bi-square').addClass('bi-check-square-fill'); + } + }); + updateSelectedLocationsInputAndStatus(); + if (preventive_draft_id !== null && preventive_draft_id !== 'None' && preventive_draft_id !== '' && preventive_element_type == 'locations') { + // not necessary to search assets if it concerns a selection for preventive interventions + } else { + updateAssets(); + } + }); + + document.getElementById("locations-deselect-all").addEventListener("click", () => { + selected_locations_set.clear(); + const allRows = $('#locations-table').bootstrapTable('getData'); + allRows.forEach(row => { + const btn = document.querySelector(`.location-btn-toggle[data-id="${row.id}"]`); + if (btn) { + $(btn).find('i').removeClass('bi-check-square-fill').addClass('bi-square'); + } + }); + updateSelectedLocationsInputAndStatus(); + if (preventive_draft_id !== null && preventive_draft_id !== 'None' && preventive_draft_id !== '' && preventive_element_type == 'locations') { + // not necessary to search assets if it concerns a selection for preventive interventions + } else { + updateAssets(); + } + }); + + // Initial state + updateSelectedLocationsInputAndStatus(); + + + document.getElementById("assets-select-all").addEventListener("click", () => { + const allRows = $('#assets-table').bootstrapTable('getData'); + console.log(allRows); + allRows.forEach(row => { + selected_assets_set.add(assetKey(row.asset_type, row.id)); + const btn = document.querySelector(`.asset-btn-toggle[data-id="${row.id}"]`); + if (btn) { + $(btn).find('i').removeClass('bi-square').addClass('bi-check-square'); + } + }); + updateSelectedAssetsInputAndStatus(); + }); + + document.getElementById("assets-deselect-all").addEventListener("click", () => { + selected_assets_set.clear(); + const allRows = $('#assets-table').bootstrapTable('getData'); + allRows.forEach(row => { + const btn = document.querySelector(`.asset-btn-toggle[data-id="${row.id}"]`); + if (btn) { + $(btn).find('i').removeClass('bi-check-square').addClass('bi-square'); + } + }); + updateSelectedAssetsInputAndStatus(); + }); + + // Initial state + updateSelectedAssetsInputAndStatus(); +}); + + +document.getElementById("assets-table").addEventListener('click', (e) => { + const btn = e.target.closest('.asset-btn-toggle'); + if (!btn) return; + + const key = btn.dataset.key; + const icon = btn.querySelector('i'); + + if (selected_assets_set.has(key)) { + selected_assets_set.delete(key); + btn.dataset.selected = 'false'; + icon.classList.remove('bi-check-square'); + icon.classList.add('bi-square'); + btn.title = gettext("Sélectionner l'asset"); + } else { + selected_assets_set.add(key); + btn.dataset.selected = 'true'; + icon.classList.remove('bi-square'); + icon.classList.add('bi-check-square'); + btn.title = gettext("Désélectionner l'asset"); + } + + updateSelectedAssetsInputAndStatus(); +}); + +// Avant soumission, sérialiser le Set -> JSON dans le champ caché +if (document.getElementById('preventive-select-form')) { + document.getElementById('preventive-select-form').addEventListener('submit', () => { + const assets = Array.from(selected_assets_set).map(k => { + const [model, id] = k.split(':'); + return { model, object_id: Number(id) }; + }); + document.getElementById('input-preventive-selected-assets').value = JSON.stringify(assets); + }); +} + + + +// Positionnne le filtre dans la toolbar du tableau +document.addEventListener("DOMContentLoaded", function () { + $('#locations-table').on('post-body.bs.table', function () { + const $toolbar = $('#locations-table-container').find('.bootstrap-table').find('.fixed-table-toolbar'); + + // Ne pas réinsérer si déjà fait + if ($toolbar.find('#filter-dropdown-wrapper').length === 0) { + $('#filter-dropdown-wrapper') + .removeClass('d-none') + .prependTo($toolbar); + } + }); + + $('#assets-table').on('post-body.bs.table', function () { + const $toolbar = $('#assets-table-container').find('.bootstrap-table').find('.fixed-table-toolbar'); + + // Ne pas réinsérer si déjà fait + if ($toolbar.find('#filter-assets-dropdown-wrapper').length === 0) { + $('#filter-assets-dropdown-wrapper') + .removeClass('d-none') + .prependTo($toolbar); + } + }); +}); + + + + +function updateLocations() { + + const alertBox = document.getElementById("location-alert"); + + const form = document.getElementById('filter-form'); + const formData = new FormData(form); + + // Update URL + if (preventive_draft_id == 'None') { + updateURLFromForms('filter-form', 'assets-filter-form'); + } + + // On convertit FormData en objet + const filters = {}; + for (const [key, value] of formData.entries()) { + if (value.trim() !== '') { + // Si le champ existe déjà (checkbox, multiple select), on stocke en tableau + if (filters[key]) { + if (!Array.isArray(filters[key])) { + filters[key] = [filters[key]]; + } + filters[key].push(value); + } else { + filters[key] = value; + } + } + } + + try { + parsedGeom = JSON.parse(filter_geom); + } catch (e) { + parsedGeom = null; + } + + fetch(locationsgeojsonUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') // si CSRF est activé + }, + body: JSON.stringify({ + filter_geom: parsedGeom, // Ajouter la géométrie + q: (document.getElementById('assets-search-input') ? document.getElementById('assets-search-input').value.trim() : ''), + search_mode: (document.querySelector('input[name="assets-search-mode"]:checked') ? document.querySelector('input[name="assets-search-mode"]:checked').value : 'and'), + ...filters // Ajout de tous les champs de filtre + }) + }) + .then(response => response.json()) + .then(geojsonData => { + if (geojsonData.error) { + // Affiche l'erreur dans le div Bootstrap + alertBox.textContent = geojsonData.error; + alertBox.classList.remove("d-none"); + return; + } + + alertBox.classList.add("d-none"); // Cache l'alerte si tout va bien + + if (map.getSource('locations')) { + map.getSource('locations').setData(geojsonData); + } + loadDataIntoLocationsTable(geojsonData.features); + }) + .catch(error => { + alertBox.textContent = gettext("Erreur lors de la récupération des données."); + alertBox.classList.remove("d-none"); + console.error("Erreur :", error); + }); +} + + +function updateAssets() { + const selectedLocationsIds = Array.from(selected_locations_set); + + const alertBox = document.getElementById("asset-alert"); + + const form = document.getElementById('assets-filter-form'); + const formData = new FormData(form); + + // Update URL + updateURLFromForms('filter-form', 'assets-filter-form'); + + // On convertit FormData en objet + const filters = {}; + for (const [key, value] of formData.entries()) { + if (value.trim() !== '') { + // Si le champ existe déjà (checkbox, multiple select), on stocke en tableau + if (filters[key]) { + if (!Array.isArray(filters[key])) { + filters[key] = [filters[key]]; + } + filters[key].push(value); + } else { + filters[key] = value; + } + } + } + + try { + parsedGeom = JSON.parse(filter_geom); + } catch (e) { + parsedGeom = null; + } + + fetch(assetsgeojsonUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') // si CSRF est activé + }, + body: JSON.stringify({ + locations_ids: selectedLocationsIds, + filter_geom: parsedGeom, + q: (document.getElementById('assets-search-input') ? document.getElementById('assets-search-input').value.trim() : ''), + search_mode: (document.querySelector('input[name="assets-search-mode"]:checked') ? document.querySelector('input[name="assets-search-mode"]:checked').value : 'and'), + ...filters // Ajout de tous les champs de filtre + }) + }) + .then(response => response.json()) + .then(geojsonData => { + if (geojsonData.error) { + // Affiche l'erreur dans le div Bootstrap + alertBox.textContent = geojsonData.error; + alertBox.classList.remove("d-none"); + return; + } + + alertBox.classList.add("d-none"); // Cache l'alerte si tout va bien + + if (map.getSource('assets')) { + map.getSource('assets').setData(geojsonData); + } + loadDataIntoAssetsTable(geojsonData.features); + }) + .catch(error => { + alertBox.textContent = gettext("Erreur lors de la récupération des données."); + alertBox.classList.remove("d-none"); + console.error("Erreur :", error); + }); +} + + +function insertNoAssetsRowMessage() { + const $table = $('#assets-table'); + const $tbody = $table.find('tbody'); + + // Supprime les messages précédents si existants + $tbody.find('tr.no-assets-message').remove(); + $tbody.find('tr.no-records-found').remove(); + + if ($tbody.find('tr').length === 0) { + const colspan = $table.find('thead th').length || 1; + + // Détecter si une recherche ou un filtre est actif + const searchVal = (document.getElementById('assets-search-input')?.value || '').trim(); + const hasSelectedLocations = selected_locations_set.size > 0; + const assetFiltersForm = document.getElementById('assets-filter-form'); + let hasAssetFilters = false; + if (assetFiltersForm) { + const formData = new FormData(assetFiltersForm); + for (const [, value] of formData.entries()) { + if (value.trim() !== '') { hasAssetFilters = true; break; } + } + } + + let noResultsText; + if (!searchVal && !hasSelectedLocations && !hasAssetFilters) { + noResultsText = gettext('Sélectionnez des localisations ou lancez une recherche pour afficher les assets.'); + } else { + noResultsText = gettext('Aucun asset ne correspond aux critères de recherche.'); + } + + const $msgRow = $(` + + + ${noResultsText} + + + `); + $tbody.append($msgRow); + } +} + +// Hook appelé après chaque mise à jour de la table +$('#assets-table').on('post-body.bs.table', function () { + insertNoAssetsRowMessage(); +}); + + + + +map.on('draw.create', e => updateSelection(e.features[0])); +map.on('draw.update', e => updateSelection(e.features[0])); +map.on('draw.delete', e => { + resetMapFilter(); +}); + + + +function updateSelection(geometry) { + filter_geom = JSON.stringify(geometry); + updateLocations(); + updateAssets(); +} + + +// Réinitialiser le filtre carte +function resetMapFilter() { + filter_geom = ''; + updateLocations(); + updateAssets(); +} + + + + +function updateURLFromForms(...formIds) { + const params = new URLSearchParams(); + + formIds.forEach(formId => { + const form = document.getElementById(formId); + if (!form) return; + + const formData = new FormData(form); + + for (const [key, value] of formData.entries()) { + if (value.trim() !== '') { + params.append(key, value); // conserve les champs multiples + } + } + }); + + const newUrl = `${window.location.pathname}?${params.toString()}`; + window.history.pushState({}, '', newUrl); +} + + + + + +// --- Layout Management --- + +const col_table = document.getElementById("col-table"); +const col_map = document.getElementById("col-map"); + +const layoutBtnTable = document.getElementById("layout-btn-table"); +const layoutBtnBoth = document.getElementById("layout-btn-both"); +const layoutBtnMap = document.getElementById("layout-btn-map"); + +// Current layout state +let currentAssetLayout = typeof INITIAL_ASSET_LAYOUT !== 'undefined' ? INITIAL_ASSET_LAYOUT : 'both'; + +// Function to save layout preference to backend +function saveAssetLayoutPreference(layout) { + if (typeof saveAssetLayoutUrl === 'undefined') return; + fetch(saveAssetLayoutUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ layout: layout }) + }).catch(err => console.warn('Could not save asset layout preference:', err)); +} + +// Function to apply layout +function applyAssetLayout(layout, savePreference = true) { + currentAssetLayout = layout; + + // Reset classes + col_table.classList.remove("col-11", "col-12", "col-md-6", "order-2", "d-none"); + col_map.classList.remove("col-11", "col-12", "col-md-6", "order-2", "d-none"); + + // Reset button active states + layoutBtnTable.classList.remove("text-primary"); + layoutBtnBoth.classList.remove("text-primary"); + layoutBtnMap.classList.remove("text-primary"); + + switch (layout) { + case 'table_only': + col_table.classList.add("col-12"); + col_map.classList.add("d-none"); + layoutBtnTable.classList.add("text-primary"); + break; + case 'map_only': + col_map.classList.add("col-12"); + col_table.classList.add("d-none"); + layoutBtnMap.classList.add("text-primary"); + break; + case 'both': + default: + col_table.classList.add("col-md-6"); + col_map.classList.add("col-md-6"); + layoutBtnBoth.classList.add("text-primary"); + break; + } + + // Refresh tables + $('#locations-table').bootstrapTable('resetView'); + $('#assets-table').bootstrapTable('resetView'); + + // Resize map if visible + if (layout !== 'table_only' && typeof map !== 'undefined') { + setTimeout(() => map.resize(), 100); + } + + // Save preference + if (savePreference) { + saveAssetLayoutPreference(layout); + } +} + +// Layout button event handlers +layoutBtnTable.addEventListener("click", function() { + applyAssetLayout('table_only'); +}); + +layoutBtnBoth.addEventListener("click", function() { + applyAssetLayout('both'); +}); + +layoutBtnMap.addEventListener("click", function() { + applyAssetLayout('map_only'); +}); + +// Initialize map resize on page load if visible +document.addEventListener('DOMContentLoaded', function() { + if (currentAssetLayout !== 'table_only' && typeof map !== 'undefined') { + setTimeout(() => map.resize(), 50); + } +}); + + + + + + + + +// Totaux // +// ------ // +const btnTotals = document.getElementById('totals_button'); + const overlay = document.getElementById('totalsOverlay'); + const closeBtn = document.getElementById('totalsClose'); + const contentEl = document.getElementById('totalsContent'); + +function getAssetsFromSource() { + const $table = $('#assets-table'); + if (!$table.length || typeof $table.bootstrapTable !== 'function') return []; + + // Récupère toutes les données en mémoire (toutes pages, filtrées côté client) + const all = $table.bootstrapTable('getData', { + useCurrentPage: false, + includeHiddenRows: false + }) || []; + + // Si la sélection custom contient des clés -> on filtre dessus + if (window.selected_assets_set && selected_assets_set.size > 0) { + const out = []; + for (const row of all) { + // on reconstruit la clé exactement comme pour l'ensemble + const key = assetKey(row.asset_type, row.id); + if (selected_assets_set.has(String(key))) out.push(row); + } + // Si rien ne matche (ex: décalage de filtre), on retombe sur "all" + return out.length ? out : all; + } + + // Sinon: pas de sélection => on prend tout ce qui est filtré/chargé + return all; +} + +// Regroupe par catégorie, puis par modèle +function groupByCategoryAndModel(assets) { + const byCat = {}; + for (const a of assets) { + const cat = a.category || gettext('Sans catégorie'); + const model = a.model || '—'; + byCat[cat] ??= {}; + byCat[cat][model] ??= { count: 0, length_m: 0, area_m2: 0 }; + byCat[cat][model].count += 1; + byCat[cat][model].length_m += Number(a.length) || 0; + byCat[cat][model].area_m2 += Number(a.area) || 0; + } + return byCat; +} + +function fmtInt(n){ return (Math.round(n)||'-').toLocaleString(); } + +function renderTotals() { + const assets = getAssetsFromSource(); + const grouped = groupByCategoryAndModel(assets); + + const isCustomSelection = window.selected_assets_set && selected_assets_set.size > 0; + + // En-tête récap global + const headerCount = interpolate( + ngettext('%(count)s élément pris en compte', + '%(count)s éléments pris en compte', + assets.length), + { count: assets.length }, + true + ); + + const selectionSuffix = isCustomSelection + ? gettext('(sélection)') + : gettext('(table)'); + + let html = ` +
+ ${headerCount} ${selectionSuffix} +
+ `; + + // Pour chaque catégorie: un tableau + const categories = Object.keys(grouped).sort((a,b)=>a.localeCompare(b, 'fr')); + if (!categories.length) { + contentEl.innerHTML = `
${gettext('Aucune donnée disponible')}
`; + return; + } + + for (const cat of categories) { + const models = grouped[cat]; + const modelNames = Object.keys(models).sort((a,b)=>a.localeCompare(b, 'fr')); + + // Lignes du body (par modèle) + const bodyRows = modelNames.map(m => { + const g = models[m]; + return ` + + ${m} + ${fmtInt(g.count)} + ${fmtInt(g.length_m)} + ${fmtInt(g.area_m2)} + + `; + }).join(''); + + // Totaux par catégorie + const catTotals = modelNames.reduce((acc, m) => { + acc.count += models[m].count; + acc.length_m += models[m].length_m; + acc.area_m2 += models[m].area_m2; + return acc; + }, {count:0, length_m:0, area_m2:0}); + + html += ` +
+
+ ${cat} + (${fmtInt(catTotals.count)} ${gettext('au total')}) +
+
+ + + + + + + + + + ${bodyRows || ` + + `} + + + + + + + + +
${gettext('Modèle')}${gettext('Nombre')}${gettext('Longueur (m)')}${gettext('Surface (m²)')}
${gettext('Aucun modèle')}
${gettext('Total catégorie')}${fmtInt(catTotals.count)}${fmtInt(catTotals.length_m)}${fmtInt(catTotals.area_m2)}
+
+
+ `; + } + + contentEl.innerHTML = html; +} + + + // Ouvrir + btnTotals.addEventListener('click', () => { + renderTotals(); + overlay.classList.add('show'); + // Accessibilité: focus sur le bouton "Fermer" + setTimeout(() => closeBtn.focus(), 0); + }); + + // Fermer + function hideOverlay() { overlay.classList.remove('show'); } + closeBtn.addEventListener('click', hideOverlay); + + // ESC pour fermer + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && overlay.classList.contains('show')) hideOverlay(); + }); + + // Option: cliquer en dehors du contenu pour fermer (si tu ajoutes un wrapper) + // overlay.addEventListener('click', (e) => { + // if (e.target === overlay) hideOverlay(); + // }); + + +// --------------------- +// Widget de recherche assets +// --------------------- +document.addEventListener('DOMContentLoaded', function () { + const searchInput = document.getElementById('assets-search-input'); + const searchBtn = document.getElementById('assets-search-btn'); + const searchClear = document.getElementById('assets-search-clear'); + + if (!searchInput) return; + + function runAssetsSearch() { + const hasVal = searchInput.value.trim() !== ''; + searchClear.style.visibility = hasVal ? 'visible' : 'hidden'; + updateLocations(); + updateAssets(); + } + + searchBtn.addEventListener('click', runAssetsSearch); + + searchInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { e.preventDefault(); runAssetsSearch(); } + else if (e.key === 'Escape') { searchInput.value = ''; runAssetsSearch(); } + }); + + searchClear.addEventListener('click', function () { + searchInput.value = ''; + searchClear.style.visibility = 'hidden'; + updateLocations(); + updateAssets(); + }); + + // Reset filters also clears search + const resetLocBtn = document.getElementById('reset-filters'); + const resetAssBtn = document.getElementById('reset-assets-filters'); + if (resetLocBtn) { + resetLocBtn.addEventListener('click', function () { + searchInput.value = ''; + searchClear.style.visibility = 'hidden'; + }); + } + if (resetAssBtn) { + resetAssBtn.addEventListener('click', function () { + searchInput.value = ''; + searchClear.style.visibility = 'hidden'; + }); + } +}); + + + + + + + +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} +const csrftoken = getCookie('csrftoken'); \ No newline at end of file diff --git a/streetup/assets/static/assets/clean_litterbin_observations.js b/streetup/assets/static/assets/clean_litterbin_observations.js new file mode 100644 index 0000000..984deaa --- /dev/null +++ b/streetup/assets/static/assets/clean_litterbin_observations.js @@ -0,0 +1,294 @@ +/** + * Inline edit mode for CleanLitterBinObservation. + * Edit mode is triggered by the main CMS asset edit mode events + * (asset-cms-edit-mode-enter / asset-cms-edit-mode-exit). + */ +(function () { + 'use strict'; + + const card = document.getElementById('observations-card'); + if (!card) return; + + const updateUrl = card.dataset.updateUrl; + const addBtn = document.getElementById('obs-add-btn'); + const accordion = document.getElementById('observationsAccordion'); + const emptyMsg = document.getElementById('obs-empty-msg'); + const obsTemplate = document.getElementById('obs-new-item-template'); + + let newCounter = 0; + let obsEditActive = false; + + // Watch the body class toggled by asset_cms_edit_mode.js + // ('asset-cms-edit-mode' is added on Edit, removed on Cancel/Save) + new MutationObserver(() => { + const cmsActive = document.body.classList.contains('asset-cms-edit-mode'); + if (cmsActive && !obsEditActive) enterEditMode(); + if (!cmsActive && obsEditActive) cancelEditMode(); + }).observe(document.body, { attributes: true, attributeFilter: ['class'] }); + + if (addBtn) addBtn.addEventListener('click', addNewObservation); + + // Register as a pre-save hook so the main Sauvegarder button saves observations first + window.assetPreSaveHooks = window.assetPreSaveHooks || []; + window.assetPreSaveHooks.push(postObservations); + + // ------------------------------------------------------------------ // + // Edit-mode lifecycle + // ------------------------------------------------------------------ // + + function enterEditMode() { + obsEditActive = true; + card.querySelectorAll('.obs-view-panel').forEach(p => p.classList.add('d-none')); + card.querySelectorAll('.obs-edit-panel').forEach(p => p.classList.remove('d-none')); + + // Expand all accordion items via Bootstrap API so its internal state stays consistent + card.querySelectorAll('.accordion-collapse').forEach(c => { + // Disable transition for instant reveal (better UX when opening many at once) + c.style.transition = 'none'; + bootstrap.Collapse.getOrCreateInstance(c, {toggle: false}).show(); + // Re-enable transition after next frame + requestAnimationFrame(() => { c.style.transition = ''; }); + }); + + if (addBtn) addBtn.classList.remove('d-none'); + + if (emptyMsg) emptyMsg.classList.add('d-none'); + + bindLiveHeaders(); + } + + function cancelEditMode() { + obsEditActive = false; + // Remove any new (unsaved) observation items + card.querySelectorAll('.obs-new-item').forEach(el => el.remove()); + + card.querySelectorAll('.obs-item').forEach(item => { + item.classList.remove('obs-to-delete'); + item.querySelectorAll('.obs-delete-badge').forEach(b => b.classList.add('d-none')); + }); + + card.querySelectorAll('.obs-view-panel').forEach(p => p.classList.remove('d-none')); + card.querySelectorAll('.obs-edit-panel').forEach(p => p.classList.add('d-none')); + + // Collapse all via Bootstrap API + card.querySelectorAll('.accordion-collapse').forEach(c => { + c.style.transition = 'none'; + const inst = bootstrap.Collapse.getInstance(c); + if (inst) inst.hide(); else c.classList.remove('show'); + requestAnimationFrame(() => { c.style.transition = ''; }); + }); + + if (addBtn) addBtn.classList.add('d-none'); + + if (emptyMsg) { + const hasItems = accordion && accordion.querySelectorAll('.obs-item').length > 0; + if (!hasItems) emptyMsg.classList.remove('d-none'); + } + + newCounter = 0; + } + + // ------------------------------------------------------------------ // + // Add new observation + // ------------------------------------------------------------------ // + + function addNewObservation() { + if (!obsTemplate) return; + const clone = obsTemplate.content.cloneNode(true); + const uid = `new-${newCounter++}`; + + // Replace placeholder with unique ID + clone.querySelectorAll('[id]').forEach(el => { + el.id = el.id.replace('_NEWID_', uid); + }); + clone.querySelectorAll('[data-obs-id]').forEach(el => { + if (el.dataset.obsId === '_NEWID_') el.dataset.obsId = ''; + }); + clone.querySelectorAll('[data-bs-target]').forEach(el => { + el.dataset.bsTarget = el.dataset.bsTarget.replace('_NEWID_', uid); + }); + clone.querySelectorAll('[aria-labelledby]').forEach(el => { + el.setAttribute('aria-labelledby', el.getAttribute('aria-labelledby').replace('_NEWID_', uid)); + }); + clone.querySelectorAll('[for]').forEach(el => { + el.setAttribute('for', el.getAttribute('for').replace('_NEWID_', uid)); + }); + clone.querySelectorAll('[name]').forEach(el => { + // keep name unchanged – only the label `for` and input `id` use the prefix + }); + + accordion.appendChild(clone); + + // Expand the new item + const newItem = accordion.lastElementChild; + bindMarkDeleteBtn(newItem); + newItem.querySelector('.accordion-collapse')?.classList.add('show'); + newItem.querySelector('.accordion-button')?.classList.remove('collapsed'); + newItem.scrollIntoView({behavior: 'smooth', block: 'nearest'}); + } + + // ------------------------------------------------------------------ // + // Live header update while typing + // ------------------------------------------------------------------ // + + function bindLiveHeaders() { + card.querySelectorAll('.obs-item').forEach(item => { + const dtInput = item.querySelector('.obs-edit-panel [name="observation_datetime"]'); + const noteInput = item.querySelector('.obs-edit-panel [name="observer_note"]'); + + function refresh() { + const dtEl = item.querySelector('.obs-header-date'); + const noteEl = item.querySelector('.obs-header-note'); + if (dtEl && dtInput) dtEl.textContent = dtInput.value ? formatDatePreview(dtInput.value) : '—'; + if (noteEl && noteInput) noteEl.textContent = noteInput.value.trim().substring(0, 80); + } + dtInput?.addEventListener('input', refresh); + noteInput?.addEventListener('input', refresh); + }); + } + + function formatDatePreview(isoStr) { + try { + const d = new Date(isoStr); + const pad = n => String(n).padStart(2, '0'); + return `${pad(d.getDate())}/${pad(d.getMonth()+1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } catch { return isoStr; } + } + + // ------------------------------------------------------------------ // + // Mark / unmark for deletion + // ------------------------------------------------------------------ // + + // Bind after DOM ready (existing observations) and after clone (new) + card.querySelectorAll('.obs-mark-delete-btn').forEach(btn => bindMarkDeleteBtn(btn.closest('.obs-item'))); + + function bindMarkDeleteBtn(item) { + if (!item) return; + const btn = item.querySelector('.obs-mark-delete-btn'); + if (!btn) return; + btn.addEventListener('click', () => { + if (item.classList.contains('obs-to-delete')) { + item.classList.remove('obs-to-delete'); + item.querySelectorAll('.obs-delete-badge').forEach(b => b.classList.add('d-none')); + btn.textContent = btn.dataset.labelDelete || btn.textContent; + } else { + item.classList.add('obs-to-delete'); + item.querySelectorAll('.obs-delete-badge').forEach(b => b.classList.remove('d-none')); + } + }); + } + + // ------------------------------------------------------------------ // + // Collect observation data + // ------------------------------------------------------------------ // + + const BOOL_FIELDS = [ + 'sol_pente_trop_forte', 'sol_instable', 'sol_reflechissant', 'sol_glissant', + 'sol_fentes', 'sol_entrave', 'sol_dalles_podo', + 'acc_approche_2m', 'acc_voirie_a_traverser', 'acc_poubelle_isolee', + 'acc_gene_flux_routier', 'acc_gene_flux_pieton', 'acc_chemin_logique', + 'acc_contraste', 'acc_indications_tri', + ]; + const TEXT_FIELDS = [ + 'observer_note', 'sol_remarque', + 'acc_type_quartier', 'acc_densite_flux_pieton', + 'acc_agent_vehicule', 'acc_agent_pied', + 'acc_point_interet_autre', 'acc_point_interet_remarque', 'acc_remarque', + ]; + const FLOAT_FIELDS = ['sol_pente_transversale', 'sol_pente_longitudinale']; + + function qv(panel, name) { + const el = panel.querySelector(`[name="${name}"]`); + return el ? el.value : ''; + } + + function collectObsData(item) { + const panel = item.querySelector('.obs-edit-panel'); + if (!panel) return null; + + const data = { id: item.dataset.obsId || null }; + + // datetime + const dtVal = qv(panel, 'observation_datetime'); + data.observation_datetime = dtVal || null; + + // booleans + BOOL_FIELDS.forEach(f => { + const el = panel.querySelector(`[name="${f}"]`); + data[f] = el ? el.checked : false; + }); + + // floats + FLOAT_FIELDS.forEach(f => { + const v = qv(panel, f); + data[f] = v !== '' ? parseFloat(v) : null; + }); + + // text / choice + TEXT_FIELDS.forEach(f => { + const v = qv(panel, f).trim(); + data[f] = v || null; + }); + + // multi-checkbox list: acc_point_interet + data.acc_point_interet = [...panel.querySelectorAll('[name="acc_point_interet"]:checked')] + .map(cb => cb.value); + + // JSON int lists (comma-separated text) + ['acc_pertinence_placement', 'acc_entrave'].forEach(f => { + const raw = qv(panel, f); + data[f] = raw.split(',').map(s => s.trim()).filter(s => s) + .map(Number).filter(n => !isNaN(n)); + }); + + return data; + } + + // ------------------------------------------------------------------ // + // Pre-save hook – called by asset_cms_edit_mode.js before main POST + // ------------------------------------------------------------------ // + + async function postObservations() { + const observations = []; + card.querySelectorAll('.obs-item:not(.obs-to-delete)').forEach(item => { + const d = collectObsData(item); + if (d) observations.push(d); + }); + + const resp = await fetch(updateUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken'), + }, + body: JSON.stringify({ observations }), + }); + const result = await resp.json(); + // Throw on error so the main save's catch block handles display + button reset + if (!resp.ok || !result.success) { + throw new Error(result.error || 'Erreur lors de la sauvegarde des observations'); + } + // No reload here – the main save will reload after its own POST + } + + // ------------------------------------------------------------------ // + // Helpers + // ------------------------------------------------------------------ // + + function getCookie(name) { + for (const c of document.cookie.split(';')) { + const [k, v] = c.trim().split('='); + if (k === name) return decodeURIComponent(v); + } + return null; + } + + function showAlert(message, type = 'danger') { + const div = document.createElement('div'); + div.className = `alert alert-${type} alert-dismissible fade show asset-cms-alert`; + div.innerHTML = `${message}`; + document.body.appendChild(div); + setTimeout(() => div.remove(), 6000); + } + +})(); diff --git a/streetup/assets/static/assets/clean_location_detail.js b/streetup/assets/static/assets/clean_location_detail.js new file mode 100644 index 0000000..7ac57b4 --- /dev/null +++ b/streetup/assets/static/assets/clean_location_detail.js @@ -0,0 +1,53 @@ + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 16, + pitch: 45, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const location_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: geom, + properties: { + name: '...' + } + } + ] + }; + + map.addSource('location', { + type: 'geojson', + data: location_data + }); + + map.addLayer({ + 'id': 'location-fill', + 'type': 'fill', + 'source': 'location', + 'paint': { + 'fill-color': '#28a745', + 'fill-opacity': 0.7, + } + }); + + map.addLayer({ + 'id': 'location-outline', + 'type': 'line', + 'source': 'location', + 'paint': { + 'line-color': '#155724', + 'line-width': 2, + } + }); + + const bounds = turf.bbox(location_data); + map.fitBounds(bounds, {}); +}); diff --git a/streetup/assets/static/assets/controlcenters_location_detail.js b/streetup/assets/static/assets/controlcenters_location_detail.js new file mode 100644 index 0000000..d32c14c --- /dev/null +++ b/streetup/assets/static/assets/controlcenters_location_detail.js @@ -0,0 +1,162 @@ + +let center = [4.351721, 50.850339]; // Coordonnées par défaut (Bruxelles) +let zoom = 11; +if (feature_lon !== null) { + center = [feature_lon, feature_lat]; + zoom = 17; +} + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: center, + zoom: zoom, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const location_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: feature_geom, + properties: { + name: '...' + } + } + ] + }; + + // Add the circle as a GeoJSON source + map.addSource('location', { + type: 'geojson', + data: location_data + }); + + map.addLayer({ + 'id': 'location', + 'type': 'fill', + 'source': 'location', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.7, + } + }); + + if (feature_geom !== null) { + const bounds = turf.bbox(location_data); + map.fitBounds(bounds, { + duration: 10 + }); + } +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +const documentTypeChoices = { + 'other': 'Autre' +}; + +if (fileInput) { + fileInput.addEventListener('change', function () { + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1'); + fileName.textContent = file.name; + row.appendChild(fileName); + + // Select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; // nécessite Bootstrap Icons ou tu peux mettre "✖" + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + fileInput.dispatchEvent(new Event('change')); // re-render + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); + }); +} + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +if (deleteDocumentModal) { + deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const fileName = button.getAttribute('data-filename'); + const deleteUrl = button.getAttribute('data-delete-url'); + + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + const deleteForm = document.getElementById('deleteDocumentForm'); + + fileNameContainer.textContent = fileName; + deleteForm.action = deleteUrl; + }); +} + +historyModal = document.getElementById('documentHistoryModal'); +if (historyModal) { + document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const locationId = button.getAttribute('data-location-id'); + + fetch(`${document_history_url}?location_id=${locationId}&document_type=${type}`) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); + }); +} + + +document.addEventListener('click', (e) => { + const btn = e.target.closest('[data-bs-toggle="collapse"]'); + if (!btn) return; + const target = document.querySelector(btn.getAttribute('data-bs-target')); + if (!target) return; + target.addEventListener('shown.bs.collapse', () => btn.textContent = '▾', {once:true}); + target.addEventListener('hidden.bs.collapse', () => btn.textContent = '▸', {once:true}); +}); \ No newline at end of file diff --git a/streetup/assets/static/assets/electrical-panel.png b/streetup/assets/static/assets/electrical-panel.png new file mode 100644 index 0000000..88d83f9 Binary files /dev/null and b/streetup/assets/static/assets/electrical-panel.png differ diff --git a/streetup/assets/static/assets/electrical_cabinet_icon.svg b/streetup/assets/static/assets/electrical_cabinet_icon.svg new file mode 100644 index 0000000..15860cc --- /dev/null +++ b/streetup/assets/static/assets/electrical_cabinet_icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/streetup/assets/static/assets/green-surfaces-comments.js b/streetup/assets/static/assets/green-surfaces-comments.js new file mode 100644 index 0000000..ea1679c --- /dev/null +++ b/streetup/assets/static/assets/green-surfaces-comments.js @@ -0,0 +1,2435 @@ +/** + * Green Surfaces Comments Component + * + * Manages geolocated comments with photo capture for the green surfaces editing interface. + * Integrates camera functionality similar to observations app. + */ + +// Translation function alias +const _ = gettext; + +/** + * InfoPanel class - manages the information panel overlay + * Uses the existing attributes-panel for mobile interface + */ +class InfoPanel { + constructor() { + // Use attributes-panel and its content containers + this.panel = document.getElementById('attributes-panel'); + this.commentContent = document.getElementById('comment-panel-content'); + this.defaultContent = document.getElementById('default-panel-content'); + this.panelTitle = document.getElementById('panel-title'); + this.selectionCount = document.getElementById('selection-count'); + + // Store original title + this.originalTitle = this.panelTitle ? this.panelTitle.textContent : ''; + + this.history = []; + this.isShowingComment = false; + this.onCloseCallback = null; + } + + setOnCloseCallback(callback) { + this.onCloseCallback = callback; + } + + show(title, content, addToHistory = true) { + if (!this.panel || !this.commentContent) { + return; + } + + // Save history if needed + if (addToHistory && this.isShowingComment) { + this.history.push({ + title: this.panelTitle.textContent, + content: this.commentContent.innerHTML + }); + } + + // Update title + if (this.panelTitle) { + this.panelTitle.textContent = title; + } + + // Hide selection count + if (this.selectionCount) { + this.selectionCount.style.display = 'none'; + } + + // Set content + if (typeof content === 'string') { + this.commentContent.innerHTML = content; + } else { + this.commentContent.innerHTML = ''; + this.commentContent.appendChild(content); + } + + // Show comment content, hide default content + this.commentContent.style.display = 'block'; + if (this.defaultContent) { + this.defaultContent.style.display = 'none'; + } + + // Expand panel + this.panel.classList.remove('collapsed'); + + this.isShowingComment = true; + + // Add close button to content if not present + this.addCloseButton(); + } + + addCloseButton() { + // Check if close button already exists + if (!this.commentContent.querySelector('.comment-close-btn')) { + const closeBtn = document.createElement('button'); + closeBtn.className = 'btn btn-sm btn-outline-secondary comment-close-btn mb-3'; + closeBtn.innerHTML = ' Retour'; + closeBtn.addEventListener('click', () => this.close()); + this.commentContent.insertBefore(closeBtn, this.commentContent.firstChild); + } + } + + updateContent(content) { + if (!this.commentContent) return; + + if (typeof content === 'string') { + this.commentContent.innerHTML = content; + } else { + this.commentContent.innerHTML = ''; + this.commentContent.appendChild(content); + } + this.addCloseButton(); + } + + goBack() { + if (this.history.length > 0) { + const previous = this.history.pop(); + this.show(previous.title, previous.content, false); + } else { + this.close(); + } + } + + close() { + if (!this.panel) return; + + // Hide comment content, show default content + if (this.commentContent) { + this.commentContent.style.display = 'none'; + this.commentContent.innerHTML = ''; + } + if (this.defaultContent) { + this.defaultContent.style.display = 'block'; + } + + // Restore original title + if (this.panelTitle) { + this.panelTitle.textContent = this.originalTitle; + } + + // Show selection count + if (this.selectionCount) { + this.selectionCount.style.display = ''; + } + + this.history = []; + this.isShowingComment = false; + + // Call close callback if set + if (this.onCloseCallback) { + this.onCloseCallback(); + } + } + + isOpen() { + return this.isShowingComment; + } +} + +class GreenSurfacesComments { + constructor(map, options = {}) { + this.map = map; + this.options = { + apiBaseUrl: options.apiBaseUrl || '/assets/nature/comments', + ...options + }; + + this.currentLocation = null; + this.lastGpsLocation = null; // Store last real GPS position (not manually set) + this.currentHeading = null; + this.capturedPhotos = []; + this.cameraStream = null; + this.commentModal = null; + this.cameraModal = null; + this.mapLayersInitialized = false; + this.locationManuallySet = false; // Flag to prevent GPS updates when user manually repositions marker + + // Initialize asynchronously to ensure map is fully ready + setTimeout(() => this.init(), 0); + } + + init() { + // Add map control immediately (MapLibre handles this even before 'load') + this.addMapControl(); + + // Don't try to initialize info panel immediately - it will be done when needed + // this.initInfoPanel(); + + this.setupMapSource(); + this.setupUI(); + this.setupEventListeners(); + this.watchLocation(); + } + + /** + * Initialize info panel if it doesn't exist + */ + initInfoPanel() { + if (!window.infoPanel) { + const panel = document.getElementById('attributes-panel'); + const commentContent = document.getElementById('comment-panel-content'); + + if (!panel || !commentContent) { + return false; + } + + try { + window.infoPanel = new InfoPanel(); + } catch (error) { + return false; + } + } + + // Always set close callback to clean up marker (use window.commentsComponent for reliable reference) + window.infoPanel.setOnCloseCallback(() => { + if (window.commentsComponent) { + window.commentsComponent.cleanupRepositionMarker(); + } + }); + + return true; + } + + /** + * Add custom control button to map toolbar + */ + addMapControl() { + if (!this.map) return; + + // Track layer visibility + this.commentsLayerVisible = true; + + const control = { + onAdd: (map) => { + this.controlContainer = document.createElement('div'); + this.controlContainer.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + + // Button to add comment + const addButton = document.createElement('button'); + addButton.type = 'button'; + addButton.className = 'maplibregl-ctrl-icon'; + addButton.setAttribute('aria-label', 'Ajouter un commentaire'); + addButton.title = 'Ajouter un commentaire géolocalisé'; + + // Add comment icon (chat-dots from Bootstrap Icons) + addButton.innerHTML = ` + + + + + `; + + addButton.style.width = '42px'; + addButton.style.height = '42px'; + addButton.style.display = 'flex'; + addButton.style.alignItems = 'center'; + addButton.style.justifyContent = 'center'; + + addButton.addEventListener('click', () => { + this.openCommentPanel(); + }); + + // Button to toggle layer visibility + const toggleButton = document.createElement('button'); + toggleButton.type = 'button'; + toggleButton.className = 'maplibregl-ctrl-icon'; + toggleButton.setAttribute('aria-label', 'Afficher/masquer les commentaires'); + toggleButton.title = 'Afficher/masquer la couche des commentaires'; + toggleButton.id = 'comments-layer-toggle'; + + // Eye icon (visible by default) + toggleButton.innerHTML = ` + + + + + `; + + toggleButton.style.width = '42px'; + toggleButton.style.height = '42px'; + toggleButton.style.display = 'flex'; + toggleButton.style.alignItems = 'center'; + toggleButton.style.justifyContent = 'center'; + + toggleButton.addEventListener('click', () => { + this.toggleCommentsLayer(); + }); + + this.controlContainer.appendChild(addButton); + this.controlContainer.appendChild(toggleButton); + return this.controlContainer; + }, + + onRemove: () => { + if (this.controlContainer && this.controlContainer.parentNode) { + this.controlContainer.parentNode.removeChild(this.controlContainer); + } + } + }; + + this.map.addControl(control, 'top-left'); + } + + /** + * Toggle comments layer visibility + */ + toggleCommentsLayer() { + if (!this.map) return; + + this.commentsLayerVisible = !this.commentsLayerVisible; + + const visibility = this.commentsLayerVisible ? 'visible' : 'none'; + + // Toggle layer and labels + if (this.map.getLayer('green-comments-layer')) { + this.map.setLayoutProperty('green-comments-layer', 'visibility', visibility); + } + if (this.map.getLayer('green-comments-labels')) { + this.map.setLayoutProperty('green-comments-labels', 'visibility', visibility); + } + + // Update button icon + const toggleButton = document.getElementById('comments-layer-toggle'); + if (toggleButton) { + if (this.commentsLayerVisible) { + // Eye icon (visible) + toggleButton.innerHTML = ` + + + + + `; + toggleButton.title = 'Masquer la couche des commentaires'; + } else { + // Eye-slash icon (hidden) + toggleButton.innerHTML = ` + + + + + + `; + toggleButton.title = 'Afficher la couche des commentaires'; + } + } + } + + /** + * Setup map source and layer for displaying comments + */ + setupMapSource() { + if (!this.map) { + return; + } + + // Function to initialize source and layers + const initializeMapLayers = () => { + // Check if source already exists + if (this.map.getSource('green-comments')) { + this.mapLayersInitialized = true; + return; + } + + // Add source for comments + this.map.addSource('green-comments', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [] + } + }); + + // Add layer for comment markers with status-based colors + this.map.addLayer({ + id: 'green-comments-layer', + type: 'circle', + source: 'green-comments', + paint: { + 'circle-radius': [ + 'interpolate', + ['linear'], + ['zoom'], + 10, 2, // zoom 10: radius 2px + 13, 4, // zoom 13: radius 4px + 15, 5, // zoom 15: radius 6px + 18, 6, // zoom 18: radius 10px + 20, 8, // zoom 20: radius 12px + 22, 10 // zoom 22: radius 14px + ], + 'circle-color': '#ffffff', // White fill + 'circle-stroke-width': [ + 'interpolate', + ['linear'], + ['zoom'], + 10, 1, // zoom 10: stroke 1px + 15, 1, // zoom 15: stroke 1px + 18, 2 // zoom 18: stroke 2px + ], + 'circle-stroke-color': [ + 'match', + ['get', 'status'], + 'memo', '#0d6efd', // Primary blue + 'attention', '#ffc107', // Warning yellow + 'todo', '#0dcaf0', // Info cyan + 'ok', '#198754', // Success green + 'problem', '#dc3545', // Danger red + '#0d6efd' // Default: blue + ] + } + }); + + // Add labels + this.map.addLayer({ + id: 'green-comments-labels', + type: 'symbol', + source: 'green-comments', + layout: { + 'text-field': ['to-string', ['get', 'photo_count']], + 'text-font': ['Noto Sans Regular'], + 'text-size': 10, + 'text-offset': [0, 0], + 'text-allow-overlap': true, + 'text-ignore-placement': true + }, + paint: { + 'text-color': '#222222' + } + }); + + // Click handler for comments + this.map.on('click', 'green-comments-layer', (e) => { + if (e.features.length > 0) { + this.showCommentDetail(e.features[0].properties.id); + } + }); + + // Change cursor on hover + this.map.on('mouseenter', 'green-comments-layer', () => { + this.map.getCanvas().style.cursor = 'pointer'; + }); + + this.map.on('mouseleave', 'green-comments-layer', () => { + this.map.getCanvas().style.cursor = ''; + }); + + // Mark as initialized + this.mapLayersInitialized = true; + + // Setup moveend listener AFTER initialization + this.map.on('moveend', () => { + this.loadComments(); + }); + + // Load comments + this.loadComments(); + }; + + // Use 'idle' event which fires when map is truly ready + // This works even if the map is already loaded + this.map.once('idle', () => { + initializeMapLayers(); + }); + } + + /** + * Setup UI elements + */ + setupUI() { + // Create comment modal + this.createCommentModal(); + + // Create camera modal (similar to observations) + this.createCameraModal(); + } + + /** + * Create the comment creation modal + */ + createCommentModal() { + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + this.commentModal = new bootstrap.Modal(document.getElementById('commentModal')); + + // Setup quick text badges + document.querySelectorAll('.quick-text-badge').forEach(badge => { + badge.addEventListener('click', (e) => { + const text = e.currentTarget.dataset.text; + const textarea = document.getElementById('comment-text'); + textarea.value = text; + textarea.focus(); + }); + }); + } + + /** + * Create the camera modal (similar to observations app) + */ + createCameraModal() { + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + this.cameraModal = new bootstrap.Modal(document.getElementById('commentCameraModal')); + } + + /** + * Setup event listeners + */ + setupEventListeners() { + // Camera button + document.getElementById('open-comment-camera-btn')?.addEventListener('click', async () => { + const ok = await this.startCamera(); + if (ok) { + this.cameraModal.show(); + } + }); + + // File select button + document.getElementById('select-comment-files-btn')?.addEventListener('click', () => { + document.getElementById('comment-files-input')?.click(); + }); + + // File input change + document.getElementById('comment-files-input')?.addEventListener('change', (e) => { + const files = Array.from(e.target.files); + files.forEach(file => this.addPhotoToPreview(file)); + e.target.value = ''; + }); + + // Camera capture + document.getElementById('comment-camera-capture-btn')?.addEventListener('click', () => { + this.triggerFlash(); + this.capturePhoto(); + }); + + // Camera stop + document.getElementById('comment-camera-stop-btn')?.addEventListener('click', () => { + this.cameraModal.hide(); + }); + + // Camera modal close + document.getElementById('commentCameraModal')?.addEventListener('hidden.bs.modal', () => { + this.stopCamera(); + }); + + // Save comment + document.getElementById('save-comment-btn')?.addEventListener('click', () => { + this.saveComment(); + }); + + // Reposition button + document.getElementById('reposition-comment-btn')?.addEventListener('click', () => { + this.startRepositionMode(); + }); + + // Comment modal close - reset state (but not during repositioning) + document.getElementById('commentModal')?.addEventListener('hidden.bs.modal', () => { + if (!this.isRepositioning) { + this.resetCommentForm(); + } + }); + } + + /** + * Watch user location and heading + */ + watchLocation() { + if (navigator.geolocation) { + navigator.geolocation.watchPosition( + (position) => { + // Always store real GPS position + this.lastGpsLocation = { + latitude: position.coords.latitude, + longitude: position.coords.longitude + }; + + // Skip updating currentLocation if user manually positioned the marker + if (this.locationManuallySet) { + return; + } + + this.currentLocation = { + latitude: position.coords.latitude, + longitude: position.coords.longitude + }; + + // Update location text + const locationText = document.getElementById('comment-location-text'); + if (locationText) { + locationText.textContent = `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`; + } + }, + (error) => { + // Silent fail + } + ); + } + + // Watch device orientation for heading + if (window.DeviceOrientationEvent) { + window.addEventListener('deviceorientation', (event) => { + if (event.alpha !== null) { + this.currentHeading = event.alpha; + } + }); + } + } + + /** + * Start camera stream + */ + async startCamera() { + const preview = document.getElementById('comment-camera-preview'); + const status = document.getElementById('comment-camera-status'); + + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + alert("La caméra n'est pas supportée sur ce navigateur."); + return false; + } + + try { + status.textContent = 'Initialisation de la caméra…'; + this.cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'environment', + width: { ideal: 1920 }, + height: { ideal: 1080 } + }, + audio: false + }); + + if ('srcObject' in preview) { + preview.srcObject = this.cameraStream; + } else { + preview.src = window.URL.createObjectURL(this.cameraStream); + } + preview.play(); + status.textContent = 'Caméra prête. Appuyez sur Capturer pour prendre une photo.'; + return true; + } catch (err) { + alert("Impossible d'accéder à la caméra."); + status.textContent = "Impossible d'accéder à la caméra."; + return false; + } + } + + /** + * Stop camera stream + */ + stopCamera() { + if (this.cameraStream) { + this.cameraStream.getTracks().forEach(track => track.stop()); + this.cameraStream = null; + } + + const preview = document.getElementById('comment-camera-preview'); + if (preview) { + preview.srcObject = null; + } + } + + /** + * Trigger camera flash effect + */ + triggerFlash() { + const flash = document.getElementById('comment-camera-flash'); + if (!flash) return; + + flash.style.opacity = '1'; + setTimeout(() => { + flash.style.opacity = '0'; + }, 150); + } + + /** + * Capture photo from camera + */ + capturePhoto() { + const preview = document.getElementById('comment-camera-preview'); + const canvas = document.getElementById('comment-camera-canvas'); + const status = document.getElementById('comment-camera-status'); + + if (!preview || !canvas) return; + + const videoWidth = preview.videoWidth; + const videoHeight = preview.videoHeight; + + if (!videoWidth || !videoHeight) { + status.textContent = "La vidéo n'est pas encore prête."; + return; + } + + canvas.width = videoWidth; + canvas.height = videoHeight; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(preview, 0, 0, videoWidth, videoHeight); + + canvas.toBlob((blob) => { + if (!blob) { + status.textContent = "Erreur lors de la capture."; + return; + } + + const filename = `comment_photo_${Date.now()}.jpg`; + const file = new File([blob], filename, { type: 'image/jpeg' }); + + // Store heading with photo + const photoData = { + file: file, + heading: this.currentHeading + }; + + this.addPhotoToPreview(photoData); + status.textContent = 'Photo capturée avec succès.'; + }, 'image/jpeg', 0.9); + } + + /** + * Add photo to preview + */ + addPhotoToPreview(photoData) { + const file = photoData.file || photoData; + const heading = photoData.heading; + + // Store photo with metadata + this.capturedPhotos.push({ file, heading }); + + const preview = document.getElementById('comment-photos-preview'); + if (!preview) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const photoIndex = this.capturedPhotos.length - 1; + const col = document.createElement('div'); + col.className = 'col-4'; + col.innerHTML = ` +
+ Photo + + ${heading !== null && heading !== undefined ? ` + + ${Math.round(heading)}° + + ` : ''} +
+ `; + + // Remove button handler + col.querySelector('button').addEventListener('click', (e) => { + const index = parseInt(e.currentTarget.dataset.photoIndex); + this.capturedPhotos.splice(index, 1); + col.remove(); + }); + + preview.appendChild(col); + }; + reader.readAsDataURL(file); + } + + /** + * Open comment modal + */ + openCommentModal() { + if (!this.currentLocation) { + alert('Position non disponible. Veuillez activer la géolocalisation.'); + return; + } + + this.commentModal.show(); + } + + /** + * Open comment panel (replaces modal) + */ + openCommentPanel() { + // Clean up any existing repositioning state first (handles double-click on add button) + this.cleanupRepositionMarker(); + + if (!this.currentLocation) { + alert('Position non disponible. Veuillez activer la géolocalisation.'); + return; + } + + // Ensure info panel exists + if (!this.initInfoPanel()) { + alert('Le panneau d\'information n\'est pas disponible. Veuillez recharger la page.'); + return; + } + + // Reset editing state + this.editingCommentId = null; + this.editingCommentData = null; + this.existingPhotosToDelete = []; + this.capturedPhotos = []; + this.locationManuallySet = false; // Reset flag so GPS updates work initially + + // Reset currentLocation to real GPS position (not last marker position) + if (this.lastGpsLocation) { + this.currentLocation = {...this.lastGpsLocation}; + } + + const content = this.generateCommentFormHTML(); + window.infoPanel.show(_('Nouveau commentaire'), content); + + // Setup event listeners after content is added to DOM + this.setupFormEventListeners(); + + // Update location text + const locationText = document.getElementById('comment-location-text'); + if (locationText && this.currentLocation) { + locationText.textContent = `${this.currentLocation.latitude.toFixed(6)}, ${this.currentLocation.longitude.toFixed(6)}`; + } + + // Immediately show marker at current position and enable repositioning + this.startRepositionModeOnOpen(); + } + + /** + * Start reposition mode immediately when opening new comment + * Shows marker at current GPS position and allows dragging + */ + startRepositionModeOnOpen() { + // Save original location for cancel + this.originalLocation = {...this.currentLocation}; + this.isRepositioning = true; + + // Lock the location so GPS updates don't change it while marker is shown + this.locationManuallySet = true; + + // Create marker at current position + this.createRepositionMarker(); + + // Set up map click handler for repositioning + this.repositionClickHandler = (e) => { + const { lng, lat } = e.lngLat; + this.updateRepositionMarker(lng, lat); + }; + + this.map.on('click', this.repositionClickHandler); + + // Center map on marker + this.map.flyTo({ + center: [this.currentLocation.longitude, this.currentLocation.latitude], + zoom: Math.max(this.map.getZoom(), 17) + }); + } + + /** + * Generate comment form HTML + */ + generateCommentFormHTML() { + return ` +
+ ${_('Statut')}: +
+ + + +
+
+ + +
+ +
+ +
+ + +
+ +
+ ${_('Localisation')}: +

${_('Calcul en cours...')}

+ +
+ +
+ ${_('Photos')}: +
+ + + +
+
+
+ +
+ + +
+ `; + } + + /** + * Setup event listeners for form elements in panel + */ + setupFormEventListeners() { + // Status badges + const statusColors = { + 'memo': 'primary', + 'attention': 'warning', + 'todo': 'info', + 'ok': 'success', + 'problem': 'danger' + }; + + document.querySelectorAll('.status-badge').forEach(badge => { + badge.addEventListener('click', (e) => { + const status = e.currentTarget.dataset.status; + const statusInput = document.getElementById('comment-status'); + if (statusInput) { + statusInput.value = status; + } + + // Update button styles + document.querySelectorAll('.status-badge').forEach(btn => { + const btnStatus = btn.dataset.status; + const color = statusColors[btnStatus]; + btn.className = `btn btn-sm ${btnStatus === status ? `btn-${color}` : `btn-outline-${color}`} status-badge${btnStatus === status ? ' active' : ''}`; + }); + }); + }); + + // Reposition button + const repositionBtn = document.getElementById('reposition-comment-btn'); + if (repositionBtn) { + repositionBtn.addEventListener('click', () => { + this.startRepositionMode(); + }); + } + + // Camera button + const cameraBtn = document.getElementById('open-camera-btn'); + if (cameraBtn) { + cameraBtn.addEventListener('click', () => { + this.openCameraPanel(); + }); + } + + // File selection button + const selectFilesBtn = document.getElementById('select-files-btn'); + const fileInput = document.getElementById('photo-file-input'); + if (selectFilesBtn && fileInput) { + selectFilesBtn.addEventListener('click', () => { + fileInput.click(); + }); + + fileInput.addEventListener('change', (e) => { + const files = Array.from(e.target.files); + files.forEach(file => { + this.addPhotoFromFile(file); + }); + fileInput.value = ''; // Reset input + }); + } + + // Save button + const saveBtn = document.getElementById('save-comment-btn'); + if (saveBtn) { + saveBtn.addEventListener('click', () => { + this.saveComment(); + }); + } + + // Cancel button + const cancelBtn = document.getElementById('cancel-comment-btn'); + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + window.infoPanel.close(); + }); + } + } + + /** + * Add photo from file (without camera) + */ + addPhotoFromFile(file) { + const reader = new FileReader(); + reader.onload = (e) => { + const photoData = { + dataUrl: e.target.result, + file: file, + heading: this.currentHeading + }; + this.capturedPhotos.push(photoData); + this.updatePhotosPreview(); + }; + reader.readAsDataURL(file); + } + + /** + * Open camera panel for photo capture + */ + async openCameraPanel() { + // Create fullscreen camera modal overlay + const modalHtml = ` +
+
+
+ +
+
+ + +
+
+
+ `; + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Start camera + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'environment', + width: { ideal: 1920 }, + height: { ideal: 1080 } + } + }); + + const video = document.getElementById('camera-video'); + if (video) { + video.srcObject = stream; + this.cameraStream = stream; + } + + // Setup button handlers + document.getElementById('capture-photo-btn').addEventListener('click', () => { + this.capturePhotoFromPanel(); + }); + + document.getElementById('close-camera-btn').addEventListener('click', () => { + this.closeCameraPanel(); + }); + } catch (error) { + alert('Erreur d\'accès à la caméra. Veuillez vérifier les permissions.'); + this.closeCameraPanel(); + } + } + + /** + * Capture photo from camera panel + */ + capturePhotoFromPanel() { + const video = document.getElementById('camera-video'); + if (!video) return; + + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(video, 0, 0); + + canvas.toBlob((blob) => { + const file = new File([blob], `photo_${Date.now()}.jpg`, { type: 'image/jpeg' }); + const photoData = { + dataUrl: canvas.toDataURL('image/jpeg'), + file: file, + heading: this.currentHeading + }; + + this.capturedPhotos.push(photoData); + + // Show visual feedback that photo was captured (flash effect) + this.showCaptureFlash(); + + // Update photo counter in camera overlay + this.updateCameraPhotoCount(); + + // Keep camera open for continuous capture - user closes with "Fermer" button + }, 'image/jpeg'); + } + + /** + * Show flash effect when capturing photo + */ + showCaptureFlash() { + const overlay = document.getElementById('camera-modal-overlay'); + if (!overlay) return; + + // Create flash element + const flash = document.createElement('div'); + flash.style.cssText = 'position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: white; opacity: 0.8; pointer-events: none; z-index: 10000;'; + overlay.appendChild(flash); + + // Fade out and remove + setTimeout(() => { + flash.style.transition = 'opacity 200ms ease-out'; + flash.style.opacity = '0'; + setTimeout(() => flash.remove(), 200); + }, 50); + } + + /** + * Update photo counter in camera overlay + */ + updateCameraPhotoCount() { + const overlay = document.getElementById('camera-modal-overlay'); + if (!overlay) return; + + // Find or create counter element + let counter = document.getElementById('camera-photo-counter'); + if (!counter) { + counter = document.createElement('div'); + counter.id = 'camera-photo-counter'; + counter.style.cssText = 'position: absolute; top: 20px; right: 20px; background: rgba(0,0,0,0.7); color: white; padding: 8px 16px; border-radius: 20px; font-size: 14px; z-index: 10001;'; + overlay.appendChild(counter); + } + + const count = this.capturedPhotos.length; + counter.innerHTML = ` ${count} photo${count > 1 ? 's' : ''}`; + } + + /** + * Close camera panel + */ + closeCameraPanel() { + if (this.cameraStream) { + this.cameraStream.getTracks().forEach(track => track.stop()); + this.cameraStream = null; + } + + // Remove camera modal overlay + const modalOverlay = document.getElementById('camera-modal-overlay'); + if (modalOverlay) { + modalOverlay.remove(); + } + + // Update photos preview + setTimeout(() => { + this.updatePhotosPreview(); + }, 100); + } + + /** + * Update photos preview in panel + */ + updatePhotosPreview() { + const preview = document.getElementById('photos-preview'); + if (!preview) return; + + preview.innerHTML = ''; + + this.capturedPhotos.forEach((photo, index) => { + const col = document.createElement('div'); + col.className = 'col-6 col-md-4'; + + const card = document.createElement('div'); + card.className = 'card position-relative'; + + const img = document.createElement('img'); + img.src = photo.dataUrl; + img.className = 'card-img-top'; + img.style.height = '120px'; + img.style.objectFit = 'cover'; + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'btn btn-sm btn-danger position-absolute top-0 end-0 m-1'; + deleteBtn.innerHTML = ''; + deleteBtn.addEventListener('click', () => { + this.capturedPhotos.splice(index, 1); + this.updatePhotosPreview(); + }); + + card.appendChild(img); + card.appendChild(deleteBtn); + + // Add heading badge if available + if (photo.heading !== null && photo.heading !== undefined) { + const badge = document.createElement('span'); + badge.className = 'badge bg-primary position-absolute bottom-0 start-0 m-1'; + badge.innerHTML = ` ${Math.round(photo.heading)}°`; + card.appendChild(badge); + } + + col.appendChild(card); + preview.appendChild(col); + }); + } + + /** + * Save comment + */ + async saveComment() { + const commentText = document.getElementById('comment-text')?.value.trim(); + const messageDiv = document.getElementById('comment-message'); + const saveBtn = document.getElementById('save-comment-btn'); + const status = document.getElementById('comment-status')?.value || 'memo'; + + // Check if we're editing an existing comment + if (this.editingCommentId && this.editingCommentData) { + await this.updateComment(this.editingCommentId); + return; + } + + // Check if we're adding photos to existing comment + if (this.editingCommentId && !this.editingCommentData) { + if (this.capturedPhotos.length === 0) { + this.showMessage('Veuillez ajouter au moins une photo.', 'warning'); + return; + } + + await this.addPhotosToComment(this.editingCommentId); + return; + } + + // Normal comment creation - text is now optional + + if (!this.currentLocation) { + this.showMessage('Position non disponible.', 'danger'); + return; + } + + saveBtn.disabled = true; + saveBtn.innerHTML = 'Enregistrement...'; + + try { + const formData = new FormData(); + formData.append('comment', commentText || ''); + formData.append('status', status); + formData.append('latitude', this.currentLocation.latitude); + formData.append('longitude', this.currentLocation.longitude); + + // Add photos with their headings + this.capturedPhotos.forEach((photoData, index) => { + formData.append('photos', photoData.file); + if (photoData.heading !== null && photoData.heading !== undefined) { + formData.append('photo_headings', photoData.heading); + } else { + formData.append('photo_headings', ''); + } + }); + + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; + + const response = await fetch(`${this.options.apiBaseUrl}/save/`, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken + }, + body: formData + }); + + const data = await response.json(); + + if (data.success) { + this.showMessage('Commentaire enregistré avec succès!', 'success'); + + // Clean up reposition marker + this.cleanupRepositionMarker(); + + // Close panel and reload + setTimeout(() => { + window.infoPanel.close(); + this.loadComments(); + }, 800); + } else { + this.showMessage(data.error || 'Erreur lors de l\'enregistrement', 'danger'); + } + } catch (error) { + this.showMessage('Erreur lors de l\'enregistrement', 'danger'); + } finally { + saveBtn.disabled = false; + saveBtn.innerHTML = 'Enregistrer'; + } + } + + /** + * Add photos to existing comment + */ + async addPhotosToComment(commentId) { + const saveBtn = document.getElementById('save-comment-btn'); + + try { + saveBtn.disabled = true; + saveBtn.innerHTML = 'Ajout...'; + + const formData = new FormData(); + formData.append('comment_id', commentId); + + // Add photos with headings + this.capturedPhotos.forEach((photoData, index) => { + formData.append('photos', photoData.file); + if (photoData.heading !== null && photoData.heading !== undefined) { + formData.append('photo_headings', photoData.heading); + } else { + formData.append('photo_headings', ''); + } + }); + + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; + + const response = await fetch(`${this.options.apiBaseUrl}/${commentId}/add-photos/`, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken + }, + body: formData + }); + + const data = await response.json(); + + if (data.success) { + this.showMessage('Photos ajoutées avec succès!', 'success'); + + // Reset form + this.resetCommentForm(); + + // Close modal and reload after a short delay + setTimeout(() => { + this.commentModal.hide(); + this.loadComments(); + // Reopen detail modal + setTimeout(() => this.showCommentDetail(commentId), 300); + }, 1000); + } else { + this.showMessage(data.error || 'Erreur lors de l\'ajout des photos', 'danger'); + } + } catch (error) { + this.showMessage('Erreur lors de l\'ajout des photos', 'danger'); + } finally { + saveBtn.disabled = false; + saveBtn.innerHTML = 'Ajouter les photos'; + } + } + + /** + * Update existing comment + */ + async updateComment(commentId) { + const commentText = document.getElementById('comment-text')?.value.trim(); + const saveBtn = document.getElementById('save-comment-btn'); + const status = document.getElementById('comment-status')?.value || 'memo'; + + // Text is now optional + + if (!this.currentLocation) { + this.showMessage('Position non disponible.', 'danger'); + return; + } + + try { + saveBtn.disabled = true; + saveBtn.innerHTML = 'Enregistrement...'; + + const formData = new FormData(); + formData.append('comment_id', commentId); + formData.append('comment', commentText || ''); + formData.append('status', status); + formData.append('latitude', this.currentLocation.latitude); + formData.append('longitude', this.currentLocation.longitude); + + // Add photos to delete + if (this.existingPhotosToDelete && this.existingPhotosToDelete.length > 0) { + this.existingPhotosToDelete.forEach(photoId => { + formData.append('photos_to_delete', photoId); + }); + } + + // Add new photos with headings + this.capturedPhotos.forEach((photoData, index) => { + formData.append('photos', photoData.file); + if (photoData.heading !== null && photoData.heading !== undefined) { + formData.append('photo_headings', photoData.heading); + } else { + formData.append('photo_headings', ''); + } + }); + + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; + + const response = await fetch(`${this.options.apiBaseUrl}/save/`, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken + }, + body: formData + }); + + const data = await response.json(); + + if (data.success) { + this.showMessage('Commentaire mis à jour avec succès!', 'success'); + + // Close panel and reload + setTimeout(() => { + window.infoPanel.close(); + this.loadComments(); + // Reopen detail + setTimeout(() => this.showCommentDetail(commentId), 300); + }, 800); + } else { + this.showMessage(data.error || 'Erreur lors de la mise à jour', 'danger'); + } + } catch (error) { + this.showMessage('Erreur lors de la mise à jour', 'danger'); + } finally { + saveBtn.disabled = false; + saveBtn.innerHTML = 'Enregistrer les modifications'; + } + } + + /** + * Reset comment form + */ + resetCommentForm() { + const commentText = document.getElementById('comment-text'); + if (commentText) commentText.value = ''; + const photosPreview = document.getElementById('comment-photos-preview'); + if (photosPreview) photosPreview.innerHTML = ''; + this.capturedPhotos = []; + const messageDiv = document.getElementById('comment-message'); + if (messageDiv) messageDiv.style.display = 'none'; + + // Always clean up reposition marker + this.cleanupRepositionMarker(); + } + + /** + * Clean up reposition marker and related state + * Called on save, cancel, panel close, and form reset + */ + cleanupRepositionMarker() { + // Remove marker from map + if (this.repositionMarker) { + this.repositionMarker.remove(); + this.repositionMarker = null; + } + + // Clear repositioning flag + this.isRepositioning = false; + + // Reset location flag to allow GPS updates again + this.locationManuallySet = false; + + // Restore currentLocation to real GPS position for next comment + if (this.lastGpsLocation) { + this.currentLocation = {...this.lastGpsLocation}; + } + + // Remove click handler if exists + if (this.repositionClickHandler) { + this.map.off('click', this.repositionClickHandler); + this.repositionClickHandler = null; + } + + // Remove instructions overlay if exists + const instructions = document.getElementById('reposition-instructions'); + if (instructions) { + instructions.remove(); + } + } + + /** + * Show message in modal + */ + showMessage(message, type) { + // Create toast container if it doesn't exist + let toastContainer = document.getElementById('toast-container'); + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.id = 'toast-container'; + toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; + toastContainer.style.zIndex = '9999'; + document.body.appendChild(toastContainer); + } + + // Create toast + const toastId = `toast-${Date.now()}`; + const toastHTML = ` + + `; + + toastContainer.insertAdjacentHTML('beforeend', toastHTML); + + const toastElement = document.getElementById(toastId); + const toast = new bootstrap.Toast(toastElement, { delay: 3000 }); + toast.show(); + + // Remove toast from DOM after it's hidden + toastElement.addEventListener('hidden.bs.toast', () => { + toastElement.remove(); + }); + } + + /** + * Load comments from server + */ + async loadComments() { + if (!this.map) return; + + // Don't try to load if layers aren't initialized yet + if (!this.mapLayersInitialized) { + return; + } + + const bounds = this.map.getBounds(); + const sw = bounds.getSouthWest(); + const ne = bounds.getNorthEast(); + + const filterGeom = { + geometry: { + type: 'Polygon', + coordinates: [[ + [sw.lng, sw.lat], + [ne.lng, sw.lat], + [ne.lng, ne.lat], + [sw.lng, ne.lat], + [sw.lng, sw.lat] + ]] + } + }; + + try { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; + + const response = await fetch(`${this.options.apiBaseUrl}/geojson/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ filter_geom: filterGeom }) + }); + + const data = await response.json(); + + const source = this.map.getSource('green-comments'); + if (source) { + source.setData(data); + } else { + // Try to initialize if not done yet + if (!this.mapLayersInitialized && this.map.loaded()) { + this.setupMapSource(); + } + } + } catch (error) { + // Silent fail + } + } + + /** + * Show comment detail + */ + async showCommentDetail(commentId) { + try { + const response = await fetch(`${this.options.apiBaseUrl}/${commentId}/`); + const result = await response.json(); + + if (result.success) { + const comment = result.data; + + // Generate photos HTML + let photosHTML = ''; + if (comment.photos && comment.photos.length > 0) { + photosHTML = '
'; + comment.photos.forEach(photo => { + photosHTML += ` +
+
+ Photo + ${photo.heading !== null ? ` + + ${Math.round(photo.heading)}° + + ` : ''} +
+
+ `; + }); + photosHTML += '
'; + } else { + photosHTML = '

Aucune photo

'; + } + + // Get status info + const statusInfo = { + 'memo': { label: 'Mémo', color: 'primary', icon: 'bi-sticky' }, + 'attention': { label: 'Attention', color: 'warning', icon: 'bi-exclamation-triangle' }, + 'todo': { label: 'À faire', color: 'info', icon: 'bi-list-check' }, + 'ok': { label: 'OK', color: 'success', icon: 'bi-check-circle' }, + 'problem': { label: 'Problème', color: 'danger', icon: 'bi-x-circle' } + }; + const status = statusInfo[comment.status] || statusInfo['memo']; + + // Generate detail content + const content = ` +
+ Statut: +

${status.label}

+
+ +
+ Commentaire: +

${comment.comment || 'Aucun commentaire'}

+
+ +
+ Auteur: +

${comment.created_by}

+ ${new Date(comment.created_at).toLocaleString('fr-BE')} +
+ +
+ Adresse: +

${comment.address || 'N/A'}

+
+ +
+ Photos: + ${photosHTML} +
+ + ${comment.can_edit ? ` +
+ + +
+ ` : ''} + `; + + // Show in panel + if (!this.initInfoPanel()) { + alert('Impossible d\'afficher les détails. Veuillez recharger la page.'); + return; + } + window.infoPanel.show('Détails du commentaire', content); + + // Setup event listeners + document.querySelectorAll('.photo-clickable-detail').forEach(img => { + img.addEventListener('click', (e) => { + this.openPhotoModal(e.currentTarget.dataset.photoUrl); + }); + }); + + if (comment.can_edit) { + const editBtn = document.getElementById('edit-comment-btn-panel'); + if (editBtn) { + editBtn.addEventListener('click', () => { + this.openEditCommentPanel(comment); + }); + } + + const deleteBtn = document.getElementById('delete-comment-btn-panel'); + if (deleteBtn) { + deleteBtn.addEventListener('click', async () => { + if (confirm('Êtes-vous sûr de vouloir supprimer ce commentaire ?')) { + await this.deleteComment(commentId); + } + }); + } + } + } + } catch (error) { + window.infoPanel.show('Erreur', '

Erreur lors du chargement du commentaire.

'); + } + } + + /** + * Delete comment + */ + async deleteComment(commentId) { + try { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; + + const response = await fetch(`${this.options.apiBaseUrl}/${commentId}/delete/`, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken + } + }); + + const data = await response.json(); + + if (data.success) { + this.showMessage('Commentaire supprimé avec succès!', 'success'); + window.infoPanel.close(); + this.loadComments(); + } else { + this.showMessage(data.error || 'Erreur lors de la suppression', 'danger'); + } + } catch (error) { + this.showMessage('Erreur lors de la suppression', 'danger'); + } + } + + /** + * Open edit comment panel (replaces modal) + */ + openEditCommentPanel(comment) { + this.editingCommentId = comment.id; + this.editingCommentData = comment; + this.existingPhotosToDelete = []; + this.capturedPhotos = []; + + // Update current location from comment + this.currentLocation = { + latitude: comment.latitude, + longitude: comment.longitude + }; + + // Get current status or default to memo + const currentStatus = comment.status || 'memo'; + + // Generate existing photos HTML + let existingPhotosHTML = ''; + if (comment.photos && comment.photos.length > 0) { + existingPhotosHTML = '
'; + comment.photos.forEach(photo => { + existingPhotosHTML += ` +
+
+ + + ${photo.heading !== null ? ` + + ${Math.round(photo.heading)}° + + ` : ''} +
+
+ `; + }); + existingPhotosHTML += '
'; + } + + const content = ` +
+ Statut: +
+ + + +
+
+ + +
+ +
+ +
+ + +
+ +
+ Localisation: +

${comment.latitude.toFixed(6)}, ${comment.longitude.toFixed(6)}

+ +
+ +
+ Photos existantes: + ${existingPhotosHTML || '

Aucune photo

'} +
+ +
+ Nouvelles photos: +
+ + + +
+
+
+ +
+ + +
+ `; + + window.infoPanel.show('Modifier le commentaire', content); + + // Setup event listeners + this.setupFormEventListeners(); + + // Setup delete existing photo buttons + document.querySelectorAll('.delete-existing-photo').forEach(btn => { + btn.addEventListener('click', (e) => { + const photoId = parseInt(e.currentTarget.dataset.photoId); + this.existingPhotosToDelete.push(photoId); + + // Remove from DOM + const photoCard = e.currentTarget.closest('[data-photo-id]'); + if (photoCard) { + photoCard.remove(); + } + }); + }); + } + + /** + * Create comment detail modal + */ + createCommentDetailModal() { + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + } + + /** + * Open photo in fullscreen modal + */ + openPhotoModal(photoUrl) { + let photoModal = document.getElementById('photoViewModal'); + if (!photoModal) { + const modalHTML = ` + + `; + document.body.insertAdjacentHTML('beforeend', modalHTML); + photoModal = document.getElementById('photoViewModal'); + } + + document.getElementById('photo-fullscreen-img').src = photoUrl; + const modal = new bootstrap.Modal(photoModal); + modal.show(); + } + + /** + * Open modal to add photos to existing comment + */ + openAddPhotosModal(commentId) { + // Store comment ID for later + this.editingCommentId = commentId; + + // Reset captured photos + this.capturedPhotos = []; + + // Change modal title + document.getElementById('modal-title-text').textContent = 'Ajouter des photos'; + + // Hide text field and quick badges + document.getElementById('comment-text').closest('.mb-3').style.display = 'none'; + document.querySelector('.quick-text-badge')?.closest('.mb-3')?.setAttribute('style', 'display: none;'); + + // Clear preview + document.getElementById('comment-photos-preview').innerHTML = ''; + + // Change save button text + document.getElementById('save-comment-btn').innerHTML = 'Ajouter les photos'; + + // Close detail modal and open comment modal + const detailModal = bootstrap.Modal.getInstance(document.getElementById('commentDetailModal')); + if (detailModal) { + detailModal.hide(); + } + + this.commentModal.show(); + + // Reset on close (but not if repositioning) + document.getElementById('commentModal').addEventListener('hidden.bs.modal', () => { + if (this.editingCommentId && !this.isRepositioning) { + this.resetCommentModal(); + this.editingCommentId = null; + } + }, { once: true }); + } + + /** + * Open modal to edit existing comment + */ + async openEditCommentModal(comment) { + // Store comment data for editing + this.editingCommentId = comment.id; + this.editingCommentData = comment; + + // Set location for this comment + this.currentLocation = { + latitude: comment.latitude, + longitude: comment.longitude + }; + + // Change modal title + document.getElementById('modal-title-text').textContent = 'Éditer le commentaire'; + + // Fill comment text + document.getElementById('comment-text').value = comment.comment; + + // Show text field and quick badges + document.getElementById('comment-text').closest('.mb-3').style.display = ''; + document.querySelector('.quick-text-badge')?.closest('.mb-3')?.removeAttribute('style'); + + // Update location text + const locationText = document.getElementById('comment-location-text'); + if (locationText) { + locationText.textContent = `${comment.latitude.toFixed(6)}, ${comment.longitude.toFixed(6)}`; + } + + // Load existing photos with delete buttons + const previewContainer = document.getElementById('comment-photos-preview'); + previewContainer.innerHTML = ''; + + // Reset captured photos (new photos to add) + this.capturedPhotos = []; + this.existingPhotosToDelete = []; + + if (comment.photos && comment.photos.length > 0) { + comment.photos.forEach(photo => { + const col = document.createElement('div'); + col.className = 'col-4'; + col.innerHTML = ` +
+ Photo + + ${photo.heading !== null ? ` + + ${Math.round(photo.heading)}° + + ` : ''} +
+ `; + + // Delete button handler + col.querySelector('button').addEventListener('click', (e) => { + const photoId = parseInt(e.currentTarget.dataset.photoId); + this.existingPhotosToDelete.push(photoId); + col.remove(); + }); + + previewContainer.appendChild(col); + }); + } + + // Change save button text + document.getElementById('save-comment-btn').innerHTML = 'Enregistrer les modifications'; + + // Close detail modal and open comment modal + const detailModal = bootstrap.Modal.getInstance(document.getElementById('commentDetailModal')); + if (detailModal) { + detailModal.hide(); + } + + this.commentModal.show(); + + // Reset on close (but not if repositioning) + document.getElementById('commentModal').addEventListener('hidden.bs.modal', () => { + if (this.editingCommentId && !this.isRepositioning) { + this.resetCommentModal(); + this.editingCommentId = null; + this.editingCommentData = null; + this.existingPhotosToDelete = []; + } + }, { once: true }); + } + + /** + * Reset comment modal to default state + */ + resetCommentModal() { + document.getElementById('modal-title-text').textContent = 'Nouveau commentaire'; + document.getElementById('comment-text').closest('.mb-3').style.display = ''; + document.querySelector('.quick-text-badge')?.closest('.mb-3')?.removeAttribute('style'); + document.getElementById('save-comment-btn').innerHTML = 'Enregistrer'; + this.capturedPhotos = []; + } + + /** + * Start reposition mode - temporarily hide modal and allow point placement + */ + startRepositionMode() { + // Save original location in case of cancel + this.originalLocation = {...this.currentLocation}; + + // Set flag to indicate repositioning + this.isRepositioning = true; + + // No need to hide panel - it stays visible on the side! + // Just update the panel to show repositioning state + const panelContent = document.getElementById('info-panel-content'); + if (panelContent) { + // Disable form inputs during repositioning + const inputs = panelContent.querySelectorAll('input, textarea, button:not(#finish-reposition-btn):not(#cancel-reposition-btn)'); + inputs.forEach(input => input.disabled = true); + + // Add repositioning instructions at the top + const instructions = document.createElement('div'); + instructions.id = 'reposition-instructions'; + instructions.className = 'alert alert-info mb-3'; + instructions.innerHTML = ` + Mode repositionnement +

Déplacez le marqueur sur la carte ou cliquez pour le repositionner.

+
+ + +
+ `; + panelContent.insertBefore(instructions, panelContent.firstChild); + + // Setup button handlers + document.getElementById('finish-reposition-btn').addEventListener('click', () => { + this.finishRepositionMode(); + }); + + document.getElementById('cancel-reposition-btn').addEventListener('click', () => { + this.cancelRepositionMode(); + }); + } + + // Create temporary marker at current location + this.createRepositionMarker(); + + // Set up map click handler + this.repositionClickHandler = (e) => { + const { lng, lat } = e.lngLat; + this.updateRepositionMarker(lng, lat); + }; + + this.map.on('click', this.repositionClickHandler); + } + + /** + * Create draggable marker for repositioning + */ + createRepositionMarker() { + if (!this.currentLocation) { + return; + } + + // Remove existing marker if any + if (this.repositionMarker) { + this.repositionMarker.remove(); + this.repositionMarker = null; + } + + // Create a simple marker element WITHOUT any CSS class + const el = document.createElement('div'); + // Pas de className pour éviter tout conflit CSS + + // Simple pin icon + el.innerHTML = ` + + + + + `; + + // Add pulse animation if not already defined + if (!document.getElementById('marker-pulse-animation')) { + const style = document.createElement('style'); + style.id = 'marker-pulse-animation'; + style.textContent = ` + .maplibregl-marker svg { + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); + } + `; + document.head.appendChild(style); + } + + // Create marker at current location (draggable pour les utilisateurs qui préfèrent) + this.repositionMarker = new maplibregl.Marker({ + element: el, + draggable: true, + anchor: 'bottom' + }) + .setLngLat([this.currentLocation.longitude, this.currentLocation.latitude]) + .addTo(this.map); + + // Update location on drag + this.repositionMarker.on('dragend', () => { + const lngLat = this.repositionMarker.getLngLat(); + this.currentLocation = { + latitude: lngLat.lat, + longitude: lngLat.lng + }; + + // Mark location as manually set to prevent GPS updates + this.locationManuallySet = true; + + // Update location text in form + const locationText = document.getElementById('comment-location-text'); + if (locationText) { + locationText.textContent = `${lngLat.lat.toFixed(6)}, ${lngLat.lng.toFixed(6)}`; + } + }); + } + + /** + * Update marker position + */ + updateRepositionMarker(lng, lat) { + // Update current location first + this.currentLocation = { + latitude: lat, + longitude: lng + }; + + // Mark location as manually set to prevent GPS updates + this.locationManuallySet = true; + + // Create marker if it doesn't exist yet + if (!this.repositionMarker) { + this.createRepositionMarker(); + } else { + // Update existing marker position + this.repositionMarker.setLngLat([lng, lat]); + } + + // Update location text in form + const locationText = document.getElementById('comment-location-text'); + if (locationText) { + locationText.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + } + } + + /** + * Show overlay with finish button + */ + showRepositionOverlay() { + let overlay = document.getElementById('reposition-overlay'); + if (!overlay) { + const overlayHTML = ` +
+
+
+
+ + Cliquez sur la carte ou déplacez le marqueur pour repositionner +
+ +
+
+
+ `; + document.body.insertAdjacentHTML('beforeend', overlayHTML); + overlay = document.getElementById('reposition-overlay'); + + // Finish button handler + document.getElementById('finish-reposition-btn').addEventListener('click', () => { + this.finishRepositionMode(); + }); + } + + overlay.style.display = 'block'; + } + + /** + * Finish reposition mode and return to form + */ + finishRepositionMode() { + // Remove map click handler + if (this.repositionClickHandler) { + this.map.off('click', this.repositionClickHandler); + this.repositionClickHandler = null; + } + + // Remove marker + if (this.repositionMarker) { + this.repositionMarker.remove(); + this.repositionMarker = null; + } + + // Clear repositioning flag + this.isRepositioning = false; + + // Remove instructions and re-enable inputs + const instructions = document.getElementById('reposition-instructions'); + if (instructions) { + instructions.remove(); + } + + const panelContent = document.getElementById('info-panel-content'); + if (panelContent) { + const inputs = panelContent.querySelectorAll('input, textarea, button'); + inputs.forEach(input => input.disabled = false); + } + + // Update location text + const locationText = document.getElementById('comment-location-text'); + if (locationText && this.currentLocation) { + locationText.textContent = `${this.currentLocation.latitude.toFixed(6)}, ${this.currentLocation.longitude.toFixed(6)}`; + } + } + + /** + * Cancel reposition mode + */ + cancelRepositionMode() { + // Restore original location + if (this.originalLocation) { + this.currentLocation = {...this.originalLocation}; + } + + // Clean up marker using dedicated method + this.cleanupRepositionMarker(); + + this.finishRepositionMode(); + } + + /** + * Diagnostic method for debugging + */ + diagnose() { + // Diagnostic method removed - use browser dev tools for debugging + } + + /** + * Force re-initialization of map layers (for debugging) + */ + forceInit() { + this.mapLayersInitialized = false; + this.setupMapSource(); + } +} diff --git a/streetup/assets/static/assets/green_detail.js b/streetup/assets/static/assets/green_detail.js new file mode 100644 index 0000000..8fcc80c --- /dev/null +++ b/streetup/assets/static/assets/green_detail.js @@ -0,0 +1,44 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 17, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const featureData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: JSON.parse(feature_geojson), + properties: { + name: '...' + } + } + ] + }; + + // Add the circle as a GeoJSON source + map.addSource('green', { + type: 'geojson', + data: featureData + }); + + map.addLayer({ + 'id': 'green', + 'type': 'fill', + 'source': 'green', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.8 + } + }); +}); + diff --git a/streetup/assets/static/assets/info-panel.css b/streetup/assets/static/assets/info-panel.css new file mode 100644 index 0000000..0546bc5 --- /dev/null +++ b/streetup/assets/static/assets/info-panel.css @@ -0,0 +1,103 @@ +/* Panneau d'information superposé sur la carte */ +#info-panel { + position: absolute; + background: white; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + z-index: 1000; + max-height: 80vh; + overflow-y: auto; + display: none; + transition: transform 0.3s ease; +} + +/* Desktop/Tablet - panneau à droite */ +@media (min-width: 768px) { + #info-panel { + top: 10px; + right: 10px; + width: 420px; + border-radius: 8px; + } +} + +/* Mobile - panneau en bas */ +@media (max-width: 767px) { + #info-panel { + bottom: 0; + left: 0; + right: 0; + width: 100%; + max-height: 70vh; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + } +} + +#info-panel.show { + display: block; +} + +#info-panel .panel-header { + padding: 15px; + border-bottom: 1px solid #dee2e6; + background-color: #f8f9fa; + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + z-index: 10; +} + +#info-panel .panel-content { + padding: 15px; +} + +#info-panel .panel-close { + cursor: pointer; + font-size: 1.5rem; + line-height: 1; + color: #000; + opacity: 0.5; + transition: opacity 0.2s; +} + +#info-panel .panel-close:hover { + opacity: 1; +} + +/* Photo preview dans le panneau */ +#info-panel .photo-preview { + width: 100%; + border-radius: 4px; + margin-bottom: 10px; + cursor: pointer; +} + +#info-panel .photo-preview img { + width: 100%; + height: auto; + border-radius: 4px; +} + +#info-panel .quick-badge { + cursor: pointer; + transition: all 0.2s; +} + +#info-panel .quick-badge:hover { + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(0,0,0,0.15); +} + +/* Style pour les boutons de photo */ +#info-panel .photo-capture-buttons .btn { + margin-bottom: 10px; +} + +/* Style pour le repositionnement */ +#info-panel .reposition-controls { + display: flex; + gap: 10px; + margin-top: 15px; +} diff --git a/streetup/assets/static/assets/info-panel.js b/streetup/assets/static/assets/info-panel.js new file mode 100644 index 0000000..602a0ec --- /dev/null +++ b/streetup/assets/static/assets/info-panel.js @@ -0,0 +1,137 @@ +/** + * Gestionnaire du panneau d'information + * Gère l'affichage des informations dans un panneau superposé sur la carte + */ + + +class InfoPanel { + constructor() { + this.panel = document.getElementById('info-panel'); + this.title = document.getElementById('info-panel-title'); + this.content = document.getElementById('info-panel-content'); + this.closeBtn = document.getElementById('info-panel-close'); + this.backBtn = document.getElementById('info-panel-back'); + + // Pile d'historique pour gérer les retours arrière + this.history = []; + + this.setupEventListeners(); + } + + setupEventListeners() { + // Bouton fermer + this.closeBtn.addEventListener('click', () => { + this.close(); + }); + + // Bouton retour + this.backBtn.addEventListener('click', () => { + this.goBack(); + }); + } + + /** + * Affiche le panneau avec un contenu + * @param {string} title - Titre du panneau + * @param {string|HTMLElement} content - Contenu HTML ou élément DOM + * @param {boolean} addToHistory - Ajouter à l'historique (par défaut true) + */ + show(title, content, addToHistory = true) { + // Sauvegarder l'état actuel dans l'historique si demandé + if (addToHistory && this.panel.classList.contains('show')) { + this.history.push({ + title: this.title.textContent, + content: this.content.innerHTML + }); + } + + // Mettre à jour le contenu + this.title.textContent = title; + + if (typeof content === 'string') { + this.content.innerHTML = content; + } else { + this.content.innerHTML = ''; + this.content.appendChild(content); + } + + // Afficher le panneau + this.panel.classList.add('show'); + + // Gérer le bouton retour + if (this.history.length > 0) { + this.backBtn.style.display = 'inline-block'; + } else { + this.backBtn.style.display = 'none'; + } + } + + /** + * Met à jour le contenu du panneau sans changer le titre + * @param {string|HTMLElement} content - Nouveau contenu + */ + updateContent(content) { + if (typeof content === 'string') { + this.content.innerHTML = content; + } else { + this.content.innerHTML = ''; + this.content.appendChild(content); + } + } + + /** + * Revient à l'état précédent dans l'historique + */ + goBack() { + if (this.history.length > 0) { + const previous = this.history.pop(); + this.show(previous.title, previous.content, false); + } + } + + /** + * Ferme le panneau et vide l'historique + */ + close() { + this.panel.classList.remove('show'); + this.history = []; + this.backBtn.style.display = 'none'; + + // Nettoyer le contenu + this.content.innerHTML = ''; + this.title.textContent = ''; + } + + /** + * Vérifie si le panneau est ouvert + */ + isOpen() { + return this.panel.classList.contains('show'); + } +} + +// Instance globale du panneau +if (typeof window.infoPanel === 'undefined') { + window.infoPanel = null; +} + +// Fonction d'initialisation +function initInfoPanel() { + if (!window.infoPanel) { + const panel = document.getElementById('info-panel'); + if (panel) { + window.infoPanel = new InfoPanel(); + } else { + console.warn('info-panel element not found in DOM'); + } + } + return window.infoPanel; +} + +// Initialiser au chargement du DOM ou immédiatement si déjà chargé +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initInfoPanel); +} else { + // DOM déjà chargé, initialiser immédiatement + initInfoPanel(); +} diff --git a/streetup/assets/static/assets/its_location_detail.js b/streetup/assets/static/assets/its_location_detail.js new file mode 100644 index 0000000..c968cc7 --- /dev/null +++ b/streetup/assets/static/assets/its_location_detail.js @@ -0,0 +1,201 @@ + +let center = [4.351721, 50.850339]; // Coordonnées par défaut (Bruxelles) +let zoom = 11; +if (feature_lon !== null) { + center = [feature_lon, feature_lat]; + zoom = 17; +} + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: center, + zoom: zoom, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const location_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: feature_geom, + properties: { + name: '...' + } + } + ] + }; + + // Add the circle as a GeoJSON source + map.addSource('location', { + type: 'geojson', + data: location_data + }); + + map.addLayer({ + 'id': 'location', + 'type': 'fill', + 'source': 'location', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.7, + } + }); + + if (feature_geom !== null) { + const bounds = turf.bbox(location_data); + map.fitBounds(bounds, { + duration: 10 + }); + } +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +const documentTypeChoices = { + 'other': 'Autre' +}; + +if (fileInput) { + fileInput.addEventListener('change', function () { + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1'); + fileName.textContent = file.name; + row.appendChild(fileName); + + // Select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; // nécessite Bootstrap Icons ou tu peux mettre "✖" + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + fileInput.dispatchEvent(new Event('change')); // re-render + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); + }); +} + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +if (deleteDocumentModal) { + let _deleteUrl = null; + let _deleteDocId = null; + + deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + _deleteUrl = button.getAttribute('data-delete-url'); + _deleteDocId = button.getAttribute('data-document-id'); + const fileName = button.getAttribute('data-filename'); + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + if (fileNameContainer) fileNameContainer.textContent = fileName; + }); + + deleteDocumentModal.addEventListener('hidden.bs.modal', function () { + _deleteUrl = null; + _deleteDocId = null; + }); + + const deleteDocumentForm = document.getElementById('deleteDocumentForm'); + if (deleteDocumentForm) { + deleteDocumentForm.addEventListener('submit', async function (e) { + e.preventDefault(); + if (!_deleteUrl) return; + + const submitBtn = deleteDocumentForm.querySelector('[type="submit"]'); + if (submitBtn) submitBtn.disabled = true; + + const csrfToken = (document.cookie.match(/csrftoken=([^;]+)/) || [])[1] || ''; + try { + const resp = await fetch(_deleteUrl, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + const data = await resp.json(); + if (resp.ok && data.success) { + bootstrap.Modal.getInstance(deleteDocumentModal).hide(); + if (_deleteDocId) { + const card = document.getElementById('doc-thumb-' + _deleteDocId); + if (card) card.remove(); + } + } else { + if (submitBtn) submitBtn.disabled = false; + } + } catch (_) { + if (submitBtn) submitBtn.disabled = false; + } + }); + } +} + +historyModal = document.getElementById('documentHistoryModal'); +if (historyModal) { + document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const locationId = button.getAttribute('data-location-id'); + + fetch(`${document_history_url}?location_id=${locationId}&document_type=${type}`) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); + }); +} + + +document.addEventListener('click', (e) => { + const btn = e.target.closest('[data-bs-toggle="collapse"]'); + if (!btn) return; + const target = document.querySelector(btn.getAttribute('data-bs-target')); + if (!target) return; + target.addEventListener('shown.bs.collapse', () => btn.textContent = '▾', {once:true}); + target.addEventListener('hidden.bs.collapse', () => btn.textContent = '▸', {once:true}); +}); \ No newline at end of file diff --git a/streetup/assets/static/assets/nature_bench_icon.svg b/streetup/assets/static/assets/nature_bench_icon.svg new file mode 100644 index 0000000..09f32ba --- /dev/null +++ b/streetup/assets/static/assets/nature_bench_icon.svg @@ -0,0 +1,18 @@ + diff --git a/streetup/assets/static/assets/nature_tree_icon.svg b/streetup/assets/static/assets/nature_tree_icon.svg new file mode 100644 index 0000000..995bb33 --- /dev/null +++ b/streetup/assets/static/assets/nature_tree_icon.svg @@ -0,0 +1,20 @@ + diff --git a/streetup/assets/static/assets/naturerwiz_detail.js b/streetup/assets/static/assets/naturerwiz_detail.js new file mode 100644 index 0000000..ff78b89 --- /dev/null +++ b/streetup/assets/static/assets/naturerwiz_detail.js @@ -0,0 +1,400 @@ +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 17, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + // 1. Prepare location GeoJSON + let featureGeom = null; + if (feature_geojson) { + try { + featureGeom = typeof feature_geojson === 'string' ? JSON.parse(feature_geojson) : feature_geojson; + } catch (e) { + console.error("Failed to parse feature_geojson", e); + } + } + + const location_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: featureGeom, + properties: { + name: 'GIEP Zone' + } + } + ] + }; + + map.addSource('location', { + type: 'geojson', + data: location_data + }); + + map.addLayer({ + 'id': 'location', + 'type': 'fill', + 'source': 'location', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.25, + } + }); + + map.addLayer({ + 'id': 'location-outline', + 'type': 'line', + 'source': 'location', + 'paint': { + 'line-color': '#0094ff', + 'line-width': 2, + 'line-dasharray': [2, 2] + } + }); + + // 2. Prepare assets GeoJSON + let assetsData = { type: 'FeatureCollection', features: [] }; + try { + const assetsJson = document.getElementById("assets_geojson_value").textContent; + const parsed = JSON.parse(assetsJson); + assetsData = typeof parsed === 'string' ? JSON.parse(parsed) : parsed; + } catch (e) { + console.error("Failed to parse assets_geojson", e); + } + + map.addSource('assets', { + type: 'geojson', + data: assetsData + }); + + // Polygons fill + map.addLayer({ + 'id': 'assets-polygons', + 'type': 'fill', + 'source': 'assets', + 'filter': ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false], + 'paint': { + 'fill-color': [ + 'match', ['get', 'structure_type'], + ['massif drainant', 'tranchee drainante', 'collecteur en beton poreux', 'empierrement sedimentation'], '#8d6e63', + ['jardin de pluie', 'noue', 'toiture verte', 'bande filtrante', 'espace vert sur dalle'], '#009688', + ['bassin sec/infiltration', 'bassin en eau', 'bassin orage', 'citerne', 'toiture stockante', 'SAUL-vide', 'SAUL-terre'], '#0277bd', + '#0288d1' + ], + 'fill-opacity': 0.6 + } + }); + + // Polygons outline + map.addLayer({ + 'id': 'assets-polygon-borders', + 'type': 'line', + 'source': 'assets', + 'filter': ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false], + 'paint': { + 'line-color': [ + 'match', ['get', 'structure_type'], + ['massif drainant', 'tranchee drainante', 'collecteur en beton poreux', 'empierrement sedimentation'], '#8d6e63', + ['jardin de pluie', 'noue', 'toiture verte', 'bande filtrante', 'espace vert sur dalle'], '#009688', + ['bassin sec/infiltration', 'bassin en eau', 'bassin orage', 'citerne', 'toiture stockante', 'SAUL-vide', 'SAUL-terre'], '#0277bd', + '#0288d1' + ], + 'line-width': 2 + } + }); + + // Lines + map.addLayer({ + 'id': 'assets-lines', + 'type': 'line', + 'source': 'assets', + 'filter': ['match', ['geometry-type'], ['LineString', 'MultiLineString'], true, false], + 'paint': { + 'line-color': [ + 'match', ['get', 'structure_type'], + ['massif drainant', 'tranchee drainante', 'drain', 'collecteur en beton poreux', 'reseau egout separatif'], '#8d6e63', + ['noue', 'jardin de pluie', 'bande filtrante'], '#009688', + ['filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'avaloir de rail', 'trop plein'], '#0277bd', + '#0288d1' + ], + 'line-width': 3 + } + }); + + // Points + map.addLayer({ + 'id': 'assets-points', + 'type': 'circle', + 'source': 'assets', + 'filter': ['==', ['geometry-type'], 'Point'], + 'paint': { + 'circle-radius': 6, + 'circle-color': '#0277bd', + 'circle-stroke-color': '#ffffff', + 'circle-stroke-width': 1.5 + } + }); + + // 3. Zoom/fit bounds + const allFeatures = []; + if (featureGeom && featureGeom.type) { + allFeatures.push({ + type: 'Feature', + geometry: featureGeom, + properties: {} + }); + } + if (assetsData && assetsData.features) { + assetsData.features.forEach(f => { + if (f && f.geometry && f.geometry.type) { + allFeatures.push(f); + } + }); + } + + if (allFeatures.length > 0) { + const bounds = turf.bbox({ + type: 'FeatureCollection', + features: allFeatures + }); + map.fitBounds(bounds, { + padding: 40, + duration: 10 + }); + } + + // 4. Popups & Interactivity + const popup = new maplibregl.Popup({ + closeButton: true, + closeOnClick: true + }); + + function onAssetClick(e) { + const coordinates = e.features[0].geometry.type === 'Point' + ? e.features[0].geometry.coordinates.slice() + : [e.lngLat.lng, e.lngLat.lat]; + + const props = e.features[0].properties; + const code = props.code; + const name = props.name; + const structType = props.structure_type_label || props.structure_type || ''; + const url = props.detail_url; + + let content = `
+ ${code}
+ ${name && name !== code ? `${name}
` : ''} + ${structType}
+ ${url ? `Détail` : ''} +
`; + + while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) { + coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360; + } + + popup.setLngLat(coordinates).setHTML(content).addTo(map); + } + + function setPointerCursor() { + map.getCanvas().style.cursor = 'pointer'; + } + + function resetCursor() { + map.getCanvas().style.cursor = ''; + } + + const assetLayers = ['assets-polygons', 'assets-lines', 'assets-points']; + assetLayers.forEach(layerId => { + map.on('mouseenter', layerId, setPointerCursor); + map.on('mouseleave', layerId, resetCursor); + map.on('click', layerId, onAssetClick); + }); +}); + +// Event listener for tree toggles +document.addEventListener('click', (e) => { + const btn = e.target.closest('[data-bs-toggle="collapse"]'); + if (!btn) return; + const targetSelector = btn.getAttribute('data-bs-target') || btn.getAttribute('href'); + if (!targetSelector) return; + const target = document.querySelector(targetSelector); + if (!target) return; + if (btn.hasAttribute('data-bs-target')) { + target.addEventListener('shown.bs.collapse', () => btn.textContent = '▾', {once:true}); + target.addEventListener('hidden.bs.collapse', () => btn.textContent = '▸', {once:true}); + } +}); + +// 5. Document management (Add/History) +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +const documentTypeChoices = { + 'as_built': 'As-built', + 'other': 'Autre' +}; + +let documentConfig = { use_managed_documents: false, can_add_unique_tag_documents: false, config_url: null }; +const configEl = document.getElementById('documentConfigData'); +if (configEl) { + documentConfig = JSON.parse(configEl.textContent); +} + +let availableTags = []; +let availableFolders = []; + +async function loadDocumentConfig() { + if (!documentConfig.config_url) return; + try { + const response = await fetch(documentConfig.config_url); + const data = await response.json(); + documentConfig.use_managed_documents = data.use_managed_documents; + documentConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents; + availableTags = data.tags; + availableFolders = data.folders; + } catch (e) { + console.error('Failed to load document config:', e); + } +} + +loadDocumentConfig(); + +if (fileInput) { + fileInput.addEventListener('change', function() { + fileContainer.innerHTML = ''; + const files = this.files; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const index = i; + + const row = document.createElement('div'); + row.className = 'card p-3 mb-3 bg-light border-0'; + + const fileName = document.createElement('span'); + fileName.className = 'fw-semibold text-secondary mb-2'; + fileName.textContent = file.name; + row.appendChild(fileName); + + if (documentConfig.use_managed_documents && availableTags && availableTags.length > 0) { + // Tag select + const tagSelect = document.createElement('select'); + tagSelect.name = `tag_${index}`; + tagSelect.className = 'form-select mb-2 tag-select'; + tagSelect.required = true; + + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'Sélectionner un type...'; + tagSelect.appendChild(defaultOption); + + availableTags.forEach(tag => { + const option = document.createElement('option'); + option.value = tag.slug; + option.textContent = tag.name; + // Filter unique tags if user cannot add them + if (tag.is_unique && !documentConfig.can_add_unique_tag_documents) { + return; + } + tagSelect.appendChild(option); + }); + row.appendChild(tagSelect); + + // Folder select + const folderSelect = document.createElement('select'); + folderSelect.name = `folder_${index}`; + folderSelect.className = 'form-select mb-2 folder-select'; + + availableFolders.forEach(folder => { + const option = document.createElement('option'); + option.value = folder.id; + option.textContent = folder.name; + folderSelect.appendChild(option); + }); + + if (availableFolders.length > 1) { + row.appendChild(folderSelect); + } else { + const hiddenFolderInput = document.createElement('input'); + hiddenFolderInput.type = 'hidden'; + hiddenFolderInput.name = `folder_${index}`; + hiddenFolderInput.value = availableFolders[0] ? availableFolders[0].id : ''; + row.appendChild(hiddenFolderInput); + } + + // Hide folder select if unique tag is chosen + tagSelect.addEventListener('change', function() { + const selectedTagSlug = this.value; + const selectedTag = availableTags.find(t => t.slug === selectedTagSlug); + if (selectedTag && selectedTag.is_unique) { + folderSelect.style.display = 'none'; + folderSelect.removeAttribute('required'); + } else if (availableFolders.length > 1) { + folderSelect.style.display = 'block'; + } + }); + + } else { + // Legacy / fallback select + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.className = 'form-select mb-2'; + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + select.appendChild(option); + } + row.appendChild(select); + } + + // Delete button + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.className = 'btn btn-sm btn-outline-danger mt-2 align-self-start'; + deleteBtn.textContent = 'Retirer ce fichier'; + deleteBtn.onclick = function() { + row.remove(); + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + } + + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); + }); +} + +// History modal handling +const historyModal = document.getElementById('documentHistoryModal'); +if (historyModal) { + historyModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const tagSlug = button.getAttribute('data-tag-slug'); + const locationId = button.getAttribute('data-location-id'); + + const contentDiv = document.getElementById('documentHistoryContent'); + contentDiv.innerHTML = '
'; + + let url = `${document_history_url}?location_id=${locationId}&tag_slug=${tagSlug}`; + + fetch(url) + .then(response => response.json()) + .then(data => { + contentDiv.innerHTML = data.html; + }) + .catch(error => { + contentDiv.innerHTML = '
Erreur de chargement de l\'historique.
'; + console.error('Error fetching history:', error); + }); + }); +} diff --git a/streetup/assets/static/assets/public_lamp_post_icon.svg b/streetup/assets/static/assets/public_lamp_post_icon.svg new file mode 100644 index 0000000..3053e98 --- /dev/null +++ b/streetup/assets/static/assets/public_lamp_post_icon.svg @@ -0,0 +1,20 @@ + diff --git a/streetup/assets/static/assets/publiclighting_street_detail.js b/streetup/assets/static/assets/publiclighting_street_detail.js new file mode 100644 index 0000000..892be04 --- /dev/null +++ b/streetup/assets/static/assets/publiclighting_street_detail.js @@ -0,0 +1,141 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 16, + pitch: 45, + // bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const street_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: geom, + properties: { + name: '...' + } + } + ] + }; + + street_source = map.addSource('street', { + type: 'geojson', + data: street_data + }); + + map.addLayer({ + 'id': 'street', + 'type': 'fill', + 'source': 'street', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.7, + } + }); + + const bounds = turf.bbox(street_data); + map.fitBounds(bounds, { + // duration: 300 + }); +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +const documentTypeChoices = { + 'as_built': 'As Built', + 'cross_plan': 'Plan de croix', + 'other': 'Autre', + 'programming_file': 'Fichier de programmation', + 'connexion_plan': 'Plan de raccordement' +}; + +fileInput.addEventListener('change', function () { + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1'); + fileName.textContent = file.name; + row.appendChild(fileName); + + // Select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; // nécessite Bootstrap Icons ou tu peux mettre "✖" + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + fileInput.dispatchEvent(new Event('change')); // re-render + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); +}); + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const fileName = button.getAttribute('data-filename'); + const deleteUrl = button.getAttribute('data-delete-url'); + + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + const deleteForm = document.getElementById('deleteDocumentForm'); + + fileNameContainer.textContent = fileName; + deleteForm.action = deleteUrl; +}); + + +document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const intersectionId = button.getAttribute('data-intersection-id'); + + fetch(`${document_history_url}?intersection_id=${intersectionId}&document_type=${type}`) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); +}); \ No newline at end of file diff --git a/streetup/assets/static/assets/radar_preventif_icon.svg b/streetup/assets/static/assets/radar_preventif_icon.svg new file mode 100644 index 0000000..0c5c7b4 --- /dev/null +++ b/streetup/assets/static/assets/radar_preventif_icon.svg @@ -0,0 +1,19 @@ + diff --git a/streetup/assets/static/assets/road_concrete_block_icon.svg b/streetup/assets/static/assets/road_concrete_block_icon.svg new file mode 100644 index 0000000..0251419 --- /dev/null +++ b/streetup/assets/static/assets/road_concrete_block_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/A11.svg b/streetup/assets/static/assets/road_signs/2025/A11.svg new file mode 100644 index 0000000..c68f32f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A11.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A13.svg b/streetup/assets/static/assets/road_signs/2025/A13.svg new file mode 100644 index 0000000..f7fe4a2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A13.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A14.svg b/streetup/assets/static/assets/road_signs/2025/A14.svg new file mode 100644 index 0000000..d5ba5ab --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A14.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A15.svg b/streetup/assets/static/assets/road_signs/2025/A15.svg new file mode 100644 index 0000000..ec741d3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A15.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A17.svg b/streetup/assets/static/assets/road_signs/2025/A17.svg new file mode 100644 index 0000000..ffa4609 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A17.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A19.svg b/streetup/assets/static/assets/road_signs/2025/A19.svg new file mode 100644 index 0000000..cb0acdc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A19.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A1A.svg b/streetup/assets/static/assets/road_signs/2025/A1A.svg new file mode 100644 index 0000000..7751880 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A1A.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A1B.svg b/streetup/assets/static/assets/road_signs/2025/A1B.svg new file mode 100644 index 0000000..95d10d6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A1B.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A1C.svg b/streetup/assets/static/assets/road_signs/2025/A1C.svg new file mode 100644 index 0000000..ab255b8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A1C.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A1D.svg b/streetup/assets/static/assets/road_signs/2025/A1D.svg new file mode 100644 index 0000000..c32ad99 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A1D.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A21.svg b/streetup/assets/static/assets/road_signs/2025/A21.svg new file mode 100644 index 0000000..a5c833a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A21.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A23.svg b/streetup/assets/static/assets/road_signs/2025/A23.svg new file mode 100644 index 0000000..f076860 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A23.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A25.svg b/streetup/assets/static/assets/road_signs/2025/A25.svg new file mode 100644 index 0000000..12be09b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A25.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A27.svg b/streetup/assets/static/assets/road_signs/2025/A27.svg new file mode 100644 index 0000000..baaf4fc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A27.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A29.svg b/streetup/assets/static/assets/road_signs/2025/A29.svg new file mode 100644 index 0000000..6eef8be --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A29.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A3.svg b/streetup/assets/static/assets/road_signs/2025/A3.svg new file mode 100644 index 0000000..59d1035 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A3.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A31.svg b/streetup/assets/static/assets/road_signs/2025/A31.svg new file mode 100644 index 0000000..580672c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A31.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A33.svg b/streetup/assets/static/assets/road_signs/2025/A33.svg new file mode 100644 index 0000000..4ec5c96 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A33.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A35.svg b/streetup/assets/static/assets/road_signs/2025/A35.svg new file mode 100644 index 0000000..6a37c9d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A35.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A37.svg b/streetup/assets/static/assets/road_signs/2025/A37.svg new file mode 100644 index 0000000..6cb462b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A37.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A39.svg b/streetup/assets/static/assets/road_signs/2025/A39.svg new file mode 100644 index 0000000..d43631f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A39.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A41.svg b/streetup/assets/static/assets/road_signs/2025/A41.svg new file mode 100644 index 0000000..5d53aba --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A41.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A43.svg b/streetup/assets/static/assets/road_signs/2025/A43.svg new file mode 100644 index 0000000..e4a0a69 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A43.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A45.svg b/streetup/assets/static/assets/road_signs/2025/A45.svg new file mode 100644 index 0000000..fde0e68 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A45.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A47.svg b/streetup/assets/static/assets/road_signs/2025/A47.svg new file mode 100644 index 0000000..37e66b8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A47.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A49.svg b/streetup/assets/static/assets/road_signs/2025/A49.svg new file mode 100644 index 0000000..c671458 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A49.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A5.svg b/streetup/assets/static/assets/road_signs/2025/A5.svg new file mode 100644 index 0000000..f039408 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A5.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A50.svg b/streetup/assets/static/assets/road_signs/2025/A50.svg new file mode 100644 index 0000000..4e1a322 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A50.svg @@ -0,0 +1,50 @@ + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/A51.svg b/streetup/assets/static/assets/road_signs/2025/A51.svg new file mode 100644 index 0000000..1a11e8e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A51.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A7A.svg b/streetup/assets/static/assets/road_signs/2025/A7A.svg new file mode 100644 index 0000000..72c6028 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A7A.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A7B.svg b/streetup/assets/static/assets/road_signs/2025/A7B.svg new file mode 100644 index 0000000..8dd06aa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A7B.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A7C.svg b/streetup/assets/static/assets/road_signs/2025/A7C.svg new file mode 100644 index 0000000..a19bd5e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A7C.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/A9.svg b/streetup/assets/static/assets/road_signs/2025/A9.svg new file mode 100644 index 0000000..088de45 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/A9.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B1.svg b/streetup/assets/static/assets/road_signs/2025/B1.svg new file mode 100644 index 0000000..77fc1f7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B1.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B11.svg b/streetup/assets/static/assets/road_signs/2025/B11.svg new file mode 100644 index 0000000..f561506 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B11.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B13.svg b/streetup/assets/static/assets/road_signs/2025/B13.svg new file mode 100644 index 0000000..b25fb2d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B13.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15A.svg b/streetup/assets/static/assets/road_signs/2025/B15A.svg new file mode 100644 index 0000000..eb66a33 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15A.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15B.svg b/streetup/assets/static/assets/road_signs/2025/B15B.svg new file mode 100644 index 0000000..5ad5fe7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15B.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15C.svg b/streetup/assets/static/assets/road_signs/2025/B15C.svg new file mode 100644 index 0000000..b3a7336 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15C.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15D.svg b/streetup/assets/static/assets/road_signs/2025/B15D.svg new file mode 100644 index 0000000..09d8288 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15D.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15E.svg b/streetup/assets/static/assets/road_signs/2025/B15E.svg new file mode 100644 index 0000000..d429276 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15E.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15F.svg b/streetup/assets/static/assets/road_signs/2025/B15F.svg new file mode 100644 index 0000000..547a13d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15F.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B15G.svg b/streetup/assets/static/assets/road_signs/2025/B15G.svg new file mode 100644 index 0000000..481fa74 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B15G.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B17.svg b/streetup/assets/static/assets/road_signs/2025/B17.svg new file mode 100644 index 0000000..83a2636 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B17.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B19.svg b/streetup/assets/static/assets/road_signs/2025/B19.svg new file mode 100644 index 0000000..f163a24 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B19.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B21.svg b/streetup/assets/static/assets/road_signs/2025/B21.svg new file mode 100644 index 0000000..d52ebcb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B21.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B22.svg b/streetup/assets/static/assets/road_signs/2025/B22.svg new file mode 100644 index 0000000..5415410 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B22.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B23.svg b/streetup/assets/static/assets/road_signs/2025/B23.svg new file mode 100644 index 0000000..77f6363 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B23.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B3.svg b/streetup/assets/static/assets/road_signs/2025/B3.svg new file mode 100644 index 0000000..b68eee3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B3.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B5.svg b/streetup/assets/static/assets/road_signs/2025/B5.svg new file mode 100644 index 0000000..1f827f8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B5.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B7.svg b/streetup/assets/static/assets/road_signs/2025/B7.svg new file mode 100644 index 0000000..48b63b6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B7.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/B9.svg b/streetup/assets/static/assets/road_signs/2025/B9.svg new file mode 100644 index 0000000..570f2ca --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/B9.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C1.svg b/streetup/assets/static/assets/road_signs/2025/C1.svg new file mode 100644 index 0000000..33159f7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C1.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C11.svg b/streetup/assets/static/assets/road_signs/2025/C11.svg new file mode 100644 index 0000000..76bcb3b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C11.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C13.svg b/streetup/assets/static/assets/road_signs/2025/C13.svg new file mode 100644 index 0000000..efe64a7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C13.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C15.svg b/streetup/assets/static/assets/road_signs/2025/C15.svg new file mode 100644 index 0000000..e31548d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C15.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C17.svg b/streetup/assets/static/assets/road_signs/2025/C17.svg new file mode 100644 index 0000000..680e384 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C17.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C19.svg b/streetup/assets/static/assets/road_signs/2025/C19.svg new file mode 100644 index 0000000..eda2b6f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C19.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C21.png b/streetup/assets/static/assets/road_signs/2025/C21.png new file mode 100644 index 0000000..d97f723 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/C21.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/C21_5.5.svg b/streetup/assets/static/assets/road_signs/2025/C21_5.5.svg new file mode 100644 index 0000000..8cb6427 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C21_5.5.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C22.svg b/streetup/assets/static/assets/road_signs/2025/C22.svg new file mode 100644 index 0000000..ad13619 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C22.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C23.svg b/streetup/assets/static/assets/road_signs/2025/C23.svg new file mode 100644 index 0000000..fd381e6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C23.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/C24A.svg b/streetup/assets/static/assets/road_signs/2025/C24A.svg new file mode 100644 index 0000000..5ce7faa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C24A.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C24B.svg b/streetup/assets/static/assets/road_signs/2025/C24B.svg new file mode 100644 index 0000000..72b9f83 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C24B.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C24C.svg b/streetup/assets/static/assets/road_signs/2025/C24C.svg new file mode 100644 index 0000000..67258e9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C24C.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C25.svg b/streetup/assets/static/assets/road_signs/2025/C25.svg new file mode 100644 index 0000000..b137011 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C25.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C27.svg b/streetup/assets/static/assets/road_signs/2025/C27.svg new file mode 100644 index 0000000..52aa6dd --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C27.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C29.svg b/streetup/assets/static/assets/road_signs/2025/C29.svg new file mode 100644 index 0000000..252cff0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C29.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C3.svg b/streetup/assets/static/assets/road_signs/2025/C3.svg new file mode 100644 index 0000000..a290698 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C3.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C31A.svg b/streetup/assets/static/assets/road_signs/2025/C31A.svg new file mode 100644 index 0000000..4563947 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C31A.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C31B.svg b/streetup/assets/static/assets/road_signs/2025/C31B.svg new file mode 100644 index 0000000..4d583f2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C31B.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C33.svg b/streetup/assets/static/assets/road_signs/2025/C33.svg new file mode 100644 index 0000000..f0408d0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C33.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C35.svg b/streetup/assets/static/assets/road_signs/2025/C35.svg new file mode 100644 index 0000000..a5109f1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C35.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C37.svg b/streetup/assets/static/assets/road_signs/2025/C37.svg new file mode 100644 index 0000000..56b77dc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C37.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C39.svg b/streetup/assets/static/assets/road_signs/2025/C39.svg new file mode 100644 index 0000000..426f65b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C39.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C41.svg b/streetup/assets/static/assets/road_signs/2025/C41.svg new file mode 100644 index 0000000..8ed9e82 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C41.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43.svg b/streetup/assets/static/assets/road_signs/2025/C43.svg new file mode 100644 index 0000000..2e77569 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_100.svg b/streetup/assets/static/assets/road_signs/2025/C43_100.svg new file mode 100644 index 0000000..893c863 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_100.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_20.svg b/streetup/assets/static/assets/road_signs/2025/C43_20.svg new file mode 100644 index 0000000..d00cc7b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_20.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_30.svg b/streetup/assets/static/assets/road_signs/2025/C43_30.svg new file mode 100644 index 0000000..a30b8fb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_30.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_50.svg b/streetup/assets/static/assets/road_signs/2025/C43_50.svg new file mode 100644 index 0000000..2e77569 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_50.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_50KM.svg b/streetup/assets/static/assets/road_signs/2025/C43_50KM.svg new file mode 100644 index 0000000..5b6a518 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_50KM.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_60.svg b/streetup/assets/static/assets/road_signs/2025/C43_60.svg new file mode 100644 index 0000000..9d987a6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_60.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_70.svg b/streetup/assets/static/assets/road_signs/2025/C43_70.svg new file mode 100644 index 0000000..cf45728 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_70.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_80.svg b/streetup/assets/static/assets/road_signs/2025/C43_80.svg new file mode 100644 index 0000000..f3372d7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_80.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C43_90.svg b/streetup/assets/static/assets/road_signs/2025/C43_90.svg new file mode 100644 index 0000000..abab591 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C43_90.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C45.svg b/streetup/assets/static/assets/road_signs/2025/C45.svg new file mode 100644 index 0000000..15d8115 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C45.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C45_50.svg b/streetup/assets/static/assets/road_signs/2025/C45_50.svg new file mode 100644 index 0000000..15d8115 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C45_50.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C45_50KM.svg b/streetup/assets/static/assets/road_signs/2025/C45_50KM.svg new file mode 100644 index 0000000..df85804 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C45_50KM.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C46.svg b/streetup/assets/static/assets/road_signs/2025/C46.svg new file mode 100644 index 0000000..6395449 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C46.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C47.svg b/streetup/assets/static/assets/road_signs/2025/C47.svg new file mode 100644 index 0000000..a5a92ff --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C47.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C48.svg b/streetup/assets/static/assets/road_signs/2025/C48.svg new file mode 100644 index 0000000..7b46737 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C48.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C49.svg b/streetup/assets/static/assets/road_signs/2025/C49.svg new file mode 100644 index 0000000..79eaa46 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C49.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C5-C7.svg b/streetup/assets/static/assets/road_signs/2025/C5-C7.svg new file mode 100644 index 0000000..1d3a864 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C5-C7.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C5.svg b/streetup/assets/static/assets/road_signs/2025/C5.svg new file mode 100644 index 0000000..5b054bc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C5.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C6.svg b/streetup/assets/static/assets/road_signs/2025/C6.svg new file mode 100644 index 0000000..c2f8ae9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C6.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C7.svg b/streetup/assets/static/assets/road_signs/2025/C7.svg new file mode 100644 index 0000000..43130a0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C7.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C9-C11.svg b/streetup/assets/static/assets/road_signs/2025/C9-C11.svg new file mode 100644 index 0000000..6dc70a7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C9-C11.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/C9.svg b/streetup/assets/static/assets/road_signs/2025/C9.svg new file mode 100644 index 0000000..401806a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/C9.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/CAMERA.png b/streetup/assets/static/assets/road_signs/2025/CAMERA.png new file mode 100644 index 0000000..5d0468a Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/CAMERA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/D10.svg b/streetup/assets/static/assets/road_signs/2025/D10.svg new file mode 100644 index 0000000..d881afe --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D10.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D11.svg b/streetup/assets/static/assets/road_signs/2025/D11.svg new file mode 100644 index 0000000..92bec38 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D11.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D13.svg b/streetup/assets/static/assets/road_signs/2025/D13.svg new file mode 100644 index 0000000..4de91bb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D13.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1A.svg b/streetup/assets/static/assets/road_signs/2025/D1A.svg new file mode 100644 index 0000000..17b5cff --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1A.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1B.png b/streetup/assets/static/assets/road_signs/2025/D1B.png new file mode 100644 index 0000000..028b608 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/D1B.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/D1B_LEFT.svg b/streetup/assets/static/assets/road_signs/2025/D1B_LEFT.svg new file mode 100644 index 0000000..48bff00 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1B_LEFT.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1B_LINKS.svg b/streetup/assets/static/assets/road_signs/2025/D1B_LINKS.svg new file mode 100644 index 0000000..48bff00 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1B_LINKS.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1B_RECHTS.svg b/streetup/assets/static/assets/road_signs/2025/D1B_RECHTS.svg new file mode 100644 index 0000000..731f193 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1B_RECHTS.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1B_RIGHT.svg b/streetup/assets/static/assets/road_signs/2025/D1B_RIGHT.svg new file mode 100644 index 0000000..731f193 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1B_RIGHT.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1C.svg b/streetup/assets/static/assets/road_signs/2025/D1C.svg new file mode 100644 index 0000000..d3e7a76 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1C.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1D.svg b/streetup/assets/static/assets/road_signs/2025/D1D.svg new file mode 100644 index 0000000..64bf60a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1D.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1E.svg b/streetup/assets/static/assets/road_signs/2025/D1E.svg new file mode 100644 index 0000000..4f8ce5f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1E.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1F.svg b/streetup/assets/static/assets/road_signs/2025/D1F.svg new file mode 100644 index 0000000..4ca0d94 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D1F.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D1G.png b/streetup/assets/static/assets/road_signs/2025/D1G.png new file mode 100644 index 0000000..0421c58 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/D1G.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/D3A.svg b/streetup/assets/static/assets/road_signs/2025/D3A.svg new file mode 100644 index 0000000..9ffc7de --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D3A.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D3B.svg b/streetup/assets/static/assets/road_signs/2025/D3B.svg new file mode 100644 index 0000000..47ba1bb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D3B.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D4.png b/streetup/assets/static/assets/road_signs/2025/D4.png new file mode 100644 index 0000000..b4be81f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/D4.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/D4_LEFT.svg b/streetup/assets/static/assets/road_signs/2025/D4_LEFT.svg new file mode 100644 index 0000000..77a3fe9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D4_LEFT.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D4_LINKS.svg b/streetup/assets/static/assets/road_signs/2025/D4_LINKS.svg new file mode 100644 index 0000000..77a3fe9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D4_LINKS.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D4_RECHTDOOR.svg b/streetup/assets/static/assets/road_signs/2025/D4_RECHTDOOR.svg new file mode 100644 index 0000000..d445f8e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D4_RECHTDOOR.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D4_RECHTS.svg b/streetup/assets/static/assets/road_signs/2025/D4_RECHTS.svg new file mode 100644 index 0000000..89b7a21 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D4_RECHTS.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D4_RIGHT.svg b/streetup/assets/static/assets/road_signs/2025/D4_RIGHT.svg new file mode 100644 index 0000000..89b7a21 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D4_RIGHT.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D4_STRAIGHT.svg b/streetup/assets/static/assets/road_signs/2025/D4_STRAIGHT.svg new file mode 100644 index 0000000..d445f8e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D4_STRAIGHT.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D5.svg b/streetup/assets/static/assets/road_signs/2025/D5.svg new file mode 100644 index 0000000..3c9d35e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D5.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D7.svg b/streetup/assets/static/assets/road_signs/2025/D7.svg new file mode 100644 index 0000000..352784c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D7.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D9.svg b/streetup/assets/static/assets/road_signs/2025/D9.svg new file mode 100644 index 0000000..dad27b7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D9.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D9A.svg b/streetup/assets/static/assets/road_signs/2025/D9A.svg new file mode 100644 index 0000000..dad27b7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D9A.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/D9B.svg b/streetup/assets/static/assets/road_signs/2025/D9B.svg new file mode 100644 index 0000000..638ba7a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/D9B.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E1.svg b/streetup/assets/static/assets/road_signs/2025/E1.svg new file mode 100644 index 0000000..171bb31 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E1.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E11.svg b/streetup/assets/static/assets/road_signs/2025/E11.svg new file mode 100644 index 0000000..243d179 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E11.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E3.svg b/streetup/assets/static/assets/road_signs/2025/E3.svg new file mode 100644 index 0000000..540a54c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E3.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E5.svg b/streetup/assets/static/assets/road_signs/2025/E5.svg new file mode 100644 index 0000000..a7cfac0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E5.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E7.svg b/streetup/assets/static/assets/road_signs/2025/E7.svg new file mode 100644 index 0000000..a1b7b75 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E7.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9A.svg b/streetup/assets/static/assets/road_signs/2025/E9A.svg new file mode 100644 index 0000000..2499ab2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9A.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9A_DISK.png b/streetup/assets/static/assets/road_signs/2025/E9A_DISK.png new file mode 100644 index 0000000..75b8c23 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/E9A_DISK.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/E9A_DISK_35TMAX.png b/streetup/assets/static/assets/road_signs/2025/E9A_DISK_35TMAX.png new file mode 100644 index 0000000..d367a89 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/E9A_DISK_35TMAX.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/E9A_HANDI.png b/streetup/assets/static/assets/road_signs/2025/E9A_HANDI.png new file mode 100644 index 0000000..51859e1 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/E9A_HANDI.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/E9A_HANDICAP.svg b/streetup/assets/static/assets/road_signs/2025/E9A_HANDICAP.svg new file mode 100644 index 0000000..74ae612 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9A_HANDICAP.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9A_PARKEERSCHIJF.svg b/streetup/assets/static/assets/road_signs/2025/E9A_PARKEERSCHIJF.svg new file mode 100644 index 0000000..70d0727 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9A_PARKEERSCHIJF.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9B.svg b/streetup/assets/static/assets/road_signs/2025/E9B.svg new file mode 100644 index 0000000..7f96fdb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9B.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9C.svg b/streetup/assets/static/assets/road_signs/2025/E9C.svg new file mode 100644 index 0000000..cfdf7e1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9C.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9D.svg b/streetup/assets/static/assets/road_signs/2025/E9D.svg new file mode 100644 index 0000000..389c1cc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9D.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9E.svg b/streetup/assets/static/assets/road_signs/2025/E9E.svg new file mode 100644 index 0000000..0ee280e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9E.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9F.svg b/streetup/assets/static/assets/road_signs/2025/E9F.svg new file mode 100644 index 0000000..0d50be4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9F.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9G.svg b/streetup/assets/static/assets/road_signs/2025/E9G.svg new file mode 100644 index 0000000..ea04df7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9G.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9H.svg b/streetup/assets/static/assets/road_signs/2025/E9H.svg new file mode 100644 index 0000000..dd8a733 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9H.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/E9I.svg b/streetup/assets/static/assets/road_signs/2025/E9I.svg new file mode 100644 index 0000000..eee637d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/E9I.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F101A.svg b/streetup/assets/static/assets/road_signs/2025/F101A.svg new file mode 100644 index 0000000..1848667 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F101A.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F101B.svg b/streetup/assets/static/assets/road_signs/2025/F101B.svg new file mode 100644 index 0000000..bdb252e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F101B.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F101C.svg b/streetup/assets/static/assets/road_signs/2025/F101C.svg new file mode 100644 index 0000000..542aefa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F101C.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F103.svg b/streetup/assets/static/assets/road_signs/2025/F103.svg new file mode 100644 index 0000000..5b687b3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F103.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F105.svg b/streetup/assets/static/assets/road_signs/2025/F105.svg new file mode 100644 index 0000000..78f6d0a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F105.svg @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F11.svg b/streetup/assets/static/assets/road_signs/2025/F11.svg new file mode 100644 index 0000000..dc58ff7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F11.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F111.svg b/streetup/assets/static/assets/road_signs/2025/F111.svg new file mode 100644 index 0000000..123c5be --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F111.svg @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F113.svg b/streetup/assets/static/assets/road_signs/2025/F113.svg new file mode 100644 index 0000000..eb2dcf0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F113.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F117.svg b/streetup/assets/static/assets/road_signs/2025/F117.svg new file mode 100644 index 0000000..a9c1cd8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F117.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F118.svg b/streetup/assets/static/assets/road_signs/2025/F118.svg new file mode 100644 index 0000000..ac55cda --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F118.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F119.svg b/streetup/assets/static/assets/road_signs/2025/F119.svg new file mode 100644 index 0000000..f0b3222 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F119.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/F120.svg b/streetup/assets/static/assets/road_signs/2025/F120.svg new file mode 100644 index 0000000..825855d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F120.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/F12A.svg b/streetup/assets/static/assets/road_signs/2025/F12A.svg new file mode 100644 index 0000000..e5145f8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F12A.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F12B.svg b/streetup/assets/static/assets/road_signs/2025/F12B.svg new file mode 100644 index 0000000..48c90a7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F12B.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F13.svg b/streetup/assets/static/assets/road_signs/2025/F13.svg new file mode 100644 index 0000000..12709ed --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F13.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F14.svg b/streetup/assets/static/assets/road_signs/2025/F14.svg new file mode 100644 index 0000000..8734e24 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F14.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F15.svg b/streetup/assets/static/assets/road_signs/2025/F15.svg new file mode 100644 index 0000000..a914e39 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F15.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F15_AFRIT.svg b/streetup/assets/static/assets/road_signs/2025/F15_AFRIT.svg new file mode 100644 index 0000000..8b184fe --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F15_AFRIT.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F17.svg b/streetup/assets/static/assets/road_signs/2025/F17.svg new file mode 100644 index 0000000..7331813 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F17.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F18.svg b/streetup/assets/static/assets/road_signs/2025/F18.svg new file mode 100644 index 0000000..721c970 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F18.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F19.svg b/streetup/assets/static/assets/road_signs/2025/F19.svg new file mode 100644 index 0000000..031a3e7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F19.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F1A.png b/streetup/assets/static/assets/road_signs/2025/F1A.png new file mode 100644 index 0000000..c03f3f0 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/F1A.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/F1A_HORIZONTAAL.svg b/streetup/assets/static/assets/road_signs/2025/F1A_HORIZONTAAL.svg new file mode 100644 index 0000000..4c7a3f1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F1A_HORIZONTAAL.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F1A_VERTICAAL.svg b/streetup/assets/static/assets/road_signs/2025/F1A_VERTICAAL.svg new file mode 100644 index 0000000..f7189b3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F1A_VERTICAAL.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F1B.png b/streetup/assets/static/assets/road_signs/2025/F1B.png new file mode 100644 index 0000000..94e42cb Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/F1B.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/F1B_HORIZONTAAL.svg b/streetup/assets/static/assets/road_signs/2025/F1B_HORIZONTAAL.svg new file mode 100644 index 0000000..ce286eb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F1B_HORIZONTAAL.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F1B_VERTICAAL.svg b/streetup/assets/static/assets/road_signs/2025/F1B_VERTICAAL.svg new file mode 100644 index 0000000..5c090b5 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F1B_VERTICAAL.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F21.svg b/streetup/assets/static/assets/road_signs/2025/F21.svg new file mode 100644 index 0000000..04d1a57 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F21.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F23A.svg b/streetup/assets/static/assets/road_signs/2025/F23A.svg new file mode 100644 index 0000000..eda90b1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F23A.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F23B.svg b/streetup/assets/static/assets/road_signs/2025/F23B.svg new file mode 100644 index 0000000..1451a02 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F23B.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F23C.svg b/streetup/assets/static/assets/road_signs/2025/F23C.svg new file mode 100644 index 0000000..a3c9c8d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F23C.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F23D.svg b/streetup/assets/static/assets/road_signs/2025/F23D.svg new file mode 100644 index 0000000..77fccbb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F23D.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F25.svg b/streetup/assets/static/assets/road_signs/2025/F25.svg new file mode 100644 index 0000000..d63ef28 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F25.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F25M.svg b/streetup/assets/static/assets/road_signs/2025/F25M.svg new file mode 100644 index 0000000..5617c22 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F25M.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F25R.svg b/streetup/assets/static/assets/road_signs/2025/F25R.svg new file mode 100644 index 0000000..23bb294 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F25R.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F25V.svg b/streetup/assets/static/assets/road_signs/2025/F25V.svg new file mode 100644 index 0000000..fb3bd68 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F25V.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F25VM.svg b/streetup/assets/static/assets/road_signs/2025/F25VM.svg new file mode 100644 index 0000000..e6e4def --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F25VM.svg @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F25VR.svg b/streetup/assets/static/assets/road_signs/2025/F25VR.svg new file mode 100644 index 0000000..3e58395 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F25VR.svg @@ -0,0 +1,455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F27.svg b/streetup/assets/static/assets/road_signs/2025/F27.svg new file mode 100644 index 0000000..004c494 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F27.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F27V.svg b/streetup/assets/static/assets/road_signs/2025/F27V.svg new file mode 100644 index 0000000..4e70c52 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F27V.svg @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F29.svg b/streetup/assets/static/assets/road_signs/2025/F29.svg new file mode 100644 index 0000000..c01f722 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F29.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F31.svg b/streetup/assets/static/assets/road_signs/2025/F31.svg new file mode 100644 index 0000000..14c92dd --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F31.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F33A.svg b/streetup/assets/static/assets/road_signs/2025/F33A.svg new file mode 100644 index 0000000..65ed586 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F33A.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F33B.svg b/streetup/assets/static/assets/road_signs/2025/F33B.svg new file mode 100644 index 0000000..1c6968f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F33B.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F33C.svg b/streetup/assets/static/assets/road_signs/2025/F33C.svg new file mode 100644 index 0000000..ec9046a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F33C.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F34A.svg b/streetup/assets/static/assets/road_signs/2025/F34A.svg new file mode 100644 index 0000000..c6e9545 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F34A.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F34B1.svg b/streetup/assets/static/assets/road_signs/2025/F34B1.svg new file mode 100644 index 0000000..4bb713a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F34B1.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F34B2.svg b/streetup/assets/static/assets/road_signs/2025/F34B2.svg new file mode 100644 index 0000000..bdbf339 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F34B2.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F34C1.svg b/streetup/assets/static/assets/road_signs/2025/F34C1.svg new file mode 100644 index 0000000..265fcaf --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F34C1.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F34C2.svg b/streetup/assets/static/assets/road_signs/2025/F34C2.svg new file mode 100644 index 0000000..0405ca7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F34C2.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F35.svg b/streetup/assets/static/assets/road_signs/2025/F35.svg new file mode 100644 index 0000000..e5a386a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F35.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F37.svg b/streetup/assets/static/assets/road_signs/2025/F37.svg new file mode 100644 index 0000000..c82011c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F37.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F39.svg b/streetup/assets/static/assets/road_signs/2025/F39.svg new file mode 100644 index 0000000..841a868 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F39.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F3A.png b/streetup/assets/static/assets/road_signs/2025/F3A.png new file mode 100644 index 0000000..4dfe65e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/F3A.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/F3A_HORIZONTAAL.svg b/streetup/assets/static/assets/road_signs/2025/F3A_HORIZONTAAL.svg new file mode 100644 index 0000000..6911552 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F3A_HORIZONTAAL.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F3A_VERTICAAL.svg b/streetup/assets/static/assets/road_signs/2025/F3A_VERTICAAL.svg new file mode 100644 index 0000000..1e01f50 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F3A_VERTICAAL.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F3B.png b/streetup/assets/static/assets/road_signs/2025/F3B.png new file mode 100644 index 0000000..8423d98 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/F3B.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/F3B_HORIZONTAAL.svg b/streetup/assets/static/assets/road_signs/2025/F3B_HORIZONTAAL.svg new file mode 100644 index 0000000..b7ab846 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F3B_HORIZONTAAL.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F3B_VERTICAAL.svg b/streetup/assets/static/assets/road_signs/2025/F3B_VERTICAAL.svg new file mode 100644 index 0000000..9402d65 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F3B_VERTICAAL.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F41.svg b/streetup/assets/static/assets/road_signs/2025/F41.svg new file mode 100644 index 0000000..c1112a6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F41.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F43.svg b/streetup/assets/static/assets/road_signs/2025/F43.svg new file mode 100644 index 0000000..eeafca8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F43.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F45.svg b/streetup/assets/static/assets/road_signs/2025/F45.svg new file mode 100644 index 0000000..9d38260 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F45.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F45B.svg b/streetup/assets/static/assets/road_signs/2025/F45B.svg new file mode 100644 index 0000000..334e309 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F45B.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F45_LINKS.svg b/streetup/assets/static/assets/road_signs/2025/F45_LINKS.svg new file mode 100644 index 0000000..c2701c9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F45_LINKS.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F45_RECHTS.svg b/streetup/assets/static/assets/road_signs/2025/F45_RECHTS.svg new file mode 100644 index 0000000..3562570 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F45_RECHTS.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F47.svg b/streetup/assets/static/assets/road_signs/2025/F47.svg new file mode 100644 index 0000000..d36ba24 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F47.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F49.svg b/streetup/assets/static/assets/road_signs/2025/F49.svg new file mode 100644 index 0000000..8a13242 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F49.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F4A.svg b/streetup/assets/static/assets/road_signs/2025/F4A.svg new file mode 100644 index 0000000..dce09cb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F4A.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F4B.svg b/streetup/assets/static/assets/road_signs/2025/F4B.svg new file mode 100644 index 0000000..b25b006 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F4B.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F5.svg b/streetup/assets/static/assets/road_signs/2025/F5.svg new file mode 100644 index 0000000..f7603ea --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F5.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F50.svg b/streetup/assets/static/assets/road_signs/2025/F50.svg new file mode 100644 index 0000000..39a5070 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F50.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F50BIS.png b/streetup/assets/static/assets/road_signs/2025/F50BIS.png new file mode 100644 index 0000000..bca8f10 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/F50BIS.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/F50BIS_A21-A25.svg b/streetup/assets/static/assets/road_signs/2025/F50BIS_A21-A25.svg new file mode 100644 index 0000000..38e3b7d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F50BIS_A21-A25.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F50BIS_A21.svg b/streetup/assets/static/assets/road_signs/2025/F50BIS_A21.svg new file mode 100644 index 0000000..78cf834 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F50BIS_A21.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F50BIS_A25.svg b/streetup/assets/static/assets/road_signs/2025/F50BIS_A25.svg new file mode 100644 index 0000000..983f501 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F50BIS_A25.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F51A.svg b/streetup/assets/static/assets/road_signs/2025/F51A.svg new file mode 100644 index 0000000..d6a3267 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F51A.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F51B.svg b/streetup/assets/static/assets/road_signs/2025/F51B.svg new file mode 100644 index 0000000..8c115ea --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F51B.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F52A.svg b/streetup/assets/static/assets/road_signs/2025/F52A.svg new file mode 100644 index 0000000..01847fa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F52A.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F52B.svg b/streetup/assets/static/assets/road_signs/2025/F52B.svg new file mode 100644 index 0000000..30a753d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F52B.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F52BISA.svg b/streetup/assets/static/assets/road_signs/2025/F52BISA.svg new file mode 100644 index 0000000..75f4216 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F52BISA.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F52BISB.svg b/streetup/assets/static/assets/road_signs/2025/F52BISB.svg new file mode 100644 index 0000000..fcb2f5d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F52BISB.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F53.svg b/streetup/assets/static/assets/road_signs/2025/F53.svg new file mode 100644 index 0000000..8519c5a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F53.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F55.svg b/streetup/assets/static/assets/road_signs/2025/F55.svg new file mode 100644 index 0000000..d9cb456 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F55.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F56.svg b/streetup/assets/static/assets/road_signs/2025/F56.svg new file mode 100644 index 0000000..9341021 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F56.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F57.svg b/streetup/assets/static/assets/road_signs/2025/F57.svg new file mode 100644 index 0000000..f840207 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F57.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F59.svg b/streetup/assets/static/assets/road_signs/2025/F59.svg new file mode 100644 index 0000000..0e06041 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F59.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F59A.svg b/streetup/assets/static/assets/road_signs/2025/F59A.svg new file mode 100644 index 0000000..eddb52b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F59A.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F59B.svg b/streetup/assets/static/assets/road_signs/2025/F59B.svg new file mode 100644 index 0000000..d79c790 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F59B.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F60.svg b/streetup/assets/static/assets/road_signs/2025/F60.svg new file mode 100644 index 0000000..ceb425f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F60.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F61.svg b/streetup/assets/static/assets/road_signs/2025/F61.svg new file mode 100644 index 0000000..b0391a0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F61.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F62.svg b/streetup/assets/static/assets/road_signs/2025/F62.svg new file mode 100644 index 0000000..4b3db89 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F62.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F63.svg b/streetup/assets/static/assets/road_signs/2025/F63.svg new file mode 100644 index 0000000..2e3dc87 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F63.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F63_CNG.svg b/streetup/assets/static/assets/road_signs/2025/F63_CNG.svg new file mode 100644 index 0000000..80919a3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F63_CNG.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F63_ELEKTRISCH.svg b/streetup/assets/static/assets/road_signs/2025/F63_ELEKTRISCH.svg new file mode 100644 index 0000000..0fc9eb3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F63_ELEKTRISCH.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F63_H2.svg b/streetup/assets/static/assets/road_signs/2025/F63_H2.svg new file mode 100644 index 0000000..96b737b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F63_H2.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F63_LNG.svg b/streetup/assets/static/assets/road_signs/2025/F63_LNG.svg new file mode 100644 index 0000000..b65d5c5 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F63_LNG.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F63_LPG.svg b/streetup/assets/static/assets/road_signs/2025/F63_LPG.svg new file mode 100644 index 0000000..3335373 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F63_LPG.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F65.svg b/streetup/assets/static/assets/road_signs/2025/F65.svg new file mode 100644 index 0000000..d4391ef --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F65.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F67.svg b/streetup/assets/static/assets/road_signs/2025/F67.svg new file mode 100644 index 0000000..2b3e1de --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F67.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F69.svg b/streetup/assets/static/assets/road_signs/2025/F69.svg new file mode 100644 index 0000000..d291417 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F69.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F7.svg b/streetup/assets/static/assets/road_signs/2025/F7.svg new file mode 100644 index 0000000..be72965 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F7.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F71.svg b/streetup/assets/static/assets/road_signs/2025/F71.svg new file mode 100644 index 0000000..c249115 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F71.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F73.svg b/streetup/assets/static/assets/road_signs/2025/F73.svg new file mode 100644 index 0000000..f3d4ea7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F73.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F75.svg b/streetup/assets/static/assets/road_signs/2025/F75.svg new file mode 100644 index 0000000..60ec065 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F75.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F77.svg b/streetup/assets/static/assets/road_signs/2025/F77.svg new file mode 100644 index 0000000..62dd877 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F77.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F79.svg b/streetup/assets/static/assets/road_signs/2025/F79.svg new file mode 100644 index 0000000..653620e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F79.svg @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F8.svg b/streetup/assets/static/assets/road_signs/2025/F8.svg new file mode 100644 index 0000000..4caa6a4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F8.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F81.svg b/streetup/assets/static/assets/road_signs/2025/F81.svg new file mode 100644 index 0000000..933ad0c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F81.svg @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F83.svg b/streetup/assets/static/assets/road_signs/2025/F83.svg new file mode 100644 index 0000000..c71002d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F83.svg @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F85.svg b/streetup/assets/static/assets/road_signs/2025/F85.svg new file mode 100644 index 0000000..64e7b2a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F85.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F87.svg b/streetup/assets/static/assets/road_signs/2025/F87.svg new file mode 100644 index 0000000..d79986a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F87.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F89.svg b/streetup/assets/static/assets/road_signs/2025/F89.svg new file mode 100644 index 0000000..a521562 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F89.svg @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F9.svg b/streetup/assets/static/assets/road_signs/2025/F9.svg new file mode 100644 index 0000000..981855e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F9.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F91.svg b/streetup/assets/static/assets/road_signs/2025/F91.svg new file mode 100644 index 0000000..e4f6342 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F91.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F93.svg b/streetup/assets/static/assets/road_signs/2025/F93.svg new file mode 100644 index 0000000..afc91d0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F93.svg @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F95.svg b/streetup/assets/static/assets/road_signs/2025/F95.svg new file mode 100644 index 0000000..0aaebbe --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F95.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F97.svg b/streetup/assets/static/assets/road_signs/2025/F97.svg new file mode 100644 index 0000000..2915a97 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F97.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F97A.png b/streetup/assets/static/assets/road_signs/2025/F97A.png new file mode 100644 index 0000000..0723e96 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/F97A.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/F97_3-2.svg b/streetup/assets/static/assets/road_signs/2025/F97_3-2.svg new file mode 100644 index 0000000..7408e6f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F97_3-2.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F98.svg b/streetup/assets/static/assets/road_signs/2025/F98.svg new file mode 100644 index 0000000..7934670 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F98.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F99A.svg b/streetup/assets/static/assets/road_signs/2025/F99A.svg new file mode 100644 index 0000000..dcc351d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F99A.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F99B.svg b/streetup/assets/static/assets/road_signs/2025/F99B.svg new file mode 100644 index 0000000..ff54eba --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F99B.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/F99C.svg b/streetup/assets/static/assets/road_signs/2025/F99C.svg new file mode 100644 index 0000000..0ac5440 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/F99C.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/FXX.png b/streetup/assets/static/assets/road_signs/2025/FXX.png new file mode 100644 index 0000000..b7f00b4 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/FXX.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GF98.svg b/streetup/assets/static/assets/road_signs/2025/GF98.svg new file mode 100644 index 0000000..60fd3c9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GF98.svg @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIA_150.svg b/streetup/assets/static/assets/road_signs/2025/GIA_150.svg new file mode 100644 index 0000000..46af753 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIA_150.svg @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIB_150.svg b/streetup/assets/static/assets/road_signs/2025/GIB_150.svg new file mode 100644 index 0000000..7cb1d7c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIB_150.svg @@ -0,0 +1,977 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_AQUAPLANING.svg b/streetup/assets/static/assets/road_signs/2025/GIII_AQUAPLANING.svg new file mode 100644 index 0000000..1dacdd1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_AQUAPLANING.svg @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_BIJ_NAT_WEGDEK.svg b/streetup/assets/static/assets/road_signs/2025/GIII_BIJ_NAT_WEGDEK.svg new file mode 100644 index 0000000..7e99626 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_BIJ_NAT_WEGDEK.svg @@ -0,0 +1,1367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_FABRIEK.svg b/streetup/assets/static/assets/road_signs/2025/GIII_FABRIEK.svg new file mode 100644 index 0000000..6116aa6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_FABRIEK.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_IJZEL.svg b/streetup/assets/static/assets/road_signs/2025/GIII_IJZEL.svg new file mode 100644 index 0000000..b368fa4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_IJZEL.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_LANGSGROEVEN.svg b/streetup/assets/static/assets/road_signs/2025/GIII_LANGSGROEVEN.svg new file mode 100644 index 0000000..f97ea29 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_LANGSGROEVEN.svg @@ -0,0 +1,835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_MIST.svg b/streetup/assets/static/assets/road_signs/2025/GIII_MIST.svg new file mode 100644 index 0000000..f39c70d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_MIST.svg @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_SPOORVORMING.svg b/streetup/assets/static/assets/road_signs/2025/GIII_SPOORVORMING.svg new file mode 100644 index 0000000..404981d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_SPOORVORMING.svg @@ -0,0 +1,1403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_STEENGROEVE.svg b/streetup/assets/static/assets/road_signs/2025/GIII_STEENGROEVE.svg new file mode 100644 index 0000000..355bac4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_STEENGROEVE.svg @@ -0,0 +1,1238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_UITRIT_VRACHTWAGENS.svg b/streetup/assets/static/assets/road_signs/2025/GIII_UITRIT_VRACHTWAGENS.svg new file mode 100644 index 0000000..8a88242 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_UITRIT_VRACHTWAGENS.svg @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_VERVORMD_WEGDEK.svg b/streetup/assets/static/assets/road_signs/2025/GIII_VERVORMD_WEGDEK.svg new file mode 100644 index 0000000..37d9814 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_VERVORMD_WEGDEK.svg @@ -0,0 +1,1571 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIII_ZACHTE_BERM.svg b/streetup/assets/static/assets/road_signs/2025/GIII_ZACHTE_BERM.svg new file mode 100644 index 0000000..a96de97 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIII_ZACHTE_BERM.svg @@ -0,0 +1,1181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GII_10.svg b/streetup/assets/static/assets/road_signs/2025/GII_10.svg new file mode 100644 index 0000000..c52591d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GII_10.svg @@ -0,0 +1,631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_-5T.svg b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_-5T.svg new file mode 100644 index 0000000..beaff55 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_-5T.svg @@ -0,0 +1,1109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_BUS.svg b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_BUS.svg new file mode 100644 index 0000000..827b43e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_BUS.svg @@ -0,0 +1,1164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_FIETSERS.svg b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_FIETSERS.svg new file mode 100644 index 0000000..9196fd2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_FIETSERS.svg @@ -0,0 +1,829 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_PLAATSELIJK_VERKEER.svg b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_PLAATSELIJK_VERKEER.svg new file mode 100644 index 0000000..116f056 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIV_UITGEZONDERD_PLAATSELIJK_VERKEER.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/GIX.svg b/streetup/assets/static/assets/road_signs/2025/GIX.svg new file mode 100644 index 0000000..f722fc8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIX.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIX_2-VAKS.png b/streetup/assets/static/assets/road_signs/2025/GIX_2-VAKS.png new file mode 100644 index 0000000..5ec4539 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GIX_2-VAKS.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GIX_3-2.svg b/streetup/assets/static/assets/road_signs/2025/GIX_3-2.svg new file mode 100644 index 0000000..208f4e2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GIX_3-2.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GIX_3-VAKS.png b/streetup/assets/static/assets/road_signs/2025/GIX_3-VAKS.png new file mode 100644 index 0000000..ad4a73e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GIX_3-VAKS.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVI.svg b/streetup/assets/static/assets/road_signs/2025/GVI.svg new file mode 100644 index 0000000..c79e8b5 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVI.svg @@ -0,0 +1,1052 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIIA_+2T.svg b/streetup/assets/static/assets/road_signs/2025/GVIIA_+2T.svg new file mode 100644 index 0000000..c7ddc61 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIIA_+2T.svg @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIIB.svg b/streetup/assets/static/assets/road_signs/2025/GVIIB.svg new file mode 100644 index 0000000..461898b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIIB.svg @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIIB_UITGEZONDERD_BEWONERS.png b/streetup/assets/static/assets/road_signs/2025/GVIIB_UITGEZONDERD_BEWONERS.png new file mode 100644 index 0000000..0d3e2f2 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVIIB_UITGEZONDERD_BEWONERS.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVIIC_30MIN.svg b/streetup/assets/static/assets/road_signs/2025/GVIIC_30MIN.svg new file mode 100644 index 0000000..0ecf266 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIIC_30MIN.svg @@ -0,0 +1,755 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIIC_30_MIN.png b/streetup/assets/static/assets/road_signs/2025/GVIIC_30_MIN.png new file mode 100644 index 0000000..07c7be6 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVIIC_30_MIN.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_CARPOOL.svg b/streetup/assets/static/assets/road_signs/2025/GVIID_CARPOOL.svg new file mode 100644 index 0000000..60f02a8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIID_CARPOOL.svg @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_PERSONENWAGEN.png b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_PERSONENWAGEN.png new file mode 100644 index 0000000..6665388 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_PERSONENWAGEN.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_PERSONENWAGENS.svg b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_PERSONENWAGENS.svg new file mode 100644 index 0000000..3eecef6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_PERSONENWAGENS.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_WAGENS.svg b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_WAGENS.svg new file mode 100644 index 0000000..3d969df --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCHE_WAGENS.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCH_VOERTUIG.png b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCH_VOERTUIG.png new file mode 100644 index 0000000..f5bd0f7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVIID_ELEKTRISCH_VOERTUIG.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_GEHANDICAPT.png b/streetup/assets/static/assets/road_signs/2025/GVIID_GEHANDICAPT.png new file mode 100644 index 0000000..5676617 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVIID_GEHANDICAPT.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_HANDICAP.svg b/streetup/assets/static/assets/road_signs/2025/GVIID_HANDICAP.svg new file mode 100644 index 0000000..4216364 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIID_HANDICAP.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_P+R.svg b/streetup/assets/static/assets/road_signs/2025/GVIID_P+R.svg new file mode 100644 index 0000000..dd4ca2e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIID_P+R.svg @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIID_TAXIS.svg b/streetup/assets/static/assets/road_signs/2025/GVIID_TAXIS.svg new file mode 100644 index 0000000..c65755f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIID_TAXIS.svg @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVIII.svg b/streetup/assets/static/assets/road_signs/2025/GVIII.svg new file mode 100644 index 0000000..a280c31 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GVIII.svg @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GVII_BETALEND.png b/streetup/assets/static/assets/road_signs/2025/GVII_BETALEND.png new file mode 100644 index 0000000..b8880a3 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVII_BETALEND.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVII_BETALEND_UITGEZONDERD_BEWONERS.png b/streetup/assets/static/assets/road_signs/2025/GVII_BETALEND_UITGEZONDERD_BEWONERS.png new file mode 100644 index 0000000..1a35aff Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVII_BETALEND_UITGEZONDERD_BEWONERS.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GVI_HERHALING.png b/streetup/assets/static/assets/road_signs/2025/GVI_HERHALING.png new file mode 100644 index 0000000..191311b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GVI_HERHALING.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GV_VAN_7_TOT_19U.svg b/streetup/assets/static/assets/road_signs/2025/GV_VAN_7_TOT_19U.svg new file mode 100644 index 0000000..b64c642 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GV_VAN_7_TOT_19U.svg @@ -0,0 +1,1163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GV_VAN_7_TOT_19_U.png b/streetup/assets/static/assets/road_signs/2025/GV_VAN_7_TOT_19_U.png new file mode 100644 index 0000000..9a68941 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/GV_VAN_7_TOT_19_U.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/GV_VAN_MAANDAG_TOT_VRIJDAG.svg b/streetup/assets/static/assets/road_signs/2025/GV_VAN_MAANDAG_TOT_VRIJDAG.svg new file mode 100644 index 0000000..1c69222 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GV_VAN_MAANDAG_TOT_VRIJDAG.svg @@ -0,0 +1,2220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GXA.svg b/streetup/assets/static/assets/road_signs/2025/GXA.svg new file mode 100644 index 0000000..af52b9f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXA.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GXB.svg b/streetup/assets/static/assets/road_signs/2025/GXB.svg new file mode 100644 index 0000000..d4d5707 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXB.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GXC.svg b/streetup/assets/static/assets/road_signs/2025/GXC.svg new file mode 100644 index 0000000..253f15b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXC.svg @@ -0,0 +1,436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GXD.svg b/streetup/assets/static/assets/road_signs/2025/GXD.svg new file mode 100644 index 0000000..a835f5a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXD.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GXI.svg b/streetup/assets/static/assets/road_signs/2025/GXI.svg new file mode 100644 index 0000000..7410ff9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXI.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/GXII_B.svg b/streetup/assets/static/assets/road_signs/2025/GXII_B.svg new file mode 100644 index 0000000..9b85520 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXII_B.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/GXII_C.svg b/streetup/assets/static/assets/road_signs/2025/GXII_C.svg new file mode 100644 index 0000000..2cd00ee --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXII_C.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/GXII_D.svg b/streetup/assets/static/assets/road_signs/2025/GXII_D.svg new file mode 100644 index 0000000..c5e7436 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXII_D.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/GXII_E.svg b/streetup/assets/static/assets/road_signs/2025/GXII_E.svg new file mode 100644 index 0000000..10d64fd --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/GXII_E.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/ICC.png b/streetup/assets/static/assets/road_signs/2025/ICC.png new file mode 100644 index 0000000..3982df9 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ICC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ICR.png b/streetup/assets/static/assets/road_signs/2025/ICR.png new file mode 100644 index 0000000..de09a7e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ICR.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/IF2.png b/streetup/assets/static/assets/road_signs/2025/IF2.png new file mode 100644 index 0000000..08d80f1 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/IF2.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/IR10B.png b/streetup/assets/static/assets/road_signs/2025/IR10B.png new file mode 100644 index 0000000..2953113 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/IR10B.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/IR11.png b/streetup/assets/static/assets/road_signs/2025/IR11.png new file mode 100644 index 0000000..da22aa7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/IR11.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/IVMS1.png b/streetup/assets/static/assets/road_signs/2025/IVMS1.png new file mode 100644 index 0000000..6b10fce Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/IVMS1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/KM.png b/streetup/assets/static/assets/road_signs/2025/KM.png new file mode 100644 index 0000000..936b9c0 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/KM.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/M1.svg b/streetup/assets/static/assets/road_signs/2025/M1.svg new file mode 100644 index 0000000..4ac68f5 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M1.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M10.svg b/streetup/assets/static/assets/road_signs/2025/M10.svg new file mode 100644 index 0000000..90e5ecc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M10.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M11.svg b/streetup/assets/static/assets/road_signs/2025/M11.svg new file mode 100644 index 0000000..4801928 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M11.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M12.svg b/streetup/assets/static/assets/road_signs/2025/M12.svg new file mode 100644 index 0000000..26323bb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M12.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M13.svg b/streetup/assets/static/assets/road_signs/2025/M13.svg new file mode 100644 index 0000000..e91d9e5 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M13.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M14.svg b/streetup/assets/static/assets/road_signs/2025/M14.svg new file mode 100644 index 0000000..a97971d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M14.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M15.svg b/streetup/assets/static/assets/road_signs/2025/M15.svg new file mode 100644 index 0000000..fcbf4c4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M15.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M16.svg b/streetup/assets/static/assets/road_signs/2025/M16.svg new file mode 100644 index 0000000..4842bcb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M16.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M17.svg b/streetup/assets/static/assets/road_signs/2025/M17.svg new file mode 100644 index 0000000..1d4d259 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M17.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M18.svg b/streetup/assets/static/assets/road_signs/2025/M18.svg new file mode 100644 index 0000000..b1bdbc6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M18.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M19.svg b/streetup/assets/static/assets/road_signs/2025/M19.svg new file mode 100644 index 0000000..ae770f6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M19.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M2.svg b/streetup/assets/static/assets/road_signs/2025/M2.svg new file mode 100644 index 0000000..844cf51 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M2.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M20.svg b/streetup/assets/static/assets/road_signs/2025/M20.svg new file mode 100644 index 0000000..4000200 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M20.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M24.png b/streetup/assets/static/assets/road_signs/2025/M24.png new file mode 100644 index 0000000..1b8bb54 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/M24.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/M3.svg b/streetup/assets/static/assets/road_signs/2025/M3.svg new file mode 100644 index 0000000..2e77e5b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M3.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M3BIS.svg b/streetup/assets/static/assets/road_signs/2025/M3BIS.svg new file mode 100644 index 0000000..b12edc3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M3BIS.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/M4.svg b/streetup/assets/static/assets/road_signs/2025/M4.svg new file mode 100644 index 0000000..184c0ee --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M4.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M5.svg b/streetup/assets/static/assets/road_signs/2025/M5.svg new file mode 100644 index 0000000..1ef6dd8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M5.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M5BIS.svg b/streetup/assets/static/assets/road_signs/2025/M5BIS.svg new file mode 100644 index 0000000..493a2a3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M5BIS.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M6.svg b/streetup/assets/static/assets/road_signs/2025/M6.svg new file mode 100644 index 0000000..539a850 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M6.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M7.svg b/streetup/assets/static/assets/road_signs/2025/M7.svg new file mode 100644 index 0000000..1a413ae --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M7.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M8.svg b/streetup/assets/static/assets/road_signs/2025/M8.svg new file mode 100644 index 0000000..1bc733d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M8.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/M9.svg b/streetup/assets/static/assets/road_signs/2025/M9.svg new file mode 100644 index 0000000..cca0bc8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/M9.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/MARK1.png b/streetup/assets/static/assets/road_signs/2025/MARK1.png new file mode 100644 index 0000000..7f4ac0e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK10.png b/streetup/assets/static/assets/road_signs/2025/MARK10.png new file mode 100644 index 0000000..ba9528b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK10.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK12.png b/streetup/assets/static/assets/road_signs/2025/MARK12.png new file mode 100644 index 0000000..344f16b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK12.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK13.png b/streetup/assets/static/assets/road_signs/2025/MARK13.png new file mode 100644 index 0000000..874b050 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK13.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK14.png b/streetup/assets/static/assets/road_signs/2025/MARK14.png new file mode 100644 index 0000000..8f44fb0 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK14.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK3.png b/streetup/assets/static/assets/road_signs/2025/MARK3.png new file mode 100644 index 0000000..3602e47 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK3.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK5.png b/streetup/assets/static/assets/road_signs/2025/MARK5.png new file mode 100644 index 0000000..96c21ba Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK5.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK6.png b/streetup/assets/static/assets/road_signs/2025/MARK6.png new file mode 100644 index 0000000..abac9be Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK6.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK8.png b/streetup/assets/static/assets/road_signs/2025/MARK8.png new file mode 100644 index 0000000..884e10d Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK8.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARK9.png b/streetup/assets/static/assets/road_signs/2025/MARK9.png new file mode 100644 index 0000000..1dd6a63 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARK9.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARKCYCLOM.png b/streetup/assets/static/assets/road_signs/2025/MARKCYCLOM.png new file mode 100644 index 0000000..6850d6d Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARKCYCLOM.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARKM1.png b/streetup/assets/static/assets/road_signs/2025/MARKM1.png new file mode 100644 index 0000000..b5b057d Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARKM1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARKM21.png b/streetup/assets/static/assets/road_signs/2025/MARKM21.png new file mode 100644 index 0000000..59ded81 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARKM21.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARKM22.png b/streetup/assets/static/assets/road_signs/2025/MARKM22.png new file mode 100644 index 0000000..3a07381 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARKM22.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARKM23.png b/streetup/assets/static/assets/road_signs/2025/MARKM23.png new file mode 100644 index 0000000..ff146be Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARKM23.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/MARKM24.png b/streetup/assets/static/assets/road_signs/2025/MARKM24.png new file mode 100644 index 0000000..1b8bb54 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/MARKM24.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/OTHERS.png b/streetup/assets/static/assets/road_signs/2025/OTHERS.png new file mode 100644 index 0000000..49f3dbe Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/OTHERS.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/S1.svg b/streetup/assets/static/assets/road_signs/2025/S1.svg new file mode 100644 index 0000000..ee8cc40 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S1.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S10.svg b/streetup/assets/static/assets/road_signs/2025/S10.svg new file mode 100644 index 0000000..d84c604 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S10.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S11.svg b/streetup/assets/static/assets/road_signs/2025/S11.svg new file mode 100644 index 0000000..c670777 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S11.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S12.svg b/streetup/assets/static/assets/road_signs/2025/S12.svg new file mode 100644 index 0000000..84d8d73 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S12.svg @@ -0,0 +1,76 @@ + + + +image/svg+xml + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/S13.svg b/streetup/assets/static/assets/road_signs/2025/S13.svg new file mode 100644 index 0000000..bafaff4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S13.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S14.svg b/streetup/assets/static/assets/road_signs/2025/S14.svg new file mode 100644 index 0000000..f9834d6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S14.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S15.svg b/streetup/assets/static/assets/road_signs/2025/S15.svg new file mode 100644 index 0000000..ba429d0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S15.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S16.svg b/streetup/assets/static/assets/road_signs/2025/S16.svg new file mode 100644 index 0000000..bc57d9e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S16.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S17.svg b/streetup/assets/static/assets/road_signs/2025/S17.svg new file mode 100644 index 0000000..2e5ceca --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S17.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S18.svg b/streetup/assets/static/assets/road_signs/2025/S18.svg new file mode 100644 index 0000000..4211a42 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S18.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S19.svg b/streetup/assets/static/assets/road_signs/2025/S19.svg new file mode 100644 index 0000000..c2e1e26 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S19.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S2.svg b/streetup/assets/static/assets/road_signs/2025/S2.svg new file mode 100644 index 0000000..874a5b4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S2.svg @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S20.svg b/streetup/assets/static/assets/road_signs/2025/S20.svg new file mode 100644 index 0000000..68ba0b2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S20.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S21.svg b/streetup/assets/static/assets/road_signs/2025/S21.svg new file mode 100644 index 0000000..4002ab6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S21.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S3.svg b/streetup/assets/static/assets/road_signs/2025/S3.svg new file mode 100644 index 0000000..6757af9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S3.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S30.svg b/streetup/assets/static/assets/road_signs/2025/S30.svg new file mode 100644 index 0000000..3747a64 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S30.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S31.svg b/streetup/assets/static/assets/road_signs/2025/S31.svg new file mode 100644 index 0000000..4f1a14b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S31.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S32.svg b/streetup/assets/static/assets/road_signs/2025/S32.svg new file mode 100644 index 0000000..bfbb897 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S32.svg @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S33.svg b/streetup/assets/static/assets/road_signs/2025/S33.svg new file mode 100644 index 0000000..ce5fda9 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S33.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S34.svg b/streetup/assets/static/assets/road_signs/2025/S34.svg new file mode 100644 index 0000000..abb892f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S34.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S35.svg b/streetup/assets/static/assets/road_signs/2025/S35.svg new file mode 100644 index 0000000..93b0285 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S35.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/S36.svg b/streetup/assets/static/assets/road_signs/2025/S36.svg new file mode 100644 index 0000000..3b951e5 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S36.svg @@ -0,0 +1,189 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/S4.svg b/streetup/assets/static/assets/road_signs/2025/S4.svg new file mode 100644 index 0000000..a363086 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S4.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/S5.svg b/streetup/assets/static/assets/road_signs/2025/S5.svg new file mode 100644 index 0000000..7ac6c08 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/S5.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE0.png b/streetup/assets/static/assets/road_signs/2025/TYPE0.png new file mode 100644 index 0000000..b8fbfa7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE0.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE0B.png b/streetup/assets/static/assets/road_signs/2025/TYPE0B.png new file mode 100644 index 0000000..599b4ae Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE0B.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE1A1.png b/streetup/assets/static/assets/road_signs/2025/TYPE1A1.png new file mode 100644 index 0000000..2ce16f4 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE1A1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE1A2.png b/streetup/assets/static/assets/road_signs/2025/TYPE1A2.png new file mode 100644 index 0000000..fd89d5b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE1A2.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE1B1.png b/streetup/assets/static/assets/road_signs/2025/TYPE1B1.png new file mode 100644 index 0000000..188c8b7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE1B1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE1B2.png b/streetup/assets/static/assets/road_signs/2025/TYPE1B2.png new file mode 100644 index 0000000..4e6de66 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE1B2.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE1C1.png b/streetup/assets/static/assets/road_signs/2025/TYPE1C1.png new file mode 100644 index 0000000..2f9c27f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE1C1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPE5.png b/streetup/assets/static/assets/road_signs/2025/TYPE5.png new file mode 100644 index 0000000..f49f1b5 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPE5.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIA_200M.png b/streetup/assets/static/assets/road_signs/2025/TYPEIA_200M.png new file mode 100644 index 0000000..be54c4d Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIA_200M.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIA_300M.png b/streetup/assets/static/assets/road_signs/2025/TYPEIA_300M.png new file mode 100644 index 0000000..b7e5349 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIA_300M.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIA_50M.png b/streetup/assets/static/assets/road_signs/2025/TYPEIA_50M.png new file mode 100644 index 0000000..e268f9e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIA_50M.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIA_GEN.png b/streetup/assets/static/assets/road_signs/2025/TYPEIA_GEN.png new file mode 100644 index 0000000..107b7b7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIA_GEN.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIB.png b/streetup/assets/static/assets/road_signs/2025/TYPEIB.png new file mode 100644 index 0000000..9a29245 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIIIA.png b/streetup/assets/static/assets/road_signs/2025/TYPEIIIA.png new file mode 100644 index 0000000..896fb2f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIIIA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIIIAC.png b/streetup/assets/static/assets/road_signs/2025/TYPEIIIAC.png new file mode 100644 index 0000000..3fee076 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIIIAC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIIIB.png b/streetup/assets/static/assets/road_signs/2025/TYPEIIIB.png new file mode 100644 index 0000000..c520522 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIIIB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEII_GEN.png b/streetup/assets/static/assets/road_signs/2025/TYPEII_GEN.png new file mode 100644 index 0000000..09b7ac2 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEII_GEN.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIV.png b/streetup/assets/static/assets/road_signs/2025/TYPEIV.png new file mode 100644 index 0000000..dc42f44 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIV.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIVA.png b/streetup/assets/static/assets/road_signs/2025/TYPEIVA.png new file mode 100644 index 0000000..84d8621 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIVA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIVD.png b/streetup/assets/static/assets/road_signs/2025/TYPEIVD.png new file mode 100644 index 0000000..21b0cbb Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIVD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIVE.png b/streetup/assets/static/assets/road_signs/2025/TYPEIVE.png new file mode 100644 index 0000000..1a85321 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIVE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIVF.png b/streetup/assets/static/assets/road_signs/2025/TYPEIVF.png new file mode 100644 index 0000000..139e134 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIVF.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEIXB.png b/streetup/assets/static/assets/road_signs/2025/TYPEIXB.png new file mode 100644 index 0000000..a19597a Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEIXB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVA.png b/streetup/assets/static/assets/road_signs/2025/TYPEVA.png new file mode 100644 index 0000000..c9a7fcb Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVB.png b/streetup/assets/static/assets/road_signs/2025/TYPEVB.png new file mode 100644 index 0000000..2286774 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVC.png b/streetup/assets/static/assets/road_signs/2025/TYPEVC.png new file mode 100644 index 0000000..a5467a2 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVD.png b/streetup/assets/static/assets/road_signs/2025/TYPEVD.png new file mode 100644 index 0000000..01941d7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVE.png b/streetup/assets/static/assets/road_signs/2025/TYPEVE.png new file mode 100644 index 0000000..6916d02 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVF.png b/streetup/assets/static/assets/road_signs/2025/TYPEVF.png new file mode 100644 index 0000000..8a6b3b6 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVF.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVG.png b/streetup/assets/static/assets/road_signs/2025/TYPEVG.png new file mode 100644 index 0000000..abe13a7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVG.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVI.png b/streetup/assets/static/assets/road_signs/2025/TYPEVI.png new file mode 100644 index 0000000..d4c2206 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVI.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)2,5T.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)2,5T.png new file mode 100644 index 0000000..d4c07c4 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)2,5T.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)2T.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)2T.png new file mode 100644 index 0000000..6d49587 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)2T.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)3,5T.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)3,5T.png new file mode 100644 index 0000000..0e0787c Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)3,5T.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)7,5T.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)7,5T.png new file mode 100644 index 0000000..44ed6f8 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(+)7,5T.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(-)3,5T.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(-)3,5T.png new file mode 100644 index 0000000..8c839bb Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_(-)3,5T.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_3,5TMAX.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_3,5TMAX.png new file mode 100644 index 0000000..23f3f42 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_3,5TMAX.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_5TMAX.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_5TMAX.png new file mode 100644 index 0000000..6f85ba9 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIA_5TMAX.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBD.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBD.png new file mode 100644 index 0000000..2c681b0 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDA.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDA.png new file mode 100644 index 0000000..b501c30 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDB.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDB.png new file mode 100644 index 0000000..f7bfd41 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDD.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDD.png new file mode 100644 index 0000000..3960f78 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDE.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDE.png new file mode 100644 index 0000000..abc1bdf Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBDE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBE.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBE.png new file mode 100644 index 0000000..b115e51 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIBEA.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBEA.png new file mode 100644 index 0000000..504087e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIBEA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_15.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_15.png new file mode 100644 index 0000000..2ca2239 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_15.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_20.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_20.png new file mode 100644 index 0000000..ff0eaaf Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_20.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_30.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_30.png new file mode 100644 index 0000000..e3fb9d5 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_30.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_5.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_5.png new file mode 100644 index 0000000..b4863ad Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIC_5.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIID.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIID.png new file mode 100644 index 0000000..0021cdd Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIID.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDA.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDA.png new file mode 100644 index 0000000..234a4e4 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDB.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDB.png new file mode 100644 index 0000000..342bbb9 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDC.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDC.png new file mode 100644 index 0000000..ce978d5 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDD.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDD.png new file mode 100644 index 0000000..a06bdef Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDE.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDE.png new file mode 100644 index 0000000..c109995 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDF.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDF.png new file mode 100644 index 0000000..c4b28eb Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDF.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIIDG.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDG.png new file mode 100644 index 0000000..c4ff028 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIIDG.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEVIII.png b/streetup/assets/static/assets/road_signs/2025/TYPEVIII.png new file mode 100644 index 0000000..3f04e95 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEVIII.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXI.png b/streetup/assets/static/assets/road_signs/2025/TYPEXI.png new file mode 100644 index 0000000..741e938 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXI.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIIB.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIIB.png new file mode 100644 index 0000000..b009f97 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIIB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIIC.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIIC.png new file mode 100644 index 0000000..17fc559 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIIC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIIE.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIIE.png new file mode 100644 index 0000000..b929163 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIIE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIIIA.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIIIA.png new file mode 100644 index 0000000..1261ff2 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIIIA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIIIB.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIIIB.png new file mode 100644 index 0000000..83da906 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIIIB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIIIC.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIIIC.png new file mode 100644 index 0000000..65fe703 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIIIC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIVA.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIVA.png new file mode 100644 index 0000000..12f8167 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIVA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIVB.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIVB.png new file mode 100644 index 0000000..624248f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIVB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIVC.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIVC.png new file mode 100644 index 0000000..73ab643 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIVC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIVD.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIVD.png new file mode 100644 index 0000000..2928dfd Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIVD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIVE.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIVE.png new file mode 100644 index 0000000..1cc1ce7 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIVE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXIVF.png b/streetup/assets/static/assets/road_signs/2025/TYPEXIVF.png new file mode 100644 index 0000000..5857e20 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXIVF.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXVA.png b/streetup/assets/static/assets/road_signs/2025/TYPEXVA.png new file mode 100644 index 0000000..8edcc35 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXVA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/TYPEXVC.png b/streetup/assets/static/assets/road_signs/2025/TYPEXVC.png new file mode 100644 index 0000000..8c7bc4e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/TYPEXVC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XA.png b/streetup/assets/static/assets/road_signs/2025/XA.png new file mode 100644 index 0000000..edf288f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XA.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XB.png b/streetup/assets/static/assets/road_signs/2025/XB.png new file mode 100644 index 0000000..9913147 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XC.png b/streetup/assets/static/assets/road_signs/2025/XC.png new file mode 100644 index 0000000..e727951 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XC.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XC12.png b/streetup/assets/static/assets/road_signs/2025/XC12.png new file mode 100644 index 0000000..d3cb6b5 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XC12.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XC18.png b/streetup/assets/static/assets/road_signs/2025/XC18.png new file mode 100644 index 0000000..d27820e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XC18.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XC6.png b/streetup/assets/static/assets/road_signs/2025/XC6.png new file mode 100644 index 0000000..7b484e0 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XC6.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XD.png b/streetup/assets/static/assets/road_signs/2025/XD.png new file mode 100644 index 0000000..208573e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XD.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XE.png b/streetup/assets/static/assets/road_signs/2025/XE.png new file mode 100644 index 0000000..5028088 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/XF.png b/streetup/assets/static/assets/road_signs/2025/XF.png new file mode 100644 index 0000000..3112a56 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/XF.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZC43.png b/streetup/assets/static/assets/road_signs/2025/ZC43.png new file mode 100644 index 0000000..0cc537b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZC43.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZC45.png b/streetup/assets/static/assets/road_signs/2025/ZC45.png new file mode 100644 index 0000000..8392764 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZC45.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE1.png b/streetup/assets/static/assets/road_signs/2025/ZE1.png new file mode 100644 index 0000000..e9c27d3 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE1.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE3.png b/streetup/assets/static/assets/road_signs/2025/ZE3.png new file mode 100644 index 0000000..d9e8f0b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE3.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A.svg b/streetup/assets/static/assets/road_signs/2025/ZE9A.svg new file mode 100644 index 0000000..70d0727 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/2025/ZE9A.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAXPB'.png b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAXPB'.png new file mode 100644 index 0000000..b9f482f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAXPB'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAXPB.png b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAXPB.png new file mode 100644 index 0000000..1887fe2 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAXPB.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAX_DISK'.png b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAX_DISK'.png new file mode 100644 index 0000000..266f02e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAX_DISK'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAX_DISK.png b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAX_DISK.png new file mode 100644 index 0000000..056dc62 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9A3T5MAX_DISK.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9AE'.png b/streetup/assets/static/assets/road_signs/2025/ZE9AE'.png new file mode 100644 index 0000000..7d7c303 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9AE'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9AE.png b/streetup/assets/static/assets/road_signs/2025/ZE9AE.png new file mode 100644 index 0000000..cd6412e Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9AE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9AG'.png b/streetup/assets/static/assets/road_signs/2025/ZE9AG'.png new file mode 100644 index 0000000..e66af68 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9AG'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9AG.png b/streetup/assets/static/assets/road_signs/2025/ZE9AG.png new file mode 100644 index 0000000..45084cc Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9AG.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A_DISK'.png b/streetup/assets/static/assets/road_signs/2025/ZE9A_DISK'.png new file mode 100644 index 0000000..7007ea0 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9A_DISK'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9A_DISK.png b/streetup/assets/static/assets/road_signs/2025/ZE9A_DISK.png new file mode 100644 index 0000000..d8f2f6b Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9A_DISK.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9B.png b/streetup/assets/static/assets/road_signs/2025/ZE9B.png new file mode 100644 index 0000000..1b4e56c Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9B.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9BE'.png b/streetup/assets/static/assets/road_signs/2025/ZE9BE'.png new file mode 100644 index 0000000..917f076 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9BE'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9BE.png b/streetup/assets/static/assets/road_signs/2025/ZE9BE.png new file mode 100644 index 0000000..ab4a59a Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9BE.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9BG'.png b/streetup/assets/static/assets/road_signs/2025/ZE9BG'.png new file mode 100644 index 0000000..d89f7c9 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9BG'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9BG.png b/streetup/assets/static/assets/road_signs/2025/ZE9BG.png new file mode 100644 index 0000000..2ee58ef Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9BG.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9B_DISK'.png b/streetup/assets/static/assets/road_signs/2025/ZE9B_DISK'.png new file mode 100644 index 0000000..96bcc64 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9B_DISK'.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZE9B_DISK.png b/streetup/assets/static/assets/road_signs/2025/ZE9B_DISK.png new file mode 100644 index 0000000..d3ab53f Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZE9B_DISK.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZF111.png b/streetup/assets/static/assets/road_signs/2025/ZF111.png new file mode 100644 index 0000000..755ddcf Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZF111.png differ diff --git a/streetup/assets/static/assets/road_signs/2025/ZF113.png b/streetup/assets/static/assets/road_signs/2025/ZF113.png new file mode 100644 index 0000000..a40ca13 Binary files /dev/null and b/streetup/assets/static/assets/road_signs/2025/ZF113.png differ diff --git a/streetup/assets/static/assets/road_signs/A/A11.svg b/streetup/assets/static/assets/road_signs/A/A11.svg new file mode 100644 index 0000000..5624d6c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A11.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A13.svg b/streetup/assets/static/assets/road_signs/A/A13.svg new file mode 100644 index 0000000..3279bf7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A13.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A14.svg b/streetup/assets/static/assets/road_signs/A/A14.svg new file mode 100644 index 0000000..437f0aa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A14.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A15.svg b/streetup/assets/static/assets/road_signs/A/A15.svg new file mode 100644 index 0000000..04e6a1b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A15.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A16.svg b/streetup/assets/static/assets/road_signs/A/A16.svg new file mode 100644 index 0000000..15c9ab6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A16.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A17.svg b/streetup/assets/static/assets/road_signs/A/A17.svg new file mode 100644 index 0000000..b5a61ee --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A17.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A18.svg b/streetup/assets/static/assets/road_signs/A/A18.svg new file mode 100644 index 0000000..6843002 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A18.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A19.svg b/streetup/assets/static/assets/road_signs/A/A19.svg new file mode 100644 index 0000000..7392d1e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A19.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A1a.svg b/streetup/assets/static/assets/road_signs/A/A1a.svg new file mode 100644 index 0000000..0213b7c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A1a.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A1b.svg b/streetup/assets/static/assets/road_signs/A/A1b.svg new file mode 100644 index 0000000..d497438 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A1b.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A1c.svg b/streetup/assets/static/assets/road_signs/A/A1c.svg new file mode 100644 index 0000000..d48040e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A1c.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A1d.svg b/streetup/assets/static/assets/road_signs/A/A1d.svg new file mode 100644 index 0000000..0bc065a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A1d.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A21.svg b/streetup/assets/static/assets/road_signs/A/A21.svg new file mode 100644 index 0000000..5061916 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A21.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A23.svg b/streetup/assets/static/assets/road_signs/A/A23.svg new file mode 100644 index 0000000..9725c48 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A23.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A24.svg b/streetup/assets/static/assets/road_signs/A/A24.svg new file mode 100644 index 0000000..71457b2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A24.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A25.svg b/streetup/assets/static/assets/road_signs/A/A25.svg new file mode 100644 index 0000000..89d5a46 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A25.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A27v.svg b/streetup/assets/static/assets/road_signs/A/A27v.svg new file mode 100644 index 0000000..49a661a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A27v.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A27w.svg b/streetup/assets/static/assets/road_signs/A/A27w.svg new file mode 100644 index 0000000..33e0bde --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A27w.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A3.svg b/streetup/assets/static/assets/road_signs/A/A3.svg new file mode 100644 index 0000000..4fb9965 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A3.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + 10% + + diff --git a/streetup/assets/static/assets/road_signs/A/A31.svg b/streetup/assets/static/assets/road_signs/A/A31.svg new file mode 100644 index 0000000..2e44966 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A31.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A32.svg b/streetup/assets/static/assets/road_signs/A/A32.svg new file mode 100644 index 0000000..bb2ebf2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A32.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A33.svg b/streetup/assets/static/assets/road_signs/A/A33.svg new file mode 100644 index 0000000..f92bb34 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A33.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A34.svg b/streetup/assets/static/assets/road_signs/A/A34.svg new file mode 100644 index 0000000..32fc6b7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A34.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A35.svg b/streetup/assets/static/assets/road_signs/A/A35.svg new file mode 100644 index 0000000..7c1a9d1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A35.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A37.svg b/streetup/assets/static/assets/road_signs/A/A37.svg new file mode 100644 index 0000000..c8a8400 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A37.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A39.svg b/streetup/assets/static/assets/road_signs/A/A39.svg new file mode 100644 index 0000000..fd90e4d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A39.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A41.svg b/streetup/assets/static/assets/road_signs/A/A41.svg new file mode 100644 index 0000000..4e9d4fd --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A41.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A43.svg b/streetup/assets/static/assets/road_signs/A/A43.svg new file mode 100644 index 0000000..c7eb6c1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A43.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A45.svg b/streetup/assets/static/assets/road_signs/A/A45.svg new file mode 100644 index 0000000..fa08e0f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A45.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A47.svg b/streetup/assets/static/assets/road_signs/A/A47.svg new file mode 100644 index 0000000..a5ba65b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A47.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A49.svg b/streetup/assets/static/assets/road_signs/A/A49.svg new file mode 100644 index 0000000..525eb84 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A49.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A5.svg b/streetup/assets/static/assets/road_signs/A/A5.svg new file mode 100644 index 0000000..65ef0f0 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A5.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + 10% + + diff --git a/streetup/assets/static/assets/road_signs/A/A51.svg b/streetup/assets/static/assets/road_signs/A/A51.svg new file mode 100644 index 0000000..f7e1078 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A51.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A52.svg b/streetup/assets/static/assets/road_signs/A/A52.svg new file mode 100644 index 0000000..2839503 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A52.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A53.svg b/streetup/assets/static/assets/road_signs/A/A53.svg new file mode 100644 index 0000000..7df5849 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A53.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A7a.svg b/streetup/assets/static/assets/road_signs/A/A7a.svg new file mode 100644 index 0000000..077c551 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A7a.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A7b.svg b/streetup/assets/static/assets/road_signs/A/A7b.svg new file mode 100644 index 0000000..239ce67 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A7b.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A7c.svg b/streetup/assets/static/assets/road_signs/A/A7c.svg new file mode 100644 index 0000000..efff787 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A7c.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A8.svg b/streetup/assets/static/assets/road_signs/A/A8.svg new file mode 100644 index 0000000..51193c3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A8.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/A/A9.svg b/streetup/assets/static/assets/road_signs/A/A9.svg new file mode 100644 index 0000000..464b587 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/A/A9.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B1.svg b/streetup/assets/static/assets/road_signs/B/B1.svg new file mode 100644 index 0000000..1aca6b8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B1.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B11.svg b/streetup/assets/static/assets/road_signs/B/B11.svg new file mode 100644 index 0000000..7249dbf --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B11.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B15.svg b/streetup/assets/static/assets/road_signs/B/B15.svg new file mode 100644 index 0000000..c5b98ee --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B15.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B17.svg b/streetup/assets/static/assets/road_signs/B/B17.svg new file mode 100644 index 0000000..7f14f43 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B17.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B19.svg b/streetup/assets/static/assets/road_signs/B/B19.svg new file mode 100644 index 0000000..fa14ec3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B19.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B21.svg b/streetup/assets/static/assets/road_signs/B/B21.svg new file mode 100644 index 0000000..35a748a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B21.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B22.svg b/streetup/assets/static/assets/road_signs/B/B22.svg new file mode 100644 index 0000000..4324f73 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B22.svg @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B5.svg b/streetup/assets/static/assets/road_signs/B/B5.svg new file mode 100644 index 0000000..8566760 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B5.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/B/B9.svg b/streetup/assets/static/assets/road_signs/B/B9.svg new file mode 100644 index 0000000..4423beb --- /dev/null +++ b/streetup/assets/static/assets/road_signs/B/B9.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C1.svg b/streetup/assets/static/assets/road_signs/C/C1.svg new file mode 100644 index 0000000..1d29141 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C1.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C11.svg b/streetup/assets/static/assets/road_signs/C/C11.svg new file mode 100644 index 0000000..ac84fa4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C11.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C13.svg b/streetup/assets/static/assets/road_signs/C/C13.svg new file mode 100644 index 0000000..2a86bdf --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C13.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C15.svg b/streetup/assets/static/assets/road_signs/C/C15.svg new file mode 100644 index 0000000..82ed0f1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C15.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C17.svg b/streetup/assets/static/assets/road_signs/C/C17.svg new file mode 100644 index 0000000..57962fa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C17.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C19.svg b/streetup/assets/static/assets/road_signs/C/C19.svg new file mode 100644 index 0000000..7f503f7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C19.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C21,5.svg b/streetup/assets/static/assets/road_signs/C/C21,5.svg new file mode 100644 index 0000000..a494d3f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C21,5.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + 5 + ,5 + t + + diff --git a/streetup/assets/static/assets/road_signs/C/C21.svg b/streetup/assets/static/assets/road_signs/C/C21.svg new file mode 100644 index 0000000..bd59fca --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C21.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + 5 + t + + diff --git a/streetup/assets/static/assets/road_signs/C/C22,5.svg b/streetup/assets/static/assets/road_signs/C/C22,5.svg new file mode 100644 index 0000000..79706a8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C22,5.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + 2 + ,5 + t + + diff --git a/streetup/assets/static/assets/road_signs/C/C22.svg b/streetup/assets/static/assets/road_signs/C/C22.svg new file mode 100644 index 0000000..9df571a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C22.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + 2 + t + + diff --git a/streetup/assets/static/assets/road_signs/C/C23.svg b/streetup/assets/static/assets/road_signs/C/C23.svg new file mode 100644 index 0000000..6ba82b3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C23.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C24.svg b/streetup/assets/static/assets/road_signs/C/C24.svg new file mode 100644 index 0000000..d75ec91 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C24.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C24t.svg b/streetup/assets/static/assets/road_signs/C/C24t.svg new file mode 100644 index 0000000..e31c2df --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C24t.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + 10t + + diff --git a/streetup/assets/static/assets/road_signs/C/C25a.svg b/streetup/assets/static/assets/road_signs/C/C25a.svg new file mode 100644 index 0000000..965523a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C25a.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C25b.svg b/streetup/assets/static/assets/road_signs/C/C25b.svg new file mode 100644 index 0000000..cdde738 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C25b.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C25c.svg b/streetup/assets/static/assets/road_signs/C/C25c.svg new file mode 100644 index 0000000..3a24bce --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C25c.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C26.svg b/streetup/assets/static/assets/road_signs/C/C26.svg new file mode 100644 index 0000000..d2fb7dd --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C26.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + 10 + + m + + diff --git a/streetup/assets/static/assets/road_signs/C/C27,5.svg b/streetup/assets/static/assets/road_signs/C/C27,5.svg new file mode 100644 index 0000000..9b16ec3 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C27,5.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + 2 + ,5 + m + + diff --git a/streetup/assets/static/assets/road_signs/C/C27.svg b/streetup/assets/static/assets/road_signs/C/C27.svg new file mode 100644 index 0000000..10cc480 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C27.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + 2 + m + + diff --git a/streetup/assets/static/assets/road_signs/C/C29,5.svg b/streetup/assets/static/assets/road_signs/C/C29,5.svg new file mode 100644 index 0000000..f91482e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C29,5.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + 2 + ,5 + m + + diff --git a/streetup/assets/static/assets/road_signs/C/C29.svg b/streetup/assets/static/assets/road_signs/C/C29.svg new file mode 100644 index 0000000..3055c83 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C29.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + 2 + m + + diff --git a/streetup/assets/static/assets/road_signs/C/C3.svg b/streetup/assets/static/assets/road_signs/C/C3.svg new file mode 100644 index 0000000..aa46d91 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C3.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C30.svg b/streetup/assets/static/assets/road_signs/C/C30.svg new file mode 100644 index 0000000..5e0a37b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C30.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C31.svg b/streetup/assets/static/assets/road_signs/C/C31.svg new file mode 100644 index 0000000..d620a4c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C31.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C32a.svg b/streetup/assets/static/assets/road_signs/C/C32a.svg new file mode 100644 index 0000000..b2a2e09 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C32a.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C32b.svg b/streetup/assets/static/assets/road_signs/C/C32b.svg new file mode 100644 index 0000000..54e1364 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C32b.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C33.svg b/streetup/assets/static/assets/road_signs/C/C33.svg new file mode 100644 index 0000000..acdd9ec --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C33.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C35.svg b/streetup/assets/static/assets/road_signs/C/C35.svg new file mode 100644 index 0000000..b5e2399 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C35.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C37.svg b/streetup/assets/static/assets/road_signs/C/C37.svg new file mode 100644 index 0000000..c450c8b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C37.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C39.svg b/streetup/assets/static/assets/road_signs/C/C39.svg new file mode 100644 index 0000000..8c6cd03 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C39.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C41.svg b/streetup/assets/static/assets/road_signs/C/C41.svg new file mode 100644 index 0000000..055ec36 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C41.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C43.svg b/streetup/assets/static/assets/road_signs/C/C43.svg new file mode 100644 index 0000000..dcc277d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C43.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + 50 + + diff --git a/streetup/assets/static/assets/road_signs/C/C45.svg b/streetup/assets/static/assets/road_signs/C/C45.svg new file mode 100644 index 0000000..3942857 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C45.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + 50 + + + diff --git a/streetup/assets/static/assets/road_signs/C/C46.svg b/streetup/assets/static/assets/road_signs/C/C46.svg new file mode 100644 index 0000000..bf441fa --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C46.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C47.svg b/streetup/assets/static/assets/road_signs/C/C47.svg new file mode 100644 index 0000000..9b1cd6e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C47.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + Douane + Zoll + + + diff --git a/streetup/assets/static/assets/road_signs/C/C47DE.svg b/streetup/assets/static/assets/road_signs/C/C47DE.svg new file mode 100644 index 0000000..d5e1867 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C47DE.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + ZOLL + + diff --git a/streetup/assets/static/assets/road_signs/C/C47FR.svg b/streetup/assets/static/assets/road_signs/C/C47FR.svg new file mode 100644 index 0000000..6728151 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C47FR.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C47NL.svg b/streetup/assets/static/assets/road_signs/C/C47NL.svg new file mode 100644 index 0000000..04dec2f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C47NL.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + TOL + + diff --git a/streetup/assets/static/assets/road_signs/C/C49.svg b/streetup/assets/static/assets/road_signs/C/C49.svg new file mode 100644 index 0000000..4136fdd --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C49.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C5.svg b/streetup/assets/static/assets/road_signs/C/C5.svg new file mode 100644 index 0000000..26fa19d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C5.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C6.svg b/streetup/assets/static/assets/road_signs/C/C6.svg new file mode 100644 index 0000000..f33ab7b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C6.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C7.svg b/streetup/assets/static/assets/road_signs/C/C7.svg new file mode 100644 index 0000000..3107eb7 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C7.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C8.svg b/streetup/assets/static/assets/road_signs/C/C8.svg new file mode 100644 index 0000000..3df2b26 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C8.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/C/C9.svg b/streetup/assets/static/assets/road_signs/C/C9.svg new file mode 100644 index 0000000..27d38fe --- /dev/null +++ b/streetup/assets/static/assets/road_signs/C/C9.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D10.svg b/streetup/assets/static/assets/road_signs/D/D10.svg new file mode 100644 index 0000000..8bd7612 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D10.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D10m.svg b/streetup/assets/static/assets/road_signs/D/D10m.svg new file mode 100644 index 0000000..9be9557 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D10m.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D11.svg b/streetup/assets/static/assets/road_signs/D/D11.svg new file mode 100644 index 0000000..e95f316 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D11.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D12.svg b/streetup/assets/static/assets/road_signs/D/D12.svg new file mode 100644 index 0000000..3e3801e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D12.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D13.svg b/streetup/assets/static/assets/road_signs/D/D13.svg new file mode 100644 index 0000000..1f5c5ac --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D13.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D14.svg b/streetup/assets/static/assets/road_signs/D/D14.svg new file mode 100644 index 0000000..481b318 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D14.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D15.svg b/streetup/assets/static/assets/road_signs/D/D15.svg new file mode 100644 index 0000000..e47c520 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D15.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D16.svg b/streetup/assets/static/assets/road_signs/D/D16.svg new file mode 100644 index 0000000..96249e8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D16.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D17.svg b/streetup/assets/static/assets/road_signs/D/D17.svg new file mode 100644 index 0000000..66058fc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D17.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D18.svg b/streetup/assets/static/assets/road_signs/D/D18.svg new file mode 100644 index 0000000..eb741e1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D18.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1a.svg b/streetup/assets/static/assets/road_signs/D/D1a.svg new file mode 100644 index 0000000..cade3ce --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1a.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1b.svg b/streetup/assets/static/assets/road_signs/D/D1b.svg new file mode 100644 index 0000000..6612fe4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1b.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1c.svg b/streetup/assets/static/assets/road_signs/D/D1c.svg new file mode 100644 index 0000000..80e6a3f --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1c.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1d.svg b/streetup/assets/static/assets/road_signs/D/D1d.svg new file mode 100644 index 0000000..bcc1567 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1d.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1e.svg b/streetup/assets/static/assets/road_signs/D/D1e.svg new file mode 100644 index 0000000..95b5f19 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1e.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1f.svg b/streetup/assets/static/assets/road_signs/D/D1f.svg new file mode 100644 index 0000000..d1ecfce --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1f.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D1g.svg b/streetup/assets/static/assets/road_signs/D/D1g.svg new file mode 100644 index 0000000..70b6383 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D1g.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D3a.svg b/streetup/assets/static/assets/road_signs/D/D3a.svg new file mode 100644 index 0000000..8fec4e6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D3a.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D3b.svg b/streetup/assets/static/assets/road_signs/D/D3b.svg new file mode 100644 index 0000000..d546574 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D3b.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D3c.svg b/streetup/assets/static/assets/road_signs/D/D3c.svg new file mode 100644 index 0000000..8e42647 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D3c.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D4.svg b/streetup/assets/static/assets/road_signs/D/D4.svg new file mode 100644 index 0000000..52f5b06 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D4.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D5.svg b/streetup/assets/static/assets/road_signs/D/D5.svg new file mode 100644 index 0000000..681aa59 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D5.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D7.svg b/streetup/assets/static/assets/road_signs/D/D7.svg new file mode 100644 index 0000000..58bb246 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D7.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D8.svg b/streetup/assets/static/assets/road_signs/D/D8.svg new file mode 100644 index 0000000..13899f6 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D8.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D9.svg b/streetup/assets/static/assets/road_signs/D/D9.svg new file mode 100644 index 0000000..fb84355 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D9.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/D/D9m.svg b/streetup/assets/static/assets/road_signs/D/D9m.svg new file mode 100644 index 0000000..8d86c48 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/D/D9m.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/E/E1.svg b/streetup/assets/static/assets/road_signs/E/E1.svg new file mode 100644 index 0000000..e7abad2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/E/E1.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/E/E3.svg b/streetup/assets/static/assets/road_signs/E/E3.svg new file mode 100644 index 0000000..3428f3e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/E/E3.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/E/E9.svg b/streetup/assets/static/assets/road_signs/E/E9.svg new file mode 100644 index 0000000..40911f2 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/E/E9.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + P + + diff --git a/streetup/assets/static/assets/road_signs/P/P1.svg b/streetup/assets/static/assets/road_signs/P/P1.svg new file mode 100644 index 0000000..aa85f95 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P1.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P10.svg b/streetup/assets/static/assets/road_signs/P/P10.svg new file mode 100644 index 0000000..bb380cf --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P10.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P11.svg b/streetup/assets/static/assets/road_signs/P/P11.svg new file mode 100644 index 0000000..30ac091 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P11.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P12a.svg b/streetup/assets/static/assets/road_signs/P/P12a.svg new file mode 100644 index 0000000..f984554 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P12a.svg @@ -0,0 +1,55 @@ + + + + + + + + AUTOCAR + + diff --git a/streetup/assets/static/assets/road_signs/P/P12b.svg b/streetup/assets/static/assets/road_signs/P/P12b.svg new file mode 100644 index 0000000..76c67c8 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P12b.svg @@ -0,0 +1,57 @@ + + + + + + + + AUTOBUS + + diff --git a/streetup/assets/static/assets/road_signs/P/P13.svg b/streetup/assets/static/assets/road_signs/P/P13.svg new file mode 100644 index 0000000..18ab89a --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P13.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P15.svg b/streetup/assets/static/assets/road_signs/P/P15.svg new file mode 100644 index 0000000..7d027ad --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P15.svg @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P16.svg b/streetup/assets/static/assets/road_signs/P/P16.svg new file mode 100644 index 0000000..e5361a1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P16.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P17.svg b/streetup/assets/static/assets/road_signs/P/P17.svg new file mode 100644 index 0000000..0e1c3be --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P17.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P18.svg b/streetup/assets/static/assets/road_signs/P/P18.svg new file mode 100644 index 0000000..35a7d54 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P18.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P19.svg b/streetup/assets/static/assets/road_signs/P/P19.svg new file mode 100644 index 0000000..ac89b93 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P19.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P2.svg b/streetup/assets/static/assets/road_signs/P/P2.svg new file mode 100644 index 0000000..c1a4889 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P2.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P20.svg b/streetup/assets/static/assets/road_signs/P/P20.svg new file mode 100644 index 0000000..5da6959 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P20.svg @@ -0,0 +1,57 @@ + + + + + + + + TAXI + + diff --git a/streetup/assets/static/assets/road_signs/P/P25.svg b/streetup/assets/static/assets/road_signs/P/P25.svg new file mode 100644 index 0000000..e470aa1 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P25.svg @@ -0,0 +1,87 @@ + + + + + + + + + + 2 + ,5 + t + + diff --git a/streetup/assets/static/assets/road_signs/P/P26.svg b/streetup/assets/static/assets/road_signs/P/P26.svg new file mode 100644 index 0000000..e4cdadc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P26.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + 10 + + m + + diff --git a/streetup/assets/static/assets/road_signs/P/P27.svg b/streetup/assets/static/assets/road_signs/P/P27.svg new file mode 100644 index 0000000..a9dfe8c --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P27.svg @@ -0,0 +1,87 @@ + + + + + + + + + + 2 + ,5 + m + + diff --git a/streetup/assets/static/assets/road_signs/P/P28.svg b/streetup/assets/static/assets/road_signs/P/P28.svg new file mode 100644 index 0000000..e440b41 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P28.svg @@ -0,0 +1,87 @@ + + + + + + + + + + 2 + ,5 + m + + diff --git a/streetup/assets/static/assets/road_signs/P/P29+2.svg b/streetup/assets/static/assets/road_signs/P/P29+2.svg new file mode 100644 index 0000000..cbc04dc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P29+2.svg @@ -0,0 +1,103 @@ + +image/svg+xml2+ diff --git a/streetup/assets/static/assets/road_signs/P/P29+3.svg b/streetup/assets/static/assets/road_signs/P/P29+3.svg new file mode 100644 index 0000000..0fe7291 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P29+3.svg @@ -0,0 +1,108 @@ + +image/svg+xml3+ diff --git a/streetup/assets/static/assets/road_signs/P/P3.svg b/streetup/assets/static/assets/road_signs/P/P3.svg new file mode 100644 index 0000000..f77713e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P3.svg @@ -0,0 +1,54 @@ + +image/svg+xml diff --git a/streetup/assets/static/assets/road_signs/P/P30.svg b/streetup/assets/static/assets/road_signs/P/P30.svg new file mode 100644 index 0000000..a6cd118 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P30.svg @@ -0,0 +1,102 @@ + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P31.svg b/streetup/assets/static/assets/road_signs/P/P31.svg new file mode 100644 index 0000000..9f96cf4 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P31.svg @@ -0,0 +1,73 @@ + +image/svg+xml diff --git a/streetup/assets/static/assets/road_signs/P/P32.svg b/streetup/assets/static/assets/road_signs/P/P32.svg new file mode 100644 index 0000000..737e8ce --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P32.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 7h30-18h00 + 18h00-7h30 + + diff --git a/streetup/assets/static/assets/road_signs/P/P34.svg b/streetup/assets/static/assets/road_signs/P/P34.svg new file mode 100644 index 0000000..ebffd8b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P34.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P35a.svg b/streetup/assets/static/assets/road_signs/P/P35a.svg new file mode 100644 index 0000000..17ea374 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P35a.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P35b.svg b/streetup/assets/static/assets/road_signs/P/P35b.svg new file mode 100644 index 0000000..fa4ec55 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P35b.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P36.svg b/streetup/assets/static/assets/road_signs/P/P36.svg new file mode 100644 index 0000000..650edbe --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P36.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P37.svg b/streetup/assets/static/assets/road_signs/P/P37.svg new file mode 100644 index 0000000..495941e --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P37.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P4.svg b/streetup/assets/static/assets/road_signs/P/P4.svg new file mode 100644 index 0000000..daf2e5d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P4.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P5.svg b/streetup/assets/static/assets/road_signs/P/P5.svg new file mode 100644 index 0000000..fca8c84 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P5.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P6.svg b/streetup/assets/static/assets/road_signs/P/P6.svg new file mode 100644 index 0000000..bbe4a2d --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P6.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P7.svg b/streetup/assets/static/assets/road_signs/P/P7.svg new file mode 100644 index 0000000..b8a39dc --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P7.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P8.svg b/streetup/assets/static/assets/road_signs/P/P8.svg new file mode 100644 index 0000000..6308ede --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P8.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/P/P8A.svg b/streetup/assets/static/assets/road_signs/P/P8A.svg new file mode 100644 index 0000000..aeebe61 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P8A.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + A + + diff --git a/streetup/assets/static/assets/road_signs/P/P8B.svg b/streetup/assets/static/assets/road_signs/P/P8B.svg new file mode 100644 index 0000000..eaf6b55 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P8B.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + B + + diff --git a/streetup/assets/static/assets/road_signs/P/P8P.svg b/streetup/assets/static/assets/road_signs/P/P8P.svg new file mode 100644 index 0000000..bf41c2b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P8P.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + P + + diff --git a/streetup/assets/static/assets/road_signs/P/P9.svg b/streetup/assets/static/assets/road_signs/P/P9.svg new file mode 100644 index 0000000..3040eaf --- /dev/null +++ b/streetup/assets/static/assets/road_signs/P/P9.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + diff --git a/streetup/assets/static/assets/road_signs/bike.svg b/streetup/assets/static/assets/road_signs/bike.svg new file mode 100644 index 0000000..463de37 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/bike.svg @@ -0,0 +1,17 @@ + + + + + + + + + image/svg+xml + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/car_side.svg b/streetup/assets/static/assets/road_signs/car_side.svg new file mode 100644 index 0000000..fe36f5b --- /dev/null +++ b/streetup/assets/static/assets/road_signs/car_side.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/road_signs/pedestrian.svg b/streetup/assets/static/assets/road_signs/pedestrian.svg new file mode 100644 index 0000000..cce2e41 --- /dev/null +++ b/streetup/assets/static/assets/road_signs/pedestrian.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/streetup/assets/static/assets/roaddrain_grille_icon.svg b/streetup/assets/static/assets/roaddrain_grille_icon.svg new file mode 100644 index 0000000..7ea705d --- /dev/null +++ b/streetup/assets/static/assets/roaddrain_grille_icon.svg @@ -0,0 +1,23 @@ + diff --git a/streetup/assets/static/assets/roads_street_detail.js b/streetup/assets/static/assets/roads_street_detail.js new file mode 100644 index 0000000..fc87579 --- /dev/null +++ b/streetup/assets/static/assets/roads_street_detail.js @@ -0,0 +1,137 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 16, + pitch: 45, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const street_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: geom, + properties: { + name: '...' + } + } + ] + }; + + street_source = map.addSource('street', { + type: 'geojson', + data: street_data + }); + + map.addLayer({ + 'id': 'street', + 'type': 'fill', + 'source': 'street', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.7, + } + }); + + const bounds = turf.bbox(street_data); + map.fitBounds(bounds, { + // duration: 300 + }); +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +const documentTypeChoices = { + 'as_built': 'As Built', + 'other': 'Autre', +}; + +fileInput.addEventListener('change', function () { + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1'); + fileName.textContent = file.name; + row.appendChild(fileName); + + // Select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + fileInput.dispatchEvent(new Event('change')); + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); +}); + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const fileName = button.getAttribute('data-filename'); + const deleteUrl = button.getAttribute('data-delete-url'); + + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + const deleteForm = document.getElementById('deleteDocumentForm'); + + fileNameContainer.textContent = fileName; + deleteForm.action = deleteUrl; +}); + + +document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const streetId = button.getAttribute('data-street-id'); + + fetch(`${document_history_url}?street_id=${streetId}&document_type=${type}`) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); +}); diff --git a/streetup/assets/static/assets/sign_panels_shared.js b/streetup/assets/static/assets/sign_panels_shared.js new file mode 100644 index 0000000..bfb712e --- /dev/null +++ b/streetup/assets/static/assets/sign_panels_shared.js @@ -0,0 +1,355 @@ +/* sign_panels_shared.js + * Shared sign panel map rendering helpers used by both: + * - sign app map + * - unified assets map + */ +(function (global) { + 'use strict'; + + const MIN_PANEL_BASE_UP_OFFSET_PX = 40; + const DEFAULT_PANEL_ICON_HEIGHT_PX = 65; + const PANEL_STACK_GAP_PX = 2; + const SCALED_PANEL_ICON_FACTOR = 1.2; + + const loadingRoadSignImagesByMap = new WeakMap(); + + function getLoadingSet(map) { + let set = loadingRoadSignImagesByMap.get(map); + if (!set) { + set = new Set(); + loadingRoadSignImagesByMap.set(map, set); + } + return set; + } + + function getRenderedPanelHeight(feature) { + const props = feature && feature.properties ? feature.properties : {}; + const scaleFactor = Number(props.panel_scale_factor) || 1.0; + const spriteId = props.panel_text_image_id; + const metrics = (typeof global !== 'undefined') ? global.panelTextSpriteMetrics : null; + const iconMetrics = (typeof global !== 'undefined') ? global.panelRoadSignSpriteMetrics : null; + const iconSpriteId = props.panel_icon_image_scaled || props.panel_icon_image; + + let height; + if (props.panel_text_can_render && spriteId && metrics && metrics[spriteId] && metrics[spriteId].height) { + height = metrics[spriteId].height * scaleFactor; + } else if (!props.panel_text_can_render && iconSpriteId && iconMetrics && iconMetrics[iconSpriteId] && iconMetrics[iconSpriteId].height) { + height = iconMetrics[iconSpriteId].height * scaleFactor; + } else if (!props.panel_text_can_render && props.panel_icon_image_scaled) { + height = (props.panel_render_height || DEFAULT_PANEL_ICON_HEIGHT_PX) * SCALED_PANEL_ICON_FACTOR * scaleFactor; + } else { + height = (props.panel_render_height || DEFAULT_PANEL_ICON_HEIGHT_PX) * scaleFactor; + } + + return Number.isFinite(height) && height > 0 ? height : DEFAULT_PANEL_ICON_HEIGHT_PX; + } + + function calculatePanelOffsets(geojsonData) { + if (!geojsonData || !Array.isArray(geojsonData.features) || geojsonData.features.length === 0) { + return geojsonData; + } + + const panelsByPole = {}; + + geojsonData.features.forEach(feature => { + if (feature && feature.properties && feature.properties.asset_type === 'signpanel') { + const poleId = feature.properties.pole_id; + if (!poleId) return; + if (!panelsByPole[poleId]) panelsByPole[poleId] = []; + panelsByPole[poleId].push(feature); + } + }); + + Object.values(panelsByPole).forEach(polePanels => { + polePanels.sort((a, b) => { + const orderA = a && a.properties ? (a.properties.panel_vertical_order || 1) : 1; + const orderB = b && b.properties ? (b.properties.panel_vertical_order || 1) : 1; + return orderA - orderB; + }); + + let cumulativeOffset = -MIN_PANEL_BASE_UP_OFFSET_PX; + let previousHeight = 0; + + for (let i = 0; i < polePanels.length; i++) { + const panel = polePanels[i]; + const renderHeight = getRenderedPanelHeight(panel); + + if (i === 0) { + cumulativeOffset = -MIN_PANEL_BASE_UP_OFFSET_PX - Math.max(0, (renderHeight - DEFAULT_PANEL_ICON_HEIGHT_PX) / 2); + } else { + cumulativeOffset -= (previousHeight + renderHeight) / 2 + PANEL_STACK_GAP_PX; + } + + if (!panel.properties) panel.properties = {}; + const panelScaleFactor = Number(panel.properties.panel_scale_factor) || 1.0; + let offsetVal = (cumulativeOffset - 64) / panelScaleFactor; + if (!Number.isFinite(offsetVal)) { + offsetVal = -64; + } + panel.properties.panel_icon_offset = [0, offsetVal]; + panel.properties.panel_text_offset = [0, offsetVal]; + + previousHeight = renderHeight; + } + }); + + return geojsonData; + } + + function addOrReplaceImage(map, name, payload, options) { + if (!map || !name || !payload) return; + + if (map.hasImage(name)) { + try { + map.updateImage(name, payload); + return; + } catch (_err) { + try { + map.removeImage(name); + } catch (_removeErr) { + // ignore + } + } + } + + if (options) { + map.addImage(name, payload, options); + } else { + map.addImage(name, payload); + } + } + + function createDefaultRoadSignPayload(size, color) { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(size / 2, size / 2, (size / 2) - 2, 0, 2 * Math.PI); + ctx.fill(); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + return { + width: canvas.width, + height: canvas.height, + data: imageData.data, + }; + } + + function createOrientationArrowPayload() { + const canvas = document.createElement('canvas'); + canvas.width = 48; + canvas.height = 48; + + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#aaaaaa'; + + ctx.beginPath(); + ctx.moveTo(24, 6); + ctx.lineTo(34, 27); + ctx.lineTo(26.5, 27); + ctx.lineTo(26.5, 42); + ctx.lineTo(21.5, 42); + ctx.lineTo(21.5, 27); + ctx.lineTo(14, 27); + ctx.closePath(); + ctx.fill(); + + return { + width: canvas.width, + height: canvas.height, + data: ctx.getImageData(0, 0, canvas.width, canvas.height).data, + }; + } + + function createPoleVerticalPayload() { + const canvas = document.createElement('canvas'); + canvas.width = 16; + canvas.height = 64; + + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#555555'; + ctx.strokeStyle = '#222222'; + ctx.lineWidth = 1; + + const poleWidth = 4; + const poleX = Math.floor((canvas.width - poleWidth) / 2); + + ctx.beginPath(); + ctx.moveTo(poleX + poleWidth, 0); + ctx.lineTo(poleX + poleWidth, canvas.height); + ctx.lineTo(poleX, canvas.height); + ctx.lineTo(poleX, 0); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + return { + width: canvas.width, + height: canvas.height, + data: ctx.getImageData(0, 0, canvas.width, canvas.height).data, + }; + } + + function loadRoadSignImages(map) { + if (!map) return; + if (map.hasImage('road-sign-images-loaded')) return; + + global.panelRoadSignSpriteMetrics = global.panelRoadSignSpriteMetrics || {}; + + addOrReplaceImage(map, 'road-sign-default', createDefaultRoadSignPayload(64, '#0022ff')); + addOrReplaceImage(map, 'orientation-arrow', createOrientationArrowPayload()); + addOrReplaceImage(map, 'sign-pole-vertical', createPoleVerticalPayload()); + + map.addImage('road-sign-images-loaded', { + width: 1, + height: 1, + data: new Uint8Array([0, 0, 0, 0]), + }); + } + + function handleRoadSignStyleImageMissing(args) { + const map = args && args.map; + const event = args && args.event; + const onImageUpdated = args && args.onImageUpdated; + const missingId = event && event.id; + + if (!map || !missingId) return false; + if (!missingId.startsWith('road-sign-') || missingId === 'road-sign-default') return false; + + const loadingSet = getLoadingSet(map); + if (map.hasImage(missingId) || loadingSet.has(missingId)) { + return true; + } + + const isScaledX12 = missingId.endsWith('-x12'); + const targetSize = isScaledX12 ? Math.round(64 * 1.2) : 64; + const signCode = missingId.replace('road-sign-', '').replace(/-x12$/, ''); + + const greyCanvas = document.createElement('canvas'); + greyCanvas.width = targetSize; + greyCanvas.height = targetSize; + const greyCtx = greyCanvas.getContext('2d'); + greyCtx.fillStyle = '#d0d0d0'; + const inset = Math.max(8, Math.round(targetSize * 0.25)); + greyCtx.fillRect(inset, inset, greyCanvas.width - (2 * inset), greyCanvas.height - (2 * inset)); + greyCtx.strokeStyle = '#7a7a7a'; + greyCtx.lineWidth = 2; + greyCtx.strokeRect(inset + 2, inset + 2, greyCanvas.width - (2 * inset) - 4, greyCanvas.height - (2 * inset) - 4); + + addOrReplaceImage(map, missingId, { + width: greyCanvas.width, + height: greyCanvas.height, + data: greyCtx.getImageData(0, 0, greyCanvas.width, greyCanvas.height).data, + }); + + const img = new Image(); + img.crossOrigin = 'anonymous'; + + let attemptCount = 0; + const codeVariations = [ + signCode.toUpperCase(), + signCode.charAt(0).toUpperCase() + signCode.slice(1), + signCode.toLowerCase(), + ]; + + function tryNextVariation() { + if (attemptCount >= codeVariations.length * 2) { + return; + } + + const currentVariation = codeVariations[Math.floor(attemptCount / 2)]; + const extension = attemptCount % 2 === 0 ? 'svg' : 'png'; + const url = `/static/assets/road_signs/2025/${currentVariation}.${extension}`; + attemptCount += 1; + img.src = url; + } + + loadingSet.add(missingId); + + img.onload = function () { + try { + const aspectRatio = img.width > 0 ? (img.height / img.width) : 1; + const spriteWidth = targetSize; + const spriteHeight = Math.round(targetSize * aspectRatio); + + const canvas = document.createElement('canvas'); + canvas.width = spriteWidth; + canvas.height = spriteHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, spriteWidth, spriteHeight); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + if (map.hasImage(missingId)) { + map.removeImage(missingId); + } + + map.addImage(missingId, { + width: canvas.width, + height: canvas.height, + data: imageData.data, + }); + + global.panelRoadSignSpriteMetrics[missingId] = { + width: canvas.width, + height: canvas.height, + }; + + map.triggerRepaint(); + if (typeof onImageUpdated === 'function') { + onImageUpdated(); + } + } catch (err) { + console.warn('Error updating road sign image', missingId, err); + } finally { + loadingSet.delete(missingId); + } + }; + + img.onerror = function () { + tryNextVariation(); + if (attemptCount >= codeVariations.length * 2) { + loadingSet.delete(missingId); + } + }; + + tryNextVariation(); + return true; + } + + function refreshPanelOffsetsForSource(map, sourceId) { + if (!map || !sourceId || !map.getSource) return; + + const source = map.getSource(sourceId); + if (!source || !source._data || !Array.isArray(source._data.features)) return; + + calculatePanelOffsets(source._data); + source.setData(source._data); + } + + function createPanelOffsetRefreshScheduler(map, sourceId, delayMs) { + const delay = Number.isFinite(delayMs) ? delayMs : 40; + let timer = null; + + return function schedule() { + if (timer) clearTimeout(timer); + timer = setTimeout(function () { + timer = null; + refreshPanelOffsetsForSource(map, sourceId); + }, delay); + }; + } + + global.SignPanelsShared = { + getRenderedPanelHeight, + calculatePanelOffsets, + loadRoadSignImages, + handleRoadSignStyleImageMissing, + refreshPanelOffsetsForSource, + createPanelOffsetRefreshScheduler, + }; +})(window); diff --git a/streetup/assets/static/assets/sign_street_detail.js b/streetup/assets/static/assets/sign_street_detail.js new file mode 100644 index 0000000..2108d78 --- /dev/null +++ b/streetup/assets/static/assets/sign_street_detail.js @@ -0,0 +1,139 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 16, + pitch: 45, + // bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const street_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: geom, + properties: { + name: '...' + } + } + ] + }; + + street_source = map.addSource('street', { + type: 'geojson', + data: street_data + }); + + map.addLayer({ + 'id': 'street', + 'type': 'fill', + 'source': 'street', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.7, + } + }); + + const bounds = turf.bbox(street_data); + map.fitBounds(bounds, { + // duration: 300 + }); +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +const documentTypeChoices = { + 'as_built': 'As Built', + 'sign_plan': 'Plan de signalisation', + 'other': 'Autre', +}; + +fileInput.addEventListener('change', function () { + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1'); + fileName.textContent = file.name; + row.appendChild(fileName); + + // Select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; // nécessite Bootstrap Icons ou tu peux mettre "✖" + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + fileInput.dispatchEvent(new Event('change')); // re-render + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); +}); + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const fileName = button.getAttribute('data-filename'); + const deleteUrl = button.getAttribute('data-delete-url'); + + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + const deleteForm = document.getElementById('deleteDocumentForm'); + + fileNameContainer.textContent = fileName; + deleteForm.action = deleteUrl; +}); + + +document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const streetId = button.getAttribute('data-street-id'); + + fetch(`${document_history_url}?street_id=${streetId}&document_type=${type}`) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); +}); \ No newline at end of file diff --git a/streetup/assets/static/assets/simple_pole_icon.svg b/streetup/assets/static/assets/simple_pole_icon.svg new file mode 100644 index 0000000..82a1597 --- /dev/null +++ b/streetup/assets/static/assets/simple_pole_icon.svg @@ -0,0 +1,23 @@ + diff --git a/streetup/assets/static/assets/structures_bridge_tunnel_icon.svg b/streetup/assets/static/assets/structures_bridge_tunnel_icon.svg new file mode 100644 index 0000000..26ac2c6 --- /dev/null +++ b/streetup/assets/static/assets/structures_bridge_tunnel_icon.svg @@ -0,0 +1,7 @@ + diff --git a/streetup/assets/static/assets/structures_location_detail.js b/streetup/assets/static/assets/structures_location_detail.js new file mode 100644 index 0000000..a98cbc1 --- /dev/null +++ b/streetup/assets/static/assets/structures_location_detail.js @@ -0,0 +1,274 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 17, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +map.on('load', () => { + + const location_data = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: feature_geom, + properties: { + name: '...' + } + } + ] + }; + + // Add the circle as a GeoJSON source + map.addSource('location', { + type: 'geojson', + data: location_data + }); + + map.addLayer({ + 'id': 'location', + 'type': 'fill', + 'source': 'location', + 'paint': { + 'fill-color': '#0094ff', + 'fill-opacity': 0.7, + } + }); + + const bounds = turf.bbox(location_data); + map.fitBounds(bounds, { + duration: 10 + }); +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +// Configuration pour l'ancien système (fallback) +const documentTypeChoices = { + 'as_built': 'As Built', + 'cme': 'CME', + 'pis': 'PIS', + 'inspection_ce': 'Inspection G.C.', + 'inspection_em': 'Inspection E.M.', + 'other': 'Autre' +}; + +// Récupérer la configuration du nouveau système de gestion documentaire +let documentConfig = { use_managed_documents: false, can_add_unique_tag_documents: false, config_url: null }; +let availableTags = []; +let availableFolders = []; + +try { + const configEl = document.getElementById('documentConfigData'); + if (configEl) { + documentConfig = JSON.parse(configEl.textContent); + } +} catch (e) { + console.log('Document config not found, using legacy system'); +} + +// Charger la configuration complète via l'API +async function loadDocumentConfig() { + if (!documentConfig.config_url) return; + + try { + const response = await fetch(documentConfig.config_url); + const data = await response.json(); + + documentConfig.use_managed_documents = data.use_managed_documents; + documentConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents; + availableTags = data.tags || []; + availableFolders = data.folders || []; + + // Exposer globalement pour le gestionnaire de fichiers + window.availableTags = availableTags; + window.availableFolders = availableFolders; + } catch (e) { + console.log('Failed to load document config:', e); + } +} + +// Charger la config au démarrage +loadDocumentConfig(); + +function renderFileFields() { + if (!fileContainer) return; + + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3', 'mb-2', 'p-2', 'border', 'rounded'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1', 'text-truncate'); + fileName.style.maxWidth = '200px'; + fileName.textContent = file.name; + fileName.title = file.name; + row.appendChild(fileName); + + if (documentConfig.use_managed_documents && availableTags && availableTags.length > 0) { + // Nouveau système: select pour le tag + const tagSelect = document.createElement('select'); + tagSelect.name = `tag_${index}`; + tagSelect.classList.add('form-select', 'w-auto'); + tagSelect.style.minWidth = '150px'; + + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = '-- Choisir un tag --'; + tagSelect.appendChild(defaultOption); + + availableTags.forEach(tag => { + const option = document.createElement('option'); + option.value = tag.slug; + option.textContent = tag.name; + option.dataset.isUnique = tag.is_unique; + // Désactiver les tags uniques si l'utilisateur n'a pas les permissions + if (tag.is_unique && !documentConfig.can_add_unique_tag_documents) { + option.disabled = true; + option.textContent += ' (non autorisé)'; + } + tagSelect.appendChild(option); + }); + row.appendChild(tagSelect); + + // Select pour le répertoire (affiché uniquement pour les tags non-uniques) + const folderSelect = document.createElement('select'); + folderSelect.name = `folder_${index}`; + folderSelect.classList.add('form-select', 'w-auto', 'd-none'); + folderSelect.style.minWidth = '180px'; + + if (availableFolders && availableFolders.length > 0) { + availableFolders.forEach(folder => { + const option = document.createElement('option'); + option.value = folder.id; + option.textContent = folder.path; + if (folder.is_root) { + option.selected = true; + } + folderSelect.appendChild(option); + }); + } + row.appendChild(folderSelect); + + // Afficher/masquer le sélecteur de répertoire selon le tag + tagSelect.addEventListener('change', function() { + const selectedOption = tagSelect.options[tagSelect.selectedIndex]; + const isUnique = selectedOption.dataset.isUnique === 'true' || selectedOption.dataset.isUnique === 'True'; + + if (isUnique || tagSelect.value === '') { + folderSelect.classList.add('d-none'); + } else { + folderSelect.classList.remove('d-none'); + } + }); + + } else { + // Ancien système: select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + } + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-outline-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + renderFileFields(); // re-render + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); +} + +if (fileInput) { + fileInput.addEventListener('change', renderFileFields); +} + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +if (deleteDocumentModal) { + deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const fileName = button.getAttribute('data-filename'); + const deleteUrl = button.getAttribute('data-delete-url'); + + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + const deleteForm = document.getElementById('deleteDocumentForm'); + + fileNameContainer.textContent = fileName; + deleteForm.action = deleteUrl; + }); +} + +historyModal = document.getElementById('documentHistoryModal'); +if (historyModal) { + document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const tagSlug = button.getAttribute('data-tag-slug'); + const locationId = button.getAttribute('data-location-id'); + const useManaged = button.getAttribute('data-use-managed') === 'true'; + + let url; + if (useManaged && tagSlug) { + // Nouveau système de gestion documentaire + url = `${document_history_url}?structure_id=${locationId}&tag_slug=${tagSlug}`; + } else { + // Ancien système + url = `${document_history_url}?structure_id=${locationId}&document_type=${type}`; + } + + fetch(url) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); + }); +} + + +document.addEventListener('click', (e) => { + const btn = e.target.closest('[data-bs-toggle="collapse"]'); + if (!btn) return; + const target = document.querySelector(btn.getAttribute('data-bs-target')); + if (!target) return; + target.addEventListener('shown.bs.collapse', () => btn.textContent = '▾', {once:true}); + target.addEventListener('hidden.bs.collapse', () => btn.textContent = '▸', {once:true}); +}); \ No newline at end of file diff --git a/streetup/assets/static/assets/traffic_light_icon.svg b/streetup/assets/static/assets/traffic_light_icon.svg new file mode 100644 index 0000000..464b8fb --- /dev/null +++ b/streetup/assets/static/assets/traffic_light_icon.svg @@ -0,0 +1,223 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/streetup/assets/static/assets/trafficlights_index.js b/streetup/assets/static/assets/trafficlights_index.js new file mode 100644 index 0000000..545cb52 --- /dev/null +++ b/streetup/assets/static/assets/trafficlights_index.js @@ -0,0 +1,542 @@ + +var tl_data = []; + +var intersections_geojson = {}; +const intersectionsgeojsonloader = new GeojsonLoader(); +const intersections_layer_def = { + 'id': 'traffic-lights', + 'type': 'circle', + 'source': 'traffic-lights', + 'filter': [ + 'all', + ['!', ['coalesce', ['get', 'is_radar'], false]] + ], + 'paint': { + 'circle-radius': [ + 'interpolate', + ['linear'], + ['zoom'], + 10, 2, + 13, 4, + 16, 7, + 19, 10 + ], + 'circle-color': [ + 'case', + ['boolean', ['feature-state', 'highlighted'], false], '#ffeb3bbf', // Highlight color + '#0094ff' // Default color + ], + 'circle-opacity': 0.7 + } +}; + +const intersections_radar_outline_layer_def = { + 'id': 'traffic-lights-radar-outline', + 'type': 'symbol', + 'source': 'traffic-lights', + 'filter': ['coalesce', ['get', 'is_radar'], false], + 'layout': { + 'icon-image': 'cross-sdf-border', + 'icon-size': ['interpolate', ['linear'], ['zoom'], 10, 0.22, 13, 0.35, 16, 0.6, 19, 0.85], + 'icon-allow-overlap': true, + 'icon-ignore-placement': true, + }, + 'paint': { + 'icon-color': [ + 'case', + ['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b', + '#ffffff' + ] + } +}; + +const intersections_radar_layer_def = { + 'id': 'traffic-lights-radar', + 'type': 'symbol', + 'source': 'traffic-lights', + 'filter': ['coalesce', ['get', 'is_radar'], false], + 'layout': { + 'icon-image': 'cross-sdf', + 'icon-size': ['interpolate', ['linear'], ['zoom'], 10, 0.22, 13, 0.35, 16, 0.6, 19, 0.85], + 'icon-allow-overlap': true, + 'icon-ignore-placement': true, + }, + 'paint': { + 'icon-color': [ + 'case', + ['boolean', ['feature-state', 'highlighted'], false], '#ffeb3bbf', // Highlight color + '#0094ff' // Default color + ] + } +}; + +const intersections_layers = [intersections_layer_def, intersections_radar_outline_layer_def, intersections_radar_layer_def]; + +handleGeojsonMapLoading(init=true, intersectionsgeojsonloader, 'traffic-lights', intersections_layers); + +// Reset table if error occurs when loading GeoJSON +intersectionsgeojsonloader.addEventListener("geojsonError", event => { + loadDataIntoIntersectionsTable([]); +}); + +// Load data into table when GeoJSON is loaded +intersectionsgeojsonloader.addEventListener("geojsonLoaded", event => { + var geojson_data = event.detail; + loadDataIntoIntersectionsTable(geojson_data.features); + intersections_geojson = geojson_data; +}); + + +var assets_geojson = {}; +const assetsgeojsonloader = new GeojsonLoader(); +const assets_layer_def = { + 'id': 'traffic-lights-assets', + 'type': 'circle', + 'source': 'traffic-lights-assets', + 'paint': { + 'circle-radius': [ + 'interpolate', + ['linear'], + ['zoom'], + 13, 1, + 22, 10 + ], + 'circle-color': [ + 'case', + ['boolean', ['feature-state', 'highlighted'], false], '#ffeb3bbf', // Highlight color + '#f24141' // Default color + ], + 'circle-opacity': 0.7 + }, + minzoom: 15 +} +handleGeojsonMapLoading(init=true, assetsgeojsonloader, 'traffic-lights-assets', [assets_layer_def]); + +assetsgeojsonloader.addEventListener("geojsonError", event => { + loadDataIntoAssetsTable([]); +}); + +assetsgeojsonloader.addEventListener("geojsonLoaded", event => { + var geojson_data = event.detail; + loadDataIntoAssetsTable(geojson_data.features); + assets_geojson = geojson_data; +}); + + + +// Load Map +maploader.loadMap("map"); + +// Map layers events +registerLayerMouseEnter(map, 'traffic-lights', popup, 'code', 13); +registerLayerMouseLeave(map, 'traffic-lights', popup); +registerLayerMouseEnter(map, 'traffic-lights-radar', popup, 'code', 13); +registerLayerMouseLeave(map, 'traffic-lights-radar', popup); +registerLayerMouseEnter(map, 'traffic-lights-assets', popup, 'name', 17); +registerLayerMouseLeave(map, 'traffic-lights-assets', popup); + +var intersectionshighlightState = { previousId: null }; // Track the previously highlighted feature +const handleIntersectionClick = (e) => { + const featureId = e.features[0].id; + const currentZoom = map.getZoom(); + if (currentZoom > 13) { + // Highlight the corresponding row in the Bootstrap table + highlightRow('#intersections-table', 'id', featureId); + + // Update feature highlight on the map + highlightMapFeature(map, 'traffic-lights', featureId, intersectionshighlightState); + highlightMapFeature(map, 'traffic-lights-radar', featureId, intersectionshighlightState); + + // Load assets GeoJSON for the selected intersection + const intersectionId = featureId; + const intersectionAssetsGeojsonUrl = `${assetsgeojsonUrl}?intersection=${intersectionId}`; + assetsgeojsonloader.loadGeojson(intersectionAssetsGeojsonUrl); + } +}; + +map.on('click', 'traffic-lights', handleIntersectionClick); +map.on('click', 'traffic-lights-radar', handleIntersectionClick); + +var assetshighlightState = { previousId: null }; // Track the previously highlighted feature +map.on('click', 'traffic-lights-assets', (e) => { + const featureId = e.features[0].id; + const currentZoom = map.getZoom(); + if (currentZoom > 13) { + //window.open(trafficlightintersectionsurl+e.features[0].id+"/","_self"); + + // Highlight the corresponding row in the Bootstrap table + highlightRow('#assets-table', 'id', featureId); + + // Update feature highlight on the map + highlightMapFeature(map, 'traffic-lights-assets', featureId, assetshighlightState); + } +}); + + + + +// Charger les données initiales +const params = new URLSearchParams(window.location.search).toString(); +intersectionsgeojsonloader.loadGeojson(intersectionsgeojsonUrl+`?${params}`); +assetsgeojsonloader.loadGeojson(assetsgeojsonUrl); + + + +function actionsFormatter(value, row, index) { + resp = ``; + + if (row.lon) { + resp = resp + + ``; + } + + if (preventive_draft_id && preventive_draft_id !== "None") { + // If we are in the context of a preventive draft, we add the button to select the intersection + if (selected_intersections_set.has(row.id)) { + icon_select = 'bi-check-square-fill'; + } else { + icon_select = 'bi-square'; + } + resp = resp + + ``; + } + + return resp; +} + +function actionsFormatterAssets(value, row, index) { + resp = ``; + + if (row.lon) { + resp = resp + + ``; + } + + if (intervention_id) { + resp = resp + + ``; + } + + return resp; +} + + + +// Loads features into the intersections table +function loadDataIntoIntersectionsTable(features) { + const tableData = features.map((feature) => { + return { + actions: '', // Column for the icons, manipulated by the formatter + id: feature.id, + code: feature.properties.code, + name: feature.properties.name, + status: '', + lon: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[0] : null, // Longitude + lat: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[1] : null // Latitude + }; + }); + + $('#intersections-spinner').hide(); + $('#intersections-table-container').show(); + $('#intersections-table').bootstrapTable('load', tableData); +} + +// Loads features into the assets table +function loadDataIntoAssetsTable(features) { + const tableData = features.map((feature) => { + return { + actions: '', // Colonne pour l'icône, manipulée par le formatter + id: feature.id, + asset_type: feature.properties.asset_type, + code: feature.properties.code, + name: feature.properties.name, + model: feature.properties.model || '', + status: '', + lon: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[0] : null, // Longitude + lat: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[1] : null // Latitude + }; + }); + $('#assets-spinner').hide(); + $('#assets-table-container').show(); + $('#assets-table').bootstrapTable('load', tableData); +} + + + +function redirectToThematic() { + const select = document.getElementById('theme-select'); + const selectedValue = select.value; + + if (selectedValue) { + // Rediriger vers l'URL sélectionnée + window.location.href = selectedValue; + } +} + + +$( document ).ready(function() { + + function calculateTableHeight() { + const windowHeight = $(window).height(); // Total window height + const offsetTop = 285; // Y position of the table + const footerHeight = $('.footer').outerHeight() || 0; + return windowHeight - offsetTop - footerHeight - 20; + } + + $('#intersections-table').bootstrapTable({ + columns: [ + { field: 'actions', title: '', formatter:'actionsFormatter', width:'130' }, + { field: 'code', title: 'Code' }, + { field: 'name', title: 'Nom' }, + { field: 'status', title: 'Statut', align: 'center' } + ], + height: calculateTableHeight(), // Apply computed height + onClickRow: function (row, $element) { + // Remove highlight from all previously highlighted cells + $('#intersections-table').find('tbody td').removeClass('rowhighlight'); + // Apply the highlight class to all `` cells in the row + $element.find('td').addClass('rowhighlight'); + + // Update feature highlight on the map + highlightMapFeature(map, 'traffic-lights', row.id, intersectionshighlightState); + + // Load assets GeoJSON for the selected intersection + const intersectionId = row.id; + const intersectionAssetsGeojsonUrl = `${assetsgeojsonUrl}?intersection=${intersectionId}`; + assetsgeojsonloader.loadGeojson(intersectionAssetsGeojsonUrl); + } + }); + + $('#assets-table').bootstrapTable({ + columns: [ + { field: 'actions', title: '', formatter:'actionsFormatterAssets', width:'120' }, + { field: 'code', title: 'Code' }, + { field: 'name', title: 'Nom' }, + { field: 'model', title: 'Modèle' }, + { field: 'status', title: 'Statut', align: 'center' } + ], + height: calculateTableHeight(), // Apply computed height + onClickRow: function (row, $element) { + // Remove highlight from all previously highlighted cells + $('#assets-table').find('tbody td').removeClass('rowhighlight'); + // Apply the highlight class to all `` cells in the row + $element.find('td').addClass('rowhighlight'); + + // Update feature highlight on the map + highlightMapFeature(map, 'traffic-lights-assets', row.id, assetshighlightState); + } + }); + + // Apply the correct width to the dropdown-menu + const parent_filter_dropdown = $('#filter-dropdown').parent().parent()[0]; + const parentWidth = parent_filter_dropdown.offsetWidth - 20; + $('#filter-dropdown').width(`${parentWidth}px`); + + + $('#theme-select').on('change', function () { + redirectToThematic(); + }); + + // reset view when tab is clicked (in case of window resize) + $('#locations-table-tab').on('click', function () { + $('#locations -table').bootstrapTable('resetView'); + }); + $('#assets-table-tab').on('click', function () { + $('#assets-table').bootstrapTable('resetView'); + }); + + + + // Apply filters + document.getElementById('apply-filters').addEventListener('click', () => { + + const form = document.getElementById('filter-form'); + const formData = new FormData(form); + const params = new URLSearchParams(); + for (const [key, value] of formData.entries()) { + if (value.trim() !== '') { // Vérifie que le champ n'est pas vide + params.append(key, value); + } + } + + // Update URL + const newUrl = `${window.location.pathname}?${params.toString()}`; + window.history.pushState({}, '', newUrl); + + // Load filtered data + handleGeojsonMapLoading(init=false, intersectionsgeojsonloader, 'traffic-lights', intersections_layers); + intersectionsgeojsonloader.loadGeojson(intersectionsgeojsonUrl+`?${params.toString()}`); + }); + + // Reset filters + document.getElementById('reset-filters').addEventListener('click', () => { + const form = document.getElementById('filter-form'); + form.reset(); // Reset form fields + + // Explicitly clear date fields + document.getElementById('begin_date').value = ''; + + // Remove URL parameters + window.history.pushState({}, '', window.location.pathname); + + // Reload all data without filters + handleGeojsonMapLoading(init=false, intersectionsgeojsonloader, 'traffic-lights', intersections_layers); + intersectionsgeojsonloader.loadGeojson(intersectionsgeojsonUrl); + }); + + // Filter tables by map bounds + $("#filterzone").on( "click", function(e) { + e.preventDefault(); + filterTableByMapBounds(map, intersections_geojson, loadDataIntoIntersectionsTable); + filterTableByMapBounds(map, assets_geojson, loadDataIntoAssetsTable); + }); + + + // Register detail action to the tables + registerDetailAction('#intersections-table', '.detail-btn', trafficlightsintersectionsurl); + registerDetailAction('#assets-table', '.detail-btn', trafficlightsassetsurl); + + // Register zoom action to the tables + registerZoomAction('#intersections-table', '.zoom-btn', map, 17); + registerZoomAction('#assets-table', '.zoom-btn', map, 18); + + // Register intervention action to table assets + registerInterventionAction('#assets-table', '.intervention-btn', intervention_id); + + // Register intersection selection action to table intersections + registerSelectIntersectionAction('#intersections-table', '.intersection-btn-toggle'); +}); + +function registerInterventionAction(tableId, buttonClass, intervention_id) { + $(tableId).on('click', buttonClass, function () { + const $button = $(this); + const assetId = $(this).data('id'); // Retrieve the ID from the button's data attribute + const assetType = $(this).data('asset_type'); // Retrieve the type from the button's data attribute + + if (assetId) { + linkAssetToIntervention(intervention_id, assetId, assetType, $button); + } + }); +} + +function linkAssetToIntervention(interventionId, assetId, assetType, $button) { + $.ajax({ + url: `/interventions/${interventionId}/link-asset/`, + method: 'POST', + headers: {'X-CSRFToken': getCookie('csrftoken')}, + data: { + intervention_id: interventionId, + asset_id: assetId, + model: assetType + }, + success: function(response) { + // Mettre le bouton en vert et désactiver + $button + .removeClass('btn-light') + .addClass('btn-success') + .prop('disabled', true); + }, + error: function(error) { + alert('Erreur lors de la liaison'); + } + }); +} + + +function registerSelectIntersectionAction(tableId, buttonClass) { + $(tableId).on('click', buttonClass, function () { + const $button = $(this); + const intersectionId = $(this).data('id'); // Retrieve the ID from the button's data attribute + + if (intersectionId) { + toggleSelectedIntersection(intersectionId, $button); + } + }); +} + +function toggleSelectedIntersection(intersectionId, $button) { + if (selected_intersections_set.has(intersectionId)) { + selected_intersections_set.delete(intersectionId); + $button + .find('i').removeClass('bi-check-square-fill').addClass('bi-square'); + } else { + selected_intersections_set.add(intersectionId); + $button + .find('i').removeClass('bi-square').addClass('bi-check-square-fill'); + } + updateInputAndStatus(); +} + +function updateInputAndStatus() { + const inputField = document.getElementById("selected-intersections-ids"); + const statusBar = document.getElementById("selection-status"); + const ids = Array.from(selected_intersections_set); + inputField.value = ids.join(","); + statusBar.innerText = `${ids.length} intersection${ids.length !== 1 ? 's' : ''} sélectionnée${ids.length !== 1 ? 's' : ''}`; +} + +// Selection of intersections for preventive draft +if (preventive_draft_id && preventive_draft_id !== "None") { + document.addEventListener("DOMContentLoaded", function () { + + document.getElementById("select-all").addEventListener("click", () => { + const allRows = $('#intersections-table').bootstrapTable('getData'); + allRows.forEach(row => { + selected_intersections_set.add(parseInt(row.id)); + const btn = document.querySelector(`.intersection-btn-toggle[data-id="${row.id}"]`); + if (btn) { + $(btn).find('i').removeClass('bi-square').addClass('bi-check-square-fill'); + } + }); + updateInputAndStatus(); + }); + + document.getElementById("deselect-all").addEventListener("click", () => { + selected_intersections_set.clear(); + const allRows = $('#intersections-table').bootstrapTable('getData'); + allRows.forEach(row => { + const btn = document.querySelector(`.intersection-btn-toggle[data-id="${row.id}"]`); + if (btn) { + $(btn).find('i').removeClass('bi-check-square-fill').addClass('bi-square'); + } + }); + updateInputAndStatus(); + }); + + // Initial state + updateInputAndStatus(); + }); +} + + + + + +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} +const csrftoken = getCookie('csrftoken'); \ No newline at end of file diff --git a/streetup/assets/static/assets/trafficlights_intersection_detail.js b/streetup/assets/static/assets/trafficlights_intersection_detail.js new file mode 100644 index 0000000..68229db --- /dev/null +++ b/streetup/assets/static/assets/trafficlights_intersection_detail.js @@ -0,0 +1,1088 @@ + + +const map = new maplibregl.Map({ + style: window.MAP_STYLE_LIGHT, + center: [feature_lon, feature_lat], + zoom: 17, + pitch: 45, + bearing: -17.6, + container: 'map', + antialias: true +}); + +// Custom Plans Control in Top Right (first, at the top) +class CustomPlansControl { + onAdd(map) { + this._container = document.createElement('div'); + this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + + const button = document.createElement('button'); + button.type = 'button'; + button.id = 'btn-load-georef-plan'; + button.className = 'btn btn-xs btn-light bg-white border shadow-sm d-flex align-items-center gap-1'; + button.style.border = 'none'; + button.style.padding = '8px 12px'; + button.style.height = 'auto'; + button.style.width = 'auto'; + button.style.fontWeight = 'bold'; + button.style.fontSize = '0.75rem'; + button.innerHTML = ' Plans'; + + // Toggle bootstrap modal natively + button.setAttribute('data-bs-toggle', 'modal'); + button.setAttribute('data-bs-target', '#georefPlanModal'); + + this._container.appendChild(button); + return this._container; + } + onRemove() { + this._container.parentNode.removeChild(this._container); + this._map = undefined; + } +} +map.addControl(new CustomPlansControl(), 'top-right'); + +// --- Contrôle photo aérienne (LayerSwitcherControl) --- (second, below Plans button) +if (typeof LayerSwitcherControl !== 'undefined') { + const layerSwitcher = new LayerSwitcherControl({ + layers: [ + { + id: 'ortho', + name: 'Orthophotos', + icon: 'satellite', + group: 'ortho', + beforeLayer: 'traffic-light', + layer: { + type: 'raster', + paint: { 'raster-opacity': 1 } + }, + sublayers: [ + { + id: 'urbis', + name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)', + default: true, + source: { + type: 'raster', + tiles: [ + window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}' + ], + tileSize: 256, + attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS' + } + }, + { + id: 'vlaanderen', + name: 'Vlaanderen 2023', + source: { + type: 'raster', + tiles: [ + 'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}' + ], + tileSize: 256, + attribution: '© Digitaal Vlaanderen - Orthofoto 2023' + } + } + ] + } + ] + }); + map.addControl(layerSwitcher, 'top-right'); +} + +// Classic Zoom & Bearing/Compass Control in Top Right (below Orthophotos control) +map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'top-right'); + +map.on('load', () => { + + const pointData = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [feature_lon, feature_lat] + }, + properties: { + name: '...', + status: feature_status + } + } + ] + }; + + // Add the circle as a GeoJSON source for the intersection + map.addSource('traffic-light', { + type: 'geojson', + data: pointData + }); + + if (typeof feature_is_radar !== 'undefined' && feature_is_radar) { + // Register cross SDF icons for radar locations + const sz = 36; + const pad = 6; + if (!map.hasImage('cross-sdf')) { + const canvas = document.createElement('canvas'); + canvas.width = sz; canvas.height = sz; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, sz, sz); + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 6; + ctx.lineCap = 'round'; + ctx.beginPath(); + // Horizontal + ctx.moveTo(pad, sz / 2); + ctx.lineTo(sz - pad, sz / 2); + // Vertical + ctx.moveTo(sz / 2, pad); + ctx.lineTo(sz / 2, sz - pad); + ctx.stroke(); + map.addImage('cross-sdf', { width: sz, height: sz, data: ctx.getImageData(0, 0, sz, sz).data }, { sdf: true }); + } + if (!map.hasImage('cross-sdf-border')) { + const bc = document.createElement('canvas'); + bc.width = sz; bc.height = sz; + const bctx = bc.getContext('2d'); + bctx.clearRect(0, 0, sz, sz); + bctx.strokeStyle = '#000000'; + bctx.lineWidth = 12; + bctx.lineCap = 'round'; + bctx.beginPath(); + // Horizontal + bctx.moveTo(pad, sz / 2); + bctx.lineTo(sz - pad, sz / 2); + // Vertical + bctx.moveTo(sz / 2, pad); + bctx.lineTo(sz / 2, sz - pad); + bctx.stroke(); + map.addImage('cross-sdf-border', { width: sz, height: sz, data: bctx.getImageData(0, 0, sz, sz).data }, { sdf: true }); + } + + map.addLayer({ + 'id': 'traffic-light-outline', + 'type': 'symbol', + 'source': 'traffic-light', + 'layout': { + 'icon-image': 'cross-sdf-border', + 'icon-size': ['interpolate', ['linear'], ['zoom'], 12, 0.4, 22, 1.2], + 'icon-allow-overlap': true, + 'icon-ignore-placement': true, + }, + 'paint': { + 'icon-color': '#ffffff' + } + }); + + map.addLayer({ + 'id': 'traffic-light', + 'type': 'symbol', + 'source': 'traffic-light', + 'layout': { + 'icon-image': 'cross-sdf', + 'icon-size': ['interpolate', ['linear'], ['zoom'], 12, 0.4, 22, 1.2], + 'icon-allow-overlap': true, + 'icon-ignore-placement': true, + }, + 'paint': { + 'icon-color': [ + 'match', ['get', 'status'], + 'active', '#0094ff', + 'in_preparation', '#f59f00', + 'under_repair', '#f76707', + 'voluntarily_out_of_service', '#e03131', + 'inactive', '#e03131', + 'archived', '#6c757d', + '#6c757d' + ] + } + }); + } else { + map.addLayer({ + 'id': 'traffic-light', + 'type': 'circle', + 'source': 'traffic-light', + 'paint': { + 'circle-radius': { + 'base': 1.75, + 'stops': [ + [12, 2], + [22, 300] + ] + }, + 'circle-color': [ + 'match', ['get', 'status'], + 'active', '#0094ff', + 'in_preparation', '#f59f00', + 'under_repair', '#f76707', + 'voluntarily_out_of_service', '#e03131', + 'inactive', '#e03131', + 'archived', '#6c757d', + '#6c757d' + ], + 'circle-opacity': 0.7 + } + }); + } + + // Add assets layer + if (typeof assets_geojson !== 'undefined' && assets_geojson.features && assets_geojson.features.length > 0) { + console.log('Adding assets layer with', assets_geojson.features.length, 'features'); + + const iconSize = [ + 'interpolate', ['linear'], ['zoom'], + 8, 0.1, + 14, 0.5, + 22, 1 + ]; + + // Normalise the image onto a fixed-size canvas so that icon-size + // acts as a true multiplier regardless of the SVG's intrinsic dimensions. + function loadImage(url, size = 64) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + const scale = Math.min(size / img.width, size / img.height); + const w = img.width * scale; + const h = img.height * scale; + ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); + resolve(ctx.getImageData(0, 0, size, size)); + }; + img.onerror = (e) => reject(e); + img.src = url; + }); + } + + function registerBootstrapGlyphSdfIcon(map, iconName, codepoint, size = 32, color = '#333333') { + if (map.hasImage(iconName)) return Promise.resolve(); + + return document.fonts.ready.then(() => { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, size, size); + ctx.font = `${Math.round(size * 0.85)}px "bootstrap-icons"`; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(String.fromCodePoint(parseInt(codepoint, 16)), size / 2, size / 2); + const imageData = ctx.getImageData(0, 0, size, size); + if (!map.hasImage(iconName)) { + map.addImage(iconName, imageData); + } + }); + } + + Promise.all([ + loadImage(traffic_light_icon_url, 64), + loadImage(simple_pole_icon_url, 64), + loadImage(electrical_cabinet_icon_url, 32), + loadImage(radar_preventif_icon_url, 64), + registerBootstrapGlyphSdfIcon(map, 'icon-trafficlightsequipment', 'f1d5', 32, '#331110') + ]).then(([tlImg, poleImg, cabinetImg, radarImg]) => { + map.addImage('traffic-light-icon', tlImg); + map.addImage('simple-pole-icon', poleImg); + map.addImage('electrical-cabinet-icon', cabinetImg); + map.addImage('radar-preventif-icon', radarImg); + + // Single source for all assets + map.addSource('assets', { + type: 'geojson', + data: assets_geojson + }); + + // Layer for cables + map.addLayer({ + id: 'assets-cables', + type: 'line', + source: 'assets', + filter: ['==', ['get', 'asset_type'], 'trafficlightcable'], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': ['coalesce', ['get', 'color'], '#e53935'], + 'line-width': 2, + 'line-opacity': 0.9, + 'line-dasharray': [2.5, 2] + } + }); + + // Layer for traffic light poles (standard and radar poles) + map.addLayer({ + id: 'assets-poles', + type: 'symbol', + source: 'assets', + filter: ['match', ['get', 'asset_type'], ['trafficlightpole', 'trafficlightradarpole'], true, false], + layout: { + 'icon-image': [ + 'match', + ['get', 'asset_type'], + 'trafficlightradarpole', 'simple-pole-icon', + 'traffic-light-icon' + ], + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'name'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 11, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-allow-overlap': false, + 'text-optional': true + }, + paint: { + 'icon-opacity': 0.9, + 'text-color': '#333333', + 'text-halo-color': 'rgba(255, 255, 255, 0.85)', + 'text-halo-width': 2 + } + }); + + // Layer for prevention radars + map.addLayer({ + id: 'assets-radars', + type: 'symbol', + source: 'assets', + filter: ['==', ['get', 'asset_type'], 'trafficlightradar'], + layout: { + 'icon-image': 'radar-preventif-icon', + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'name'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 11, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-allow-overlap': false, + 'text-optional': true + }, + paint: { + 'icon-opacity': 0.9, + 'text-color': '#333333', + 'text-halo-color': 'rgba(255, 255, 255, 0.85)', + 'text-halo-width': 2 + } + }); + + // Layer for electrical cabinets + map.addLayer({ + id: 'assets-cabinets', + type: 'symbol', + source: 'assets', + filter: ['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'], + layout: { + 'icon-image': 'electrical-cabinet-icon', + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'name'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 11, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-allow-overlap': false, + 'text-optional': true + }, + paint: { + 'icon-opacity': 0.9, + 'text-color': '#333333', + 'text-halo-color': 'rgba(255, 255, 255, 0.85)', + 'text-halo-width': 2 + } + }); + + // Layer for generic equipment (controllers, detectors, hardwares) + map.addLayer({ + id: 'assets-equipment', + type: 'symbol', + source: 'assets', + filter: ['match', ['get', 'asset_type'], ['trafficlightcontroller', 'trafficlightdetector', 'trafficlightcontrollerhardware', 'trafficlightnetworkhardware', 'trafficlightvoltagehardware'], true, false], + layout: { + 'icon-image': 'icon-trafficlightsequipment', + 'icon-size': iconSize, + 'icon-allow-overlap': true, + 'text-field': ['get', 'name'], + 'text-font': ['Noto Sans Regular'], + 'text-size': 11, + 'text-anchor': 'top', + 'text-offset': [0, 1.2], + 'text-allow-overlap': false, + 'text-optional': true + }, + paint: { + 'icon-opacity': 0.9, + 'text-color': '#333333', + 'text-halo-color': 'rgba(255, 255, 255, 0.85)', + 'text-halo-width': 2 + } + }); + + // Click handler for all layers + ['assets-poles', 'assets-cabinets', 'assets-radars', 'assets-equipment', 'assets-cables'].forEach(layerId => { + map.on('click', layerId, (e) => { + if (e.features.length > 0) { + const feature = e.features[0]; + if (feature.properties.detail_url) { + window.location.href = feature.properties.detail_url; + } + } + }); + map.on('mouseenter', layerId, () => { + map.getCanvas().style.cursor = 'pointer'; + }); + map.on('mouseleave', layerId, () => { + map.getCanvas().style.cursor = ''; + }); + }); + + }).catch(e => { + console.error('Failed to load asset icons:', e); + }); + } + +}); + + + +const fileInput = document.getElementById('fileInput'); +const fileContainer = document.getElementById('fileFieldsContainer'); + +// Configuration pour l'ancien système (fallback) +const documentTypeChoices = { + 'as_built': 'As Built', + 'cross_plan': 'Plan de croix', + 'other': 'Autre', + 'programming_file': 'Fichier de programmation', + 'connexion_plan': 'Plan de raccordement' +}; + +// Récupérer la configuration du nouveau système de gestion documentaire +let documentConfig = { use_managed_documents: false, can_add_unique_tag_documents: false, config_url: null }; +let availableTags = []; +let availableFolders = []; + +try { + const configEl = document.getElementById('documentConfigData'); + if (configEl) { + documentConfig = JSON.parse(configEl.textContent); + } +} catch (e) { + console.log('Document config not found, using legacy system'); +} + +// Charger la configuration complète via l'API +async function loadDocumentConfig() { + if (!documentConfig.config_url) return; + + try { + const response = await fetch(documentConfig.config_url); + const data = await response.json(); + + documentConfig.use_managed_documents = data.use_managed_documents; + documentConfig.can_add_unique_tag_documents = data.can_add_unique_tag_documents; + availableTags = data.tags || []; + availableFolders = data.folders || []; + + // Exposer globalement pour le gestionnaire de fichiers + window.availableTags = availableTags; + window.availableFolders = availableFolders; + } catch (e) { + console.log('Failed to load document config:', e); + } +} + +// Charger la config au démarrage +loadDocumentConfig(); + +function renderFileFields() { + if (!fileContainer) return; + + fileContainer.innerHTML = ''; + + const files = Array.from(fileInput.files); + files.forEach((file, index) => { + const row = document.createElement('div'); + row.classList.add('d-flex', 'align-items-center', 'gap-3', 'mb-2', 'p-2', 'border', 'rounded'); + + // Nom du fichier (en lecture seule) + const fileName = document.createElement('span'); + fileName.classList.add('flex-grow-1', 'text-truncate'); + fileName.style.maxWidth = '200px'; + fileName.textContent = file.name; + fileName.title = file.name; + row.appendChild(fileName); + + if (documentConfig.use_managed_documents && availableTags && availableTags.length > 0) { + // Nouveau système: select pour le tag + const tagSelect = document.createElement('select'); + tagSelect.name = `tag_${index}`; + tagSelect.classList.add('form-select', 'w-auto'); + tagSelect.style.minWidth = '150px'; + + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = '-- Choisir un tag --'; + tagSelect.appendChild(defaultOption); + + availableTags.forEach(tag => { + const option = document.createElement('option'); + option.value = tag.slug; + option.textContent = tag.name; + option.dataset.isUnique = tag.is_unique; + // Désactiver les tags uniques si l'utilisateur n'a pas les permissions + if (tag.is_unique && !documentConfig.can_add_unique_tag_documents) { + option.disabled = true; + option.textContent += ' (non autorisé)'; + } + tagSelect.appendChild(option); + }); + row.appendChild(tagSelect); + + // Select pour le répertoire (affiché uniquement pour les tags non-uniques) + const folderSelect = document.createElement('select'); + folderSelect.name = `folder_${index}`; + folderSelect.classList.add('form-select', 'w-auto', 'd-none'); + folderSelect.style.minWidth = '180px'; + + if (availableFolders && availableFolders.length > 0) { + availableFolders.forEach(folder => { + const option = document.createElement('option'); + option.value = folder.id; + option.textContent = folder.path; + if (folder.is_root) { + option.selected = true; + } + folderSelect.appendChild(option); + }); + } + row.appendChild(folderSelect); + + // Champ pour les paramètres d'unicité (cables pour crossplan) + const uniqueParamInput = document.createElement('input'); + uniqueParamInput.type = 'text'; + uniqueParamInput.name = `unique_param_${index}`; + uniqueParamInput.classList.add('form-control', 'w-auto', 'd-none'); + uniqueParamInput.style.minWidth = '120px'; + uniqueParamInput.placeholder = 'Ex: A,B'; + row.appendChild(uniqueParamInput); + + // Afficher/masquer le sélecteur de répertoire et les paramètres selon le tag + tagSelect.addEventListener('change', function() { + const selectedOption = tagSelect.options[tagSelect.selectedIndex]; + const isUnique = selectedOption.dataset.isUnique === 'true' || selectedOption.dataset.isUnique === 'True'; + const tagSlug = tagSelect.value; + + if (isUnique || tagSelect.value === '') { + folderSelect.classList.add('d-none'); + } else { + folderSelect.classList.remove('d-none'); + } + + // Afficher le champ de paramètre pour crossplan + if (tagSlug === 'crossplan') { + uniqueParamInput.classList.remove('d-none'); + uniqueParamInput.placeholder = 'Câbles (ex: C1,C2)'; + } else { + uniqueParamInput.classList.add('d-none'); + } + }); + + } else { + // Ancien système: select type de document + const select = document.createElement('select'); + select.name = `document_type_${index}`; + select.classList.add('form-select', 'w-25'); + for (const [value, label] of Object.entries(documentTypeChoices)) { + const option = document.createElement('option'); + option.value = value; + option.textContent = label; + if (value === 'other') { + option.selected = true; + } + select.appendChild(option); + } + row.appendChild(select); + } + + // Bouton de suppression + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.classList.add('btn', 'btn-outline-danger', 'btn-sm'); + deleteBtn.innerHTML = ''; + deleteBtn.onclick = () => { + files.splice(index, 1); + const dt = new DataTransfer(); + files.forEach(f => dt.items.add(f)); + fileInput.files = dt.files; + renderFileFields(); // re-render + }; + row.appendChild(deleteBtn); + + fileContainer.appendChild(row); + }); + + // Total + const countInput = document.createElement('input'); + countInput.type = 'hidden'; + countInput.name = 'file_count'; + countInput.value = files.length; + fileContainer.appendChild(countInput); +} + +if (fileInput) { + fileInput.addEventListener('change', renderFileFields); +} + + +const deleteDocumentModal = document.getElementById('documentDeleteModal'); +if (deleteDocumentModal) { + deleteDocumentModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const fileName = button.getAttribute('data-filename'); + const deleteUrl = button.getAttribute('data-delete-url'); + + const fileNameContainer = document.getElementById('deleteDocumentFileName'); + const deleteForm = document.getElementById('deleteDocumentForm'); + + fileNameContainer.textContent = fileName; + deleteForm.action = deleteUrl; + + // Ensure the page reloads back to the current URL after deletion + let nextInput = deleteForm.querySelector('input[name="next"]'); + if (!nextInput) { + nextInput = document.createElement('input'); + nextInput.type = 'hidden'; + nextInput.name = 'next'; + deleteForm.appendChild(nextInput); + } + nextInput.value = window.location.href; + }); +} + +historyModal = document.getElementById('documentHistoryModal'); +if (historyModal) { + document.getElementById('documentHistoryModal').addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const type = button.getAttribute('data-document-type'); + const tagSlug = button.getAttribute('data-tag-slug'); + // Support both the generic data-location-id (document_viewer component) + // and the legacy data-intersection-id (old-system buttons). + const intersectionId = button.getAttribute('data-location-id') || button.getAttribute('data-intersection-id'); + const useManaged = button.getAttribute('data-use-managed') === 'true'; + const uniqueParams = button.getAttribute('data-unique-params'); + + let url; + if (useManaged && tagSlug) { + // Nouveau système de gestion documentaire + url = `${document_history_url}?intersection_id=${intersectionId}&tag_slug=${tagSlug}`; + if (uniqueParams && uniqueParams !== '{}' && uniqueParams !== 'None') { + url += `&unique_params=${encodeURIComponent(uniqueParams)}`; + } + } else { + // Ancien système + url = `${document_history_url}?intersection_id=${intersectionId}&document_type=${type}`; + } + + fetch(url) + .then(response => response.json()) + .then(data => { + document.getElementById('documentHistoryContent').innerHTML = data.html; + }); + }); +} + +// ── DXF Background Layer Loader ── +function ensureDxfSourcesAndLayers() { + if (!map.getSource('dxf-layout')) { + map.addSource('dxf-layout', { + type: 'geojson', + data: { type: "FeatureCollection", features: [] } + }); + + const beforeId = map.getLayer('traffic-light') ? 'traffic-light' : undefined; + + map.addLayer({ + 'id': 'dxf-layout-fills', + 'type': 'fill', + 'source': 'dxf-layout', + 'layout': { + 'visibility': 'visible' + }, + 'paint': { + 'fill-color': ['coalesce', ['get', 'color'], '#94a3b8'], + 'fill-opacity': 0.1 + } + }, beforeId); + + map.addLayer({ + 'id': 'dxf-layout-lines', + 'type': 'line', + 'source': 'dxf-layout', + 'layout': { + 'visibility': 'visible' + }, + 'paint': { + 'line-color': ['coalesce', ['get', 'color'], '#475569'], + 'line-width': 1.2, + 'line-opacity': 0.7 + } + }, beforeId); + + map.addLayer({ + 'id': 'dxf-layout-symbols', + 'type': 'symbol', + 'source': 'dxf-layout', + 'filter': ['has', 'Text'], + 'layout': { + 'visibility': 'visible', + 'text-field': ['get', 'Text'], + 'text-font': ['Noto Sans Regular'], + 'text-size': [ + 'interpolate', + ['linear'], + ['zoom'], + 17, ['coalesce', ['*', ['get', 'text_height'], 6], 10], + 21, ['coalesce', ['*', ['get', 'text_height'], 24], 14] + ], + 'text-rotate': ['coalesce', ['*', ['get', 'text_angle'], -1], 0], + 'text-rotation-alignment': 'map', + 'text-anchor': ['coalesce', ['get', 'text_anchor'], 'center'], + 'text-allow-overlap': true, + 'text-ignore-placement': true + }, + 'paint': { + 'text-color': ['coalesce', ['get', 'color'], '#000000'], + 'text-halo-color': '#ffffff', + 'text-halo-width': 2 + } + }, beforeId); + } +} + +// ── Georeferenced Plans & DXF Background Loader ── +const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData'); +let georeferencedDocuments = []; +if (georeferencedDocumentsDataEl) { + try { + georeferencedDocuments = JSON.parse(georeferencedDocumentsDataEl.textContent); + } catch (e) { + console.error("Error parsing georeferenced documents data:", e); + } +} + + + +function showGeorefImage(doc, active, opacity) { + const srcId = `georef-image-overlay-${doc.id}`; + const lyrId = `georef-image-overlay-layer-${doc.id}`; + + if (!active) { + if (map.getLayer(lyrId)) { + map.setLayoutProperty(lyrId, 'visibility', 'none'); + } + return; + } + + if (map.getSource(srcId)) { + if (map.getLayer(lyrId)) { + map.setLayoutProperty(lyrId, 'visibility', 'visible'); + map.setPaintProperty(lyrId, 'raster-opacity', opacity); + } + } else { + map.addSource(srcId, { + type: 'image', + url: doc.url, + coordinates: doc.coords + }); + + let beforeId = undefined; + if (map.getLayer('dxf-layout-fills')) { + beforeId = 'dxf-layout-fills'; + } else if (map.getLayer('dxf-layout-lines')) { + beforeId = 'dxf-layout-lines'; + } else if (map.getLayer('traffic-light')) { + beforeId = 'traffic-light'; + } + + map.addLayer({ + 'id': lyrId, + 'type': 'raster', + 'source': srcId, + 'layout': { 'visibility': 'visible' }, + 'paint': { 'raster-opacity': opacity } + }, beforeId); + } + + // Zoom/fitBounds to document + const coords = doc.coords; + const bounds = coords.reduce((acc, coord) => { + return acc.extend(coord); + }, new maplibregl.LngLatBounds(coords[0], coords[0])); + map.fitBounds(bounds, { padding: 40, maxZoom: 19 }); +} + +let fullDxfGeoJson = null; + +function fetchDxfGeoJson(file) { + const spinner = document.getElementById('dxf-detail-layers-spinner'); + if (spinner) spinner.style.display = 'block'; + + const url = `/assets/trafficlights/intersections/${intersectionId}/dxf-layout-geojson/?file=${encodeURIComponent(file)}&detail=true&layers=all`; + fetch(url) + .then(response => response.json()) + .then(data => { + fullDxfGeoJson = data; + applyDxfVectorFilters(); + if (spinner) spinner.style.display = 'none'; + }) + .catch(err => { + console.error("Error fetching DXF GeoJSON:", err); + if (spinner) spinner.style.display = 'none'; + }); +} + +function applyDxfVectorFilters() { + if (!fullDxfGeoJson) return; + + const selectedLayers = []; + document.querySelectorAll('.dxf-vector-layer-toggle:checked').forEach(cb => { + selectedLayers.push(cb.dataset.layer); + }); + + const filteredFeatures = fullDxfGeoJson.features.filter(f => { + const layerName = f.properties && (f.properties.Layer || f.properties.layer); + return selectedLayers.includes(layerName); + }); + + const filteredData = { + ...fullDxfGeoJson, + features: filteredFeatures + }; + + ensureDxfSourcesAndLayers(); + if (map.getSource('dxf-layout')) { + map.getSource('dxf-layout').setData(filteredData); + + ['dxf-layout-fills', 'dxf-layout-lines', 'dxf-layout-symbols'].forEach(lId => { + if (map.getLayer(lId)) { + map.setLayoutProperty(lId, 'visibility', 'visible'); + } + }); + } +} + +function updateButtonActiveState() { + const btn = document.getElementById('btn-load-georef-plan'); + if (!btn) return; + + let anyImageActive = false; + document.querySelectorAll('.image-plan-toggle').forEach(cb => { + if (cb.checked) anyImageActive = true; + }); + + let anyVectorActive = false; + document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => { + if (cb.checked) anyVectorActive = true; + }); + + if (anyImageActive || anyVectorActive) { + btn.classList.remove('btn-light', 'bg-white', 'border'); + btn.classList.add('btn-primary'); + } else { + btn.classList.remove('btn-primary'); + btn.classList.add('btn-light', 'bg-white', 'border'); + } +} + +const noGeorefImagesAlert = document.getElementById('no-georef-images-alert'); +const imagePlansList = document.getElementById('image-plans-list'); +if (imagePlansList) { + imagePlansList.innerHTML = ''; + if (georeferencedDocuments.length === 0) { + if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'block'; + } else { + if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'none'; + + georeferencedDocuments.forEach(doc => { + const item = document.createElement('div'); + item.className = 'image-plan-item p-3 bg-light border rounded-3 d-flex flex-column gap-2'; + item.innerHTML = ` +
+ + ${doc.title} + +
+ +
+
+ + `; + imagePlansList.appendChild(item); + + const toggle = item.querySelector('.image-plan-toggle'); + const opacityContainer = item.querySelector('.image-plan-opacity-container'); + const slider = item.querySelector('.image-plan-opacity-slider'); + const opacityVal = item.querySelector('.opacity-val'); + + toggle.addEventListener('change', () => { + const active = toggle.checked; + if (active) { + opacityContainer.style.display = 'block'; + } else { + opacityContainer.style.display = 'none'; + } + const opacity = parseFloat(slider.value) / 100.0; + showGeorefImage(doc, active, opacity); + updateButtonActiveState(); + }); + + slider.addEventListener('input', () => { + const val = slider.value; + opacityVal.textContent = val + '%'; + const opacity = parseFloat(val) / 100.0; + showGeorefImage(doc, toggle.checked, opacity); + }); + }); + } +} + +const dxfFileSelect = document.getElementById('select-dxf-file'); +let detailLayersFetched = {}; +let detailLayersResponseData = {}; + +function fetchDetailLayers() { + if (!dxfFileSelect) return; + const file = dxfFileSelect.value; + if (!file) { + const section = document.getElementById('dxf-detail-layers-section'); + if (section) section.style.display = 'none'; + return; + } + + if (detailLayersFetched[file]) { + renderDetailLayers(file); + fetchDxfGeoJson(file); + return; + } + + const spinner = document.getElementById('dxf-detail-layers-spinner'); + const list = document.getElementById('dxf-detail-layers-list'); + const section = document.getElementById('dxf-detail-layers-section'); + if (section) section.style.display = 'block'; + if (spinner) spinner.style.display = 'block'; + if (list) list.style.display = 'none'; + + fetch(`/assets/trafficlights/intersections/${intersectionId}/dxf-layers/?file=${encodeURIComponent(file)}`) + .then(response => response.json()) + .then(data => { + detailLayersFetched[file] = data.layers || []; + detailLayersResponseData[file] = data; + renderDetailLayers(file); + fetchDxfGeoJson(file); + if (spinner) spinner.style.display = 'none'; + if (list) list.style.display = 'block'; + }) + .catch(err => { + console.error("Error fetching detail layers:", err); + if (spinner) { + spinner.innerHTML = `
Erreur de chargement.
`; + } + }); +} + +function renderDetailLayers(file) { + const container = document.getElementById('dxf-detail-layers-list'); + if (!container) return; + container.innerHTML = ''; + const layers = detailLayersFetched[file] || []; + const responseData = detailLayersResponseData[file] || {}; + const selectedLayers = responseData.selected_layers || []; + + layers.forEach(layer => { + const isChecked = false; + const div = document.createElement('div'); + div.className = 'form-check py-1 d-flex align-items-center justify-content-between'; + div.innerHTML = ` +
+ + +
+
+ +
+ `; + container.appendChild(div); + + const toggle = div.querySelector('.dxf-vector-layer-toggle'); + toggle.addEventListener('change', () => { + applyDxfVectorFilters(); + updateButtonActiveState(); + }); + }); +} + +if (dxfFileSelect) { + dxfFileSelect.addEventListener('change', () => { + fetchDetailLayers(); + }); + + // Auto-load if there is a default select value or single dxf + if (dxfFileSelect.value) { + fetchDetailLayers(); + } +} + +const btnToggleAllLayers = document.getElementById('dxf-detail-toggle-all-layers'); +if (btnToggleAllLayers) { + btnToggleAllLayers.addEventListener('click', () => { + const checkboxes = document.querySelectorAll('.dxf-vector-layer-toggle'); + const allChecked = Array.from(checkboxes).every(cb => cb.checked); + checkboxes.forEach(cb => { + cb.checked = !allChecked; + }); + btnToggleAllLayers.textContent = allChecked ? "Tout cocher" : "Décocher tout"; + applyDxfVectorFilters(); + updateButtonActiveState(); + }); +} + +const btnDisableAllPlans = document.getElementById('btn-disable-all-plans'); +if (btnDisableAllPlans) { + btnDisableAllPlans.addEventListener('click', () => { + document.querySelectorAll('.image-plan-toggle').forEach(cb => { + if (cb.checked) { + cb.checked = false; + cb.dispatchEvent(new Event('change')); + } + }); + + document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => { + if (cb.checked) { + cb.checked = false; + } + }); + applyDxfVectorFilters(); + + const modalEl = document.getElementById('georefPlanModal'); + if (modalEl) { + const modal = bootstrap.Modal.getInstance(modalEl); + if (modal) modal.hide(); + } + + updateButtonActiveState(); + }); +} \ No newline at end of file diff --git a/streetup/assets/static/assets/unified_index.js b/streetup/assets/static/assets/unified_index.js new file mode 100644 index 0000000..fab0123 --- /dev/null +++ b/streetup/assets/static/assets/unified_index.js @@ -0,0 +1,3896 @@ +/** + * unified_index.js + * Handles the unified assets+locations index page. + * Expects the following globals injected by the template: + * THEMATIC_CONFIG, CONDITIONAL_FILTER_CONFIG, THEMATIC_CATEGORIES, SELECTED_THEMATICS, + * PREVENTIVE_DRAFT_ID, LOCKED_THEMATIC_CODE, PREVENTIVE_ELEMENT_TYPE, + * SELECTED_ASSETS, SELECTED_LOCATION_IDS, LINK_ASSETS_URL, + * ASSETS_INDEX_PREFS, DEFAULT_THEMATIC_CODE, SAVE_ASSETS_PREFS_URL + */ + +'use strict'; + +// ─── State ──────────────────────────────────────────────────────────────────── + +var allFeatures = []; // merged features from all loaded thematics +var filteredFeatures = []; // after type+search+status+category filters +var activeThematics = new Set(SELECTED_THEMATICS || []); +var activeCategories = new Set(SELECTED_CATEGORIES || []); +var activeModels = new Set(); +var modelFilterCollapsed = true; +var activeConditionalFilters = {}; +var currentTypeFilter = 'all'; // 'all' | 'locations' | 'assets' +var searchTerms = []; +var searchMode = 'and'; +var shouldFitSearch = false; +var searchIsGlobal = false; +var selectedIds = new Set(); // ids of checked rows +var loadingCount = 0; + +var categoryLabelByThematicAndId = {}; +var categoryValueByThematicAndLabel = {}; +var modelsByCategoryKey = {}; +var pendingModelFetches = {}; + +// GeoJSON caches per thematic { thematic_code: { locations: [], assets: [] } } +var geojsonCache = {}; +var locationsFetchSignatures = {}; +var assetsFetchSignatures = {}; + +// Assets: thematics that always load all assets (small datasets) +const ALWAYS_LOAD_ASSETS_THEMATICS = new Set(['artworks', 'controlcenters']); +const MIN_ASSETS_ZOOM = 14; // zoom level at which bbox-based asset loading triggers +var assetsLoadModes = {}; // thematic_code → null | 'always' | 'filter' | 'bbox' + +// MapLibre sources +var mergedLocationsGeojson = { type: 'FeatureCollection', features: [] }; +var mergedAssetsGeojson = { type: 'FeatureCollection', features: [] }; +var currentFetchAbortController = null; +var assetClickPopup = null; +var zoomFocusMarker = null; +var loadWarningVisible = false; +var unifiedAddAssetThematic = null; +var unifiedAddLocationThematic = null; +var unifiedLocationPickerMap = null; +var unifiedLocationPickerMarker = null; +var unifiedLocationPickerControl = null; +var panelTextSpriteMap = null; +var unifiedPanelOffsetRefreshTimer = null; + +function shouldUseTrafficlightsLocationCreateFlow(thematic) { + return !!(thematic && thematic.code === 'trafficlights' && thematic.location_type); +} + +function getUnifiedAddModalConfig() { + if (typeof ADD_MODAL_CONFIG !== 'object' || ADD_MODAL_CONFIG === null) { + return { asset_thematics: [], location_thematics: [] }; + } + return ADD_MODAL_CONFIG; +} + +function setModalAlert(alertId, message, variant) { + const alert = document.getElementById(alertId); + if (!alert) return; + if (!message) { + alert.classList.add('d-none'); + alert.textContent = ''; + return; + } + alert.className = `alert alert-${variant || 'info'}`; + alert.innerHTML = message; + alert.classList.remove('d-none'); +} + +function fillSelectWithOptions(select, items, placeholder) { + if (!select) return; + select.innerHTML = ''; + if (placeholder) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = placeholder; + select.appendChild(option); + } + (items || []).forEach(item => { + const option = document.createElement('option'); + option.value = item.code; + option.textContent = item.name; + if (item.icon) { + option.dataset.icon = item.icon; + } + if (item.location_type) { + option.dataset.locationType = item.location_type; + } + if (item.location_label) { + option.dataset.locationLabel = item.location_label; + } + select.appendChild(option); + }); +} + +async function loadUnifiedAssetCategories(thematicCode) { + const categorySelect = document.getElementById('unifiedAddAssetCategory'); + const modelSelect = document.getElementById('unifiedAddAssetModel'); + if (!categorySelect || !modelSelect || !thematicCode) return; + + categorySelect.disabled = true; + modelSelect.disabled = true; + modelSelect.innerHTML = ``; + categorySelect.innerHTML = ``; + + try { + const response = await fetch(`/assets/${thematicCode}/categories/`); + const data = await response.json(); + const categories = Array.isArray(data.categories) ? data.categories : []; + categorySelect.innerHTML = ``; + categories.forEach(category => { + const option = document.createElement('option'); + option.value = category.id; + option.textContent = category.name || category.code || category.id; + categorySelect.appendChild(option); + }); + categorySelect.disabled = false; + if (!categories.length) { + categorySelect.innerHTML = ``; + } + } catch (error) { + console.error('Error loading asset categories:', error); + categorySelect.innerHTML = ``; + } +} + +function getUnifiedAssetLocationConfig(thematicCode) { + const thematicConfig = THEMATIC_CONFIG && THEMATIC_CONFIG[thematicCode] ? THEMATIC_CONFIG[thematicCode] : null; + if (!thematicConfig || !thematicConfig.locations_geojson_url) { + return null; + } + return thematicConfig; +} + +async function loadUnifiedAssetLocations(thematicCode) { + const group = document.getElementById('unifiedAddAssetLocationGroup'); + const locationSelect = document.getElementById('unifiedAddAssetLocation'); + const locationHelp = document.getElementById('unifiedAddAssetLocationHelp'); + const submitBtn = document.getElementById('unifiedAddAssetSubmit'); + const locationConfig = getUnifiedAssetLocationConfig(thematicCode); + + if (!group || !locationSelect) return; + + if (!locationConfig) { + group.classList.add('d-none'); + locationSelect.innerHTML = ''; + locationSelect.disabled = true; + locationSelect.required = false; + if (submitBtn) submitBtn.disabled = false; + if (locationHelp) locationHelp.textContent = ''; + return; + } + + group.classList.remove('d-none'); + locationSelect.disabled = true; + locationSelect.required = true; + locationSelect.innerHTML = ``; + if (locationHelp) { + locationHelp.textContent = gettext('Sélectionnez la localisation liée à ce nouvel asset.'); + } + + try { + const response = await fetch(locationConfig.locations_geojson_url); + const data = await response.json(); + const features = Array.isArray(data.features) ? data.features : []; + locationSelect.innerHTML = ``; + + features.forEach(feature => { + const props = feature.properties || {}; + const locationId = feature.id ?? props.id ?? props.object_id ?? ''; + if (locationId === '') return; + const code = props.code || ''; + const name = props.name || props.name_fr || props.name_nl || ''; + const label = [code, name].filter(Boolean).join(' - ') || String(locationId); + const option = document.createElement('option'); + option.value = locationId; + option.textContent = label; + locationSelect.appendChild(option); + }); + + if (features.length > 0) { + locationSelect.disabled = false; + if (submitBtn) submitBtn.disabled = false; + } else { + locationSelect.innerHTML = ``; + locationSelect.disabled = true; + if (submitBtn) submitBtn.disabled = true; + if (locationHelp) { + locationHelp.textContent = gettext('Aucune localisation disponible pour créer cet asset.'); + } + } + } catch (error) { + console.error('Error loading asset locations:', error); + locationSelect.innerHTML = ``; + locationSelect.disabled = true; + if (submitBtn) submitBtn.disabled = true; + if (locationHelp) { + locationHelp.textContent = gettext('Impossible de charger les localisations pour cette thématique.'); + } + } +} + +async function loadUnifiedAssetModels(thematicCode, categoryId) { + const modelSelect = document.getElementById('unifiedAddAssetModel'); + if (!modelSelect || !thematicCode) return; + + modelSelect.disabled = true; + if (!categoryId) { + modelSelect.innerHTML = ``; + return; + } + + modelSelect.innerHTML = ``; + + try { + const response = await fetch(`/assets/${thematicCode}/category/${categoryId}/models/`); + const data = await response.json(); + const models = Array.isArray(data.models) ? data.models : []; + modelSelect.innerHTML = ``; + models.forEach(model => { + const option = document.createElement('option'); + option.value = model.id; + option.textContent = model.name || model.code || model.id; + if (model.model_type) { + option.dataset.modelType = model.model_type; + } + modelSelect.appendChild(option); + }); + if (models.length) { + modelSelect.disabled = false; + } else { + modelSelect.innerHTML = ``; + } + } catch (error) { + console.error('Error loading asset models:', error); + modelSelect.innerHTML = ``; + } +} + +function resetUnifiedAssetForm() { + const form = document.getElementById('unifiedAddAssetForm'); + const categorySelect = document.getElementById('unifiedAddAssetCategory'); + const modelSelect = document.getElementById('unifiedAddAssetModel'); + const submitBtn = document.getElementById('unifiedAddAssetSubmit'); + if (form) form.reset(); + if (categorySelect) { + categorySelect.innerHTML = ``; + categorySelect.disabled = true; + } + if (modelSelect) { + modelSelect.innerHTML = ``; + modelSelect.disabled = true; + } + const locationGroup = document.getElementById('unifiedAddAssetLocationGroup'); + const locationSelect = document.getElementById('unifiedAddAssetLocation'); + const locationHelp = document.getElementById('unifiedAddAssetLocationHelp'); + const codeGroup = document.getElementById('unifiedAddAssetCodeGroup'); + const codeInput = document.getElementById('unifiedAddAssetCode'); + const codeHelp = document.getElementById('unifiedAddAssetCodeHelp'); + if (locationGroup) locationGroup.classList.add('d-none'); + if (locationSelect) { + locationSelect.innerHTML = ''; + locationSelect.disabled = true; + locationSelect.required = false; + } + if (locationHelp) locationHelp.textContent = ''; + if (codeGroup) codeGroup.classList.remove('d-none'); + if (codeInput) codeInput.required = true; + if (codeHelp) { + codeHelp.textContent = ''; + codeHelp.classList.add('d-none'); + } + if (submitBtn) submitBtn.disabled = false; + setModalAlert('unifiedAddAssetAlert', '', 'info'); +} + +async function openUnifiedAssetModal(thematicCode) { + const config = getUnifiedAddModalConfig(); + const thematic = (config.asset_thematics || []).find(item => item.code === thematicCode); + if (!thematic) return; + + unifiedAddAssetThematic = thematic; + document.getElementById('unifiedAddAssetModalLabel').textContent = `${gettext('Créer un asset')} - ${thematic.name}`; + resetUnifiedAssetForm(); + document.getElementById('unified-add-asset-thematic-code').value = thematic.code; + await loadUnifiedAssetCategories(thematic.code); + + const codeGroup = document.getElementById('unifiedAddAssetCodeGroup'); + const codeInput = document.getElementById('unifiedAddAssetCode'); + const codeHelp = document.getElementById('unifiedAddAssetCodeHelp'); + + if (shouldUseTrafficlightsLocationCreateFlow(thematic)) { + await loadUnifiedAssetLocations(thematic.code); + if (codeGroup) codeGroup.classList.add('d-none'); + if (codeInput) codeInput.required = false; + if (codeHelp) { + codeHelp.textContent = gettext('Le code est généré automatiquement.'); + codeHelp.classList.remove('d-none'); + } + } + + bootstrap.Modal.getOrCreateInstance(document.getElementById('unifiedAddAssetModal')).show(); +} + +function initUnifiedLocationPickerMap() { + const mapDiv = document.getElementById('locPickerMap'); + if (!mapDiv || typeof maplibregl === 'undefined') return; + + if (unifiedLocationPickerMap) { + unifiedLocationPickerMap.remove(); + unifiedLocationPickerMap = null; + unifiedLocationPickerMarker = null; + unifiedLocationPickerControl = null; + } + + const lonInput = document.getElementById('id_lon'); + const latInput = document.getElementById('id_lat'); + + let center = [4.3572, 50.8477]; + let zoom = 11; + if (lonInput && latInput && lonInput.value && latInput.value) { + center = [parseFloat(lonInput.value), parseFloat(latInput.value)]; + zoom = 17; + } + + unifiedLocationPickerMap = new maplibregl.Map({ + container: mapDiv, + style: window.MAP_STYLE_LIGHT, + center, + zoom, + minZoom: 9, + maxZoom: 24, + cooperativeGestures: true, + }); + + const setPoint = (lng, lat, fly = false) => { + if (lonInput) lonInput.value = String(lng); + if (latInput) latInput.value = String(lat); + + if (!unifiedLocationPickerMarker) { + unifiedLocationPickerMarker = new maplibregl.Marker({ draggable: true }) + .setLngLat([lng, lat]) + .addTo(unifiedLocationPickerMap); + unifiedLocationPickerMarker.on('dragend', () => { + const ll = unifiedLocationPickerMarker.getLngLat(); + if (lonInput) lonInput.value = String(ll.lng); + if (latInput) latInput.value = String(ll.lat); + }); + } else { + unifiedLocationPickerMarker.setLngLat([lng, lat]); + } + + if (fly) { + unifiedLocationPickerMap.flyTo({ center: [lng, lat], zoom: 17, duration: 600 }); + } + }; + + if (typeof AddressSearchControl !== 'undefined' && typeof AUTOCOMPLETE_STREET_URL !== 'undefined') { + unifiedLocationPickerControl = new AddressSearchControl({ + autocompleteUrl: AUTOCOMPLETE_STREET_URL, + lonInputId: 'id_lon', + latInputId: 'id_lat', + }); + unifiedLocationPickerMap.addControl(unifiedLocationPickerControl, 'top-left'); + + unifiedLocationPickerMap.on('load', () => { + if (lonInput && latInput && lonInput.value && latInput.value && unifiedLocationPickerControl) { + unifiedLocationPickerControl.setMarker(parseFloat(lonInput.value), parseFloat(latInput.value), false); + } + unifiedLocationPickerMap.resize(); + }); + + unifiedLocationPickerMap.on('click', (e) => { + if (unifiedLocationPickerControl) { + unifiedLocationPickerControl.setMarker(e.lngLat.lng, e.lngLat.lat); + } + }); + } else { + unifiedLocationPickerMap.on('load', () => { + if (lonInput && latInput && lonInput.value && latInput.value) { + setPoint(parseFloat(lonInput.value), parseFloat(latInput.value), false); + } + unifiedLocationPickerMap.resize(); + }); + unifiedLocationPickerMap.on('click', (e) => { + setPoint(e.lngLat.lng, e.lngLat.lat); + }); + } + + document.getElementById('locPickerGeoBtn')?.addEventListener('click', (e) => { + e.preventDefault(); + if (!navigator.geolocation) return; + navigator.geolocation.getCurrentPosition( + (pos) => { + const lng = pos.coords.longitude; + const lat = pos.coords.latitude; + if (unifiedLocationPickerControl) { + unifiedLocationPickerControl.setMarker(lng, lat); + } else { + setPoint(lng, lat, true); + } + }, + () => {}, + { enableHighAccuracy: true, timeout: 10000 } + ); + }); + + document.getElementById('locPickerClearBtn')?.addEventListener('click', (e) => { + e.preventDefault(); + if (lonInput) lonInput.value = ''; + if (latInput) latInput.value = ''; + if (unifiedLocationPickerControl) { + unifiedLocationPickerControl.clearMarker(); + } + if (unifiedLocationPickerMarker) { + unifiedLocationPickerMarker.remove(); + unifiedLocationPickerMarker = null; + } + unifiedLocationPickerMap.flyTo({ center: [4.3572, 50.8477], zoom: 11, duration: 600 }); + }); +} + +async function loadUnifiedLocationForm(thematicCode, locationType) { + const fieldsContainer = document.getElementById('unifiedAddLocationFields'); + const metaContainer = document.getElementById('unifiedAddLocationMeta'); + if (!fieldsContainer || !metaContainer || !thematicCode || !locationType) return; + + fieldsContainer.innerHTML = '
'; + + try { + const response = await fetch(`/assets/locations/${thematicCode}/${locationType}/form/`); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || gettext('Erreur lors du chargement du formulaire.')); + } + fieldsContainer.innerHTML = data.html || ''; + } catch (error) { + console.error('Error loading location form:', error); + fieldsContainer.innerHTML = `
${gettext('Erreur lors du chargement du formulaire.')}
`; + } +} + +function resetUnifiedLocationForm() { + const form = document.getElementById('unifiedAddLocationForm'); + if (form) form.reset(); + document.getElementById('unifiedAddLocationFields').innerHTML = '
'; + setModalAlert('unifiedAddLocationAlert', '', 'info'); +} + +async function openUnifiedLocationModal(thematicCode) { + const config = getUnifiedAddModalConfig(); + const thematic = (config.location_thematics || []).find(item => item.code === thematicCode); + if (!thematic) return; + + unifiedAddLocationThematic = thematic; + document.getElementById('unifiedAddLocationModalLabel').textContent = `${gettext('Créer une localisation')} - ${thematic.name}`; + document.getElementById('unifiedAddLocationMeta').textContent = `${gettext('Type')}: ${thematic.location_label}`; + resetUnifiedLocationForm(); + document.getElementById('unified-add-location-thematic-code').value = thematic.code; + document.getElementById('unified-add-location-type').value = thematic.location_type; + await loadUnifiedLocationForm(thematic.code, thematic.location_type); + + const modalEl = document.getElementById('unifiedAddLocationModal'); + const onShown = () => { + initUnifiedLocationPickerMap(); + bootstrap.Modal.getOrCreateInstance(modalEl).handleUpdate(); + modalEl.removeEventListener('shown.bs.modal', onShown); + }; + modalEl.addEventListener('shown.bs.modal', onShown); + bootstrap.Modal.getOrCreateInstance(modalEl).show(); +} + +// ─── Bootstrap-table columns ────────────────────────────────────────────────── + +function zoomFormatter(value, row) { + if (!row.lon) return ''; + return ``; +} + +function buildColumns() { + return [ + { field: 'check', checkbox: true, width: 36 }, + { field: 'actions', title: '', formatter: zoomFormatter, width: 50 }, + { field: 'code', title: gettext('Code'), sortable: true, + formatter: function(v, row) { + if (row.detail_url) return `${v || '-'}`; + return v || '-'; + } + }, + { field: 'name', title: gettext('Nom'), sortable: true }, + { field: 'element_type', title: gettext('Type'), sortable: true }, + { field: 'thematic_name', title: gettext('Thématique'), sortable: true }, + { field: 'category', title: gettext('Catégorie'), sortable: true, visible: false }, + { field: 'model', title: gettext('Modèle'), sortable: true, visible: false }, + { field: 'status', title: gettext('Statut'), sortable: true, visible: false, + formatter: function(v) { return v ? ` ${v}` : ''; } + }, + { field: 'validation_status', title: gettext('Statut de validation'), sortable: true, visible: true, + formatter: function(v) { + const labels = { + 'to_be_validated': gettext('À valider'), + 'validated': gettext('Validé'), + 'to_be_completed': gettext('À compléter'), + 'rejected': gettext('Rejeté') + }; + return labels[v] || v || '-'; + } + }, + ]; +} + +const UNIFIED_PREFERENCE_COLUMN_FIELDS = [ + 'code', + 'name', + 'element_type', + 'thematic_name', + 'category', + 'model', + 'status', + 'validation_status', +]; + +var isApplyingSavedColumnVisibility = false; + +function getVisibleColumnsPreference() { + const table = $('#unified-table'); + if (table.length && typeof table.bootstrapTable === 'function') { + const visibleColumns = table.bootstrapTable('getVisibleColumns') || []; + return visibleColumns + .map(col => col.field) + .filter(field => UNIFIED_PREFERENCE_COLUMN_FIELDS.includes(field)); + } + + return buildColumns() + .filter(col => UNIFIED_PREFERENCE_COLUMN_FIELDS.includes(col.field) && col.visible !== false) + .map(col => col.field); +} + +function applyVisibleColumnsPreference(visibleColumns) { + if (!Array.isArray(visibleColumns)) return; + + const table = $('#unified-table'); + if (!table.length || typeof table.bootstrapTable !== 'function') return; + + const wanted = new Set(visibleColumns.map(String)); + isApplyingSavedColumnVisibility = true; + UNIFIED_PREFERENCE_COLUMN_FIELDS.forEach(field => { + if (wanted.has(field)) { + table.bootstrapTable('showColumn', field); + } else { + table.bootstrapTable('hideColumn', field); + } + }); + isApplyingSavedColumnVisibility = false; +} + +// ─── Preferences: save / restore ───────────────────────────────────────────── + +/** + * Applies saved preferences (or the user's default thematic on first visit). + * Called inside document.ready BEFORE the initial refreshData() / showEmptyState(). + * Skipped entirely in preventive-intervention mode or when the URL provides + * explicit thematic selection (GET params / locked_thematic_code). + */ +function applyInitialPreferences() { + if (PREVENTIVE_DRAFT_ID || LOCKED_THEMATIC_CODE || SELECTED_THEMATICS.length > 0) return; + + if (ASSETS_INDEX_PREFS && typeof ASSETS_INDEX_PREFS === 'object' && Object.keys(ASSETS_INDEX_PREFS).length > 0) { + const prefs = ASSETS_INDEX_PREFS; + + // Thematics — only restore codes that are still accessible + if (Array.isArray(prefs.thematics) && prefs.thematics.length > 0) { + const valid = prefs.thematics.filter(code => code in THEMATIC_CONFIG); + if (valid.length > 0) { + activeThematics = new Set(valid); + document.querySelectorAll('.thematic-checkbox').forEach(cb => { + cb.checked = activeThematics.has(cb.value); + }); + } + } + + // Type filter + if (prefs.type_filter) { + currentTypeFilter = prefs.type_filter; + const rb = document.querySelector(`input[name="unified-type-filter"][value="${prefs.type_filter}"]`); + if (rb) rb.checked = true; + } + + // Include no geometry filter + const includeNoGeomCheckbox = document.getElementById('include-no-geom'); + if (includeNoGeomCheckbox && prefs.hasOwnProperty('include_no_geom')) { + includeNoGeomCheckbox.checked = prefs.include_no_geom; + } + + // Status filters + if (Array.isArray(prefs.status_filters) && prefs.status_filters.length > 0) { + prefs.status_filters.forEach(status => { + const cb = document.querySelector(`[name="filter-status"][value="${status}"]`); + if (cb) cb.checked = true; + }); + } + + // Validation status filters + if (Array.isArray(prefs.validation_status_filters) && prefs.validation_status_filters.length > 0) { + prefs.validation_status_filters.forEach(status => { + const cb = document.querySelector(`[name="filter-validation-status"][value="${status}"]`); + if (cb) cb.checked = true; + }); + } + + if (Array.isArray(prefs.categories) && prefs.categories.length > 0) { + activeCategories = new Set(prefs.categories); + } + + if (Array.isArray(prefs.models) && prefs.models.length > 0) { + activeModels = new Set(prefs.models); + } + + if (prefs.conditional_filters && typeof prefs.conditional_filters === 'object') { + activeConditionalFilters = prefs.conditional_filters; + } + + if (Array.isArray(prefs.visible_columns)) { + applyVisibleColumnsPreference(prefs.visible_columns); + } + + // Map position — stored for use inside the mapLoaded handler + if (prefs.map_center && prefs.map_zoom) { + window._restoredMapCenter = prefs.map_center; + window._restoredMapZoom = prefs.map_zoom; + } + } else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) { + // First visit: pre-select the user's default thematic + activeThematics = new Set([DEFAULT_THEMATIC_CODE]); + document.querySelectorAll('.thematic-checkbox').forEach(cb => { + cb.checked = cb.value === DEFAULT_THEMATIC_CODE; + }); + } +} + +var _prefsSaveTimer = null; + +function schedulePreferencesSave() { + if (typeof SAVE_ASSETS_PREFS_URL === 'undefined') return; + clearTimeout(_prefsSaveTimer); + _prefsSaveTimer = setTimeout(savePreferences, 1000); +} + +function savePreferences() { + if (typeof SAVE_ASSETS_PREFS_URL === 'undefined') return; + const statusFilters = Array.from( + document.querySelectorAll('[name="filter-status"]:checked') + ).map(cb => cb.value); + const validationStatusFilters = Array.from( + document.querySelectorAll('[name="filter-validation-status"]:checked') + ).map(cb => cb.value); + const includeNoGeomCheckbox = document.getElementById('include-no-geom'); + const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true; + const prefs = { + thematics: Array.from(activeThematics), + categories: Array.from(activeCategories), + models: Array.from(activeModels), + conditional_filters: activeConditionalFilters, + visible_columns: getVisibleColumnsPreference(), + type_filter: currentTypeFilter, + status_filters: statusFilters, + validation_status_filters: validationStatusFilters, + include_no_geom: includeNoGeom, + map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null, + map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null, + }; + fetch(SAVE_ASSETS_PREFS_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken() }, + body: JSON.stringify(prefs), + keepalive: true, + }).catch(() => {}); +} + +// ─── Table init ─────────────────────────────────────────────────────────────── + +$(document).ready(function () { + if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.forEach(function (el) { + new bootstrap.Tooltip(el); + }); + } + + function calcHeight() { + return Math.max(300, $(window).height() - 250); + } + + $('#unified-table').bootstrapTable({ + columns: buildColumns(), + height: calcHeight(), + search: false, + pagination: true, + pageSize: 100, + clickToSelect: true, + ignoreClickToSelectOn: function (element) { + return $(element).closest('.zoom-btn, .unified-code-link').length > 0; + }, + onCheck: function(row) { selectedIds.add(row.id); updateSelectionStatus(); }, + onUncheck: function(row) { selectedIds.delete(row.id); updateSelectionStatus(); }, + onCheckAll: function(rows) { rows.forEach(r => selectedIds.add(r.id)); updateSelectionStatus(); }, + onUncheckAll: function(rows) { rows.forEach(r => selectedIds.delete(r.id)); updateSelectionStatus(); }, + onClickRow: function(row) { + if (!row.lon || !row.lat) return; + const source = row.is_location ? 'locations' : 'assets'; + if (typeof highlightMapFeature === 'function') { + highlightMapFeature(map, source, row.id, row.is_location ? locationshighlightState : assetshighlightState); + } + }, + }); + + $(window).on('resize', function () { + $('#unified-table').bootstrapTable('resetView', { height: calcHeight() }); + }); + + $('#unified-table').on('column-switch.bs.table', function () { + if (isApplyingSavedColumnVisibility) return; + schedulePreferencesSave(); + }); + + // ─── Thematic checkboxes ──────────────────────────────────────────────── + + document.querySelectorAll('.thematic-checkbox').forEach(cb => { + cb.addEventListener('change', function () { + if (this.checked) activeThematics.add(this.value); + else activeThematics.delete(this.value); + renderCategoryFilters(); + renderModelFilters(); + renderConditionalFilters(); + }); + }); + + document.getElementById('thematic-select-all').addEventListener('click', function () { + document.querySelectorAll('.thematic-checkbox').forEach(cb => { + cb.checked = true; + activeThematics.add(cb.value); + }); + renderCategoryFilters(); + renderModelFilters(); + renderConditionalFilters(); + }); + + document.getElementById('thematic-deselect-all').addEventListener('click', function () { + document.querySelectorAll('.thematic-checkbox').forEach(cb => { + cb.checked = false; + }); + activeThematics.clear(); + renderCategoryFilters(); + renderModelFilters(); + renderConditionalFilters(); + }); + + document.getElementById('category-select-all').addEventListener('click', function () { + getVisibleCategoryOptions().forEach(category => activeCategories.add(category.value)); + renderCategoryFilters(); + renderModelFilters(); + }); + + document.getElementById('category-deselect-all').addEventListener('click', function () { + activeCategories.clear(); + renderCategoryFilters(); + renderModelFilters(); + }); + + document.getElementById('model-select-all').addEventListener('click', function () { + getVisibleModelOptions().forEach(model => activeModels.add(model.value)); + renderModelFilters(); + }); + + document.getElementById('model-deselect-all').addEventListener('click', function () { + activeModels.clear(); + renderModelFilters(); + }); + + document.getElementById('model-filter-toggle').addEventListener('click', function () { + modelFilterCollapsed = !modelFilterCollapsed; + renderModelFilters(); + }); + + // ─── Type radio ──────────────────────────────────────────────────────── + + document.querySelectorAll('input[name="unified-type-filter"]').forEach(rb => { + rb.addEventListener('change', function () { + if (this.checked) currentTypeFilter = this.value; + }); + }); + + // ─── include-no-geom checkbox ────────────────────────────────────────── + + const includeNoGeomCheckbox = document.getElementById('include-no-geom'); + if (includeNoGeomCheckbox) { + includeNoGeomCheckbox.addEventListener('change', function () { + refreshData(); + schedulePreferencesSave(); + }); + } + + // init from locked element type + if (PREVENTIVE_ELEMENT_TYPE === 'assets') currentTypeFilter = 'assets'; + else if (PREVENTIVE_ELEMENT_TYPE === 'locations') currentTypeFilter = 'locations'; + + // ─── Add actions ───────────────────────────────────────────────────── + + const addConfig = getUnifiedAddModalConfig(); + const assetThematics = Array.isArray(addConfig.asset_thematics) ? addConfig.asset_thematics : []; + const locationThematics = Array.isArray(addConfig.location_thematics) ? addConfig.location_thematics : []; + const addButton = document.getElementById('btn-add-asset'); + const assetThematicSelect = document.getElementById('unified-add-asset-thematic'); + const locationThematicSelect = document.getElementById('unified-add-location-thematic'); + const chooserEmpty = document.getElementById('unified-add-chooser-empty'); + + fillSelectWithOptions(assetThematicSelect, assetThematics, assetThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible')); + fillSelectWithOptions(locationThematicSelect, locationThematics, locationThematics.length ? gettext('Sélectionner une thématique...') : gettext('Aucune thématique disponible')); + + if (assetThematicSelect) assetThematicSelect.disabled = !assetThematics.length; + if (locationThematicSelect) locationThematicSelect.disabled = !locationThematics.length; + if (chooserEmpty) chooserEmpty.classList.toggle('d-none', assetThematics.length > 0 || locationThematics.length > 0); + if (addButton && !assetThematics.length && !locationThematics.length) { + addButton.disabled = true; + addButton.title = gettext('Vous n\'avez pas de droits de création'); + } + + addButton?.addEventListener('click', function () { + bootstrap.Modal.getOrCreateInstance(document.getElementById('unifiedAddChooserModal')).show(); + }); + + assetThematicSelect?.addEventListener('change', function () { + unifiedAddAssetThematic = assetThematics.find(item => item.code === this.value) || null; + }); + + locationThematicSelect?.addEventListener('change', function () { + unifiedAddLocationThematic = locationThematics.find(item => item.code === this.value) || null; + }); + + document.getElementById('unified-add-asset-open')?.addEventListener('click', function () { + const thematicCode = assetThematicSelect ? assetThematicSelect.value : ''; + if (!thematicCode) { + setModalAlert('unified-add-chooser-alert', gettext('Sélectionnez une thématique pour créer un asset.'), 'warning'); + return; + } + setModalAlert('unified-add-chooser-alert', '', 'info'); + const chooserEl = document.getElementById('unifiedAddChooserModal'); + const chooser = bootstrap.Modal.getInstance(chooserEl); + if (chooser) { + const onHidden = () => { + openUnifiedAssetModal(thematicCode); + chooserEl.removeEventListener('hidden.bs.modal', onHidden); + }; + chooserEl.addEventListener('hidden.bs.modal', onHidden); + chooser.hide(); + } else { + openUnifiedAssetModal(thematicCode); + } + }); + + document.getElementById('unified-add-location-open')?.addEventListener('click', function () { + const thematicCode = locationThematicSelect ? locationThematicSelect.value : ''; + if (!thematicCode) { + setModalAlert('unified-add-chooser-alert', gettext('Sélectionnez une thématique pour créer une localisation.'), 'warning'); + return; + } + setModalAlert('unified-add-chooser-alert', '', 'info'); + const chooserEl = document.getElementById('unifiedAddChooserModal'); + const chooser = bootstrap.Modal.getInstance(chooserEl); + if (chooser) { + const onHidden = () => { + openUnifiedLocationModal(thematicCode); + chooserEl.removeEventListener('hidden.bs.modal', onHidden); + }; + chooserEl.addEventListener('hidden.bs.modal', onHidden); + chooser.hide(); + } else { + openUnifiedLocationModal(thematicCode); + } + }); + + document.getElementById('unifiedAddAssetCategory')?.addEventListener('change', function () { + const thematicCode = document.getElementById('unified-add-asset-thematic-code')?.value || ''; + loadUnifiedAssetModels(thematicCode, this.value); + }); + + document.getElementById('unifiedAddAssetForm')?.addEventListener('submit', async function (e) { + e.preventDefault(); + const locationGroup = document.getElementById('unifiedAddAssetLocationGroup'); + const locationSelect = document.getElementById('unifiedAddAssetLocation'); + if (shouldUseTrafficlightsLocationCreateFlow(unifiedAddAssetThematic) && locationGroup && !locationGroup.classList.contains('d-none') && locationSelect && !locationSelect.value) { + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(gettext('Sélectionnez une localisation avant de créer cet asset.'))}`, 'warning'); + return; + } + + const submitBtn = document.getElementById('unifiedAddAssetSubmit'); + const originalText = submitBtn ? submitBtn.innerHTML : ''; + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = `${gettext('Création...')}`; + } + + setModalAlert('unifiedAddAssetAlert', '', 'info'); + + const formData = new FormData(this); + const data = {}; + formData.forEach((value, key) => { + data[key] = value; + }); + + if (!data.location_id) { + delete data.location_id; + } + + let createUrl = '/assets/api/create-asset/'; + let payload = data; + + if (shouldUseTrafficlightsLocationCreateFlow(unifiedAddAssetThematic)) { + const selectedModelOption = document.getElementById('unifiedAddAssetModel')?.selectedOptions?.[0]; + const modelType = selectedModelOption?.dataset?.modelType || ''; + const assetType = modelType.replace(/model$/i, ''); + if (!assetType) { + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(gettext('Type d\'asset introuvable pour ce modèle.'))}`, 'danger'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + return; + } + createUrl = `/assets/${data.thematic_code}/${unifiedAddAssetThematic.location_type}/${data.location_id}/create-asset/`; + payload = { + asset_type: assetType, + model_id: data.model_id || null, + category_id: data.category_id || null, + data: { + name_fr: data.name_fr || '', + name_nl: data.name_nl || '', + }, + }; + } + + try { + const response = await fetch(createUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken(), + }, + body: JSON.stringify(payload), + }); + const result = await response.json(); + if (result.success) { + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(result.message || gettext('Asset créé.'))}`, 'success'); + setTimeout(() => { + bootstrap.Modal.getInstance(document.getElementById('unifiedAddAssetModal'))?.hide(); + const fallbackUrl = (result.asset_id && payload.asset_type) + ? `/assets/${data.thematic_code}/${payload.asset_type}/${result.asset_id}/` + : ''; + const redirectUrl = result.redirect_url || fallbackUrl; + if (redirectUrl) window.location.href = redirectUrl; + else window.location.reload(); + }, 800); + } else { + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(result.error || gettext('Erreur lors de la création.'))}`, 'danger'); + } + } catch (error) { + console.error('Error creating asset:', error); + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(gettext('Erreur de connexion.'))}`, 'danger'); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + } + }); + + document.getElementById('unifiedAddLocationForm')?.addEventListener('submit', async function (e) { + e.preventDefault(); + const thematicCode = document.getElementById('unified-add-location-thematic-code')?.value || ''; + const locationType = document.getElementById('unified-add-location-type')?.value || ''; + if (!thematicCode || !locationType) return; + + const submitBtn = document.getElementById('unifiedAddLocationSubmit'); + const originalText = submitBtn ? submitBtn.innerHTML : ''; + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = `${gettext('Création...')}`; + } + + setModalAlert('unifiedAddLocationAlert', '', 'info'); + + const formData = new FormData(this); + try { + const response = await fetch(`/assets/locations/${thematicCode}/${locationType}/create/`, { + method: 'POST', + headers: { + 'X-CSRFToken': getCsrfToken(), + 'X-Requested-With': 'XMLHttpRequest', + }, + body: formData, + }); + const result = await response.json(); + if (result.success) { + setModalAlert('unifiedAddLocationAlert', `${escapeHtml(result.label || gettext('Localisation créée.'))}`, 'success'); + setTimeout(() => { + bootstrap.Modal.getInstance(document.getElementById('unifiedAddLocationModal'))?.hide(); + window.location.reload(); + }, 800); + } else { + const errors = result.errors ? Object.values(result.errors).flat().join(' ') : result.error; + setModalAlert('unifiedAddLocationAlert', `${escapeHtml(errors || gettext('Erreur lors de la création.'))}`, 'danger'); + } + } catch (error) { + console.error('Error creating location:', error); + setModalAlert('unifiedAddLocationAlert', `${escapeHtml(gettext('Erreur de connexion.'))}`, 'danger'); + } finally { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + } + } + }); + + document.getElementById('unifiedAddAssetModal')?.addEventListener('hidden.bs.modal', resetUnifiedAssetForm); + document.getElementById('unifiedAddLocationModal')?.addEventListener('hidden.bs.modal', function () { + if (unifiedLocationPickerMap) { + unifiedLocationPickerMap.remove(); + unifiedLocationPickerMap = null; + unifiedLocationPickerMarker = null; + unifiedLocationPickerControl = null; + } + resetUnifiedLocationForm(); + }); + + // ─── Search ──────────────────────────────────────────────────────────── + + function doSearch() { + const raw = document.getElementById('assets-search-input').value.trim(); + searchTerms = raw ? raw.toLowerCase().split(/\s+/) : []; + const clearBtn = document.getElementById('assets-search-clear'); + if (clearBtn) clearBtn.style.visibility = raw ? 'visible' : 'hidden'; + if (searchTerms.length > 0) { + shouldFitSearch = true; + searchIsGlobal = true; + } else { + searchIsGlobal = false; + } + refreshData(); + } + + document.getElementById('assets-search-btn').addEventListener('click', doSearch); + document.getElementById('assets-search-input').addEventListener('keydown', function (e) { + if (e.key === 'Enter') doSearch(); + }); + document.getElementById('assets-search-clear').addEventListener('click', function () { + document.getElementById('assets-search-input').value = ''; + doSearch(); + }); + document.querySelectorAll('input[name="assets-search-mode"]').forEach(rb => { + rb.addEventListener('change', function () { + searchMode = this.value; + if (searchTerms.length) { + shouldFitSearch = true; + searchIsGlobal = true; + refreshData(); + } + }); + }); + + // ─── Filters ─────────────────────────────────────────────────────────── + + document.getElementById('apply-filters').addEventListener('click', function () { + refreshData(); + updateFilterBadge(); + schedulePreferencesSave(); + const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal')); + if (modal) modal.hide(); + }); + + document.getElementById('reset-filters').addEventListener('click', function () { + document.querySelectorAll('.thematic-checkbox').forEach(cb => { cb.checked = false; }); + activeThematics.clear(); + activeCategories.clear(); + activeModels.clear(); + modelFilterCollapsed = true; + activeConditionalFilters = {}; + const allRadio = document.getElementById('type-all'); + if (allRadio) { allRadio.checked = true; currentTypeFilter = 'all'; } + document.querySelectorAll('[name="filter-status"]').forEach(cb => { cb.checked = false; }); + document.querySelectorAll('[name="filter-validation-status"]').forEach(cb => { cb.checked = false; }); + const includeNoGeomCheckbox = document.getElementById('include-no-geom'); + if (includeNoGeomCheckbox) includeNoGeomCheckbox.checked = true; + renderCategoryFilters(); + renderModelFilters(); + renderConditionalFilters(); + refreshData(); + updateFilterBadge(); + schedulePreferencesSave(); + const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal')); + if (modal) modal.hide(); + }); + + // ─── Select all / deselect all ───────────────────────────────────────── + + document.getElementById('unified-select-all').addEventListener('click', function () { + $('#unified-table').bootstrapTable('checkAll'); + }); + document.getElementById('unified-deselect-all').addEventListener('click', function () { + $('#unified-table').bootstrapTable('uncheckAll'); + selectedIds.clear(); + updateSelectionStatus(); + }); + + bindUnifiedAssetsExport(); + bindUnifiedBulkActions(); + + // ─── Layout buttons ──────────────────────────────────────────────────── + + document.getElementById('layout-btn-table').addEventListener('click', function () { + document.getElementById('col-table').className = 'col-12'; + document.getElementById('col-map').classList.add('d-none'); + setLayoutActive('table'); + }); + document.getElementById('layout-btn-both').addEventListener('click', function () { + document.getElementById('col-table').className = 'col-md-6'; + const colMap = document.getElementById('col-map'); + colMap.className = 'col-md-6'; + colMap.style.cssText = 'position: relative; margin-top: 50px;'; + setLayoutActive('both'); + }); + document.getElementById('layout-btn-map').addEventListener('click', function () { + document.getElementById('col-table').classList.add('d-none'); + document.getElementById('col-map').className = 'col-12'; + setLayoutActive('map'); + }); + + function setLayoutActive(mode) { + ['table', 'both', 'map'].forEach(m => { + const btn = document.getElementById(`layout-btn-${m}`); + btn.classList.toggle('text-primary', m === mode); + }); + if (typeof map !== 'undefined' && map.resize) map.resize(); + $('#unified-table').bootstrapTable('resetView', { height: calcHeight() }); + } + + // ─── Init ────────────────────────────────────────────────────────────── + + applyInitialPreferences(); + buildCategoryLookup(); + renderCategoryFilters(); + renderModelFilters(); + renderConditionalFilters(); + updateFilterBadge(); + + if (activeThematics.size > 0) { + refreshData(); + } else { + showEmptyState(); + } + + // Pre-select items from intervention draft + if (SELECTED_LOCATION_IDS && SELECTED_LOCATION_IDS.length) { + SELECTED_LOCATION_IDS.forEach(id => selectedIds.add(id)); + } + if (SELECTED_ASSETS && SELECTED_ASSETS.length) { + SELECTED_ASSETS.forEach(a => selectedIds.add(a.id || a)); + } + + // Zoom button: fly to location then refresh data (moveend from flyTo is + // programmatic and would be filtered out by the standard moveend handler) + $('#unified-table').on('click', '.zoom-btn', function (e) { + e.preventDefault(); + e.stopPropagation(); + const lon = parseFloat($(this).data('lon')); + const lat = parseFloat($(this).data('lat')); + if (!isNaN(lon) && !isNaN(lat)) { + showZoomFocusMarker(lon, lat); + searchIsGlobal = false; + map.flyTo({ center: [lon, lat], zoom: 19, essential: true }); + map.once('moveend', function () { + refreshData(); + }); + } + }); +}); + +// ─── Asset loading helpers ───────────────────────────────────────────────────── + +function hasTriggerFilter() { + if (activeCategories.size > 0) return true; + if (activeModels.size > 0) return true; + const publiclightingFilters = activeConditionalFilters.publiclighting || {}; + if (Array.isArray(publiclightingFilters.municipality) && publiclightingFilters.municipality.length) return true; + const trafficlightsFilters = activeConditionalFilters.trafficlights || {}; + if (Array.isArray(trafficlightsFilters.status) && trafficlightsFilters.status.length) return true; + if (Array.isArray(trafficlightsFilters.voltage) && trafficlightsFilters.voltage.length) return true; + if (trafficlightsFilters.connected) return true; + if (trafficlightsFilters.controller_maintenance_provider) return true; + if (trafficlightsFilters.asset_begin_date) return true; + const structuresFilters = activeConditionalFilters.structures || {}; + if (Array.isArray(structuresFilters.structure_type) && structuresFilters.structure_type.length) return true; + if (structuresFilters.is_main_location) return true; + const natureFilters = activeConditionalFilters.nature || {}; + if (natureFilters.location_type) return true; + if (natureFilters.highway) return true; + const roadsFilters = activeConditionalFilters.roads || {}; + if (Array.isArray(roadsFilters.municipality) && roadsFilters.municipality.length) return true; + if (Array.isArray(roadsFilters.quality_level) && roadsFilters.quality_level.length) return true; + const signFilters = activeConditionalFilters.sign || {}; + if (Array.isArray(signFilters.municipality) && signFilters.municipality.length) return true; + if (Array.isArray(signFilters.panel_type) && signFilters.panel_type.length) return true; + return false; +} + +function getMapBbox() { + if (typeof map === 'undefined' || !map.getBounds) return null; + const b = map.getBounds(); + const sw = b.getSouthWest(), ne = b.getNorthEast(); + return { + type: 'Polygon', + coordinates: [[ + [sw.lng, sw.lat], [ne.lng, sw.lat], + [ne.lng, ne.lat], [sw.lng, ne.lat], + [sw.lng, sw.lat], + ]], + }; +} + +function getSelectedStatusFilters() { + return Array.from(document.querySelectorAll('[name="filter-status"]:checked')).map(cb => cb.value); +} + +function getSelectedValidationStatusFilters() { + return Array.from(document.querySelectorAll('[name="filter-validation-status"]:checked')).map(cb => cb.value); +} + +function getActiveCategoryIdsForThematic(thematicCode) { + const categories = THEMATIC_CATEGORIES[thematicCode] || []; + const ids = []; + + categories.forEach(category => { + const categoryValue = `${thematicCode}::${category.name || ''}`; + if (activeCategories.has(categoryValue)) { + ids.push(String(category.id)); + } + }); + + return ids; +} + +function getActiveModelIdsForThematic(thematicCode) { + const ids = []; + + getVisibleModelOptions().forEach(model => { + if (model.thematicCode !== thematicCode) return; + if (!activeModels.has(model.value)) return; + if (model.modelId == null) return; + const parsed = Number(model.modelId); + if (!Number.isFinite(parsed)) return; + ids.push(parsed); + }); + + return Array.from(new Set(ids)); +} + +function buildLocationsGeojsonPostBody(thematicCode) { + const payload = {}; + const thematicFilters = getConditionalFiltersForThematic(thematicCode); + + if (thematicCode === 'trafficlights') { + if (Array.isArray(thematicFilters.status) && thematicFilters.status.length) { + payload.status = thematicFilters.status; + } + if (Array.isArray(thematicFilters.voltage) && thematicFilters.voltage.length) { + payload.voltage = thematicFilters.voltage; + } + if (thematicFilters.connected) payload.connected = thematicFilters.connected; + if (thematicFilters.asset_begin_date) payload.begin_date = thematicFilters.asset_begin_date; + if (thematicFilters.controller_maintenance_provider) { + payload.controller_maintenance_provider = thematicFilters.controller_maintenance_provider; + } + } + + if (['roads', 'publiclighting', 'sign', 'artworks'].includes(thematicCode)) { + if (Array.isArray(thematicFilters.municipality) && thematicFilters.municipality.length) { + payload.municipality = thematicFilters.municipality; + } + } + + if (['structures', 'its', 'controlcenters'].includes(thematicCode)) { + if (thematicFilters.is_main_location) payload.is_main_location = thematicFilters.is_main_location; + } + + if (thematicCode === 'structures') { + if (Array.isArray(thematicFilters.structure_type) && thematicFilters.structure_type.length) { + payload.structure_type = thematicFilters.structure_type; + } + } + + if (thematicCode === 'nature') { + if (thematicFilters.location_type) payload.location_type = thematicFilters.location_type; + if (thematicFilters.highway) payload.highway = thematicFilters.highway; + } + + return payload; +} + +function buildAssetsGeojsonPostBody(thematicCode, bbox, includeNonGeoFilters) { + const includeNoGeomCheckbox = document.getElementById('include-no-geom'); + const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true; + + const payload = { + include_no_geom: includeNoGeom, + }; + if (bbox) { + payload.filter_geom = { type: 'Feature', geometry: bbox }; + } + + const assetsSearchInput = document.getElementById('assets-search-input'); + const rawSearch = assetsSearchInput ? assetsSearchInput.value.trim() : ''; + if (rawSearch) { + payload.q = rawSearch; + payload.search_mode = searchMode; + } + + if (!includeNonGeoFilters) return payload; + + const modelIds = getActiveModelIdsForThematic(thematicCode); + if (modelIds.length) payload.model = modelIds; + else { + const categoryIds = getActiveCategoryIdsForThematic(thematicCode); + if (categoryIds.length) payload.category = categoryIds; + } + + const statusFilters = getSelectedStatusFilters(); + if (statusFilters.length) payload.asset_status = statusFilters; + + const validationStatusFilters = getSelectedValidationStatusFilters(); + if (validationStatusFilters.length) payload.asset_validation_status = validationStatusFilters; + + const thematicFilters = getConditionalFiltersForThematic(thematicCode); + if (thematicCode === 'roads') { + if (Array.isArray(thematicFilters.municipality) && thematicFilters.municipality.length) { + payload.municipality = thematicFilters.municipality; + } + if (Array.isArray(thematicFilters.quality_level) && thematicFilters.quality_level.length) { + payload.quality_level = thematicFilters.quality_level; + } + } + if (thematicCode === 'trafficlights') { + if (Array.isArray(thematicFilters.status) && thematicFilters.status.length) { + payload.status = thematicFilters.status; + } + if (Array.isArray(thematicFilters.voltage) && thematicFilters.voltage.length) { + payload.voltage = thematicFilters.voltage; + } + if (thematicFilters.connected) { + payload.connected = thematicFilters.connected; + } + if (thematicFilters.controller_maintenance_provider) { + payload.controller_maintenance_provider = thematicFilters.controller_maintenance_provider; + } + if (thematicFilters.asset_begin_date) { + payload.asset_begin_date = thematicFilters.asset_begin_date; + } + } + if (thematicCode === 'structures') { + if (Array.isArray(thematicFilters.structure_type) && thematicFilters.structure_type.length) { + payload.structure_type = thematicFilters.structure_type; + } + if (thematicFilters.is_main_location) { + payload.is_main_location = thematicFilters.is_main_location; + } + } + if (thematicCode === 'publiclighting') { + if (Array.isArray(thematicFilters.municipality) && thematicFilters.municipality.length) { + payload.municipality = thematicFilters.municipality; + } + } + if (thematicCode === 'nature') { + if (thematicFilters.location_type) { + payload.location_type = thematicFilters.location_type; + } + if (thematicFilters.highway) { + payload.highway = thematicFilters.highway; + } + } + if (thematicCode === 'sign') { + if (Array.isArray(thematicFilters.municipality) && thematicFilters.municipality.length) { + payload.municipality = thematicFilters.municipality; + } + if (Array.isArray(thematicFilters.panel_type) && thematicFilters.panel_type.length) { + payload.panel_type = thematicFilters.panel_type; + } + } + + const searchInput = document.getElementById('search'); + const searchQuery = searchInput ? (searchInput.value || '').trim() : ''; + if (searchQuery) payload.q = searchQuery; + + return payload; +} + +function getCsrfToken() { + const m = document.cookie.match(/csrftoken=([^;]+)/); + return m ? m[1] : ''; +} + +function getSelectedAssetKeysForExport() { + const rows = $('#unified-table').bootstrapTable('getSelections') || []; + const keys = []; + const seen = new Set(); + + rows.forEach(row => { + if (!row || row.is_location) return; + + const modelName = String(row.asset_type || '').toLowerCase().trim(); + const objectId = Number(row.id); + if (!modelName || !Number.isFinite(objectId)) return; + + const key = `${modelName}:${objectId}`; + if (seen.has(key)) return; + seen.add(key); + keys.push(key); + }); + + return keys; +} + +function bindUnifiedAssetsExport() { + const exportConfirmBtn = document.getElementById('unified-confirm-export-btn'); + const exportError = document.getElementById('unified-export-error'); + const exportModalEl = document.getElementById('unifiedExportModal'); + if (!exportConfirmBtn || !exportModalEl) return; + + exportModalEl.addEventListener('show.bs.modal', function () { + if (!exportError) return; + exportError.classList.add('d-none'); + exportError.textContent = ''; + }); + + exportConfirmBtn.addEventListener('click', function () { + const ids = getSelectedAssetKeysForExport(); + if (!ids.length) { + if (exportError) { + exportError.textContent = gettext('Veuillez sélectionner au moins un asset.'); + exportError.classList.remove('d-none'); + } + return; + } + + if (exportError) { + exportError.classList.add('d-none'); + exportError.textContent = ''; + } + + const format = document.querySelector('input[name="unified-export-format"]:checked')?.value || 'csv'; + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = exportAssetsUrl; + form.style.display = 'none'; + + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = 'csrfmiddlewaretoken'; + csrfInput.value = getCsrfToken(); + form.appendChild(csrfInput); + + const formatInput = document.createElement('input'); + formatInput.type = 'hidden'; + formatInput.name = 'format'; + formatInput.value = format; + form.appendChild(formatInput); + + const idsInput = document.createElement('input'); + idsInput.type = 'hidden'; + idsInput.name = 'ids'; + idsInput.value = JSON.stringify(ids); + form.appendChild(idsInput); + + document.body.appendChild(form); + + bootstrap.Modal.getOrCreateInstance(exportModalEl).hide(); + + form.submit(); + setTimeout(() => { + if (form.parentNode) form.parentNode.removeChild(form); + }, 1000); + }); +} + +function buildBulkSelectedAssetsPayload() { + const rows = $('#unified-table').bootstrapTable('getData') || []; + const byId = new Map(rows.filter(r => !r.is_location).map(r => [String(r.id), r])); + const payload = []; + + Array.from(selectedIds).forEach(rawId => { + const id = String(rawId); + const row = byId.get(id); + if (!row || !row.asset_type) return; + payload.push({ model: String(row.asset_type).toLowerCase().trim(), object_id: Number(id) }); + }); + + return payload; +} + +function bindUnifiedBulkActions() { + // 1. Bulk Status + const bulkStatusConfirmBtn = document.getElementById('bulk-status-confirm-btn'); + const bulkStatusError = document.getElementById('bulk-status-error'); + const bulkStatusModalEl = document.getElementById('bulkStatusModal'); + + if (bulkStatusConfirmBtn && bulkStatusModalEl) { + bulkStatusModalEl.addEventListener('show.bs.modal', function () { + if (bulkStatusError) { + bulkStatusError.classList.add('d-none'); + bulkStatusError.textContent = ''; + } + }); + + bulkStatusConfirmBtn.addEventListener('click', function () { + const assets = buildBulkSelectedAssetsPayload(); + if (!assets.length) { + if (bulkStatusError) { + bulkStatusError.textContent = gettext('Veuillez sélectionner au moins un asset.'); + bulkStatusError.classList.remove('d-none'); + } + return; + } + + const newValue = document.getElementById('bulk-status-select').value; + + fetch(bulkEditAssetsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() + }, + body: JSON.stringify({ + assets: assets, + field_name: 'status', + new_value: newValue + }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + bootstrap.Modal.getOrCreateInstance(bulkStatusModalEl).hide(); + selectedIds.clear(); + updateSelectionStatus(); + refreshData(); + } else { + if (bulkStatusError) { + bulkStatusError.textContent = data.error || gettext('Une erreur est survenue.'); + bulkStatusError.classList.remove('d-none'); + } + } + }) + .catch(err => { + if (bulkStatusError) { + bulkStatusError.textContent = gettext('Une erreur est survenue.'); + bulkStatusError.classList.remove('d-none'); + } + }); + }); + } + + // 2. Bulk Validation Status + const bulkValConfirmBtn = document.getElementById('bulk-validation-status-confirm-btn'); + const bulkValError = document.getElementById('bulk-validation-status-error'); + const bulkValModalEl = document.getElementById('bulkValidationStatusModal'); + + if (bulkValConfirmBtn && bulkValModalEl) { + bulkValModalEl.addEventListener('show.bs.modal', function () { + if (bulkValError) { + bulkValError.classList.add('d-none'); + bulkValError.textContent = ''; + } + }); + + bulkValConfirmBtn.addEventListener('click', function () { + const assets = buildBulkSelectedAssetsPayload(); + if (!assets.length) { + if (bulkValError) { + bulkValError.textContent = gettext('Veuillez sélectionner au moins un asset.'); + bulkValError.classList.remove('d-none'); + } + return; + } + + const newValue = document.getElementById('bulk-validation-status-select').value; + + fetch(bulkEditAssetsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() + }, + body: JSON.stringify({ + assets: assets, + field_name: 'validation_status', + new_value: newValue + }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + bootstrap.Modal.getOrCreateInstance(bulkValModalEl).hide(); + selectedIds.clear(); + updateSelectionStatus(); + refreshData(); + } else { + if (bulkValError) { + bulkValError.textContent = data.error || gettext('Une erreur est survenue.'); + bulkValError.classList.remove('d-none'); + } + } + }) + .catch(err => { + if (bulkValError) { + bulkValError.textContent = gettext('Une erreur est survenue.'); + bulkValError.classList.remove('d-none'); + } + }); + }); + } +} + +function showLoadWarning(message) { + const warning = document.getElementById('unified-load-warning'); + if (!warning) return; + warning.textContent = message; + warning.classList.remove('d-none'); + loadWarningVisible = true; +} + +function hideLoadWarning() { + const warning = document.getElementById('unified-load-warning'); + if (!warning) return; + warning.textContent = ''; + warning.classList.add('d-none'); + loadWarningVisible = false; +} + +function handleLoadTooLarge() { + showLoadWarning(gettext("Trop de données à afficher. Ajoutez des filtres ou réduisez le périmètre pour charger le tableau et la carte.")); +} + +async function fetchGeojsonFeatures(url, options, signal) { + const response = await fetch(url, options); + const contentType = response.headers.get('content-type') || ''; + const bodyText = await response.text().catch(() => ''); + + if (response.status === 413 || /Content Too Large|Trop d'objets à afficher|too large/i.test(bodyText)) { + const error = new Error('Too Large'); + error.code = 'TOO_LARGE'; + throw error; + } + + if (!response.ok) { + const error = new Error(`HTTP ${response.status}`); + error.status = response.status; + error.contentType = contentType; + error.bodyText = bodyText; + throw error; + } + + if (bodyText.trim() === '') return { type: 'FeatureCollection', features: [] }; + + if (contentType.includes('json') || bodyText.trim().startsWith('{') || bodyText.trim().startsWith('[')) { + return JSON.parse(bodyText); + } + + return { type: 'FeatureCollection', features: [] }; +} + +function showZoomFocusMarker(lon, lat) { + if (typeof map === 'undefined') return; + if (!Number.isFinite(lon) || !Number.isFinite(lat) || lat < -90 || lat > 90 || lon < -180 || lon > 180) { + return; + } + + if (zoomFocusMarker) { + zoomFocusMarker.setLngLat([lon, lat]); + return; + } + + zoomFocusMarker = new maplibregl.Marker({ color: '#dc3545' }) + .setLngLat([lon, lat]) + .addTo(map); +} + +function escapeHtml(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function escapeCssSelectorValue(value) { + if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { + return CSS.escape(value); + } + return String(value).replace(/(["\\#.:\[\]\(\)=,>+~*^$|!])/g, '\\$1'); +} + +function findUnifiedRowIndex(rowRef) { + const $table = $('#unified-table'); + const rows = $table.bootstrapTable('getData') || []; + + const isLocation = !!rowRef.is_location; + const matches = (row) => { + if (!!row.is_location !== isLocation) return false; + if (String(row.id) !== String(rowRef.id)) return false; + + if (!isLocation) { + if (String(row.thematic || '') !== String(rowRef.thematic || '')) return false; + if (String(row.asset_type || '') !== String(rowRef.asset_type || '')) return false; + } else { + if (rowRef.thematic && String(row.thematic || '') !== String(rowRef.thematic || '')) return false; + } + + return true; + }; + + let idx = rows.findIndex(matches); + if (idx !== -1) return idx; + + // Fallback in case some rows do not expose all discriminators. + idx = rows.findIndex(row => !!row.is_location === isLocation && String(row.id) === String(rowRef.id)); + return idx; +} + +function focusRowInUnifiedTable(rowRef) { + const bothBtn = document.getElementById('layout-btn-both'); + if (bothBtn && document.getElementById('col-table').classList.contains('d-none')) { + bothBtn.click(); + } + + const $table = $('#unified-table'); + if (!$table.length) return; + + const rowIndex = findUnifiedRowIndex(rowRef); + if (rowIndex === -1) { + console.warn('Row not found in unified table', rowRef); + return; + } + + const options = $table.bootstrapTable('getOptions') || {}; + const pageSize = options.pageSize || 100; + const pageNumber = Math.floor(rowIndex / pageSize) + 1; + $table.bootstrapTable('selectPage', pageNumber); + + setTimeout(() => { + $table.find('tbody td').removeClass('rowhighlight'); + const $row = $table.find(`tbody tr[data-index="${rowIndex}"]`); + if ($row.length) { + $row.find('td').addClass('rowhighlight'); + $row[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 250); +} + +function collectClickedAssets(clickEvent) { + const layers = [ + 'assets', + 'assets-fill', + 'assets-line', + 'assets-point', + 'sign-panels-circles', + 'sign-panels-symbols', + 'sign-poles-orientation', + 'sign-poles', + 'sign-poles-vertical', + ].filter(layerId => map.getLayer(layerId)); + if (!layers.length) return []; + + const rendered = map.queryRenderedFeatures(clickEvent.point, { layers }); + const unique = new Map(); + + rendered.forEach(f => { + const p = f.properties || {}; + const id = f.id ?? p.id; + if (id == null) return; + const key = `${p.thematic || ''}|${p.asset_type || ''}|${id}`; + if (unique.has(key)) return; + unique.set(key, { + id: id, + code: p.code || '', + name: p.name || p.name_fr || p.name_nl || '', + thematic: p.thematic || '', + thematic_name: f._thematic_name || p.thematic || '', + asset_type: p.asset_type || '', + element_type: p.element_type || '', + detail_url: p.detail_url || null, + feature_id: id, + }); + }); + + return Array.from(unique.values()); +} + +function collectClickedLocations(clickEvent) { + const layers = ['locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].filter(layerId => map.getLayer(layerId)); + if (!layers.length) return []; + + const rendered = map.queryRenderedFeatures(clickEvent.point, { layers }); + const unique = new Map(); + + rendered.forEach(f => { + const p = f.properties || {}; + const id = f.id ?? p.id; + if (id == null) return; + const key = `${p.thematic || ''}|${id}`; + if (unique.has(key)) return; + unique.set(key, { + id: id, + code: p.code || '', + name: p.name || p.name_fr || p.name_nl || '', + thematic: p.thematic || '', + thematic_name: f._thematic_name || p.thematic || '', + element_type: p.element_type || '', + detail_url: p.detail_url || null, + feature_id: id, + }); + }); + + return Array.from(unique.values()); +} + +function bindAssetPopupActions(popupNode) { + if (!popupNode) return; + popupNode.querySelectorAll('.asset-popup-focus-btn').forEach(btn => { + btn.addEventListener('click', function (e) { + e.preventDefault(); + const rowRef = { + id: this.dataset.assetId, + thematic: this.dataset.thematic, + asset_type: this.dataset.assetType, + is_location: false, + }; + focusRowInUnifiedTable(rowRef); + + if (typeof highlightMapFeature === 'function') { + highlightMapFeature(map, 'assets', rowRef.id, assetshighlightState); + } + }); + }); + + popupNode.querySelectorAll('.location-popup-focus-btn').forEach(btn => { + btn.addEventListener('click', function (e) { + e.preventDefault(); + const rowRef = { + id: this.dataset.locationId, + thematic: this.dataset.thematic, + is_location: true, + }; + focusRowInUnifiedTable(rowRef); + + if (typeof highlightMapFeature === 'function') { + highlightMapFeature(map, 'locations', rowRef.id, locationshighlightState); + } + }); + }); +} + +function showAssetClickPopup(clickEvent) { + const assets = collectClickedAssets(clickEvent); + const locations = collectClickedLocations(clickEvent); + if (!assets.length && !locations.length) return; + + const maxItems = 8; + const visibleAssets = assets.slice(0, maxItems); + const hiddenCount = Math.max(0, assets.length - visibleAssets.length); + + const itemsHtml = visibleAssets.map(a => { + const code = escapeHtml(a.code || '-'); + const type = escapeHtml(a.element_type || a.asset_type || ''); + const thematic = escapeHtml(a.thematic_name || a.thematic || ''); + const name = escapeHtml(a.name || ''); + const detailAnchor = a.detail_url + ? `${code}` + : `${code}`; + + return ` +
+
+
${detailAnchor}
+
${type}${thematic ? ' - ' + thematic : ''}
+ ${name ? `
${name}
` : ''} +
+ +
+ `; + }).join(''); + + const visibleLocations = locations.slice(0, maxItems); + const hiddenLocationsCount = Math.max(0, locations.length - visibleLocations.length); + + const locationItemsHtml = visibleLocations.map(l => { + const code = escapeHtml(l.code || '-'); + const type = escapeHtml(l.element_type || ''); + const thematic = escapeHtml(l.thematic_name || l.thematic || ''); + const name = escapeHtml(l.name || ''); + const detailAnchor = l.detail_url + ? `${code}` + : `${code}`; + + return ` +
+
+
${detailAnchor}
+
${type}${thematic ? ' - ' + thematic : ''}
+ ${name ? `
${name}
` : ''} +
+ +
+ `; + }).join(''); + + const moreHtml = hiddenCount > 0 + ? `
+${hiddenCount} ${escapeHtml(gettext('autre(s) asset(s)'))}
` + : ''; + + const moreLocationsHtml = hiddenLocationsCount > 0 + ? `
+${hiddenLocationsCount} ${escapeHtml(gettext('autre(s) localisation(s)'))}
` + : ''; + + const assetsSectionHtml = assets.length > 0 + ? ` +
${escapeHtml(gettext('Assets trouvés'))}: ${assets.length}
+
${itemsHtml}
+ ${moreHtml} + ` + : ''; + + const locationsSectionHtml = locations.length > 0 + ? ` +
${escapeHtml(gettext('Localisations trouvées'))}: ${locations.length}
+
${locationItemsHtml}
+ ${moreLocationsHtml} + ` + : ''; + + const popupHtml = ` +
+ ${assetsSectionHtml} + ${locationsSectionHtml} +
+ `; + + if (!assetClickPopup) { + // Keep control of close behavior so consecutive clicks on features always reopen/update the popup. + assetClickPopup = new maplibregl.Popup({ closeButton: true, closeOnClick: false }); + } + + assetClickPopup + .setLngLat(clickEvent.lngLat) + .setHTML(popupHtml) + .addTo(map); + + const popupNode = assetClickPopup.getElement(); + bindAssetPopupActions(popupNode); +} + +function updateAssetsHint() { + const hint = document.getElementById('assets-hint'); + if (!hint) return; + const noThematic = activeThematics.size === 0; + const hasConditional = Array.from(activeThematics).some( + code => !ALWAYS_LOAD_ASSETS_THEMATICS.has(code) + ); + const filterActive = hasTriggerFilter(); + const bboxActive = typeof map !== 'undefined' && map.getZoom && map.getZoom() >= MIN_ASSETS_ZOOM; + const showHint = noThematic || (hasConditional && !filterActive && !bboxActive); + hint.style.visibility = showHint ? 'visible' : 'hidden'; +} + +function getViewportFeatures(features) { + if (typeof map === 'undefined' || !map.getBounds) return features; + if (searchTerms && searchTerms.length > 0 && searchIsGlobal) return features; + const bounds = map.getBounds(); + return features.filter(f => { + const geom = f.geometry; + if (!geom || !geom.coordinates) { + // Keep non-geolocated assets in the table; they cannot be spatially clipped. + return !!(f.properties && !f.properties.is_location); + } + let lng, lat; + try { + if (geom.type === 'Point') { + [lng, lat] = geom.coordinates; + } else if (geom.type === 'LineString' || geom.type === 'MultiPoint') { + [lng, lat] = geom.coordinates[0]; + } else if (geom.type === 'Polygon' || geom.type === 'MultiLineString') { + [lng, lat] = geom.coordinates[0][0]; + } else if (geom.type === 'MultiPolygon') { + [lng, lat] = geom.coordinates[0][0][0]; + } else { + return true; + } + if (!Number.isFinite(lng) || !Number.isFinite(lat) || lat < -90 || lat > 90 || lng < -180 || lng > 180) { + return false; + } + return bounds.contains([lng, lat]); + } catch (e) { + console.warn('getViewportFeatures: Invalid coordinate or geometry structure', f, e); + return false; + } + }); +} + +function fitMapToFeatures(features) { + if (typeof map === 'undefined' || !map.fitBounds || !features || features.length === 0) return; + + let minLng = Infinity, minLat = Infinity, maxLng = -Infinity, maxLat = -Infinity; + let hasCoords = false; + + features.forEach(f => { + const geom = f.geometry; + if (!geom || !geom.coordinates) return; + + function processCoords(coords) { + if (!Array.isArray(coords)) return; + if (typeof coords[0] === 'number') { + const lng = coords[0]; + const lat = coords[1]; + if (Number.isFinite(lng) && Number.isFinite(lat) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + if (lng < minLng) minLng = lng; + if (lat < minLat) minLat = lat; + if (lng > maxLng) maxLng = lng; + if (lat > maxLat) maxLat = lat; + hasCoords = true; + } + } else { + coords.forEach(processCoords); + } + } + processCoords(geom.coordinates); + }); + + if (hasCoords) { + map.fitBounds([ + [minLng, minLat], + [maxLng, maxLat] + ], { + padding: 80, + maxZoom: 17, + duration: 1000 + }); + } +} + +// ─── Data loading ───────────────────────────────────────────────────────────── + +function refreshData() { + // Cancel any in-flight requests from the previous call + if (currentFetchAbortController) { currentFetchAbortController.abort(); } + currentFetchAbortController = new AbortController(); + const signal = currentFetchAbortController.signal; + hideLoadWarning(); + + const thematics = Array.from(activeThematics); + if (thematics.length === 0) { + allFeatures = []; + applyFilters(); + return; + } + + const filterActive = hasTriggerFilter(); + const mapZoom = (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : 0; + const bboxActive = mapZoom >= MIN_ASSETS_ZOOM; + + const fetches = []; + thematics.forEach(code => { + const cfg = THEMATIC_CONFIG[code]; + if (!cfg) return; + if (!geojsonCache[code]) geojsonCache[code] = {}; + + // ── Locations: fetch once per thematic conditional-filter signature + const locationPayload = buildLocationsGeojsonPostBody(code); + const locationSignature = JSON.stringify(locationPayload); + if (cfg.locations_geojson_url && (!('locations' in geojsonCache[code]) || locationsFetchSignatures[code] !== locationSignature)) { + fetches.push( + fetchGeojsonFeatures(cfg.locations_geojson_url + '?unified=true', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken(), + }, + body: JSON.stringify(locationPayload), + signal, + }, signal) + .then(data => { + geojsonCache[code].locations = data.features || []; + locationsFetchSignatures[code] = locationSignature; + }) + .catch(err => { + if (err.name === 'AbortError') throw err; + if (err.code === 'TOO_LARGE') handleLoadTooLarge(); + geojsonCache[code].locations = []; + locationsFetchSignatures[code] = locationSignature; + }) + ); + } + + // ── Assets: conditional loading + const alwaysLoad = ALWAYS_LOAD_ASSETS_THEMATICS.has(code); + const prevMode = assetsLoadModes[code] || null; + + if (alwaysLoad) { + // artworks / controlcenters: fetch once, cache forever + if (!('assets' in geojsonCache[code])) { + const url = cfg.assets_geojson_url + '?unified=true'; + fetches.push( + fetchGeojsonFeatures(url, { signal }, signal) + .then(data => { geojsonCache[code].assets = data.features || []; }) + .catch(err => { + if (err.name === 'AbortError') throw err; + if (err.code === 'TOO_LARGE') handleLoadTooLarge(); + geojsonCache[code].assets = []; + }) + ); + assetsLoadModes[code] = 'always'; + } + } else if (bboxActive || filterActive || (searchTerms && searchTerms.length > 0)) { + const useBbox = !searchIsGlobal; + const bbox = useBbox ? getMapBbox() : null; + const targetMode = useBbox ? 'bbox' : 'global'; + + const assetPayload = buildAssetsGeojsonPostBody(code, bbox, filterActive); + const assetSignature = JSON.stringify(assetPayload); + + if (assetsLoadModes[code] !== targetMode || assetsFetchSignatures[code] !== assetSignature) { + if (!useBbox || bbox) { + fetches.push( + fetchGeojsonFeatures(cfg.assets_geojson_url + '?unified=true', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken(), + }, + body: JSON.stringify(assetPayload), + signal, + }) + .then(data => { + geojsonCache[code].assets = data.features || []; + assetsFetchSignatures[code] = assetSignature; + }) + .catch(err => { + if (err.name === 'AbortError') throw err; + if (err.code === 'TOO_LARGE') handleLoadTooLarge(); + geojsonCache[code].assets = []; + assetsFetchSignatures[code] = assetSignature; + }) + ); + assetsLoadModes[code] = targetMode; + } + } + } else { + // No condition met (search cleared, zoom out, etc): clear assets + geojsonCache[code].assets = []; + assetsLoadModes[code] = null; + assetsFetchSignatures[code] = null; + } + }); + + function afterFetch() { + allFeatures = []; + thematics.forEach(code => { + const cfg = THEMATIC_CONFIG[code]; + const cache = geojsonCache[code] || {}; + const thematicName = cfg ? cfg.name : code; + (cache.locations || []).forEach(f => { + f._thematic_name = thematicName; + allFeatures.push(f); + }); + (cache.assets || []).forEach(f => { + f._thematic_name = thematicName; + allFeatures.push(f); + }); + }); + renderModelFilters(); + applyFilters(); + if (loadWarningVisible && allFeatures.length === 0) { + showEmptyState(); + } + } + + if (fetches.length > 0) { + showSpinner(); + Promise.all(fetches) + .then(afterFetch) + .catch(err => { if (err.name !== 'AbortError') afterFetch(); }); + } else { + afterFetch(); + } +} + +// ─── Filtering ──────────────────────────────────────────────────────────────── + +function applyFilters() { + let features = allFeatures.slice(); + + // Type filter + if (currentTypeFilter === 'locations') { + features = features.filter(f => f.properties && f.properties.is_location); + } else if (currentTypeFilter === 'assets') { + features = features.filter(f => f.properties && !f.properties.is_location); + } + + // Include no geom filter + const includeNoGeomCheckbox = document.getElementById('include-no-geom'); + const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true; + if (!includeNoGeom) { + features = features.filter(f => { + // Locations are never filtered by geometry checkbox (only assets) + if (f.properties && f.properties.is_location) return true; + return f.geometry && (f.geometry.coordinates || f.geometry.type); + }); + } + + // Status filter + const selectedStatuses = Array.from(document.querySelectorAll('[name="filter-status"]:checked')).map(cb => cb.value); + if (selectedStatuses.length) { + features = features.filter(f => { + return selectedStatuses.includes(f.properties && f.properties.status); + }); + } + + // Validation status filter + const selectedValidationStatuses = Array.from(document.querySelectorAll('[name="filter-validation-status"]:checked')).map(cb => cb.value); + if (selectedValidationStatuses.length) { + features = features.filter(f => { + if (f.properties && f.properties.is_location) return true; + return selectedValidationStatuses.includes(f.properties && f.properties.validation_status); + }); + } + + // Category filter + if (activeCategories.size) { + features = features.filter(f => { + if (f.properties && f.properties.is_location) return true; + return activeCategories.has(getFeatureCategoryValue(f)); + }); + } + + // Model filter + if (activeModels.size) { + features = features.filter(f => { + if (f.properties && f.properties.is_location) return true; + return activeModels.has(getFeatureModelValue(f)); + }); + } + + // Text search + if (searchTerms.length) { + features = features.filter(f => { + const p = f.properties || {}; + const haystack = [p.code, p.name, p.name_fr, p.name_nl, p.element_type, p.thematic] + .filter(Boolean).join(' ').toLowerCase(); + if (searchMode === 'or') return searchTerms.some(t => haystack.includes(t)); + return searchTerms.every(t => haystack.includes(t)); + }); + } + + filteredFeatures = features; + loadTableData(getViewportFeatures(features)); + rebuildMapSources(); + updateAssetsHint(); + + if (shouldFitSearch) { + shouldFitSearch = false; + fitMapToFeatures(features); + } +} + +// ─── Table data ─────────────────────────────────────────────────────────────── + +function loadTableData(features) { + hideSpinner(); + + if (features.length === 0 && allFeatures.length === 0) { + $('#unified-spinner').hide(); + $('#unified-table').bootstrapTable('load', []); + showEmptyState(); + return; + } + hideEmptyState(); + + const rows = features.map(f => { + const p = f.properties || {}; + const name = p.name || p.name_fr || p.name_nl || ''; + const length = Number(p.length ?? p.length_m ?? 0); + const area = Number(p.area ?? p.area_m2 ?? p.surface_area ?? p.surface ?? 0); + return { + id: f.id, + code: p.code || '', + name: name, + asset_type: p.asset_type || '', + element_type: p.element_type || '', + thematic: p.thematic || '', + thematic_name: f._thematic_name || p.thematic || '', + category_id: p.category_id != null ? String(p.category_id) : '', + category: p.category || '', + model: p.model || '', + status: p.status || '', + detail_url: p.detail_url || null, + lon: p.lon, + lat: p.lat, + length: Number.isFinite(length) ? length : 0, + area: Number.isFinite(area) ? area : 0, + is_location: p.is_location, + }; + }); + + $('#unified-spinner').hide(); + $('#unified-table').bootstrapTable('load', rows); + + // Restore checked state + if (selectedIds.size) { + rows.forEach((row, idx) => { + if (selectedIds.has(row.id)) { + $('#unified-table').bootstrapTable('check', idx); + } + }); + } +} + +// ─── Map integration ────────────────────────────────────────────────────────── + +var locationshighlightState = {}; +var assetshighlightState = {}; + +function isPanelTextSpriteId(missingId) { + if (!missingId) return false; + return missingId.startsWith('panel-text-f34a-') || + missingId.startsWith('panel-text-type') || + missingId.startsWith('panel-text-fxx-') || + missingId.startsWith('panel-text-f33c-') || + missingId.startsWith('panel-text-xc-') || + missingId.startsWith('panel-text-typexivf-') || + missingId.startsWith('panel-text-icr-') || + missingId.startsWith('panel-text-zone-'); +} + +function applyUnifiedSignPanelOffsets(featureCollection) { + if (!featureCollection || !Array.isArray(featureCollection.features)) return; + if (!window.SignPanelsShared || typeof window.SignPanelsShared.calculatePanelOffsets !== 'function') return; + window.SignPanelsShared.calculatePanelOffsets(featureCollection); +} + +function scheduleUnifiedSignPanelOffsetRefresh() { + if (unifiedPanelOffsetRefreshTimer) { + clearTimeout(unifiedPanelOffsetRefreshTimer); + } + + unifiedPanelOffsetRefreshTimer = setTimeout(function () { + unifiedPanelOffsetRefreshTimer = null; + if (!window.SignPanelsShared || typeof window.SignPanelsShared.refreshPanelOffsetsForSource !== 'function') { + return; + } + window.SignPanelsShared.refreshPanelOffsetsForSource(map, 'assets'); + }, 40); +} + +function enqueueUnifiedSignPanelTextSprites(features) { + if (!Array.isArray(features)) return; + if (typeof enqueuePanelTextSprites !== 'function') return; + + try { + enqueuePanelTextSprites(features); + } catch (err) { + console.warn('Error enqueueing panel text sprites:', err); + } +} + +function hasValidCoordinates(f) { + if (!f || !f.geometry) return false; + const geom = f.geometry; + if (!geom.coordinates) return false; + try { + if (geom.type === 'Point') { + return Number.isFinite(geom.coordinates[0]) && Number.isFinite(geom.coordinates[1]); + } + if (geom.type === 'LineString' || geom.type === 'MultiPoint') { + return Array.isArray(geom.coordinates) && geom.coordinates.length > 0 && Array.isArray(geom.coordinates[0]) && Number.isFinite(geom.coordinates[0][0]); + } + if (geom.type === 'Polygon' || geom.type === 'MultiLineString') { + return Array.isArray(geom.coordinates) && geom.coordinates.length > 0 && Array.isArray(geom.coordinates[0]) && geom.coordinates[0].length > 0 && Number.isFinite(geom.coordinates[0][0][0]); + } + if (geom.type === 'MultiPolygon') { + return Array.isArray(geom.coordinates) && geom.coordinates.length > 0 && Array.isArray(geom.coordinates[0]) && geom.coordinates[0].length > 0 && geom.coordinates[0][0].length > 0 && Number.isFinite(geom.coordinates[0][0][0][0]); + } + } catch (e) { + return false; + } + return true; +} + +function rebuildMapSources() { + if (typeof map === 'undefined') return; + const locSrc = map.getSource('locations'); + const asstSrc = map.getSource('assets'); + if (!locSrc || !asstSrc) return; // sources not yet added (before mapLoaded) + + mergedLocationsGeojson = { + type: 'FeatureCollection', + features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && f.properties.is_location), + }; + mergedAssetsGeojson = { + type: 'FeatureCollection', + features: filteredFeatures.filter(f => hasValidCoordinates(f) && f.properties && !f.properties.is_location), + }; + + applyUnifiedSignPanelOffsets(mergedAssetsGeojson); + enqueueUnifiedSignPanelTextSprites(mergedAssetsGeojson.features); + + locSrc.setData(mergedLocationsGeojson); + asstSrc.setData(mergedAssetsGeojson); +} + + +// ─── Map layers & initialization ────────────────────────────────────────────── +// Layer definitions and icon loading are in unified_map_layers.js (loaded first). + +maploader.loadMap('map'); + +maploader.addEventListener('mapLoaded', () => { + // Create empty sources so setData can be called immediately after data loads + map.addSource('locations', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); + map.addSource('assets', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); + + // Add layers in render order: fills → lines → circles/symbols (see UNIFIED_LAYERS) + UNIFIED_LAYERS.forEach(layer => { + if (!map.getLayer(layer.id)) map.addLayer(layer); + }); + + // Load per-thematic Bootstrap Icons glyphs as SDF sprites + loadUnifiedMapIcons(map); + + panelTextSpriteMap = map; + + if (window.SignPanelsShared && typeof window.SignPanelsShared.loadRoadSignImages === 'function') { + window.SignPanelsShared.loadRoadSignImages(map); + } + + map.on('styleimagemissing', function (e) { + const missingId = e && e.id; + if (!missingId) return; + + if (isPanelTextSpriteId(missingId)) { + const hasDefinition = (typeof ensurePanelTextDefinitionFromMap === 'function') + ? ensurePanelTextDefinitionFromMap(map, missingId) + : false; + if (!hasDefinition) return; + + if (typeof ensurePanelTextSprite === 'function') { + ensurePanelTextSprite(map, missingId); + } + return; + } + + if (window.SignPanelsShared && typeof window.SignPanelsShared.handleRoadSignStyleImageMissing === 'function') { + window.SignPanelsShared.handleRoadSignStyleImageMissing({ + map, + event: e, + onImageUpdated: scheduleUnifiedSignPanelOffsetRefresh, + }); + } + }); + + window.addEventListener('panelTextSpriteRendered', scheduleUnifiedSignPanelOffsetRefresh); + + // Push any data already loaded before the map was ready + rebuildMapSources(); + + // ── Map controls ────────────────────────────────────────────────────────── + + // Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost) + if (typeof LayerSwitcherControl !== 'undefined') { + const layerSwitcher = new LayerSwitcherControl({ + layers: [{ + id: 'ortho', + name: 'Orthophotos', + icon: 'satellite', + group: 'ortho', + beforeLayer: 'locations', + layer: { type: 'raster', paint: { 'raster-opacity': 1 } }, + sublayers: [ + { + id: 'urbis', + name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)', + default: true, + source: { + type: 'raster', + tiles: [ + window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}' + ], + tileSize: 256, + attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '\u00a9 Brussels UrbIS', + }, + }, + { + id: 'vlaanderen', + name: 'Vlaanderen 2023', + source: { + type: 'raster', + tiles: ['https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'], + tileSize: 256, + attribution: '\u00a9 Digitaal Vlaanderen - Orthofoto 2023', + }, + }, + ], + }], + }); + // Reset view — fly back to initial Brussels Region view + map.addControl({ + onAdd(m) { + this._map = m; + this._container = document.createElement('div'); + this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.title = gettext('Vue initiale (Bruxelles)'); + btn.style.cssText = 'display:flex;align-items:center;justify-content:center;'; + btn.innerHTML = ''; + btn.addEventListener('click', () => { + searchIsGlobal = false; + m.flyTo({ center: [4.3572, 50.8477], zoom: 11, duration: 1000 }); + m.once('moveend', function () { refreshData(); }); + }); + this._container.appendChild(btn); + return this._container; + }, + onRemove() { + this._container.parentNode?.removeChild(this._container); + this._map = undefined; + } + }, 'top-right'); + + map.addControl(layerSwitcher, 'top-right'); + } + + // Navigation (zoom +/-) + map.addControl(new maplibregl.NavigationControl(), 'top-right'); + + // Address search (custom autocomplete) + if (typeof AddressSearchControl !== 'undefined' && typeof AUTOCOMPLETE_STREET_URL !== 'undefined') { + map.addControl(new AddressSearchControl({ autocompleteUrl: AUTOCOMPLETE_STREET_URL }), 'top-left'); + } + + // Restore saved map position (set by applyInitialPreferences) + if (window._restoredMapCenter && window._restoredMapZoom) { + map.jumpTo({ center: window._restoredMapCenter, zoom: window._restoredMapZoom }); + delete window._restoredMapCenter; + delete window._restoredMapZoom; + // Re-run with the correct zoom/bbox now that the map is at its saved position + refreshData(); + } + + let isBoxZooming = false; + map.on('boxzoomend', function () { + isBoxZooming = true; + }); + + let moveEndTimer = null; + map.on('moveend', function (e) { + // Ignore moveend not triggered by a real user gesture (e.g. from map.resize() + // called by MapLibre's internal ResizeObserver after a layout change). + // User-initiated events (pan, zoom, keyboard) always carry a native browser + // Event as originalEvent; programmatic / resize-triggered ones do not. + const isUserGesture = e && e.originalEvent && typeof e.originalEvent.type === 'string'; + const isBoxZoomMove = isBoxZooming; + if (!isUserGesture && !isBoxZoomMove) return; + + searchIsGlobal = false; + isBoxZooming = false; + + // Compute immediately (moveend fires after the gesture, zoom is already final) + const zoom = map.getZoom(); + const anyZoomRelevant = Array.from(activeThematics).some( + code => !ALWAYS_LOAD_ASSETS_THEMATICS.has(code) && + (zoom >= MIN_ASSETS_ZOOM || assetsLoadModes[code] === 'bbox') + ); + + // Show spinner right away for instant visual feedback — before the debounce delay + if (anyZoomRelevant) showSpinner(); + + clearTimeout(moveEndTimer); + moveEndTimer = setTimeout(() => { + if (anyZoomRelevant) refreshData(); + else applyFilters(); // update table to current viewport without re-fetching + schedulePreferencesSave(); + }, 200); + }); + + // Click interaction on map elements: show popup when assets are found at click position. + ['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].forEach(layerId => { + if (!map.getLayer(layerId)) return; + map.on('click', layerId, showAssetClickPopup); + if (['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical'].includes(layerId)) { + map.on('mouseenter', layerId, function () { map.getCanvas().style.cursor = 'pointer'; }); + map.on('mouseleave', layerId, function () { map.getCanvas().style.cursor = ''; }); + } + }); + + // Close popup only when clicking outside interactive layers. + map.on('click', function (e) { + if (!assetClickPopup || !assetClickPopup.isOpen()) return; + + const interactiveLayers = ['assets', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'] + .filter(layerId => map.getLayer(layerId)); + if (!interactiveLayers.length) { + assetClickPopup.remove(); + return; + } + + const hits = map.queryRenderedFeatures(e.point, { layers: interactiveLayers }); + if (hits.length === 0) assetClickPopup.remove(); + }); +}); + +// ─── UI helpers ─────────────────────────────────────────────────────────────── + +function showSpinner() { + const spinner = document.getElementById('unified-spinner'); + // Position the overlay to start at the table body (below toolbar + thead) + // so headers and the columns button remain visible. + const tableBody = spinner.parentElement.querySelector('.fixed-table-body'); + spinner.style.top = tableBody ? ((tableBody.offsetTop + 50) + 'px') : '0'; + spinner.style.display = 'flex'; +} + +function hideSpinner() { + const spinner = document.getElementById('unified-spinner'); + spinner.style.display = 'none'; + spinner.style.top = '0'; // reset for next show +} + +function showEmptyState() { + hideSpinner(); + document.getElementById('unified-empty-state').style.display = ''; + updateAssetsHint(); +} + +function hideEmptyState() { + document.getElementById('unified-empty-state').style.display = 'none'; +} + +function updateFilterBadge() { + const badge = document.getElementById('filter-badge'); + const countEl = document.getElementById('filter-count'); + if (!badge) return; + const hasFilterValue = (value) => { + if (Array.isArray(value)) return value.length > 0; + return value !== null && value !== undefined && value !== '' && value !== false; + }; + + let n = 0; + if (activeThematics.size > 0) n += 1; + if (activeCategories.size > 0) n += 1; + if (activeModels.size > 0) n += 1; + + const thematicBlocksCount = Object.values(activeConditionalFilters).reduce((acc, filterMap) => { + if (!filterMap || typeof filterMap !== 'object') return acc; + return acc + Object.values(filterMap).reduce((subtotal, value) => { + return subtotal + (hasFilterValue(value) ? 1 : 0); + }, 0); + }, 0); + n += thematicBlocksCount; + + if (currentTypeFilter !== 'all') n += 1; + if (document.querySelectorAll('[name="filter-status"]:checked').length > 0) n += 1; + if (document.querySelectorAll('[name="filter-validation-status"]:checked').length > 0) n += 1; + + if (countEl) countEl.textContent = n; + badge.classList.toggle('d-none', n === 0); +} + +function updateSelectionStatus() { + const el = document.getElementById('unified-selection-status'); + if (el) el.textContent = `${selectedIds.size} ${gettext('sélectionné(s)')}`; + // Update hidden form for preventive + const hiddenInput = document.getElementById('preventive-selected-ids'); + if (hiddenInput) hiddenInput.value = Array.from(selectedIds).join(','); + const hiddenLocationsInput = document.getElementById('preventive-selected-locations-ids'); + if (hiddenLocationsInput) hiddenLocationsInput.value = Array.from(selectedIds).join(','); +} + +function buildPreventiveSelectedAssetsPayload() { + const rows = $('#unified-table').bootstrapTable('getData') || []; + const byId = new Map(rows.filter(r => !r.is_location).map(r => [String(r.id), r])); + const payload = []; + + Array.from(selectedIds).forEach(rawId => { + const id = String(rawId); + const row = byId.get(id); + if (!row || !row.asset_type) return; + payload.push({ model: String(row.asset_type).toLowerCase().trim(), object_id: Number(id) }); + }); + + return payload; +} + +function bindPreventiveSelectionSubmit() { + const form = document.getElementById('preventive-select-form'); + if (!form) return; + + form.addEventListener('submit', function () { + const selectedCsv = Array.from(selectedIds).join(','); + const selectedIdsInput = document.getElementById('preventive-selected-ids'); + const selectedLocationsInput = document.getElementById('preventive-selected-locations-ids'); + const selectedAssetsInput = document.getElementById('preventive-selected-assets'); + + if (selectedIdsInput) selectedIdsInput.value = selectedCsv; + if (selectedLocationsInput) selectedLocationsInput.value = selectedCsv; + + if (selectedAssetsInput) { + const assetsPayload = (PREVENTIVE_ELEMENT_TYPE === 'assets') + ? buildPreventiveSelectedAssetsPayload() + : []; + selectedAssetsInput.value = JSON.stringify(assetsPayload); + } + }); +} + +function buildCategoryLookup() { + categoryLabelByThematicAndId = {}; + categoryValueByThematicAndLabel = {}; + + Object.entries(THEMATIC_CATEGORIES || {}).forEach(([thematicCode, categories]) => { + categoryLabelByThematicAndId[thematicCode] = {}; + categoryValueByThematicAndLabel[thematicCode] = {}; + + (categories || []).forEach(category => { + const categoryId = String(category.id); + const categoryLabel = category.name || ''; + const categoryValue = `${thematicCode}::${categoryLabel}`; + categoryLabelByThematicAndId[thematicCode][categoryId] = categoryLabel; + categoryValueByThematicAndLabel[thematicCode][categoryLabel] = categoryValue; + }); + }); +} + +function getFeatureCategoryValue(feature) { + const p = feature.properties || {}; + const thematicCode = p.thematic || feature._thematic_code || ''; + const categoryLabel = p.category || ''; + const categoryId = p.category_id != null ? String(p.category_id) : ''; + + if (thematicCode && categoryLabel && categoryValueByThematicAndLabel[thematicCode] && categoryValueByThematicAndLabel[thematicCode][categoryLabel]) { + return categoryValueByThematicAndLabel[thematicCode][categoryLabel]; + } + + if (thematicCode && categoryId && categoryLabelByThematicAndId[thematicCode] && categoryLabelByThematicAndId[thematicCode][categoryId]) { + return `${thematicCode}::${categoryLabelByThematicAndId[thematicCode][categoryId]}`; + } + + if (thematicCode && categoryLabel) { + return `${thematicCode}::${categoryLabel}`; + } + + return ''; +} + +function getFeatureModelValue(feature) { + const p = feature.properties || {}; + const thematicCode = p.thematic || feature._thematic_code || ''; + const modelLabel = p.model || ''; + if (!modelLabel) return ''; + + const categoryValue = getFeatureCategoryValue(feature); + if (categoryValue) { + const sepIndex = categoryValue.indexOf('::'); + const categoryLabel = sepIndex >= 0 ? categoryValue.slice(sepIndex + 2) : ''; + + const categoryOption = getVisibleCategoryOptions().find(option => ( + option.thematicCode === thematicCode && + option.label === categoryLabel + )); + + if (categoryOption) { + const cacheKey = `${categoryOption.thematicCode}::${categoryOption.categoryId}`; + const models = modelsByCategoryKey[cacheKey] || []; + const matchedModel = models.find(model => model.label === modelLabel); + if (matchedModel && matchedModel.modelId != null) { + return `${categoryValue}::${String(matchedModel.modelId)}`; + } + } + + // Fallback kept for robustness when model catalogs are not loaded yet. + return `${categoryValue}::${modelLabel}`; + } + + if (thematicCode) { + return `${thematicCode}::::${modelLabel}`; + } + + return `::::${modelLabel}`; +} + +function getConditionalFilterValue(thematicCode, filterKey, filterType) { + const thematicFilters = activeConditionalFilters[thematicCode] || {}; + const raw = thematicFilters[filterKey]; + + if (filterType === 'multi') { + return Array.isArray(raw) ? raw : []; + } + return typeof raw === 'string' ? raw : ''; +} + +function setConditionalFilterValue(thematicCode, filterKey, filterType, value) { + if (!activeConditionalFilters[thematicCode]) activeConditionalFilters[thematicCode] = {}; + + if (filterType === 'multi') { + activeConditionalFilters[thematicCode][filterKey] = value.filter(Boolean); + if (!activeConditionalFilters[thematicCode][filterKey].length) { + delete activeConditionalFilters[thematicCode][filterKey]; + } + } else if (value) { + activeConditionalFilters[thematicCode][filterKey] = value; + } else { + delete activeConditionalFilters[thematicCode][filterKey]; + } + + if (Object.keys(activeConditionalFilters[thematicCode]).length === 0) { + delete activeConditionalFilters[thematicCode]; + } +} + +function pruneConditionalFiltersForActiveThematics() { + Object.keys(activeConditionalFilters).forEach(thematicCode => { + if (!activeThematics.has(thematicCode)) { + delete activeConditionalFilters[thematicCode]; + } + }); +} + +function getConditionalFiltersForThematic(thematicCode) { + return activeConditionalFilters[thematicCode] || {}; +} + +function getVisibleCategoryOptions() { + const visible = []; + Array.from(activeThematics).forEach(thematicCode => { + const categories = THEMATIC_CATEGORIES[thematicCode] || []; + categories.forEach(category => { + visible.push({ + thematicCode, + thematicName: (THEMATIC_CONFIG[thematicCode] && THEMATIC_CONFIG[thematicCode].name) || thematicCode, + categoryId: String(category.id), + label: category.name || '', + value: `${thematicCode}::${category.name || ''}`, + icon: (THEMATIC_CONFIG[thematicCode] && THEMATIC_CONFIG[thematicCode].icon) || '', + }); + }); + }); + return visible; +} + +function getSelectedCategoryOptions() { + return getVisibleCategoryOptions().filter(category => activeCategories.has(category.value)); +} + +function buildAssetModelsUrl(thematicCode, categoryId) { + if (!ASSET_MODELS_BY_CATEGORY_URL_TEMPLATE) return ''; + return ASSET_MODELS_BY_CATEGORY_URL_TEMPLATE + .replace('THEMATIC_CODE', encodeURIComponent(thematicCode)) + .replace('/0/models/', `/${encodeURIComponent(String(categoryId))}/models/`); +} + +function ensureModelsLoadedForSelectedCategories() { + const selectedCategories = getSelectedCategoryOptions(); + + selectedCategories.forEach(category => { + const cacheKey = `${category.thematicCode}::${category.categoryId}`; + if (modelsByCategoryKey[cacheKey] || pendingModelFetches[cacheKey]) return; + + const url = buildAssetModelsUrl(category.thematicCode, category.categoryId); + if (!url) { + modelsByCategoryKey[cacheKey] = []; + return; + } + + pendingModelFetches[cacheKey] = fetch(url) + .then(response => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then(data => { + const byLabel = new Map(); + (data.models || []).forEach(model => { + const label = String(model.name || model.code || '').trim(); + if (!label) return; + + const modelId = model.id != null ? String(model.id) : ''; + if (!modelId) return; + + const modelValue = `${category.value}::${modelId}`; + if (!byLabel.has(modelValue)) { + byLabel.set(modelValue, { + thematicCode: category.thematicCode, + thematicName: category.thematicName, + categoryLabel: category.label || gettext('Sans catégorie'), + label, + modelId, + value: modelValue, + }); + } + }); + modelsByCategoryKey[cacheKey] = Array.from(byLabel.values()); + }) + .catch(() => { + modelsByCategoryKey[cacheKey] = []; + }) + .finally(() => { + delete pendingModelFetches[cacheKey]; + renderModelFilters(); + }); + }); +} + +function getVisibleModelOptions() { + const modelsByValue = new Map(); + const selectedCategories = getSelectedCategoryOptions(); + + selectedCategories.forEach(category => { + const cacheKey = `${category.thematicCode}::${category.categoryId}`; + (modelsByCategoryKey[cacheKey] || []).forEach(model => { + if (!modelsByValue.has(model.value)) { + modelsByValue.set(model.value, model); + } + }); + }); + + return Array.from(modelsByValue.values()).sort((a, b) => { + const thematicCmp = a.thematicName.localeCompare(b.thematicName, 'fr'); + if (thematicCmp !== 0) return thematicCmp; + const categoryCmp = (a.categoryLabel || '').localeCompare((b.categoryLabel || ''), 'fr'); + if (categoryCmp !== 0) return categoryCmp; + return a.label.localeCompare(b.label, 'fr'); + }); +} + +function getVisibleConditionalFilterDefinitions() { + const defs = []; + Array.from(activeThematics).forEach(thematicCode => { + const thematicDefs = CONDITIONAL_FILTER_CONFIG[thematicCode] || []; + thematicDefs.forEach(def => { + defs.push({ + thematicCode, + thematicName: (THEMATIC_CONFIG[thematicCode] && THEMATIC_CONFIG[thematicCode].name) || thematicCode, + icon: (THEMATIC_CONFIG[thematicCode] && THEMATIC_CONFIG[thematicCode].icon) || '', + ...def, + }); + }); + }); + return defs; +} + +function normalizeSignPanelTypeOption(option) { + const value = option && option.value != null ? String(option.value) : ''; + const code = String((option && (option.code || option.label)) || '').trim(); + const name = String((option && option.name) || '').trim(); + const imageUrl = String((option && option.image_url) || '').trim(); + + let group = String((option && option.group) || '').trim(); + let groupLabel = String((option && option.group_label) || '').trim(); + if (!group) { + const firstChar = code ? code[0].toUpperCase() : ''; + group = /^[A-Za-z]$/.test(firstChar) ? firstChar : 'Autre'; + } + if (!groupLabel) { + groupLabel = group; + } + + return { value, code, name, imageUrl, group, groupLabel }; +} + +function buildSignPanelTypeGroups(options) { + const grouped = new Map(); + const groupLabels = new Map(); + + (options || []).map(normalizeSignPanelTypeOption).forEach(option => { + if (!option.value || !option.code) return; + if (!grouped.has(option.group)) { + grouped.set(option.group, []); + groupLabels.set(option.group, option.groupLabel); + } + grouped.get(option.group).push(option); + }); + + const definedOrder = ['danger', 'priority', 'prohibition', 'obligation', 'parking', 'indication', 'zone', 'additional', 'other']; + const groupKeys = []; + + definedOrder.forEach(key => { + if (grouped.has(key)) { + groupKeys.push(key); + } + }); + + Array.from(grouped.keys()).forEach(key => { + if (!definedOrder.includes(key)) { + groupKeys.push(key); + } + }); + + return { grouped, groupLabels, groupKeys }; +} + +function normalizePanelTypeCodeForImage(code) { + let normalized = String(code || '').trim(); + if (!normalized) return ''; + + const upper = normalized.toUpperCase(); + if (upper === 'XA') { + normalized = 'GXA'; + } else if (upper === 'XB') { + normalized = 'GXB'; + } else if (upper === 'XD') { + normalized = 'GXD'; + } else if (upper.startsWith('Z') && upper.length > 1) { + normalized = normalized.slice(1); + } + + return normalized.replace(/["']/g, '').trim(); +} + +function buildPanelTypeImageCandidates(code) { + const normalizedCode = normalizePanelTypeCodeForImage(code); + if (!normalizedCode) return []; + + const upperCode = normalizedCode.toUpperCase(); + const titleCode = upperCode.charAt(0) + upperCode.slice(1).toLowerCase(); + const lowerCode = upperCode.toLowerCase(); + const uniqueCodes = Array.from(new Set([upperCode, titleCode, lowerCode])); + const candidates = []; + + uniqueCodes.forEach(item => { + candidates.push(`/static/assets/road_signs/2025/${encodeURIComponent(item)}.svg`); + }); + uniqueCodes.forEach(item => { + candidates.push(`/static/assets/road_signs/2025/${encodeURIComponent(item)}.png`); + }); + + return candidates; +} + +function appendPanelTypeImage(container, code, imageUrl, options = {}) { + if (!container) return; + + const imageHeight = options.height || '40px'; + const imageWidth = options.width || '40px'; + const objectFit = options.objectFit || 'contain'; + + function createImage(src) { + const img = document.createElement('img'); + img.src = src; + img.alt = code || ''; + img.style.maxHeight = '100%'; + img.style.maxWidth = '100%'; + img.style.height = imageHeight; + img.style.width = imageWidth; + img.style.objectFit = objectFit; + return img; + } + + const candidates = buildPanelTypeImageCandidates(code); + let candidateIndex = 0; + + function tryFallbackCandidate() { + const src = candidates[candidateIndex++]; + if (!src) { + container.textContent = code || ''; + return; + } + + const fallbackImg = createImage(src); + fallbackImg.onerror = tryFallbackCandidate; + container.innerHTML = ''; + container.appendChild(fallbackImg); + } + + if (imageUrl) { + const img = createImage(imageUrl); + img.onerror = tryFallbackCandidate; + container.innerHTML = ''; + container.appendChild(img); + return; + } + + tryFallbackCandidate(); +} + +function renderSignPanelTypeConditionalFilter(thematicCode, def, selectedValues) { + const filterName = `${thematicCode}::${def.key}`; + const selectId = `cond-${thematicCode}-${def.key}-select`; + const selectedSet = new Set((selectedValues || []).map(value => String(value))); + const { grouped, groupLabels, groupKeys } = buildSignPanelTypeGroups(def.options || []); + + const hiddenOptionsHtml = Array.from(grouped.values()).flat().map(option => { + const isSelected = selectedSet.has(option.value); + return ` + + `; + }).join(''); + + const styles = ` + + `; + + const accordionId = `accordion-sign-panel-types`; + const accordionItemsHtml = groupKeys.map((group, index) => { + const groupLabel = groupLabels.get(group) || group; + const options = grouped.get(group) || []; + + const itemsHtml = options.map(option => { + const title = option.name ? `${option.code} - ${option.name}` : option.code; + return ` +
+
+
+ ${escapeHtml(option.code)} +
+
+ `; + }).join(''); + + const collapseId = `collapse-sign-${group}`; + const headingId = `heading-sign-${group}`; + + return ` +
+

+ +

+
+
+
+ ${itemsHtml} +
+
+
+
+ `; + }).join(''); + + return ` + ${styles} +
+
${escapeHtml(def.label || def.key)}
+ + + + ${groupKeys.length ? ` +
+ ${accordionItemsHtml} +
+ +
+
+ ${escapeHtml(gettext('Aucun type sélectionné'))} +
+
+ ` : ` +
${escapeHtml(gettext('Aucun type disponible.'))}
+ `} +
+ `; +} + +function initSignPanelTypePickers() { + document.querySelectorAll('.js-sign-panel-type-picker').forEach(picker => { + const panelTypeSelect = picker.querySelector('.js-sign-panel-type-select'); + if (!panelTypeSelect || panelTypeSelect.dataset.bound === '1') return; + + panelTypeSelect.dataset.bound = '1'; + + const thematicCode = panelTypeSelect.dataset.thematic || picker.dataset.thematic || ''; + const filterKey = panelTypeSelect.dataset.filterKey || picker.dataset.filterKey || ''; + if (!thematicCode || !filterKey) return; + + const accordionButtons = picker.querySelectorAll('.accordion-button'); + const selectedList = picker.querySelector('.js-sign-selected-list'); + const emptyMessage = selectedList ? selectedList.querySelector('.empty-message') : null; + + function getSelectedValues() { + return Array.from(panelTypeSelect.options) + .filter(option => option.selected) + .map(option => String(option.value)); + } + + function syncFilterState() { + setConditionalFilterValue(thematicCode, filterKey, 'multi', getSelectedValues()); + } + + function updateSelectedList() { + const selectedOptions = Array.from(panelTypeSelect.options).filter(option => option.selected); + + if (selectedList) { + Array.from(selectedList.querySelectorAll('.selected-panel-item')).forEach(item => item.remove()); + + if (emptyMessage) { + emptyMessage.style.display = selectedOptions.length ? 'none' : 'block'; + } + + selectedOptions.forEach(option => { + const id = String(option.value); + const code = String(option.dataset.code || option.text || id).trim(); + const imageUrl = String(option.dataset.imageUrl || '').trim(); + + const item = document.createElement('div'); + item.className = 'selected-panel-item d-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1'; + + const content = document.createElement('div'); + content.className = 'd-flex align-items-center'; + + const thumb = document.createElement('div'); + thumb.className = 'd-flex align-items-center justify-content-center me-2'; + thumb.style.width = '28px'; + thumb.style.height = '28px'; + appendPanelTypeImage(thumb, code, imageUrl, { width: '28px', height: '28px' }); + + const label = document.createElement('span'); + label.className = 'small fw-bold ms-1'; + label.textContent = code; + + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'btn btn-sm btn-link text-danger p-0 me-1 remove-panel-type'; + removeBtn.dataset.id = id; + removeBtn.innerHTML = ''; + + content.appendChild(thumb); + content.appendChild(label); + item.appendChild(content); + item.appendChild(removeBtn); + selectedList.appendChild(item); + }); + } + + picker.querySelectorAll('.panel-item').forEach(item => { + const id = item.dataset.id; + const option = Array.from(panelTypeSelect.options).find(opt => opt.value === id); + const isSelected = !!(option && option.selected); + + item.classList.toggle('bg-primary', isSelected); + item.classList.toggle('text-white', isSelected); + item.classList.toggle('bg-white', !isSelected); + }); + + syncFilterState(); + } + + accordionButtons.forEach(btn => { + btn.addEventListener('click', function () { + const group = this.dataset.group; + const collapseDiv = picker.querySelector(`#collapse-sign-${group}`); + if (collapseDiv) { + collapseDiv.querySelectorAll('.panel-item').forEach(item => { + const placeholder = item.querySelector('.panel-img-placeholder'); + if (!placeholder || placeholder.children.length > 0) return; + + const imageUrl = item.dataset.imageUrl || ''; + const code = item.dataset.code || ''; + appendPanelTypeImage(placeholder, code, imageUrl); + }); + } + }); + }); + + picker.addEventListener('click', function (event) { + const panelItem = event.target.closest('.panel-item'); + if (panelItem && picker.contains(panelItem)) { + const id = panelItem.dataset.id; + const option = Array.from(panelTypeSelect.options).find(opt => opt.value === id); + if (option) { + option.selected = !option.selected; + updateSelectedList(); + } + return; + } + + const removeBtn = event.target.closest('.remove-panel-type'); + if (removeBtn && picker.contains(removeBtn)) { + const id = removeBtn.dataset.id; + const option = Array.from(panelTypeSelect.options).find(opt => opt.value === id); + if (option) { + option.selected = false; + updateSelectedList(); + } + } + }); + + updateSelectedList(); + }); +} + +function renderConditionalFilters() { + const container = document.getElementById('conditional-filter-options'); + if (!container) return; + + pruneConditionalFiltersForActiveThematics(); + + const defs = getVisibleConditionalFilterDefinitions(); + if (!defs.length) { + container.innerHTML = `${gettext("Sélectionnez d'abord une ou plusieurs thématiques.")}`; + return; + } + + const grouped = new Map(); + defs.forEach(def => { + if (!grouped.has(def.thematicCode)) grouped.set(def.thematicCode, []); + grouped.get(def.thematicCode).push(def); + }); + + container.innerHTML = Array.from(grouped.entries()).map(([thematicCode, thematicDefs]) => { + const thematic = THEMATIC_CONFIG[thematicCode] || {}; + const heading = thematic.name || thematicCode; + const icon = thematic.icon ? `` : ''; + + const blocks = thematicDefs.map(def => { + const filterName = `${thematicCode}::${def.key}`; + const selected = getConditionalFilterValue(thematicCode, def.key, def.type); + + if (thematicCode === 'sign' && def.key === 'panel_type' && def.type === 'multi') { + return renderSignPanelTypeConditionalFilter(thematicCode, def, selected); + } + + if (def.type === 'checkbox') { + const inputId = `cond-${thematicCode}-${def.key}`; + const checkedValue = String(def.value || '1'); + const isChecked = selected === checkedValue; + + return ` +
+
+
+ + +
+
+
+ `; + } + + if (def.type === 'date') { + const inputId = `cond-${thematicCode}-${def.key}`; + return ` +
+
${escapeHtml(def.label || def.key)}
+
+ +
+
+ `; + } + + if (def.type === 'select') { + const inputId = `cond-${thematicCode}-${def.key}`; + const optionsHtml = [``].concat( + (def.options || []).map(option => { + const isSelected = selected === String(option.value); + return ``; + }) + ).join(''); + + return ` +
+
${escapeHtml(def.label || def.key)}
+
+ +
+
+ `; + } + + const optionsHtml = (def.options || []).map((option, idx) => { + const optionId = `cond-${thematicCode}-${def.key}-${idx}`; + const inputType = def.type === 'single' ? 'radio' : 'checkbox'; + const isChecked = def.type === 'single' + ? (selected === String(option.value)) + : selected.includes(String(option.value)); + + return ` +
+ + +
+ `; + }).join(''); + + return ` +
+
${escapeHtml(def.label || def.key)}
+
${optionsHtml}
+
+ `; + }).join(''); + + return ` +
+
${icon}${escapeHtml(heading)}
+ ${blocks} +
+ `; + }).join(''); + + initSignPanelTypePickers(); + + document.querySelectorAll('.conditional-filter-input').forEach(input => { + input.addEventListener('change', function () { + const thematicCode = this.dataset.thematic; + const filterKey = this.dataset.filterKey; + const filterType = this.dataset.filterType || 'multi'; + + if (filterType === 'single') { + const checked = document.querySelector(`input[name="${escapeCssSelectorValue(`${thematicCode}::${filterKey}`)}"]:checked`); + setConditionalFilterValue(thematicCode, filterKey, 'single', checked ? checked.value : ''); + } else if (filterType === 'checkbox') { + setConditionalFilterValue(thematicCode, filterKey, 'single', this.checked ? this.value : ''); + } else if (filterType === 'date' || filterType === 'select') { + setConditionalFilterValue(thematicCode, filterKey, 'single', this.value || ''); + } else { + const selected = Array.from(document.querySelectorAll( + `input[name="${escapeCssSelectorValue(`${thematicCode}::${filterKey}`)}"]:checked` + )).map(el => el.value); + setConditionalFilterValue(thematicCode, filterKey, 'multi', selected); + } + }); + }); +} + +bindPreventiveSelectionSubmit(); + +function renderCategoryFilters() { + const container = document.getElementById('category-filter-options'); + if (!container) return; + + const visibleCategories = getVisibleCategoryOptions(); + const visibleValues = new Set(visibleCategories.map(item => item.value)); + activeCategories = new Set(Array.from(activeCategories).filter(value => visibleValues.has(value))); + + if (!visibleCategories.length) { + container.innerHTML = `${gettext("Sélectionnez d'abord une ou plusieurs thématiques.")}`; + return; + } + + const grouped = new Map(); + visibleCategories.forEach(item => { + if (!grouped.has(item.thematicCode)) grouped.set(item.thematicCode, []); + grouped.get(item.thematicCode).push(item); + }); + + container.innerHTML = Array.from(grouped.entries()).map(([thematicCode, categories]) => { + const thematic = THEMATIC_CONFIG[thematicCode] || {}; + const heading = thematic.name || thematicCode; + const icon = thematic.icon ? `` : ''; + const items = categories.map(category => { + const checkboxId = `category-cb-${thematicCode}-${category.categoryId}`; + const checked = activeCategories.has(category.value) ? 'checked' : ''; + return ` +
+ + +
+ `; + }).join(''); + + return ` +
+
${icon}${escapeHtml(heading)}
+
${items}
+
+ `; + }).join(''); + + document.querySelectorAll('.category-checkbox').forEach(cb => { + cb.addEventListener('change', function () { + if (this.checked) activeCategories.add(this.value); + else activeCategories.delete(this.value); + renderModelFilters(); + }); + }); +} + +function renderModelFilters() { + const container = document.getElementById('model-filter-options'); + const toggleBtn = document.getElementById('model-filter-toggle'); + if (!container || !toggleBtn) return; + + const modelSection = document.getElementById('model-filter-section'); + const sectionButtons = modelSection ? modelSection.querySelector('.filter-section-buttons') : null; + if (sectionButtons && sectionButtons.firstElementChild !== toggleBtn) { + sectionButtons.insertBefore(toggleBtn, sectionButtons.firstChild); + } + + if (activeThematics.size === 0) { + container.style.display = ''; + container.innerHTML = `${gettext("Sélectionnez d'abord une ou plusieurs thématiques.")}`; + toggleBtn.classList.add('d-none'); + return; + } + + if (activeCategories.size === 0) { + container.style.display = ''; + container.innerHTML = `${gettext("Sélectionnez d'abord une ou plusieurs catégories.")}`; + toggleBtn.classList.add('d-none'); + activeModels.clear(); + return; + } + + ensureModelsLoadedForSelectedCategories(); + + const selectedCategories = getSelectedCategoryOptions(); + const hasPendingFetch = selectedCategories.some(category => { + const cacheKey = `${category.thematicCode}::${category.categoryId}`; + return !!pendingModelFetches[cacheKey]; + }); + + if (hasPendingFetch) { + container.style.display = ''; + container.innerHTML = `${gettext('Chargement des modèles...')}`; + toggleBtn.classList.add('d-none'); + return; + } + + const visibleModels = getVisibleModelOptions(); + const visibleValues = new Set(visibleModels.map(item => item.value)); + activeModels = new Set(Array.from(activeModels).filter(value => visibleValues.has(value))); + + if (!visibleModels.length) { + container.style.display = ''; + container.innerHTML = `${gettext("Aucun modèle disponible pour les catégories sélectionnées.")}`; + toggleBtn.classList.add('d-none'); + return; + } + + const shouldCollapse = visibleModels.length > 10; + if (!shouldCollapse) modelFilterCollapsed = false; + + toggleBtn.classList.toggle('d-none', !shouldCollapse); + if (shouldCollapse) { + toggleBtn.textContent = modelFilterCollapsed + ? gettext('Afficher les modèles') + : gettext('Replier les modèles'); + } + + if (shouldCollapse && modelFilterCollapsed) { + container.style.display = ''; + container.innerHTML = `${gettext('Bloc replié. Cliquez sur "Afficher les modèles" pour voir les options.')}`; + } else { + container.style.display = ''; + + const groupedByThematic = new Map(); + visibleModels.forEach(item => { + if (!groupedByThematic.has(item.thematicCode)) groupedByThematic.set(item.thematicCode, []); + groupedByThematic.get(item.thematicCode).push(item); + }); + + container.innerHTML = Array.from(groupedByThematic.entries()).map(([thematicCode, models]) => { + const thematic = THEMATIC_CONFIG[thematicCode] || {}; + const heading = thematic.name || thematicCode; + const icon = thematic.icon ? `` : ''; + + const groupedByCategory = new Map(); + models.forEach(model => { + const categoryKey = model.categoryLabel || gettext('Sans catégorie'); + if (!groupedByCategory.has(categoryKey)) groupedByCategory.set(categoryKey, []); + groupedByCategory.get(categoryKey).push(model); + }); + + const categoryBlocks = Array.from(groupedByCategory.entries()).map(([categoryLabel, categoryModels]) => { + const items = categoryModels.map(model => { + const checkboxId = `model-cb-${String(model.value).replace(/[^a-zA-Z0-9_-]/g, '-')}`; + const checked = activeModels.has(model.value) ? 'checked' : ''; + return ` +
+ + +
+ `; + }).join(''); + + return ` +
+
${escapeHtml(categoryLabel)}
+
${items}
+
+ `; + }).join(''); + + return ` +
+
${icon}${escapeHtml(heading)}
+ ${categoryBlocks} +
+ `; + }).join(''); + + document.querySelectorAll('.model-checkbox').forEach(cb => { + cb.addEventListener('change', function () { + if (this.checked) activeModels.add(this.value); + else activeModels.delete(this.value); + }); + }); + } +} + +// Totaux +function getAssetsForTotals() { + const $table = $('#unified-table'); + if (!$table.length || typeof $table.bootstrapTable !== 'function') return []; + + const rows = $table.bootstrapTable('getData', { + useCurrentPage: false, + includeHiddenRows: false, + }) || []; + + const assetRows = rows.filter(row => !row.is_location); + + if (!selectedIds || selectedIds.size === 0) return assetRows; + + const selectedAssetRows = assetRows.filter(row => ( + selectedIds.has(row.id) || selectedIds.has(String(row.id)) + )); + return selectedAssetRows.length ? selectedAssetRows : assetRows; +} + +function groupByCategoryAndModel(assets) { + const byCat = {}; + for (const a of assets) { + const cat = a.category || gettext('Sans catégorie'); + const model = a.model || '—'; + const lengthValue = Number(a.length ?? a.length_m ?? 0); + const areaValue = Number(a.area ?? a.area_m2 ?? a.surface_area ?? a.surface ?? 0); + byCat[cat] ??= {}; + byCat[cat][model] ??= { count: 0, length_m: 0, area_m2: 0 }; + byCat[cat][model].count += 1; + byCat[cat][model].length_m += Number.isFinite(lengthValue) ? lengthValue : 0; + byCat[cat][model].area_m2 += Number.isFinite(areaValue) ? areaValue : 0; + } + return byCat; +} + +function fmtInt(n) { + return (Math.round(n) || '-').toLocaleString(); +} + +function renderTotals() { + const contentEl = document.getElementById('totalsContent'); + if (!contentEl) return; + + const assets = getAssetsForTotals(); + const grouped = groupByCategoryAndModel(assets); + const hasSelection = selectedIds && selectedIds.size > 0; + + const headerCount = interpolate( + ngettext('%(count)s élément pris en compte', '%(count)s éléments pris en compte', assets.length), + { count: assets.length }, + true + ); + + const selectionSuffix = hasSelection ? gettext('(sélection)') : gettext('(table)'); + let html = `
${headerCount} ${selectionSuffix}
`; + + const categories = Object.keys(grouped).sort((a, b) => a.localeCompare(b, 'fr')); + if (!categories.length) { + contentEl.innerHTML = `
${gettext('Aucune donnée disponible')}
`; + return; + } + + for (const cat of categories) { + const models = grouped[cat]; + const modelNames = Object.keys(models).sort((a, b) => a.localeCompare(b, 'fr')); + + const bodyRows = modelNames.map(m => { + const g = models[m]; + return ` + + ${m} + ${fmtInt(g.count)} + ${fmtInt(g.length_m)} + ${fmtInt(g.area_m2)} + + `; + }).join(''); + + const catTotals = modelNames.reduce((acc, m) => { + acc.count += models[m].count; + acc.length_m += models[m].length_m; + acc.area_m2 += models[m].area_m2; + return acc; + }, { count: 0, length_m: 0, area_m2: 0 }); + + html += ` +
+
+ ${cat} + (${fmtInt(catTotals.count)} ${gettext('au total')}) +
+
+ + + + + + + + + + ${bodyRows || ``} + + + + + + + + +
${gettext('Modèle')}${gettext('Nombre')}${gettext('Longueur (m)')}${gettext('Surface (m²)')}
${gettext('Aucun modèle')}
${gettext('Total catégorie')}${fmtInt(catTotals.count)}${fmtInt(catTotals.length_m)}${fmtInt(catTotals.area_m2)}
+
+
+ `; + } + + contentEl.innerHTML = html; +} + +function initTotalsOverlay() { + const btnTotals = document.getElementById('totals_button'); + const overlay = document.getElementById('totalsOverlay'); + const closeBtn = document.getElementById('totalsClose'); + if (!btnTotals || !overlay || !closeBtn) return; + + btnTotals.addEventListener('click', function () { + renderTotals(); + overlay.classList.add('show'); + setTimeout(() => closeBtn.focus(), 0); + }); + + function hideOverlay() { + overlay.classList.remove('show'); + } + + closeBtn.addEventListener('click', hideOverlay); + + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && overlay.classList.contains('show')) { + hideOverlay(); + } + }); +} + +document.addEventListener('DOMContentLoaded', initTotalsOverlay); diff --git a/streetup/assets/static/assets/unified_map_layers.js b/streetup/assets/static/assets/unified_map_layers.js new file mode 100644 index 0000000..40a150d --- /dev/null +++ b/streetup/assets/static/assets/unified_map_layers.js @@ -0,0 +1,812 @@ +// unified_map_layers.js +// Layer definitions and icon loading for the unified assets page. +// +// Render order enforced by UNIFIED_LAYERS (bottom → top): +// 1. fill — polygon locations (streets / areas) +// 2. fill — polygon assets (road surfaces, nature green spaces) +// 3. line — location outlines +// 4. circle — point locations (traffic-light intersections, …) +// 5. symbol — point assets (thematic icons from Bootstrap Icons font) + +// ─── Icon helpers ───────────────────────────────────────────────────────────── + +/** * Normalises an image from a URL onto a fixed-size canvas so that MapLibre's + * `icon-size` is a true pixel-size multiplier regardless of the source image's + * intrinsic dimensions. Matches the helper used in trafficlights_index.html. + * + * @param {string} url + * @param {number} [size=64] + * @returns {Promise} + */ +function loadNormalisedIcon(url, size = 64) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + const scale = Math.min(size / img.width, size / img.height); + const w = img.width * scale; + const h = img.height * scale; + ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); + resolve(ctx.getImageData(0, 0, size, size)); + }; + img.onerror = () => reject(new Error('Failed to load icon: ' + url)); + img.src = url; + }); +} + +/** * Renders a Bootstrap Icons font glyph onto a canvas and registers it in the + * map as a non-SDF sprite. The glyph is drawn in the given colour so it is + * always visible regardless of whether MapLibre's icon-color works for custom + * images. + * + * Relies on the `bootstrap-icons` font-face already being loaded by CSS. + * Uses `document.fonts.ready` so registration is deferred until fonts are + * available. + * + * @param {maplibregl.Map} map + * @param {string} iconName – name to register with map.addImage() + * @param {string} codepoint – hex codepoint string, e.g. 'f453' + * @param {number} [size=32] – canvas size in pixels + * @param {string} [color='#333333'] – fill colour for the glyph + * @returns {Promise} + */ +function registerBootstrapGlyphSdfIcon(map, iconName, codepoint, size = 32, color = '#333333') { + if (map.hasImage(iconName)) return Promise.resolve(); + + return document.fonts.ready.then(() => { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, size, size); + ctx.font = `${Math.round(size * 0.85)}px "bootstrap-icons"`; + ctx.fillStyle = color; // thematic colour baked in — no reliance on icon-color SDF + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(String.fromCodePoint(parseInt(codepoint, 16)), size / 2, size / 2); + const imageData = ctx.getImageData(0, 0, size, size); + if (!map.hasImage(iconName)) { + map.addImage(iconName, imageData); // no sdf:true — colour is baked in + } + }); +} + +/** + * Loads one icon per thematic into the map. + * + * Priority: + * 1. If `UNIFIED_ICON_URLS` (injected by the Django template) provides a URL + * for a thematic, the image is loaded via `loadNormalisedIcon` and kept as + * a full-colour (non-SDF) sprite — it retains its original colours. + * 2. All other thematics use Bootstrap Icons font glyphs rendered as SDF + * sprites, recoloured at runtime via the `icon-color` paint property. + * + * @param {maplibregl.Map} map + */ +function loadUnifiedMapIcons(map) { + // ── File-based icons (full colour, non-SDF) ───────────────────────────── + // UNIFIED_ICON_URLS is set in the inline + +{% endblock script %} \ No newline at end of file diff --git a/streetup/assets/templates/assets/artworks_index.html b/streetup/assets/templates/assets/artworks_index.html new file mode 100644 index 0000000..d96f1e5 --- /dev/null +++ b/streetup/assets/templates/assets/artworks_index.html @@ -0,0 +1,154 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} + diff --git a/streetup/assets/templates/assets/artworks_monuments_detail.html b/streetup/assets/templates/assets/artworks_monuments_detail.html new file mode 100644 index 0000000..79e82c3 --- /dev/null +++ b/streetup/assets/templates/assets/artworks_monuments_detail.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+

+ Statue{{ monument.code}} - {{ monument.name_fr }} +

+
+
+
+ {{ monument.get_status_display }}
+ {{ monument.get_monument_type_display|default:"" }}
+
+
+ {{ monument.location_fr|default:"" }}
+ {% if monument.lon %} + + Street View + View in Google Street View + + {% endif %} +
+
+
+
+ {{ monument.installation_date|date:"F Y"|default:"" }}
+ {{ monument.artist|default:"" }}
+
+ {% if monument.description_fr %} +
{{ monument.description_fr }}
+ {% endif %} +
+
+
+
+ {{ monument.last_preventive_date|default:"" }}
+
+
+ + + +
+
+

Documents

+
+
+
+ {% for document in monument.documents.all %} + + {% endfor %} +
+
+
+ + +
+
+

Interventions

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
CodeTitreDébutStatut
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ monument.lon|json_script:"lon_value" }} + {{ monument.lat|json_script:"lat_value" }} + + +{% endblock script %} \ No newline at end of file diff --git a/streetup/assets/templates/assets/artworks_street_detail.html b/streetup/assets/templates/assets/artworks_street_detail.html new file mode 100644 index 0000000..b6426ef --- /dev/null +++ b/streetup/assets/templates/assets/artworks_street_detail.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+ + {% translate "Retour à la vue Artworks" %} + + +
+
+

{{ street.code }} - {{ street.get_name }}

+
+
+
{% translate "Commune" %}
+
{{ street.municipality|default:"—" }}
+
+
+
{% translate "Code voirie" %}
+
{{ street.road_code|default:"—" }}
+
+
+
{% translate "Section" %}
+
{{ street.section_code|default:"—" }}
+
+
+
{% translate "Niveau" %}
+
{{ street.street_level|default:"—" }}
+
+
+
+
+ +
+
+
+
+

{% translate "Monuments" %}

+ {{ monuments_count }} +
+
+ {% if monuments_count %} + + {% else %} +

{% translate "Aucun monument actif n'est lié à cette localisation." %}

+ {% endif %} +
+
+
+
+
+
+

{% translate "Fontaines" %}

+ {{ fountains_count }} +
+
+ {% if fountains_count %} + + {% else %} +

{% translate "Aucune fontaine active n'est liée à cette localisation." %}

+ {% endif %} +
+
+
+
+
+{% endblock content %} diff --git a/streetup/assets/templates/assets/base_assets_index.html b/streetup/assets/templates/assets/base_assets_index.html new file mode 100644 index 0000000..2d2a973 --- /dev/null +++ b/streetup/assets/templates/assets/base_assets_index.html @@ -0,0 +1,425 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block head %} + + + + + +{% endblock head %} + +{% block content %} +
+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ + + +
+
+ + +
+
+ +
+ + +
+ + +
+ + + + +
+
+ + +
+
+ +
+
+
+
+ + {% with default_tab_value=default_tab|default:'locations' %} + +
+
+ + +
+ + + {% if preventive_draft_id and preventive_element_type == 'locations' %} + + {% endif %} +
+ {% blocktranslate count counter=selected_locations_ids|length %} + {{ counter }} localisation sélectionnée + {% plural %} + {{ counter }} localisations sélectionnées + {% endblocktranslate %} +
+
{% block location_row_actions %}{% endblock %}
+
+ + + + +
+
+ {% translate "Loading..." %} +
+
+ +
+ +
+
+
+
+ + +
+ + + {% if preventive_draft_id and preventive_element_type == 'assets' %} + + {% endif %} +
+ {% blocktranslate count counter=selected_assets_ids|length %} + {{ counter }} asset sélectionné + {% plural %} + {{ counter }} assets sélectionnés + {% endblocktranslate %} +
+
+ {% block asset_row_actions %}{% endblock %} + {% if not preventive_draft_id %} + + {% endif %} +
+
+ + + + +
+
+ {% translate "Loading..." %} +
+
+ + +
+ +
+
+
+
+ {% endwith %} +
+
+ +
+
+ +
+ × +
+
+ +
+
+ +
+ + +
+ + +
+
+ +
+ + +
+
+ {% translate "Totaux des assets" %} + +
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + + {% if preventive_draft_id %} +
+ {% csrf_token %} + + + + +
+ {% endif %} + + +{% endblock content%} + + +{% block script %} + + + + {% block const_script %} + {% endblock const_script %} + + + + + {% block extra_script %} + {% endblock extra_script %} + +{% endblock script %} diff --git a/streetup/assets/templates/assets/clean_index.html b/streetup/assets/templates/assets/clean_index.html new file mode 100644 index 0000000..2ce0178 --- /dev/null +++ b/streetup/assets/templates/assets/clean_index.html @@ -0,0 +1,214 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+{% endblock locations_filter_form %} + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} diff --git a/streetup/assets/templates/assets/clean_location_detail.html b/streetup/assets/templates/assets/clean_location_detail.html new file mode 100644 index 0000000..44f4fae --- /dev/null +++ b/streetup/assets/templates/assets/clean_location_detail.html @@ -0,0 +1,210 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} + +{% block content %} +
+
+

+ + {{ location.code }} - {{ location.get_name }} +

+
+ + +
+
+ {% if location.municipality %} + {{ location.municipality }}
+ {% endif %} + {% if location.road_code %} + {{ location.road_code }}
+ {% endif %} + {% if location.section_code %} + {{ location.section_code }}
+ {% endif %} + {% if location.street_level %} + {{ location.street_level }}
+ {% endif %} +
+
+ {% if location.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + +
+
+

{% translate "Assets" %}

+
+
+ + +
+ + +
+
    + +
  • + {% translate "Litter bins" %} ({{ assets.litter_bins.count }}) +
      + {% for bin in assets.litter_bins %} +
    • + {{ bin.code }} + {% if bin.model %}{{ bin.model.get_name }}{% endif %} + {{ bin.get_status_display }} + {% if bin.volume %}({{ bin.volume }} L){% endif %} +
    • + {% empty %} +
    • {% translate "No litter bins" %}
    • + {% endfor %} +
    +
  • + +
  • + {% translate "Glass containers" %} ({{ assets.glass_containers.count }}) +
      + {% for container in assets.glass_containers %} +
    • + {{ container.code }} + {% if container.model %}{{ container.model.get_name }}{% endif %} + {{ container.get_status_display }} + {% if container.capacity %}({{ container.capacity }} L){% endif %} + {% if container.is_underground %}{% translate "Underground" %}{% endif %} +
    • + {% empty %} +
    • {% translate "No glass containers" %}
    • + {% endfor %} +
    +
  • +
+
+ + +
+
    +
  • + {% translate "Litter bins" %} ({{ all_assets.litter_bins.count }}) +
      + {% for bin in all_assets.litter_bins %} +
    • + {{ bin.code }} + {% if bin.model %}{{ bin.model.get_name }}{% endif %} + {{ bin.get_status_display }} +
    • + {% empty %} +
    • {% translate "No litter bins" %}
    • + {% endfor %} +
    +
  • +
  • + {% translate "Glass containers" %} ({{ all_assets.glass_containers.count }}) +
      + {% for container in all_assets.glass_containers %} +
    • + {{ container.code }} + {% if container.model %}{{ container.model.get_name }}{% endif %} + {{ container.get_status_display }} +
    • + {% empty %} +
    • {% translate "No glass containers" %}
    • + {% endfor %} +
    +
  • +
+
+ +
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% empty %} + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Title" %}{% translate "Start" %}{% translate "Status" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
{% translate "No interventions" %}
+
+
+ +
+ + +
+
+
+
+{% endblock content %} + + +{% block script %} + {{ location.lon|json_script:"lon_value" }} + {{ location.lat|json_script:"lat_value" }} + {{ geom|json_script:"geom_value" }} + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/cleanlitterbin_detail.html b/streetup/assets/templates/assets/cleanlitterbin_detail.html new file mode 100644 index 0000000..d28b919 --- /dev/null +++ b/streetup/assets/templates/assets/cleanlitterbin_detail.html @@ -0,0 +1,419 @@ +{% extends "assets/detail_generic.html" %} +{% load i18n %} +{% load static %} + +{% block script %} +{{ block.super }} + +{% endblock script %} + +{% block extra_blocks %} +{% with observations=asset.observations.all %} +
+ + {# ── Header ── #} +
+
+ {% translate "Observations" %} + {{ observations|length }} +
+ {% if allowed_update_fields %} + + {% endif %} +
+ + {# ── Body ── #} +
+ {% if not observations %} +

{% translate "Aucune observation enregistrée." %}

+ {% endif %} + +
+ + {% for obs in observations %} +
+

+ +

+
+
+ + {# ════ VIEW PANEL ════ #} +
+ + {# Note générale #} + {% if obs.observer_note %} +

{{ obs.observer_note }}

+ {% endif %} + +
+ + {# ── Colonne Sol ── #} +
+

{% translate "Sol" %}

+ + + {% with yes='' no='' %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endwith %} + + + + + + + + + {% if obs.sol_remarque %} + + + + + {% endif %} + +
{% translate "Pente trop forte" %}{% if obs.sol_pente_trop_forte %}{% else %}{% endif %}
{% translate "Sol instable" %}{% if obs.sol_instable %}{% else %}{% endif %}
{% translate "Surface réfléchissante" %}{% if obs.sol_reflechissant %}{% else %}{% endif %}
{% translate "Surface glissante" %}{% if obs.sol_glissant %}{% else %}{% endif %}
{% translate "Fissures" %}{% if obs.sol_fentes %}{% else %}{% endif %}
{% translate "Entrave" %}{% if obs.sol_entrave %}{% else %}{% endif %}
{% translate "Dalles podotactiles" %}{% if obs.sol_dalles_podo %}{% else %}{% endif %}
{% translate "Pente transversale" %}{% if obs.sol_pente_transversale is not None %}{{ obs.sol_pente_transversale|stringformat:"g" }} %{% else %}—{% endif %}
{% translate "Pente longitudinale" %}{% if obs.sol_pente_longitudinale is not None %}{{ obs.sol_pente_longitudinale|stringformat:"g" }} %{% else %}—{% endif %}
{% translate "Remarque" %}{{ obs.sol_remarque }}
+
+ + {# ── Colonne Accessibilité ── #} +
+

{% translate "Accessibilité" %}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if obs.acc_point_interet %} + + + + + {% endif %} + {% if obs.acc_remarque %} + + + + + {% endif %} + +
{% translate "Type de quartier" %}{% if obs.acc_type_quartier %}{{ obs.get_acc_type_quartier_display }}{% else %}—{% endif %}
{% translate "Densité flux piéton" %}{% if obs.acc_densite_flux_pieton %}{{ obs.get_acc_densite_flux_pieton_display }}{% else %}—{% endif %}
{% translate "Approche 2 m dégagée" %}{% if obs.acc_approche_2m %}{% else %}{% endif %}
{% translate "Voirie à traverser" %}{% if obs.acc_voirie_a_traverser %}{% else %}{% endif %}
{% translate "Poubelle isolée" %}{% if obs.acc_poubelle_isolee %}{% else %}{% endif %}
{% translate "Gêne flux routier" %}{% if obs.acc_gene_flux_routier %}{% else %}{% endif %}
{% translate "Gêne flux piéton" %}{% if obs.acc_gene_flux_pieton %}{% else %}{% endif %}
{% translate "Itinéraire logique" %}{% if obs.acc_chemin_logique %}{% else %}{% endif %}
{% translate "Contraste visuel" %}{% if obs.acc_contraste %}{% else %}{% endif %}
{% translate "Indicateurs de tri" %}{% if obs.acc_indications_tri %}{% else %}{% endif %}
{% translate "Accès agent (véhicule)" %} + {% if obs.acc_agent_vehicule %} + {% if obs.acc_agent_vehicule == "normal" %}{% translate "Normal" %} + {% elif obs.acc_agent_vehicule == "difficult" %}{% translate "Difficile" %} + {% else %}{% translate "Impossible" %}{% endif %} + {% else %}—{% endif %} +
{% translate "Accès agent (pied)" %} + {% if obs.acc_agent_pied %} + {% if obs.acc_agent_pied == "normal" %}{% translate "Normal" %} + {% elif obs.acc_agent_pied == "difficult" %}{% translate "Difficile" %} + {% else %}{% translate "Impossible" %}{% endif %} + {% else %}—{% endif %} +
{% translate "Points d'intérêt" %} +
+ {% for poi in obs.acc_point_interet %}{{ poi }}{% endfor %} + {% if obs.acc_point_interet_autre %}{{ obs.acc_point_interet_autre }}{% endif %} +
+
{% translate "Remarque" %}{{ obs.acc_remarque }}
+
+ +
{# /flex #} +
{# /obs-view-panel #} + + {# ════ EDIT PANEL ════ #} +
+ +

{% translate "Général" %}

+
+
+ + +
+
+ + +
+
+ +

{% translate "Sol" %}

+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +

{% translate "Accessibilité" %}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+
+ +
+ {% with poi=obs.acc_point_interet %} +
+
+
+
+
+
+
+
+
+ {% endwith %} +
+
+
+ + +
+
+ + +
+
+ + {% if allowed_update_fields %} + + {% endif %} + +
{# /obs-edit-panel #} + +
+
+
+ {% endfor %} + +
{# /accordion #} +
{# /card-body #} +
{# /card #} + +{# ── Template for NEW observations ── #} +{% if allowed_update_fields %} + +{% endif %} + +{% endwith %} +{% endblock extra_blocks %} diff --git a/streetup/assets/templates/assets/components/_documents_inner.html b/streetup/assets/templates/assets/components/_documents_inner.html new file mode 100644 index 0000000..aa56c12 --- /dev/null +++ b/streetup/assets/templates/assets/components/_documents_inner.html @@ -0,0 +1,2 @@ +{% load document_viewer_tags %} +{% render_document_viewer documents modal_id="assetDocViewerModal" allow_delete=True show_tags=True %} diff --git a/streetup/assets/templates/assets/components/add_asset_modal.html b/streetup/assets/templates/assets/components/add_asset_modal.html new file mode 100644 index 0000000..2479f99 --- /dev/null +++ b/streetup/assets/templates/assets/components/add_asset_modal.html @@ -0,0 +1,147 @@ +{% load i18n %} + +{# Template réutilisable pour ajouter un nouvel équipement #} +{# Variables requises: #} +{# - modal_id: ID unique de la modale (default: 'addAssetModal') #} +{# - thematic_code: Code de la thématique #} +{# - categories: Liste des catégories disponibles #} +{# - locations: Liste des localisations disponibles (optionnel) #} +{# - intervention_id: ID de l'intervention (optionnel, pour le lien automatique) #} +{# - is_mobile: Boolean pour version mobile simplifiée #} + + diff --git a/streetup/assets/templates/assets/components/asset_children_tree.html b/streetup/assets/templates/assets/components/asset_children_tree.html new file mode 100644 index 0000000..3540d66 --- /dev/null +++ b/streetup/assets/templates/assets/components/asset_children_tree.html @@ -0,0 +1,112 @@ +{% load i18n %} +{# Recursive component: renders a tree of related child assets. #} +{# Expected context variable: children_tree (output of get_asset_children_tree) #} +{# Optional: editable (truthy = user can edit), parent_model, parent_id #} + +{% for group in children_tree %} +
+
+ +
{{ group.label }}
+ {{ group.items|length }} + {% if editable and not group.is_parent_group %} + + {% endif %} + {% if editable and group.is_parent_group %} + + {% endif %} +
+
    + {% for item in group.items %} +
  • +
    +
    +
    + {% if item.url %} + {{ item.asset.code }} + {% else %} + {{ item.asset.code }} + {% endif %} + + {% if item.asset.status %} + {% if item.asset.status == 'active' %} + {{ item.asset.get_status_display }} + {% elif item.asset.status == 'damaged' %} + {{ item.asset.get_status_display }} + {% elif item.asset.status == 'to_replace' %} + {{ item.asset.get_status_display }} + {% else %} + {{ item.asset.get_status_display }} + {% endif %} + {% endif %} +
    + {% if item.asset.name_fr %} + {{ item.asset.name_fr }} + {% endif %} +
    + {% if editable and not group.is_parent_group %} + + {% endif %} + {% if editable and group.is_parent_group %} + + {% endif %} +
    + + {# Recursive children – no edit buttons at deeper levels #} + {% if item.children %} +
    + {% include "assets/components/asset_children_tree.html" with children_tree=item.children only %} +
    + {% endif %} +
  • + {% endfor %} +
+
+{% endfor %} diff --git a/streetup/assets/templates/assets/components/asset_documents.html b/streetup/assets/templates/assets/components/asset_documents.html new file mode 100644 index 0000000..4c181e5 --- /dev/null +++ b/streetup/assets/templates/assets/components/asset_documents.html @@ -0,0 +1,349 @@ +{% load i18n %} +{% load documents_tags document_viewer_tags %} + +
+
+
{% translate "Documents associés" %}
+ {% if asset %} + + {% endif %} +
+
+ +
+ {% render_document_viewer documents modal_id="assetDocViewerModal" allow_delete=True show_tags=True %} +
+
+
+ +{% if asset %} +{# ─── Upload modal ─── #} + + +{# Tags JSON for JS (safe – no user content, only DB-managed strings) #} + + + +{% endif %} diff --git a/streetup/assets/templates/assets/components/asset_history.html b/streetup/assets/templates/assets/components/asset_history.html new file mode 100644 index 0000000..ae1102b --- /dev/null +++ b/streetup/assets/templates/assets/components/asset_history.html @@ -0,0 +1,360 @@ +{% load i18n %} +{% load static %} +{% load short_name %} + +{# Historique des modifications de l'asset #} +
+
+
{% translate "Historique des modifications" %}
+ +
+
+
+ {# Tableau de l'historique #} +
+
+
+ {% translate "Chargement..." %} +
+
+ +
+
+ + + + + + + + + + + + {# Rows will be populated by JavaScript #} + +
{% translate "Date" %}{% translate "Utilisateur" %}{% translate "Champ" %}{% translate "Ancienne valeur" %}{% translate "Nouvelle valeur" %}
+
+
+ {% translate "Aucune modification enregistrée pour cet asset." %} +
+
+ +
+ + +
+
+ + {# Timeline de l'historique #} +
+
+
+ {# Timeline will be populated by JavaScript #} +
+
+ {% translate "Aucune modification enregistrée pour cet asset." %} +
+
+
+
+
+
+ + + + diff --git a/streetup/assets/templates/assets/components/asset_location_link.html b/streetup/assets/templates/assets/components/asset_location_link.html new file mode 100644 index 0000000..44c7e1e --- /dev/null +++ b/streetup/assets/templates/assets/components/asset_location_link.html @@ -0,0 +1,83 @@ +{% load i18n %} + +{% if location or can_edit_location %} +
+
+
+ {% if location_type == 'street' %} + {% translate "Rue associée" %} + {% elif location_type == 'intersection' %} + {% translate "Carrefour associé" %} + {% else %} + {% translate "Localisation associée" %} + {% endif %} +
+ {% if can_edit_location %} + + {% endif %} +
+
+ {% if location %} +

+ {% if location.get_absolute_url %} + + {{ location.code }} + + {% else %} + {{ location.code }} + {% endif %} + - {{ location.get_name }} +

+ {% else %} +

{% translate "Aucune localisation associée" %}

+ {% endif %} +
+
+{% endif %} + +{# Modal pour changer la localisation #} +{% if can_edit_location %} + +{% endif %} diff --git a/streetup/assets/templates/assets/components/asset_model_card.html b/streetup/assets/templates/assets/components/asset_model_card.html new file mode 100644 index 0000000..ff28351 --- /dev/null +++ b/streetup/assets/templates/assets/components/asset_model_card.html @@ -0,0 +1,110 @@ +{% load i18n %} + +{% if model %} +
+
+
{% translate "Modèle" %}
+ {% if can_edit_model %} + + {% endif %} +
+
+ + + + + + + + + + + {% if model.brand %} + + + + + {% endif %} + {% if model.category %} + + + + + {% endif %} + {% if model.description %} + + + + + {% endif %} + {% for field in model_specific_fields %} + + + + + {% endfor %} + +
{% translate "Code" %}{{ model.code }}
{% translate "Nom" %}{{ model.get_name }}
{% translate "Marque" %}{{ model.brand }}
{% translate "Catégorie" %}{{ model.category }}
{% translate "Description" %}{{ model.description }}
{{ field.label }}{{ field.display_value }}
+
+
+ +{# Modal pour changer le modèle #} +{% if can_edit_model %} + +{% endif %} + +{% endif %} diff --git a/streetup/assets/templates/assets/components/location_edit_modals.html b/streetup/assets/templates/assets/components/location_edit_modals.html new file mode 100644 index 0000000..bfe4c6f --- /dev/null +++ b/streetup/assets/templates/assets/components/location_edit_modals.html @@ -0,0 +1,554 @@ +{% load i18n %} +{% comment %} + Reusable component: modals + JS for editing, creating, and deleting locations. + Include this in any location detail or list template. + + Required template variables: + thematic_code – e.g. "trafficlights" + location_type – e.g. "trafficlightintersection" + + Optional: + location_instance – model instance (for the edit/delete modals) + entry_label – human-readable label, e.g. "Intersection" +{% endcomment %} + +{# ── Edit Modal ──────────────────────────────────────────────────────── #} + + +{# ── Delete Modal ────────────────────────────────────────────────────── #} + + +{# ── Create Modal ────────────────────────────────────────────────────── #} + + +{# ── JavaScript ──────────────────────────────────────────────────────── #} + diff --git a/streetup/assets/templates/assets/components/map_location_picker.html b/streetup/assets/templates/assets/components/map_location_picker.html new file mode 100644 index 0000000..2109982 --- /dev/null +++ b/streetup/assets/templates/assets/components/map_location_picker.html @@ -0,0 +1,46 @@ +{% load i18n %} +{% comment %} + Reusable map location picker component. + Renders a MapLibre map. Address search is provided by AddressSearchControl + (address_search_control.js), which is added to the map programmatically. + Clicking the map places a draggable marker that writes back to id_lat / id_lon. + + Required context: form (must have lat and lon fields) + JS initialisation: call window.initLocationPickerMap() after injecting this + HTML into the DOM (handled by location_edit_modals.html). +{% endcomment %} + +{# Hidden lat/lon inputs – submitted with the form; JS writes to them. #} +
+ {{ form.lat }} + {{ form.lon }} +
+ +
+ +
+ + {% translate "Position géographique" %} + ({% translate "facultatif" %}) + +
+ + +
+
+ +{# Map container – AddressSearchControl renders the search box inside the map. #} +
+
+ + {% translate "Cliquez sur la carte pour positionner le marqueur, ou glissez-le pour l'ajuster." %} + diff --git a/streetup/assets/templates/assets/components/map_polygon_picker.html b/streetup/assets/templates/assets/components/map_polygon_picker.html new file mode 100644 index 0000000..d71800f --- /dev/null +++ b/streetup/assets/templates/assets/components/map_polygon_picker.html @@ -0,0 +1,52 @@ +{% load i18n %} +{% comment %} + Reusable polygon picker component. + Renders a MapLibre map with a simple click-to-draw polygon tool. + Each click adds a vertex; clicking the first vertex (or the Finish button) + closes the polygon. The resulting GeoJSON is written to id_geom_geojson. + + If the form already has a geometry (edit mode), it is displayed on the map + and the user can redraw it. + + Required context: form (must have geom_geojson field) + JS initialisation: call window.initLocationPickerMap() after injecting this + HTML into the DOM (handled by location_edit_modals.html). +{% endcomment %} + +{# Hidden geom_geojson input – submitted with the form; JS writes to it. #} +
+ {{ form.geom_geojson }} +
+ +
+ +
+ + {% translate "Géométrie (polygone)" %} + ({% translate "facultatif" %}) + +
+ + + +
+
+ +
+
+ + {% translate "Cliquez sur « Dessiner » pour commencer à tracer un polygone sur la carte." %} + diff --git a/streetup/assets/templates/assets/controlcenters_index.html b/streetup/assets/templates/assets/controlcenters_index.html new file mode 100644 index 0000000..ce56bf8 --- /dev/null +++ b/streetup/assets/templates/assets/controlcenters_index.html @@ -0,0 +1,180 @@ +{% extends "assets/base_assets_index.html" %} +{% load static %} +{% load i18n %} + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + +{% block const_script %} + +{% endblock const_script %} diff --git a/streetup/assets/templates/assets/controlcenters_location_detail.html b/streetup/assets/templates/assets/controlcenters_location_detail.html new file mode 100644 index 0000000..4ddc668 --- /dev/null +++ b/streetup/assets/templates/assets/controlcenters_location_detail.html @@ -0,0 +1,290 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} + +{% block content %} +
+
+

+ {{ location.code}} - {{ location.get_name }} +

+
+
+
+ {{ location.control_center }}
+ +
+ + {% if location.is_main_location %} + + {% else %} + {{ location.control_center.main_location }} + {% endif %} +
+ {% if location.parent_location %} + + {{ location.parent_location }} + {% endif %} +
+
+
+ {% if location.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + + {% if location.is_main_location %} +
+
+

{% translate "Documents" %}

+ +
+
+
+ {% for document in other_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+
+ + + + + + + {% endif %} + + + +
+
+

{% translate "Assets" %}

+
+ +
+ + +
+
+
+ {{ location.get_name }} + {% if location_tree.children %} + {{ location_tree.children|length }} {% translate "sous-localisations" %} + {% endif %} + {% if location_tree.assets %} + {{ location_tree.assets|length }} {% translate "assets" %} + {% endif %} +
+ {% include "assets/controlcenters_location_tree.html" with node=location_tree only %} +
+ +
+ ... +
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ geom|json_script:"geom_value" }} + {{ location.lon|json_script:"lon_value" }} + {{ location.lat|json_script:"lat_value" }} + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/controlcenters_location_tree.html b/streetup/assets/templates/assets/controlcenters_location_tree.html new file mode 100644 index 0000000..287ecbc --- /dev/null +++ b/streetup/assets/templates/assets/controlcenters_location_tree.html @@ -0,0 +1,43 @@ +{% load i18n %} +
    + + {% if node.children %} + {% for child in node.children %} +
  • + + + + {{ child.loc.code }} + + {{ child.loc.get_name }} + {% if child.children %} + {{ child.children|length }} {% translate "sous-localisations" %} + {% endif %} + {% if child.assets %} + {{ child.assets|length }} assets + {% endif %} + +
    + {% include "assets/controlcenters_location_tree.html" with node=child only %} +
    +
  • + {% endfor %} + {% endif %} + + + {% for asset in node.assets %} +
  • + + {{ asset.code }} + {{ asset.get_name }} +
  • + {% endfor %} +
\ No newline at end of file diff --git a/streetup/assets/templates/assets/detail_generic.html b/streetup/assets/templates/assets/detail_generic.html new file mode 100644 index 0000000..547c2b3 --- /dev/null +++ b/streetup/assets/templates/assets/detail_generic.html @@ -0,0 +1,1702 @@ +{% extends base_template|default:"base.html" %} +{% load i18n %} +{% load static %} +{% load asset_tags %} + +{% block head %} + + + +{% if asset.geom or asset.lon or asset.lat %} + + +{% endif %} + +{% endblock head %} + +{% block extra_head %} + + + +{% if asset.geom or asset.lon or asset.lat %} + + +{% endif %} +{% endblock extra_head %} + +{% block upleft %} +{% if mobile_view and back_url %} + + + +{% else %} + + + +{% endif %} +{% endblock upleft %} + +{% block title %} +{% if mobile_view %} + {{ asset.code }} - {% translate "Détails" %} +{% else %} + {% translate "StreetUp" %} +{% endif %} +{% endblock title %} + +{% block upright %} + + + +{% endblock upright %} + +{% block content %} +
+ + + {% if allowed_update_fields and not mobile_view %} +
+
+
+

+ {% translate "Asset" %} : {{ asset.code }} +

+
+ {% if thematic.icon %} + + {% endif %} + {% if thematic.name %} + {{ thematic.name }} + — + {% endif %} + {{ asset_verbose_name }} +
+
+
+
+ {% if 'archive' in allowed_actions %} + + {% endif %} + {% if 'replace' in allowed_actions %} + + {% endif %} + + + +
+
+ {% else %} +
+

+ {% if thematic.icon %} + + {% endif %} + {% translate "Asset" %} : {{ asset }} +

+
+ {% if thematic.icon %} + + {% endif %} + {% if thematic.name %} + {{ thematic.name }} + — + {% endif %} + {{ asset_verbose_name }} +
+
+ {% endif %} + + {% if mobile_view and allowed_update_fields %} +
+ {% if 'archive' in allowed_actions %} + + {% endif %} + {% if 'replace' in allowed_actions %} + + {% endif %} + + + +
+ {% endif %} + +
+ +
+
+
+
{% translate "Informations générales" %}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + {% if asset.validation_status is not None or 'validation_status' in allowed_update_fields %} + + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% get_asset_specific_fields asset as asset_specific_fields %} + {% if asset_specific_fields %} + + + + + + + + {% endif %} + {% for field in asset_specific_fields %} + + + + + {% endfor %} + +
{% translate "Code" %} +
+ {{ asset.code }} +
+
+ +
+
{% translate "Nom" %} (FR) +
+ {{ asset.name_fr|default:"-" }} +
+
+ +
+
{% translate "Nom" %} (NL) +
+ {{ asset.name_nl|default:"-" }} +
+
+ +
+
{% translate "Statut" %} +
+ + {% if asset.status == 'active' %} + {{ asset.get_status_display }} + {% elif asset.status == 'damaged' %} + {{ asset.get_status_display }} + {% elif asset.status == 'to_replace' %} + {{ asset.get_status_display }} + {% else %} + {{ asset.get_status_display }} + {% endif %} + +
+
+ +
+
{% translate "Statut de validation" %} +
+ + {% if asset.validation_status == 'validated' %} + {{ asset.get_validation_status_display }} + {% elif asset.validation_status == 'to_be_validated' %} + {{ asset.get_validation_status_display }} + {% elif asset.validation_status == 'to_be_completed' %} + {{ asset.get_validation_status_display }} + {% elif asset.validation_status == 'rejected' %} + {{ asset.get_validation_status_display }} + {% else %} + {{ asset.get_validation_status_display|default:"-" }} + {% endif %} + +
+
+ +
+
{% translate "Catégorie" %} +
+ {{ asset.category|default:"-" }} +
+
+ +
+
{% translate "Installé le" %} +
+ {{ asset.installation_date|date:"d/m/Y"|default:"-" }} +
+
+ +
+
{% translate "N° de série" %} +
+ {{ asset.serial_number|default:"-" }} +
+
+ +
+
{% translate "Marque" %} +
+ {{ asset.brand|default:"-" }} +
+
+ +
+
{% translate "Durée de garantie" %} +
+ + {% if asset.warranty_duration %} + {{ asset.warranty_duration }} {% translate "mois" %} + {% with remaining=asset|warranty_remaining_months %} + {% if remaining is not None %} + — + {% if remaining > 0 %} + {% blocktranslate with n=remaining %}{{ n }} mois restants{% endblocktranslate %} + {% elif remaining == 0 %} + {% translate "Expire ce mois-ci" %} + {% else %} + {% blocktranslate with n=remaining|cut:"-" %}Expirée ({{ n }} mois){% endblocktranslate %} + {% endif %} + {% endif %} + {% endwith %} + {% else %}-{% endif %} + +
+
+ +
+
{% translate "Dernière inspection" %} +
+ {{ asset.last_inspection_date|date:"d/m/Y"|default:"-" }} +
+
+ +
+
+ {% translate "Attributs spécifiques" %} +
{{ field.label }} +
+ {{ field.display_value }} +
+
+ {% if field.field_type == 'boolean' %} + + {% elif field.field_type == 'select' %} + + {% elif field.field_type == 'number' %} + + {% elif field.field_type == 'date' %} + + {% else %} + + {% endif %} +
+
+
+
+ + + {% asset_documents asset %} + + + {% block extra_blocks %}{% endblock extra_blocks %} + + + {% if is_admin and content_type %} +
+
+ +
+
+
+
+ + {% translate "Chargement des permissions…" %} +
+
+
+
+ + {% endif %} + + + {% get_asset_parents asset as parent_groups %} + {% get_asset_children_tree asset as children_tree %} + {% if parent_groups or children_tree or allowed_update_fields %} +
+
+
{% translate "Assets liés" %}
+ {% if allowed_update_fields %} + + {% endif %} +
+
+ {% if not parent_groups and not children_tree %} +

{% translate "Aucun asset lié." %}

+ {% endif %} + {% include "assets/components/asset_children_tree.html" with children_tree=parent_groups editable=allowed_update_fields parent_model=asset_model parent_id=asset.id %} + {% include "assets/components/asset_children_tree.html" with children_tree=children_tree editable=allowed_update_fields parent_model=asset_model parent_id=asset.id %} +
+
+ {% endif %} + + + {% if asset_replacements and asset_replacements|length > 1 %} +
+
+
{% translate "Historique des remplacements" %}
+
+
+ +
+ {% for replacement in asset_replacements %} +
+
+ {% if thematic_code %} + + {{ replacement.code }} + + {% else %} + {{ replacement.code }} + {% endif %} +
+ {% if not forloop.last %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+ + +
+ + + + + + + + + + + + {% for replacement in asset_replacements %} + + + + + + + + {% endfor %} + +
{% translate "Code" %}{% translate "Statut" %}{% translate "Installé le" %}{% translate "Désinstallé le" %}{% translate "Durée" %}
+ {% if thematic_code %} + + {{ replacement.code }} + + {% else %} + {{ replacement.code }} + {% endif %} + + {% if replacement.status == 'active' %} + {{ replacement.get_status_display }} + {% elif replacement.status == 'damaged' %} + {{ replacement.get_status_display }} + {% elif replacement.status == 'to_replace' %} + {{ replacement.get_status_display }} + {% elif replacement.status == 'removed' %} + {{ replacement.get_status_display }} + {% else %} + {{ replacement.get_status_display }} + {% endif %} + {{ replacement.installation_date|date:"d/m/Y H:i"|default:"-" }}{{ replacement.uninstallation_date|date:"d/m/Y H:i"|default:"-" }} + {% if replacement.installation_date and replacement.uninstallation_date %} + {% with duration=replacement.uninstallation_date|timeuntil:replacement.installation_date %} + {{ duration }} + {% endwith %} + {% elif replacement.installation_date %} + {% translate "En service" %} + {% else %} + - + {% endif %} +
+
+
+
+ {% endif %} +
+ + +
+ + {% asset_model_card asset %} + + + {% asset_location_link asset %} + + +
+
+
{% translate "Géolocalisation" %}
+ {% if asset.lon and asset.lat %} + {% load l10n %} + + {% endif %} +
+
+ +
+ + +
+
+
+
+ +
+ {{ asset.lon|default:"-" }} +
+
+ +
+
+
+
+
+ +
+ {{ asset.lat|default:"-" }} +
+
+ +
+
+
+
+ {% if asset.geom %} +
+ + {% if asset.geom.geom_type == 'Point' %} + Point + {% elif asset.geom.geom_type == 'LineString' %} + Ligne: {{ asset.geom.num_points }} points + {% elif asset.geom.geom_type == 'Polygon' %} + Polygone: {{ asset.geom.num_points }} points + {% else %} + {{ asset.geom.geom_type }} + {% endif %} + +
+ {% endif %} +
+
+
+
+
+ + +
+
+ {% include 'assets/components/asset_history.html' %} +
+
+
+ + +{% if 'replace' in allowed_actions %} + +{% endif %} + + +{% if allowed_update_fields %} + + + +{% endif %} + +{% if intersection_assets_geojson %} +{{ intersection_assets_geojson|json_script:"intersection-assets-geojson" }} +{% endif %} + +{% if connection_lines_geojson %} +{{ connection_lines_geojson|json_script:"connection-lines-geojson" }} +{% endif %} + +{% endblock content %} + +{% block script %} + + + + + + + + + + +{% endblock script %} + +{% block extra_scripts %} + + + + + + + + + + +{% endblock extra_scripts %} \ No newline at end of file diff --git a/streetup/assets/templates/assets/document_history_list.html b/streetup/assets/templates/assets/document_history_list.html new file mode 100644 index 0000000..dc3286d --- /dev/null +++ b/streetup/assets/templates/assets/document_history_list.html @@ -0,0 +1,43 @@ +{% load i18n %} +{% for doc in documents %} +
+ + +
+ {% if doc.thumbnail %} + {{ doc.filename }} + {% else %} + {{ doc.filename }} + {% endif %} +
+
+ + +
+
{{ doc.filename }}
+ + {% translate "Ajouté le" %} {{ doc.upload_date|date:"d/m/Y" }} + {% if doc.uploaded_by %} + {% translate "par" %} {{ doc.uploaded_by.username }} + {% endif %} + +
+ + + {% if is_delete_document_allowed %} +
+ +
+ {% endif %} + +
+{% empty %} +

{% translate "Aucun document trouvé." %}

+{% endfor %} diff --git a/streetup/assets/templates/assets/document_version_history_list.html b/streetup/assets/templates/assets/document_version_history_list.html new file mode 100644 index 0000000..daaa95e --- /dev/null +++ b/streetup/assets/templates/assets/document_version_history_list.html @@ -0,0 +1,67 @@ +{% load i18n %} +{% load tz %} +{% load documents_tags %} +{% for version in versions %} +
+ + +
+ {% if version.thumbnail %} + {{ version.get_title }} + {% else %} + + {{ version.filename }} + + {% endif %} +
+
+ + +
+
+ v{{ version.version_number }} + {{ version.get_title }} +
+ + {% translate "Ajouté le" %} {{ version.uploaded_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }} + {% if version.uploaded_by %} + {% translate "par" %} {{ version.uploaded_by.username }} + {% endif %} + + {% if version.description %} +
{{ version.description }}
+ {% endif %} +
+ {% if version.status == 'approved' %} + {% translate "Approuvé" %} + {% elif version.status == 'pending' %} + {% translate "En attente" %} + {% elif version.status == 'rejected' %} + {% translate "Rejeté" %} + {% else %} + {% translate "Brouillon" %} + {% endif %} +
+
+ + +
+ + + + {% if is_delete_document_allowed %} + + {% endif %} +
+
+{% empty %} +

{% translate "Aucune version trouvée." %}

+{% endfor %} diff --git a/streetup/assets/templates/assets/includes/location_form_fragment.html b/streetup/assets/templates/assets/includes/location_form_fragment.html new file mode 100644 index 0000000..33581e2 --- /dev/null +++ b/streetup/assets/templates/assets/includes/location_form_fragment.html @@ -0,0 +1,44 @@ +{% load i18n %} +{% comment %} + Fragment template loaded asynchronously into a Bootstrap modal. + Variables: + form – Django Form instance (bound or unbound) + instance – Model instance (None for create) + thematic_code – e.g. "trafficlights" + location_type – e.g. "trafficlightintersection" + entry_label – Human-readable type label, e.g. "Intersection" +{% endcomment %} + +{% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+{% endif %} + +
+ {% for field in form %} + {% if field.name != 'lat' and field.name != 'lon' and field.name != 'geom_geojson' %} +
+ + {{ field }} + {% if field.errors %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ +{% if form.geom_geojson %} +{% include "assets/components/map_polygon_picker.html" %} +{% else %} +{% include "assets/components/map_location_picker.html" %} +{% endif %} diff --git a/streetup/assets/templates/assets/includes/panel_rendering_functions.html b/streetup/assets/templates/assets/includes/panel_rendering_functions.html new file mode 100644 index 0000000..2dc3e10 --- /dev/null +++ b/streetup/assets/templates/assets/includes/panel_rendering_functions.html @@ -0,0 +1,928 @@ +{% load static %} +{# Panel rendering JavaScript functions - shared between assets and sign views #} + diff --git a/streetup/assets/templates/assets/its_index.html b/streetup/assets/templates/assets/its_index.html new file mode 100644 index 0000000..64920fa --- /dev/null +++ b/streetup/assets/templates/assets/its_index.html @@ -0,0 +1,199 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block location_row_actions %} +{% if can_create_location %} + +{% endif %} +{% endblock location_row_actions %} + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block asset_row_actions %} +{% if can_create_asset and not preventive_draft_id %} + +{% endif %} +{% endblock asset_row_actions %} + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} +{% if can_create_location %} +{% include "assets/components/location_edit_modals.html" with thematic_code=thematic_code location_type=location_type entry_label="Localisation ITS" %} +{% endif %} +{% if can_create_asset %} +{% include "assets/components/add_asset_modal.html" with modal_id="addItsAssetModal" thematic_code="its" categories=its_categories %} + +{% endif %} +{% endblock extra_script %} diff --git a/streetup/assets/templates/assets/its_location_detail.html b/streetup/assets/templates/assets/its_location_detail.html new file mode 100644 index 0000000..6fbb44e --- /dev/null +++ b/streetup/assets/templates/assets/its_location_detail.html @@ -0,0 +1,214 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} +{% load document_viewer_tags %} + +{% block content %} +
+
+

+ {{ location.code}} - {{ location.get_name }} +

+
+
+
+ +
+ {% if location.is_main_location %} + + + {% elif location.parent_location %} + + {{ location.parent }} + {% endif %} +
+
+
+ {% if location.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + +
+
+

{% translate "Documents" %}

+ +
+
+ {% render_document_viewer documents modal_id="itsDocViewerModal" %} +
+
+ + + + + +
+
+

{% translate "Assets" %}

+
+ +
+ + +
+
+
+ {{ location.get_name }} + {% if location_tree.children %} + {{ location_tree.children|length }} {% translate "sous-localisations" %} + {% endif %} + {% if location_tree.assets %} + {{ location_tree.assets|length }} {% translate "assets" %} + {% endif %} +
+ {% include "assets/its_location_tree.html" with node=location_tree only %} +
+ +
+ ... +
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ geom|json_script:"geom_value" }} + {{ location.lon|json_script:"lon_value" }} + {{ location.lat|json_script:"lat_value" }} + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/its_location_tree.html b/streetup/assets/templates/assets/its_location_tree.html new file mode 100644 index 0000000..89e64f9 --- /dev/null +++ b/streetup/assets/templates/assets/its_location_tree.html @@ -0,0 +1,43 @@ +{% load i18n %} +
    + + {% if node.children %} + {% for child in node.children %} +
  • + + + + {{ child.loc.code }} + + {{ child.loc.get_name }} + {% if child.children %} + {{ child.children|length }} {% translate "sous-localisations" %} + {% endif %} + {% if child.assets %} + {{ child.assets|length }} {% translate "assets" %} + {% endif %} + +
    + {% include "assets/its_location_tree.html" with node=child only %} +
    +
  • + {% endfor %} + {% endif %} + + + {% for asset in node.assets %} +
  • + + {{ asset.code }} + {{ asset.get_name }} +
  • + {% endfor %} +
\ No newline at end of file diff --git a/streetup/assets/templates/assets/nature_index.html b/streetup/assets/templates/assets/nature_index.html new file mode 100644 index 0000000..29d24a7 --- /dev/null +++ b/streetup/assets/templates/assets/nature_index.html @@ -0,0 +1,225 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} + +{% endblock extra_script %} \ No newline at end of file diff --git a/streetup/assets/templates/assets/nature_locations_detail.html b/streetup/assets/templates/assets/nature_locations_detail.html new file mode 100644 index 0000000..66f3f38 --- /dev/null +++ b/streetup/assets/templates/assets/nature_locations_detail.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block content %} +
+
+

+ {{ section.code}} - {{ section.name_fr}} +

+
+
+
+ {{ section.zone|default:"" }}
+ {{ section.is_highway|default:"" }}
+
+
+ {% if section.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+
+
+
+
+
+
+
+ + +
+
+

{% translate "Documents" %}

+
+
+
+ {% for document in section.documents.all %} + + {% endfor %} +
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ section.lon|json_script:"lon_value" }} + {{ section.lat|json_script:"lat_value" }} + {{ section.geojson|json_script:"geojson_value" }} + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/naturegreensurface_detail.html b/streetup/assets/templates/assets/naturegreensurface_detail.html new file mode 100644 index 0000000..d74680f --- /dev/null +++ b/streetup/assets/templates/assets/naturegreensurface_detail.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+

+ {{ asset.code}} +

+
+
+
+ {{ asset.get_status_display }}
+ {{ asset.model.name_fr|default:"" }}
+
+
+ {% if asset.lon %} + + Street View + View in Google Street View + + {% endif %} +
+
+
+
+ {{ asset.density }}
+ {{ asset.weighting }}
+ {{ asset.weighting }}
+
+
+
+
+ {{ asset.last_preventive_date|default:"" }}
+
+
+ + + +
+
+

Documents

+
+
+
+ {% for document in asset.documents.all %} + + {% endfor %} +
+
+
+ + +
+
+

Interventions

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
CodeTitreDébutStatut
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ asset.lon|json_script:"lon_value" }} + {{ asset.lat|json_script:"lat_value" }} + {{ asset.geojson|json_script:"geojson_value" }} + + +{% endblock script %} \ No newline at end of file diff --git a/streetup/assets/templates/assets/naturerwiz_detail.html b/streetup/assets/templates/assets/naturerwiz_detail.html new file mode 100644 index 0000000..50368db --- /dev/null +++ b/streetup/assets/templates/assets/naturerwiz_detail.html @@ -0,0 +1,346 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load documents_tags %} +{% load document_viewer_tags %} + +{% block content %} +
+
+

+ + {{ location.code }} - {{ location.get_name }} +

+
+ +
+
+ {% if location.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + + + +
+
+

{% translate "Documents" %}

+ {% if can_add_unique_tag_documents and use_managed_documents %} + + {% endif %} +
+
+ + + +
+ +
+ {% if can_browse_folder and document_folder %} +
+ + {{ location.code }} +
+ {% endif %} + + {% if use_managed_documents %} + {% render_document_viewer main_documents modal_id="zirwDocViewerModal" show_tags=True show_versions=True location_id=location.id %} + {% else %} +
{% translate "Aucun document de zone configuré." %}
+ {% endif %} +
+ + +
+ {% if documents %} +
+ {% for document in documents %} + + {% endfor %} +
+ {% else %} +
{% translate "Aucun document d'asset associé." %}
+ {% endif %} +
+
+
+
+ + +
+
+ +
+ +
+ + +
+
+
+
+ {{ location.get_name }} + {% if top_level_nodes %} + {{ assets_count }} {% translate "assets" %} + {% endif %} +
+ + {% if show_inactive_assets %} + {% translate "Masquer les non-actifs" %} + {% else %} + {% translate "Afficher les non-actifs" %} + {% endif %} + +
+ {% if top_level_nodes %} + {% include "assets/naturerwiz_location_tree.html" with nodes=top_level_nodes show_inactive_assets=show_inactive_assets only %} + {% else %} +
{% translate "Aucun asset associé." %}
+ {% endif %} +
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+ + + + + + + +
+ +
+
+
+
+{% endblock content %} + + +{% block script %} + {{ location.lon|json_script:"lon_value" }} + {{ location.lat|json_script:"lat_value" }} + {{ location.geojson|json_script:"geojson_value" }} + {{ assets_geojson|json_script:"assets_geojson_value" }} + + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/naturerwiz_location_tree.html b/streetup/assets/templates/assets/naturerwiz_location_tree.html new file mode 100644 index 0000000..fc9598e --- /dev/null +++ b/streetup/assets/templates/assets/naturerwiz_location_tree.html @@ -0,0 +1,42 @@ +{% load i18n %} +
    + {% for node in nodes %} +
  • + {% if node.children %} + + {% endif %} + + + {{ node.asset.code }} + + + {% if node.asset.get_name %} + {{ node.asset.get_name }} + {% endif %} + {% if node.asset.structure_type %} + ({{ node.asset.get_structure_type_display }}) + {% endif %} + + + {% if show_inactive_assets %} + + {{ node.asset.get_status_display }} + + {% endif %} + + {% if node.children %} + {{ node.children|length }} {% translate "assets" %} +
    + {% include "assets/naturerwiz_location_tree.html" with nodes=node.children show_inactive_assets=show_inactive_assets only %} +
    + {% endif %} +
  • + {% endfor %} +
diff --git a/streetup/assets/templates/assets/permissions/access_edit.html b/streetup/assets/templates/assets/permissions/access_edit.html new file mode 100644 index 0000000..bb1c560 --- /dev/null +++ b/streetup/assets/templates/assets/permissions/access_edit.html @@ -0,0 +1,139 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block content %} +
+ + + +
+
+ + {% translate "Modifier l'accès" %} + — {{ access.user_config.user.get_full_name|default:access.user_config.user.username }} +
+
+ +
+ {% csrf_token %} + + +
+ + {% translate "Type :" %} {{ access.content_type.model }} +  |  + {% translate "Objet #" %}{{ access.object_id }} + +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ + +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ + + +
+ + + {% translate "Annuler" %} + +
+
+ +
+
+ +
+ + + +{% endblock content %} diff --git a/streetup/assets/templates/assets/permissions/panel.html b/streetup/assets/templates/assets/permissions/panel.html new file mode 100644 index 0000000..a4bc42f --- /dev/null +++ b/streetup/assets/templates/assets/permissions/panel.html @@ -0,0 +1,293 @@ +{% load i18n %} +{% comment %} + Permission management panel for a single asset/location instance. + Partial template: permission panel for a single asset/location instance. + Included inside the asset detail page or served standalone via the URL. + + Required context: + ct — ContentType instance + object_id — int, PK of the object + Optional (auto-populated when used standalone, must be provided when included): + obj — the model instance (for display) + type_config — AssetTypePermissionConfig instance + accesses — queryset of UserAssetAccess for this object + all_user_configs — queryset of UserConfig for the user selector + field_choices — list of (value, label) tuples for editable fields +{% endcomment %} +{% load i18n %} + +
+ + +
+
+ {% csrf_token %} +
+
+ {% translate "Type d'objet :" %} {{ ct.model }} +
+
+ + +
+ {% if type_config.requires_explicit_permissions %} + + {% translate "Accès restreint" %} + + {% endif %} +
+
+
+ + + {% if type_user_accesses %} +
+
+ + {% translate "Accès au niveau du type (tous les objets)" %} + + {% translate "Gérer" %} + +
+

+ {% translate "Les utilisateurs ci-dessous ont un accès défini pour TOUS les objets de ce type. Ces droits s'appliquent à cet objet sauf si une ligne d'accès spécifique ci-dessous les écrase." %} +

+ + + + + + + + + + + + {% for ta in type_user_accesses %} + + + + + + + + {% endfor %} + +
{% translate "Utilisateur" %}{% translate "Voir" %}{% translate "Modifier" %}{% translate "Supprimer" %}{% translate "Champs" %}
{{ ta.user_config.user.get_full_name|default:ta.user_config.user.username }} + {% if ta.can_view %} + {% else %}{% endif %} + + {% if ta.can_edit %} + {% else %}{% endif %} + + {% if ta.can_delete %} + {% else %}{% endif %} + + {% if ta.editable_fields %} + {{ ta.editable_fields|join:", " }} + {% else %} + {% translate "Par rôle" %} + {% endif %} +
+
+ {% endif %} + + + {% if accesses %} + + + + + + + + + + + + + + {% for access in accesses %} + + + + + + + + + + {% endfor %} + +
{% translate "Utilisateur" %}{% translate "Voir" %}{% translate "Modifier" %}{% translate "Supprimer" %}{% translate "Champs visibles" %}{% translate "Champs modifiables" %}
{{ access.user_config.user.get_full_name|default:access.user_config.user.username }} + {% if access.can_view %} + + {% else %} + + {% endif %} + + {% if access.can_edit %} + + {% else %} + + {% endif %} + + {% if access.can_delete %} + + {% else %} + + {% endif %} + + {% if access.visible_fields %} + {{ access.visible_fields|join:", " }} + {% else %} + {% translate "Tous" %} + {% endif %} + + {% if access.editable_fields %} + {{ access.editable_fields|join:", " }} + {% else %} + {% translate "Par rôle" %} + {% endif %} + + + + +
+ {% csrf_token %} + +
+
+ {% else %} +

{% translate "Aucun accès spécifique défini pour cet objet." %}

+ {% endif %} + + +
+
+ {% translate "Ajouter / mettre à jour un accès" %} +
+
+
+ {% csrf_token %} +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ + + +
+
+ +
+
+
+
+ +
+ + diff --git a/streetup/assets/templates/assets/permissions/type_access_edit.html b/streetup/assets/templates/assets/permissions/type_access_edit.html new file mode 100644 index 0000000..62e0fdc --- /dev/null +++ b/streetup/assets/templates/assets/permissions/type_access_edit.html @@ -0,0 +1,144 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block content %} +
+ + + +
+
+ + {% translate "Modifier l'accès de type" %} + — {{ type_access.user_config.user.get_full_name|default:type_access.user_config.user.username }} +
+
+ +
+ {% csrf_token %} + + +
+ + {% translate "Type d'asset :" %} {{ type_access.content_type.model }} +  —  + {% translate "s'applique à tous les objets de ce type" %} + +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ + +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ + + +
+ + + {% translate "Annuler" %} + +
+
+ +
+
+ +
+ + + +{% endblock content %} diff --git a/streetup/assets/templates/assets/permissions/type_page.html b/streetup/assets/templates/assets/permissions/type_page.html new file mode 100644 index 0000000..1e9acc3 --- /dev/null +++ b/streetup/assets/templates/assets/permissions/type_page.html @@ -0,0 +1,447 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block content %} +
+ + + +
+

+ {{ model_verbose_name }} +

+ {{ ct.model }} +
+ + +
+
{% translate "Configuration du type" %}
+
+
+ {% csrf_token %} + +
+ + +
+

+ {% translate "Si activé, les droits thématiques seuls ne suffisent pas pour accéder aux objets de ce type. Chaque utilisateur doit avoir une ligne d'accès explicite ci-dessous." %} +

+ {% if type_config.requires_explicit_permissions %} +
+ + {% translate "Accès restreint actif — les droits thématiques ne sont pas pris en compte pour ce type." %} +
+ {% endif %} +
+
+
+ + +
+
+ + + {% translate "Accès utilisateur (tous les objets de ce type)" %} + + {{ type_user_accesses|length }} +
+
+ {% if type_user_accesses %} +
+ + + + + + + + + + + + + + {% for ta in type_user_accesses %} + + + + + + + + + + {% endfor %} + +
{% translate "Utilisateur" %}{% translate "Voir" %}{% translate "Modifier" %}{% translate "Supprimer" %}{% translate "Champs visibles" %}{% translate "Champs modifiables" %}
{{ ta.user_config.user.get_full_name|default:ta.user_config.user.username }} + {% if ta.can_view %} + {% else %}{% endif %} + + {% if ta.can_edit %} + {% else %}{% endif %} + + {% if ta.can_delete %} + {% else %}{% endif %} + + {% if ta.visible_fields %} + {{ ta.visible_fields|join:", " }} + {% else %} + {% translate "Tous" %} + {% endif %} + + {% if ta.editable_fields %} + {{ ta.editable_fields|join:", " }} + {% else %} + {% translate "Par rôle" %} + {% endif %} + + + + +
+ {% csrf_token %} + +
+
+
+ {% else %} +

+ {% translate "Aucun accès de type défini. Les droits thématiques s'appliquent (sauf restriction ci-dessus)." %} +

+ {% endif %} +
+ + +
+ + +
+
+ {% translate "Accès par utilisateur" %} + {{ resolved_accesses|length }} +
+
+ {% if resolved_accesses %} +
+ + + + + + + + + + + + + + + {% for item in resolved_accesses %} + {% with access=item.access obj=item.obj %} + + + + + + + + + + + {% endwith %} + {% endfor %} + +
{% translate "Utilisateur" %}{% translate "Objet" %}{% translate "Voir" %}{% translate "Modifier" %}{% translate "Supprimer" %}{% translate "Champs visibles" %}{% translate "Champs modifiables" %}
{{ access.user_config.user.get_full_name|default:access.user_config.user.username }} + {% if obj %} + {{ obj }} + {% else %} + #{{ access.object_id }} + {% endif %} + + {% if access.can_view %} + + {% else %} + + {% endif %} + + {% if access.can_edit %} + + {% else %} + + {% endif %} + + {% if access.can_delete %} + + {% else %} + + {% endif %} + + {% if access.visible_fields %} + {{ access.visible_fields|join:", " }} + {% else %} + {% translate "Tous" %} + {% endif %} + + {% if access.editable_fields %} + {{ access.editable_fields|join:", " }} + {% else %} + {% translate "Par rôle" %} + {% endif %} + + + + +
+ {% csrf_token %} + +
+
+
+ {% else %} +

+ {% translate "Aucun accès spécifique défini pour ce type d'objet." %} +

+ {% endif %} +
+
+ + +
+
+ {% translate "Ajouter un accès sur un objet spécifique" %} +
+
+
+ {% csrf_token %} + +
+ +
+ + +
+ +
+ + +
{% translate "ID de l'instance précise" %}
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ +
+
+ + +
+
+ {% for value, label in field_choices %} +
+ + +
+ {% empty %} + {% translate "Aucun champ disponible" %} + {% endfor %} +
+
+ +
+ +
+ +
+
+
+
+ +
+ + +{% endblock content %} diff --git a/streetup/assets/templates/assets/publiclighting_index.html b/streetup/assets/templates/assets/publiclighting_index.html new file mode 100644 index 0000000..cf597b7 --- /dev/null +++ b/streetup/assets/templates/assets/publiclighting_index.html @@ -0,0 +1,204 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} + +{% endblock extra_script %} diff --git a/streetup/assets/templates/assets/publiclighting_street_detail.html b/streetup/assets/templates/assets/publiclighting_street_detail.html new file mode 100644 index 0000000..622de42 --- /dev/null +++ b/streetup/assets/templates/assets/publiclighting_street_detail.html @@ -0,0 +1,350 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} + +{% block content %} +
+
+

+ {{ street.code }} - {{ street.get_name }} +

+
+
+
+ {{ street.municipality }}
+ {% if street.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + +
+
+

{% translate "Documents" %}

+ +
+
+
+ {% for document in main_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + + + + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+ {% for document in other_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+
+ + + + + + + + +
+
+

{% translate "Assets" %}

+
+
+ +
+
+
+
+
    +
  • + {% translate "Poteaux" %} +
      + {% for pole in assets.poles.all %} +
    • {{ pole.code }} {{ pole.name }}
    • +
        + {% for light in pole.light_fixtures.all %} +
      • {{ light.code }} {{ light.name }}
      • + {% endfor %} +
      + {% endfor %} +
    +
  • + +
+
+
+
    +
  • {% translate "Alimentation:" %} ...
  • +
  • + {% translate "Câbles" %} +
      + {% for cable in assets.cables.all %} +
    • {{ cable.code }} {{ cable.name }}
    • + {% endfor %} +
    +
  • +
+
+
+
+
+ ... +
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ street.lon|json_script:"lon_value" }} + {{ street.lat|json_script:"lat_value" }} + {{ geom|json_script:"geom_value" }} + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/roads_index.html b/streetup/assets/templates/assets/roads_index.html new file mode 100644 index 0000000..799b03b --- /dev/null +++ b/streetup/assets/templates/assets/roads_index.html @@ -0,0 +1,292 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} + +{% endblock extra_script %} diff --git a/streetup/assets/templates/assets/roads_street_detail.html b/streetup/assets/templates/assets/roads_street_detail.html new file mode 100644 index 0000000..3840167 --- /dev/null +++ b/streetup/assets/templates/assets/roads_street_detail.html @@ -0,0 +1,369 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} + +{% block content %} +
+
+

+ {{ street.code }} - {{ street.get_name }} +

+
+
+
+ {{ street.municipality }}
+ {% if street.road_code %} + {{ street.road_code }}
+ {% endif %} + {% if street.section_code %} + {{ street.section_code }}
+ {% endif %} + {% if street.street_level %} + {{ street.street_level }}
+ {% endif %} + {% if street.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + +
+
+

{% translate "Documents" %}

+ +
+
+
+ {% for document in main_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + + + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+ {% for document in other_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+
+ + + + + + + + +
+
+

{% translate "Assets" %}

+
+
+ +
+
+
+
+
    +
  • + {% translate "Chaussées" %} ({{ assets.roadways.count }}) +
      + {% for roadway in assets.roadways %} +
    • + {{ roadway.code }} + {% if roadway.coating_type %}{{ roadway.get_coating_type_display }}{% endif %} + {% if roadway.quality_level %}{% translate "Qualité" %}: {{ roadway.quality_level }}{% endif %} + {% if roadway.area %}({{ roadway.area|floatformat:2 }} m²){% endif %} +
    • + {% empty %} +
    • {% translate "Aucune chaussée" %}
    • + {% endfor %} +
    +
  • +
  • + {% translate "Trottoirs" %} ({{ assets.sidewalks.count }}) +
      + {% for sidewalk in assets.sidewalks %} +
    • + {{ sidewalk.code }} + {% if sidewalk.coating_type %}{{ sidewalk.get_coating_type_display }}{% endif %} + {% if sidewalk.quality_level %}{% translate "Qualité" %}: {{ sidewalk.quality_level }}{% endif %} + {% if sidewalk.area %}({{ sidewalk.area|floatformat:2 }} m²){% endif %} +
    • + {% empty %} +
    • {% translate "Aucun trottoir" %}
    • + {% endfor %} +
    +
  • +
  • + {% translate "Mobilier urbain" %} ({{ assets.furnitures.count }}) +
      + {% for furniture in assets.furnitures %} +
    • + {{ furniture.code }} + {% if furniture.model %}{{ furniture.model.name }}{% endif %} +
    • + {% empty %} +
    • {% translate "Aucun mobilier urbain" %}
    • + {% endfor %} +
    +
  • +
+
+
+
+
+

{% translate "Aucun asset archivé" %}

+
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% empty %} + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
{% translate "Aucune intervention" %}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ street.lon|json_script:"lon_value" }} + {{ street.lat|json_script:"lat_value" }} + {{ geom|json_script:"geom_value" }} + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/sign_index.html b/streetup/assets/templates/assets/sign_index.html new file mode 100644 index 0000000..719d4c7 --- /dev/null +++ b/streetup/assets/templates/assets/sign_index.html @@ -0,0 +1,1536 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} + +{% endblock extra_script %} diff --git a/streetup/assets/templates/assets/sign_street_detail.html b/streetup/assets/templates/assets/sign_street_detail.html new file mode 100644 index 0000000..6545602 --- /dev/null +++ b/streetup/assets/templates/assets/sign_street_detail.html @@ -0,0 +1,342 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} + +{% block content %} +
+
+

+ {{ street.code }} - {{ street.get_name }} +

+
+
+
+ {{ street.municipality }}
+ {% if street.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + +
+
+

{% translate "Documents" %}

+ +
+
+
+ {% for document in main_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + + + + + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+ {% for document in other_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + + +
+
+
+
+ {% endif %} + {% endfor %} +
+
+
+ + + + + + + + +
+
+

{% translate "Assets" %}

+
+
+ +
+
+
+
+
    +
  • + {% translate "Poteaux" %} +
      + {% for pole in assets.signpoles.all %} +
    • {{ pole.code }}
    • +
        + {% for panel in pole.signpanels.all %} +
      • + {{ panel.code }} + {{ panel.signpanel_type }} + {% if panel.signpanel_text %}{{ panel.signpanel_text }}{% endif %} +
      • + {% endfor %} +
      + {% endfor %} +
    +
  • + +
+
+
+
+
+
+
+ ... +
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ +{% endblock content%} + + +{% block script %} + {{ street.lon|json_script:"lon_value" }} + {{ street.lat|json_script:"lat_value" }} + {{ geom|json_script:"geom_value" }} + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/structures_index.html b/streetup/assets/templates/assets/structures_index.html new file mode 100644 index 0000000..1b3fa83 --- /dev/null +++ b/streetup/assets/templates/assets/structures_index.html @@ -0,0 +1,204 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} diff --git a/streetup/assets/templates/assets/structures_location_detail.html b/streetup/assets/templates/assets/structures_location_detail.html new file mode 100644 index 0000000..17583e0 --- /dev/null +++ b/streetup/assets/templates/assets/structures_location_detail.html @@ -0,0 +1,408 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} +{% load documents_tags %} + +{% block content %} +
+
+

+ {{ location.code}} - {{ location.get_name }} +

+
+
+
+ {{ location.structure }}
+ +
+ + {% if location.is_main_location %} + + {% else %} + {{ location.structure.main_location }} + {% endif %} +
+ {% if location.parent_location %} + + {{ location.parent_location }} + {% endif %} +
+
+
+ {% if location.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + + {% if location.is_main_location %} +
+
+

{% translate "Documents" %}

+ +
+
+
+
+ + {% if document_folder %} + {{ location.structure }} + {% else %} + {{ location.structure }} + {% endif %} +
+
+
+ {% if use_managed_documents %} + {# Nouveau système de gestion documentaire #} + {% for doc_info in main_documents %} + {% if doc_info.document %} +
+
+
+ +
+
+ +
+ {{ doc_info.tag.name }} +
+ + + {% if doc_info.version and doc_info.version.file %} + +
+ {% if doc_info.version.thumbnail %} + {{ doc_info.document.title }} + {% else %} + + {{ doc_info.version.filename }} + + {% endif %} +
+
+ {% endif %} +
+ +
+ {{ doc_info.document.updated_at|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {% if doc_info.version %}v{{ doc_info.version.version_number }}{% endif %} +
+
+ + +
+ + + + + + +
+
+
+
+ {% endif %} + {% endfor %} + {% else %} + {# Ancien système - StructureDocument #} + {% for document in main_documents %} + {% if document %} +
+
+
+ +
+ + +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + +
+
+
+
+ {% endif %} + {% endfor %} + {% endif %} +
+
+
+ + + + + + + + + + {% endif %} + + + +
+
+

{% translate "Assets" %}

+ {% if can_create_assets %} + + {% endif %} +
+ +
+ + +
+
+
+
+ {{ location.get_name }} + {% if location_tree.children %} + {% blocktrans count c=location_tree.children|length %} + {{ c }} sous-localisation + {% plural %} + {{ c }} sous-localisations + {% endblocktrans %} + {% endif %} + {% if location_tree.assets %} + {% blocktrans count a=location_tree.assets|length %} + {{ a }} asset + {% plural %} + {{ a }} assets + {% endblocktrans %} + {% endif %} +
+ + {% if show_inactive_assets %} + {% translate "Masquer les non-actifs" %} + {% else %} + {% translate "Afficher les non-actifs" %} + {% endif %} + +
+ {% include "assets/structures_location_tree.html" with node=location_tree include_inactive=show_inactive_assets only %} +
+ +
+ … +
+
+
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
+
+
+ + +{% if can_create_assets %} +{% include "assets/components/add_asset_modal.html" with modal_id="addAssetLocationModal" thematic_code="structures" location=location %} +{% endif %} + +{% endblock content%} + + +{% block script %} + {{ geom|json_script:"geom_value" }} + {{ location.lon|json_script:"lon_value" }} + {{ location.lat|json_script:"lat_value" }} + + + + + {% if can_create_assets %} + + {% endif %} +{% endblock script %} diff --git a/streetup/assets/templates/assets/structures_location_tree.html b/streetup/assets/templates/assets/structures_location_tree.html new file mode 100644 index 0000000..8da449a --- /dev/null +++ b/streetup/assets/templates/assets/structures_location_tree.html @@ -0,0 +1,48 @@ +{% load i18n %} +
    + + {% if node.children %} + {% for child in node.children %} +
  • + + + + {{ child.loc.code }} + + {{ child.loc.get_name }} + {% if child.children %} + {{ child.children|length }} {% translate "sous-localisations" %} + {% endif %} + {% if child.assets %} + {{ child.assets|length }} assets + {% endif %} + +
    + {% include "assets/structures_location_tree.html" with node=child include_inactive=include_inactive only %} +
    +
  • + {% endfor %} + {% endif %} + + + {% for asset in node.assets %} +
  • + + {{ asset.code }} + {{ asset.get_name }} + {% if include_inactive %} + + {{ asset.get_status_display }} + + {% endif %} +
  • + {% endfor %} +
\ No newline at end of file diff --git a/streetup/assets/templates/assets/trafficlights_import_cross_plan.html b/streetup/assets/templates/assets/trafficlights_import_cross_plan.html new file mode 100644 index 0000000..f787f43 --- /dev/null +++ b/streetup/assets/templates/assets/trafficlights_import_cross_plan.html @@ -0,0 +1,2076 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block content %} + + + + + + +
+ +
+
+ +
+

+ + {% translate "Importation d'un plan de croix (Excel)" %} +

+ + {% translate "Retour au carrefour" %} + +
+
+
+ + + {% if step == 'select' %} +
+
+
+
+
+ {% translate "Sélectionner / Téléverser le plan de croix (.xlsx)" %} +
+
+
+ {% if error %} + + {% endif %} + +
+ {% csrf_token %} + + + {% if cross_plan_choices %} +
+ +
+ {% for cp in cross_plan_choices %} +
+ + +
+ {% endfor %} +
+
+ {% translate "Sélectionnez un ou plusieurs plans de croix déjà associés à ce carrefour, ou téléversez-en de nouveaux ci-dessous." %} +
+
+ +
+ — {% translate "OU" %} — +
+ {% endif %} + + +
+ + +
+ {% translate "Téléversez un ou plusieurs classeurs Excel de plans de croix (contenant l'onglet 'kruisjesplan' ou 'Plan croix')." %} +
+
+ +
+ +
+
+
+
+
+
+ + + {% endif %} + + + {% if step == 'preview' %} +
+ {% csrf_token %} + + + + + {% for item in poles %} + {% if item.status != 'db_only' %} + + + + + {% endif %} + {% endfor %} + + + {% for item in cables %} + {% if item.status != 'db_only' %} + + {% endif %} + {% endfor %} + + +
+
+ +
+
+
+
{% translate "Aperçu de la comparaison des assets" %}
+

+ {% if uploaded_filenames %} + {% translate "Fichiers :" %} {{ uploaded_filenames|join:", " }} + {% else %} + {% translate "Fichier :" %} {{ cross_plan_filename }} + {% endif %} + | {% translate "Carrefour :" %} {{ intersection.code }} +

+
+
+ + +
+
+
+ + +
+
+ +
+ +
+ + +
+
+ + +
+
+ + {% translate "Vous pouvez géolocaliser les poteaux en les plaçant sur le plan de la carte." %} + + +
+
+ + + + + + + + + + + + + + + {% for item in poles %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Code Asset" %}{% translate "Nom" %}{% translate "Statut" %}{% translate "Modèle Déduit (Excel)" %}{% translate "Modèle Actuel (DB)" %}{% translate "Décision" %}{% translate "Forcer le Modèle" %}
{{ item.code }} + + + {% if item.status == 'new' %} + {% translate "Nouveau" %} + {% elif item.status == 'modified' %} + {% translate "Modifié" %} + {% elif item.status == 'identical' %} + {% translate "Identique" %} + {% elif item.status == 'db_only' %} + {% translate "DB uniquement" %} + {% endif %} + {{ item.deduced_model_name|default:"—" }}{{ item.db_model_name|default:"—" }} + {% if item.status == 'db_only' %} + + {% else %} + + {% endif %} + + {% if item.status != 'db_only' %} + + {% else %} + — + {% endif %} +
{% translate "Aucun poteau détecté." %}
+
+
+ + +
+
+ + + + + + + + + + + + + + + + {% for item in lanterns %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Code Asset" %}{% translate "Nom" %}{% translate "Poteau" %}{% translate "Statut" %}{% translate "Modèle Déduit (Excel)" %}{% translate "Modèle Actuel (DB)" %}{% translate "Décision" %}{% translate "Forcer le Modèle" %}
{{ item.code }} + + {{ item.pole_code }} + {% if item.status == 'new' %} + {% translate "Nouveau" %} + {% elif item.status == 'modified' %} + {% translate "Modifié" %} + {% elif item.status == 'identical' %} + {% translate "Identique" %} + {% elif item.status == 'db_only' %} + {% translate "DB uniquement" %} + {% endif %} + {{ item.deduced_model_name|default:"—" }}{{ item.db_model_name|default:"—" }} + {% if item.status == 'db_only' %} + + {% else %} + + {% endif %} + + {% if item.status != 'db_only' %} + + {% else %} + — + {% endif %} +
{% translate "Aucune lanterne détectée." %}
+
+
+ + +
+
+ + + + + + + + + + + + + + + + {% for item in detectors %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Code Asset" %}{% translate "Nom" %}{% translate "Poteau" %}{% translate "Statut" %}{% translate "Modèle Déduit (Excel)" %}{% translate "Modèle Actuel (DB)" %}{% translate "Décision" %}{% translate "Forcer le Modèle" %}
{{ item.code }} + + {{ item.pole_code|default:"—" }} + {% if item.status == 'new' %} + {% translate "Nouveau" %} + {% elif item.status == 'modified' %} + {% translate "Modifié" %} + {% elif item.status == 'identical' %} + {% translate "Identique" %} + {% elif item.status == 'db_only' %} + {% translate "DB uniquement" %} + {% endif %} + {{ item.deduced_model_name|default:"—" }}{{ item.db_model_name|default:"—" }} + {% if item.status == 'db_only' %} + + {% else %} + + {% endif %} + + {% if item.status != 'db_only' %} + + {% else %} + — + {% endif %} +
{% translate "Aucun détecteur détecté." %}
+
+
+ + +
+
+ + {% translate "Vous pouvez tracer précisément les câbles d'interconnexion le long des tranchées de la carte." %} + + +
+
+ + + + + + + + + + + + + + + {% for item in cables %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Code Asset" %}{% translate "Nom" %}{% translate "Statut" %}{% translate "Modèle Déduit (Excel)" %}{% translate "Modèle Actuel (DB)" %}{% translate "Décision" %}{% translate "Forcer le Modèle" %}
{{ item.code }} + + + {% if item.status == 'new' %} + {% translate "Nouveau" %} + {% elif item.status == 'modified' %} + {% translate "Modifié" %} + {% elif item.status == 'identical' %} + {% translate "Identique" %} + {% elif item.status == 'db_only' %} + {% translate "DB uniquement" %} + {% endif %} + {{ item.deduced_model_name|default:"—" }}{{ item.db_model_name|default:"—" }} + {% if item.status == 'db_only' %} + + {% else %} + + {% endif %} + + {% if item.status != 'db_only' %} + + {% else %} + — + {% endif %} +
{% translate "Aucun câble détecté." %}
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+ {% translate "Poteaux positionnés :" %} + 0 / 0 +
+
+ + +
+ +
+ + +
+
+ + +
+
+
+
+ {% translate "Aide au placement" %} +
+

+ {% translate "Cliquez sur 'Placer sur la carte' pour un poteau, puis cliquez sur la carte pour le positionner." %} +

+
+ +
+ {% for item in poles %} + {% if item.status != 'db_only' %} +
+
+ {{ item.short_code }} + {% translate "À placer" %} +
+
{{ item.deduced_model_name|default:item.db_model_name }}
+
+ 0 Lanternes | + 0 Détecteurs +
+
+ +
+
+ {% endif %} + {% endfor %} +
+
+
+ + +
+
+
+
+ {% translate "Tracé des câbles" %} +
+

+ {% translate "Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer la ligne du câble." %} +

+
+ +
+ {% for item in cables %} + {% if item.status != 'db_only' %} +
+
+ {{ item.name }} +
+
{{ item.deduced_model_name }}
+
+ +
+
+ {% endif %} + {% endfor %} +
+
+
+
+
+ +
+
+ + +
+
+ +
+ {% translate "Mode: Lecture" %} + +
+ +
+
+
+
+
+
+ + + + {% endif %} +
+ +{% if step == 'preview' %} + +{{ existing_poles_geojson|json_script:"existing_poles_geojson" }} +{{ intersection.lon|json_script:"intersection_lon" }} +{{ intersection.lat|json_script:"intersection_lat" }} +{{ dxf_poles|json_script:"dxf_poles" }} +{{ dxf_files|json_script:"dxf_files" }} +{{ georeferenced_documents_json|json_script:"georeferencedDocumentsData" }} +{% endif %} + + + + + +{% if step == 'preview' %} + +{% endif %} +{% endblock %} diff --git a/streetup/assets/templates/assets/trafficlights_import_dxf.html b/streetup/assets/templates/assets/trafficlights_import_dxf.html new file mode 100644 index 0000000..8a36ba4 --- /dev/null +++ b/streetup/assets/templates/assets/trafficlights_import_dxf.html @@ -0,0 +1,351 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block content %} +
+ +
+
+ +
+

+ + {% translate "Importation d'un plan DXF — Poteaux" %} +

+ + {% translate "Retour au carrefour" %} + +
+
+
+ + + {% if step == 'select' %} +
+
+
+
+
+ {% translate "Choisir un plan DXF" %} +
+
+
+ {% if error %} + + {% endif %} + +
+ {% csrf_token %} + + +
+ + +
+ {% translate "Sélectionnez un fichier DXF parmi les fichiers déjà disponibles sur le serveur." %} +
+
+ +
+
+ {% translate "OU" %} +
+
+ + +
+ + +
+ +
+ +
+
+
+
+
+
+ {% endif %} + + + + + +
+{% endblock %} diff --git a/streetup/assets/templates/assets/trafficlights_index.html b/streetup/assets/templates/assets/trafficlights_index.html new file mode 100644 index 0000000..7950dc8 --- /dev/null +++ b/streetup/assets/templates/assets/trafficlights_index.html @@ -0,0 +1,419 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + + +{% block locations_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {% if is_intern %} +
+ + +
+ {% endif %} +
+ + +
+
+{% endblock locations_filter_form %} + + + +{% block location_row_actions %} +{% if can_create_location %} + +{% endif %} +{% endblock location_row_actions %} + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} + + +{% if can_create_location %} +{% include "assets/components/location_edit_modals.html" with thematic_code=thematic_code location_type=location_type entry_label="Intersection" %} +{% endif %} +{% endblock extra_script %} \ No newline at end of file diff --git a/streetup/assets/templates/assets/trafficlights_intersection_detail.html b/streetup/assets/templates/assets/trafficlights_intersection_detail.html new file mode 100644 index 0000000..c1e4160 --- /dev/null +++ b/streetup/assets/templates/assets/trafficlights_intersection_detail.html @@ -0,0 +1,1387 @@ +{% extends "base.html" %} +{% load static %} +{% load tz %} +{% load i18n %} +{% load documents_tags %} +{% load asset_tags %} +{% load document_viewer_tags %} + +{% block content %} + +
+
+
+

+ {{ intersection.code}} - {{ intersection.get_name }} +

+ {% if can_edit_location %} +
+ +
+ {% endif %} +
+
+
+
+ {{ intersection.get_status_display }}
+ {{ intersection.get_voltage_type_display }}
+
+ + {% if intersection.is_connected_to_control_center %} + + {% elif intersection.is_connected_to_control_center == False %} + + {% else %} + + {% endif %} +
+ {{ intersection.current_index|default:""}} + {% if intersection.master %} +
+ {{ intersection.master.code|default:"/" }} + {% endif %} +
+ {% if can_edit_location %} +
+ + + {% if trafficlight_contracts %} + {% for tlc in trafficlight_contracts %} + + {{ tlc.contract.contract_number }} + ({{ tlc.get_maintenance_type_display }}) + + {% endfor %} + {% else %} + {% translate "Aucun" %} + {% endif %} + + +
+ {% endif %} +
+
+ {{ intersection.municipality }}
+ {% if intersection.lon %} + + {% translate 'Street View' %} + {% translate "View in Google Street View" %} + + {% endif %} +
+
+ + {% if staged_assets_count > 0 %} +
+
+
+ + {% translate "Assets importés par DXF en attente de validation" %} +
+

+ {% blocktranslate count count=staged_assets_count %} + Il y a {{ count }} poteau importé depuis un plan DXF qui est en attente de validation pour ce carrefour. + {% plural %} + Il y a {{ count }} poteaux importés depuis un plan DXF qui sont en attente de validation pour ce carrefour. + {% endblocktranslate %} +

+ + +
+ + +
+ +
+
+ {% csrf_token %} + + + +
+ +
+ {% csrf_token %} + + + +
+ + + {% translate "Réimporter / Corriger" %} + +
+
+
+ + + {% endif %} + + +
+
+

{% translate "Documents" %}

+ +
+
+ {% if can_browse_folder and document_folder %} +
+
+ + {{ intersection.code }} +
+
+ {% endif %} + {# ── Document grid ── #} + {% if use_managed_documents %} + {# Nouveau système : le composant gère sa propre grille + modale visionneuse #} + {% render_document_viewer main_documents modal_id="tliDocViewerModal" show_tags=True show_versions=True location_id=intersection.id %} + {% else %} + {# Ancien système - TrafficLightIntersectionDocument #} +
+ {% for document in main_documents %} + {% if document %} +
+
+
+ +
+
+ +
+ {{ document.get_document_type_display }} +
+ + + +
+ {% if document.thumbnail %} + {{ document.filename }} + {% else %} + + {% endif %} +
+
+
+ +
+ {{ document.upload_date|timezone:"Europe/Paris"|date:"d/m/Y" }}
+ {{ document.uploaded_by.username }} +
+
+ + +
+ + +
+
+
+
+ {% endif %} + {% endfor %} +
+ {% endif %} +
+
+ + + + + + + + + + + + +
+
+

{% translate "Assets" %}

+ {% if allowed_location_actions %} +
+ {% if 'create' in allowed_location_actions %} + + {% endif %} + {% if can_edit_location %} + + {% translate "Importer plan DXF" %} + + + {% translate "Importer plan de croix" %} + + {% endif %} + {% if 'bulk_archive' in allowed_location_actions %} + + {% endif %} +
+ {% endif %} +
+
+ + +
+ + {# ── Onglet Installation extérieure ── #} +
+
+ + {# ── Colonne Poteaux ── #} +
+
+
+ + {% translate "Poteaux" %} + {{ assets.poles|length }} + +
+
+ {% if assets.poles %} +
    + {% for pole in assets.poles %} +
  • + {# ── En-tête du poteau ── #} +
    + {% if pole.sorted_radars and not pole.sorted_lanterns and not pole.sorted_accessories %} + + {% else %} + + {% endif %} + {{ pole.code }} + {% if pole.name %}{{ pole.name }}{% endif %} + {% if pole.status != 'active' %}{{ pole.get_status_display }}{% endif %} + {% if pole.sorted_lanterns or pole.sorted_accessories or pole.sorted_radars or pole.sorted_detectors or pole.sorted_signpanels %} +
    + {% if pole.sorted_accessories %} + {{ pole.sorted_accessories|length }} + {% endif %} + {% if pole.sorted_lanterns %} + {{ pole.sorted_lanterns|length }} + {% endif %} + {% if pole.sorted_radars %} + {{ pole.sorted_radars|length }} + {% endif %} + {% if pole.sorted_detectors %} + {{ pole.sorted_detectors|length }} + {% endif %} + {% if pole.sorted_signpanels %} + {{ pole.sorted_signpanels|length }} + {% endif %} +
    + + {% endif %} +
    + {# ── Lanternes + Accessoires + Radars + Détecteurs + Panneaux ── #} + {% if pole.sorted_lanterns or pole.sorted_accessories or pole.sorted_radars or pole.sorted_detectors or pole.sorted_signpanels %} +
    +
      + {% for lantern in pole.sorted_lanterns %} +
    • + + {{ lantern.code }} + {% if lantern.get_name %}{{ lantern.get_name }}{% endif %} + {% if lantern.status != 'active' %}{{ lantern.get_status_display }}{% endif %} +
    • + {% endfor %} + {% for accessory in pole.sorted_accessories %} +
    • + + {{ accessory.code }} + {% if accessory.get_name %}{{ accessory.get_name }}{% endif %} + {% if accessory.status != 'active' %}{{ accessory.get_status_display }}{% endif %} +
    • + {% endfor %} + {% for radar in pole.sorted_radars %} +
    • + + {{ radar.code }} + {% if radar.get_name %}{{ radar.get_name }}{% endif %} + {% if radar.status != 'active' %}{{ radar.get_status_display }}{% endif %} +
    • + {% endfor %} + {% for detector in pole.sorted_detectors %} +
    • + + {{ detector.code }} + {% if detector.get_name %}{{ detector.get_name }}{% endif %} + {% if detector.status != 'active' %}{{ detector.get_status_display }}{% endif %} +
    • + {% endfor %} + {% for panel in pole.sorted_signpanels %} +
    • + + {{ panel.code }} + {% if panel.signpanel_type %}{{ panel.signpanel_type }}{% endif %} + {% if panel.signpanel_text %}"{{ panel.signpanel_text }}"{% endif %} + {% if panel.status != 'active' %}{{ panel.get_status_display }}{% endif %} +
    • + {% endfor %} +
    +
    + {% endif %} +
  • + {% endfor %} +
+ {% else %} +

{% translate "Aucun poteau" %}

+ {% endif %} +
+
+
+ + {# ── Colonne Armoires + Câbles ── #} +
+ + {# Armoires électriques #} +
+
+ + {% translate "Armoires électriques" %} + {{ assets.electrical_cabinets|length }} +
+ {% if assets.electrical_cabinets %} +
    + {% for cabinet in assets.electrical_cabinets %} +
  • + + {{ cabinet.code }} + {{ cabinet.get_name }} + {% if cabinet.status != 'active' %}{{ cabinet.get_status_display }}{% endif %} +
  • + {% endfor %} +
+ {% else %} +
+

{% translate "Aucune armoire électrique" %}

+
+ {% endif %} +
+ + {# Câbles #} +
+
+ + {% translate "Câbles" %} + {{ assets.cables|length }} +
+ {% if assets.cables %} +
    + {% for cable in assets.cables %} +
  • + + + + + {{ cable.code }} + {% if cable.name %}{{ cable.name }}{% endif %} + {% if cable.status != 'active' %}{{ cable.get_status_display }}{% endif %} +
  • + {% endfor %} +
+ {% else %} +
+

{% translate "Aucun câble" %}

+
+ {% endif %} +
+ + {# Radars #} +
+
+ + {% translate "Radars préventifs" %} + {{ assets.radars|length }} +
+ {% if assets.radars %} +
    + {% for radar in assets.radars %} +
  • + + {{ radar.code }} + {% if radar.get_name %}{{ radar.get_name }}{% endif %} + {% if radar.status != 'active' %}{{ radar.get_status_display }}{% endif %} +
  • + {% endfor %} +
+ {% else %} +
+

{% translate "Aucun radar préventif" %}

+
+ {% endif %} +
+ +
{# /col armoires+câbles #} +
{# /row #} +
+ + {% if not intersection.is_radar %} + {# ── Onglet Contrôleur ── #} +
+ {% if assets.controllers.first %} + {% with ctrl=assets.controllers.first %} + + {# ── Carte contrôleur ── #} +
+
+ + {% translate "Contrôleur de trafic" %} +
+
+
+ +
+ {{ ctrl.code }} + {% if ctrl.name %}
{{ ctrl.name }}
{% endif %} +
+
+
+
+ {% translate "Modèle" %} : + {% if ctrl.model %}{{ ctrl.model.get_name }}{% else %}—{% endif %} +
+
+ {% translate "Type" %} : + {{ ctrl.cabinet_type|default:"—" }} +
+
+ {% translate "Plan" %} : + {{ ctrl.plan|default:"—" }} +
+
+ {% translate "Puissance" %} : + {{ ctrl.power|default:"—" }} +
+
+ {% translate "Date de mise en service" %} : + {% if ctrl.installation_date %}{{ ctrl.installation_date|date:"d/m/Y" }}{% else %}—{% endif %} +
+
+
+
+ + {# ── Accordéons liés au contrôleur ── #} +
+ + {# ── Block 1 : Hardwares du contrôleur ── #} +
+

+ +

+
+
+ {% with hw_list=ctrl.sorted_hardwares %} + {% if hw_list %} +
    + {% for hw in hw_list %} +
  • + + {{ hw.code }} + {% if hw.name %}{{ hw.name }}{% endif %} + + {% if hw.hardware %}{{ hw.hardware }}{% endif %} + {% if hw.status != 'active' %}{{ hw.get_status_display }}{% endif %} + +
  • + {% endfor %} +
+ {% else %} +

{% translate "Aucun hardware" %}

+ {% endif %} + {% endwith %} +
+
+
+ + {# ── Block 2 : Détecteurs ── #} +
+

+ +

+
+
+ {% with det_list=ctrl.sorted_detectors %} + {% if det_list %} +
    + {% for det in det_list %} +
  • + + {{ det.code }} + {% if det.name %}{{ det.name }}{% endif %} + + {% if det.hardware %}{{ det.hardware }}{% endif %} + {% if det.status != 'active' %}{{ det.get_status_display }}{% endif %} + +
  • + {% endfor %} +
+ {% else %} +

{% translate "Aucun détecteur" %}

+ {% endif %} + {% endwith %} +
+
+
+ + {# ── Block 3 : Hardware réseau ── #} +
+

+ +

+
+
+ {% with net_list=ctrl.sorted_network_hardwares %} + {% if net_list %} +
    + {% for net in net_list %} +
  • + + {{ net.code }} + {% if net.name %}{{ net.name }}{% endif %} + + {% if net.hardware %}{{ net.hardware }}{% endif %} + {% if net.status != 'active' %}{{ net.get_status_display }}{% endif %} + +
  • + {% endfor %} +
+ {% else %} +

{% translate "Aucun hardware réseau" %}

+ {% endif %} + {% endwith %} +
+
+
+ + {# ── Block 4 : Hardware Tension ── #} +
+

+ +

+
+
+ {% with volt_list=ctrl.sorted_voltage_hardwares %} + {% if volt_list %} +
    + {% for volt in volt_list %} +
  • + + {{ volt.code }} + {% if volt.name %}{{ volt.name }}{% endif %} + + {% if volt.hardware %}{{ volt.hardware }}{% endif %} + {% if volt.status != 'active' %}{{ volt.get_status_display }}{% endif %} + +
  • + {% endfor %} +
+ {% else %} +

{% translate "Aucun hardware tension" %}

+ {% endif %} + {% endwith %} +
+
+
+ +
{# /accordion #} + + {% endwith %} + {% else %} +
+ + {% translate "Aucun contrôleur" %} +
+ {% endif %} +
+ {% endif %} + + {% if not intersection.is_radar %} + {# ── Onglet Programmation ── #} +
+ + {# ── Programmations ── #} + {% with prog_list=intersection.programmations.all %} + {% if prog_list %} +
+
+ + {% translate "Programmations" %} + {{ prog_list|length }} +
+
    + {% for prog in prog_list %} +
  • + + {# Index - colonne fixe #} + + {% if prog.index %}{{ prog.index }}{% endif %} + + {# Statut - colonne fixe #} + + {{ prog.get_status_display }} + + {# Code + nom #} + {{ prog.code }} + {% if prog.get_name %}{{ prog.get_name }}{% endif %} + {# Date alignée à droite #} + {% if prog.commissioning_date %}{{ prog.commissioning_date|date:"d/m/Y" }}{% endif %} +
  • + {% endfor %} +
+
+ {% else %} +
+ + {% translate "Aucune programmation" %} +
+ {% endif %} + {% endwith %} + + {# ── Log files du contrôleur ── #} + {% with ctrl=assets.controllers.first %} + {% if ctrl %} + {% with log_list=ctrl.logs.all|dictsortreversed:"filedate" %} +
+
+

+ +

+
+
+ {% if log_list %} +
    + {% for log in log_list %} +
  • + + {% private_file_url log 'file' as log_url %} + {{ log.file.name|slice:"42:" }} + {% if log.filedate %}{{ log.filedate|date:"d/m/Y" }}{% endif %} +
  • + {% endfor %} +
+ {% else %} +

{% translate "Aucun fichier de log" %}

+ {% endif %} +
+
+
+
+ {% endwith %} + {% endif %} + {% endwith %} + +
+ {% endif %} + +
{# /tab-content #} +
+
+ + +
+
+

{% translate "Interventions" %}

+
+
+ + + + + + + + + + + {% for interv in interventions %} + + + + + + + + {% endfor %} + + +
{% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
+ + + + {{ interv.code }}{{ interv.title }}{{ interv.begin_time|date:"d-m-Y H:i" }}{{ interv.get_status_display }}
+
+
+
+ +
+
+
{% translate "Plan" %}
+
+
+
+
+
+ + +{% if 'create' in allowed_location_actions %} + +{% endif %} + + + + +{% endblock content%} + + +{% block script %} + {{ intersection.lon|json_script:"lon_value" }} + {{ intersection.lat|json_script:"lat_value" }} + {{ intersection.status|json_script:"status_value" }} + {{ intersection.is_radar|json_script:"is_radar_value" }} + {{ assets_geojson|json_script:"assets_geojson" }} + + + + {% if allowed_location_actions %} + + {% endif %} + + {# ── Location editing modals (edit / delete) ── #} + {% if can_edit_location %} + {% include "assets/components/location_edit_modals.html" with thematic_code=thematic_code location_type=location_type entry_label="Intersection" %} + + {# ── Contracts management modal ── #} + + + {% endif %} + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/unified_index.html b/streetup/assets/templates/assets/unified_index.html new file mode 100644 index 0000000..a95872b --- /dev/null +++ b/streetup/assets/templates/assets/unified_index.html @@ -0,0 +1,733 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block head %} + + + + +{% endblock head %} + +{% block content %} + +{# ─── Filter modal ──────────────────────────────────────────────────────────── #} + + +{# ─── Add chooser modal ────────────────────────────────────────────────────── #} + + +{# ─── Asset create modal ──────────────────────────────────────────────────── #} + + +{# ─── Location create modal ───────────────────────────────────────────────── #} + + +{# ─── Intervention mode banner ───────────────────────────────────────────── #} +{% if preventive_draft_id %} +
+ + {% translate "Mode sélection intervention préventive" %} + {% if locked_thematic_code %} — {{ locked_thematic_code }}{% endif %} + + {% if link_assets_url %} + + {% endif %} +
+{% endif %} + + + +{# ─── Control block: Search widget + Layout buttons ──────────────────────── #} +
+ {# Search widget - perfectly centered horizontally #} +
+
+
+ + +
+ +
+ + + + +
+
+ +
+
+ + {# Layout selection buttons - aligned to the right #} +
+
+ + + +
+
+
+ +
+
+ + {# ─── Table column ───────────────────────────────────────────────── #} +
+ + {# Tab nav with + and filter buttons #} + + + {# Tab content #} +
+
+ + {# Selection toolbar #} +
+ + + {% if preventive_draft_id %} + + {% endif %} +
0 {% translate "sélectionné(s)" %}
+ {% if not preventive_draft_id %} + + {% endif %} +
+ + {# Table area: spinner overlay + empty state + table #} +
+ + {# Spinner overlay — shown on top of existing table content during loading #} + + + {# Empty state #} +
+ +

{% translate "Sélectionnez une ou plusieurs thématiques dans les filtres, saisissez une recherche ou zoomez sur la carte." %}

+
+ + {# Table #} + +
+ +
+ +
+
+
+ + {# ─── Map column ──────────────────────────────────────────────────── #} +
+
+
+ +
+ × +
+
+
+ + + +
+
+
+ {% translate "Totaux des assets" %} + +
+
+
+
+
+ +
+
+
+
+ +{% if not preventive_draft_id %} + +{% endif %} + +{# ─── Modal bulk status ────────────────────────────────────────────────── #} +{% if has_bulk_edit_status_permission %} + +{% endif %} + +{# ─── Modal bulk validation status ──────────────────────────────────────── #} +{% if has_bulk_edit_validation_status_permission %} + +{% endif %} + +{# Hidden preventive form #} +{% if preventive_draft_id %} + +{% endif %} + +{% endblock content %} + +{% block script %} + + + + {% include "assets/includes/panel_rendering_functions.html" %} + + + + + +{% endblock script %} diff --git a/streetup/assets/templates/assets/water_index.html b/streetup/assets/templates/assets/water_index.html new file mode 100644 index 0000000..e249cdc --- /dev/null +++ b/streetup/assets/templates/assets/water_index.html @@ -0,0 +1,226 @@ +{% extends "assets/base_assets_index.html" %} +{% load static i18n %} + +{% block locations_filter_form %} +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+{% endblock locations_filter_form %} + +{% block location_row_actions %} +{% if can_create_location %} + +{% endif %} +{% endblock location_row_actions %} + +{% block assets_filter_form %} +
+
+ + +
+
+ + +
+
+{% endblock assets_filter_form %} + +{% block asset_row_actions %} +{% if can_create_asset and not preventive_draft_id %} + +{% endif %} +{% endblock asset_row_actions %} + +{% block const_script %} + +{% endblock const_script %} + +{% block extra_script %} +{% if can_create_location %} +{% include "assets/components/location_edit_modals.html" with thematic_code=thematic_code location_type=location_type entry_label="Zone GIEP" %} +{% endif %} +{% if can_create_asset %} +{% include "assets/components/add_asset_modal.html" with modal_id="addWaterAssetModal" thematic_code="water" categories=water_categories %} + +{% endif %} +{% endblock extra_script %} diff --git a/streetup/assets/templatetags/__init__.py b/streetup/assets/templatetags/__init__.py new file mode 100644 index 0000000..6bd41f7 --- /dev/null +++ b/streetup/assets/templatetags/__init__.py @@ -0,0 +1 @@ +# Assets templatetags diff --git a/streetup/assets/templatetags/asset_tags.py b/streetup/assets/templatetags/asset_tags.py new file mode 100644 index 0000000..a42bafb --- /dev/null +++ b/streetup/assets/templatetags/asset_tags.py @@ -0,0 +1,438 @@ +from django import template +import json + +from common.private_files.helpers import build_private_url + +register = template.Library() + + +@register.simple_tag +def private_file_url(instance, field_name="file"): + """ + Builds a signed private-file download URL. + Usage: {% private_file_url obj 'file' %} + """ + from django.contrib.contenttypes.models import ContentType + ct = ContentType.objects.get_for_model(instance.__class__) + return build_private_url(ct.app_label, instance.__class__.__name__, instance.pk, field_name) + + +@register.filter(name='tojson') +def tojson(value): + """ + Convertit un objet Python en JSON. + Utile pour passer des dictionnaires/listes aux attributs data-* HTML. + """ + if value is None: + return '{}' + try: + return json.dumps(value) + except (TypeError, ValueError): + return '{}' + + +@register.inclusion_tag('assets/components/asset_model_card.html', takes_context=True) +def asset_model_card(context, asset): + """ + Display the model information for an asset in a Bootstrap card. + Works with any asset that has a 'model' ForeignKey. + Includes edit capability if user has permission. + """ + model = getattr(asset, 'model', None) + + # Check if model editing is allowed (from parent context) + can_edit_model = 'model' in context.get('allowed_update_fields', []) + + # Get asset models and categories for the dropdown (if editable) + asset_models = [] + asset_categories = [] + if can_edit_model and model: + # Get the model class for the asset's model + model_class = type(model) + asset_models = list(model_class.objects.all().order_by('code')) + + # Get categories from thematic that are allowed for this type of asset + from assets.models import AssetCategory + from django.contrib.contenttypes.models import ContentType + from django.db.models import Q + if hasattr(model, 'category') and model.category and hasattr(model.category, 'thematic'): + asset_content_type = ContentType.objects.get_for_model(asset) + asset_categories = list(AssetCategory.objects.filter( + thematic=model.category.thematic + ).filter( + Q(allowed_models=asset_content_type) | Q(id=model.category.id) + ).distinct().order_by('name_fr')) + + # Collect specific fields (not in the base AbstractAssetModel) with their verbose_name + specific_fields = [] + if model: + from django.db.models import AutoField, ForeignKey, OneToOneField, ManyToManyField + from assets.models import AbstractAssetModel + base_field_names = {f.name for f in AbstractAssetModel._meta.fields} + excluded_model_field_types = (AutoField, ForeignKey, OneToOneField, ManyToManyField) + for field in type(model)._meta.fields: + if field.name in base_field_names: + continue + if isinstance(field, excluded_model_field_types): + continue + value = getattr(model, field.name, None) + label = str(field.verbose_name).capitalize() if hasattr(field, 'verbose_name') else field.name + display_value = str(value) if value is not None else '-' + specific_fields.append({'label': label, 'value': value, 'display_value': display_value}) + + return { + 'asset': asset, + 'model': model, + 'can_edit_model': can_edit_model, + 'asset_models': asset_models, + 'asset_categories': asset_categories, + 'model_specific_fields': specific_fields, + } + + +@register.simple_tag +def get_asset_specific_fields(asset): + """ + Returns the list of specific fields of an asset (not present in AbstractAsset / + AbstractGeoAsset) with verbose_name label and metadata for editable rendering. + FK, geometry and auto fields are excluded. Null values are always included. + """ + from django.contrib.gis.db.models import GeometryField + from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField, + BooleanField, FloatField, DecimalField, IntegerField, + DateField, DateTimeField) + from django.utils.translation import gettext_lazy as _ + from assets.models import AbstractAsset, AbstractGeoAsset + + excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField) + + base_field_names = {f.name for f in AbstractAsset._meta.fields} + base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields} + base_field_names.add('geojson') + + specific_fields = [] + for field in type(asset)._meta.fields: + if field.name in base_field_names: + continue + if isinstance(field, excluded_types): + continue + # Only point-type RWI assets need has_filter + if field.name == 'has_filter' and type(asset).__name__ == 'NatureRWIAsset': + if getattr(asset, 'geometry_type', None) != 'point': + continue + + value = getattr(asset, field.name, None) + label = str(field.verbose_name).capitalize() if hasattr(field, 'verbose_name') else field.name + + if isinstance(field, BooleanField): + field_type = 'boolean' + display_value = _('Oui') if value is True else (_('Non') if value is False else '-') + input_value = 'true' if value is True else ('false' if value is False else '') + elif isinstance(field, (FloatField, DecimalField, IntegerField)): + field_type = 'number' + display_value = str(value) if value is not None else '-' + input_value = str(value) if value is not None else '' + elif isinstance(field, (DateTimeField, DateField)): + field_type = 'date' + display_value = value.strftime('%d/%m/%Y') if value else '-' + input_value = value.strftime('%Y-%m-%d') if value else '' + elif field.choices: + field_type = 'select' + choices_dict = dict(field.choices) + display_value = str(choices_dict.get(value, value)) if value else '-' + input_value = str(value) if value is not None else '' + else: + field_type = 'text' + display_value = str(value) if value is not None else '-' + input_value = str(value) if value is not None else '' + + specific_fields.append({ + 'name': field.name, + 'label': label, + 'value': value, + 'display_value': display_value, + 'input_value': input_value, + 'field_type': field_type, + 'choices': list(field.choices) if field.choices else [], + }) + + return specific_fields + + +@register.inclusion_tag('assets/components/asset_location_link.html', takes_context=True) +def asset_location_link(context, asset): + """ + Display a link to the location associated with an asset. + Works with assets that have a 'street', 'location', or 'intersection' ForeignKey. + """ + # Try different possible location field names + location = None + location_type = None + + if hasattr(asset, 'street'): + location = getattr(asset, 'street', None) + location_type = 'street' + elif hasattr(asset, 'location'): + location = getattr(asset, 'location', None) + location_type = 'location' + elif hasattr(asset, 'intersection'): + location = getattr(asset, 'intersection', None) + location_type = 'intersection' + + # Determine if user can edit location (similar to model editing) + can_edit_location = False + user = context.get('request').user if context.get('request') else None + if user: + from assets.permissions import can_edit_asset + can_edit_location = can_edit_asset(user, asset) + + # Get thematic code from asset + thematic_code = None + if hasattr(asset, '_meta'): + app_label = asset._meta.app_label + if app_label == 'assets': + model_name = asset._meta.model_name + if 'structure' in model_name.lower(): + thematic_code = 'structures' + elif 'trafficlight' in model_name.lower(): + thematic_code = 'trafficlights' + elif 'publiclighting' in model_name.lower(): + thematic_code = 'publiclighting' + # Add more thematic mappings as needed + + return { + 'asset': asset, + 'location': location, + 'location_type': location_type, + 'can_edit_location': can_edit_location, + 'thematic_code': thematic_code, + } + + +@register.inclusion_tag('assets/components/asset_documents.html') +def asset_documents(asset): + """ + Display all documents associated with an asset. + Uses the generic DocumentAttachment system. + Passes available DocumentTags filtered by: + - the asset's thematic + - related_content_types (empty = all CTs; set = only matching CTs) + """ + import json + from django.contrib.contenttypes.models import ContentType + from django.db.models import Count, Q + from documents.models import DocumentAttachment, DocumentTag + + if asset is None: + return { + 'asset': asset, + 'documents': DocumentAttachment.objects.none(), + 'available_tags': [], + 'available_tags_json': '[]', + } + + asset_ct = ContentType.objects.get_for_model(asset) + documents = DocumentAttachment.objects.select_related( + "document", + "document__current_version", + ).prefetch_related( + "document__tags", + ).filter( + content_type=asset_ct, + object_id=asset.pk, + ).order_by('-attached_at') + + # Thematic filter + thematic = None + if hasattr(asset, 'category') and asset.category and asset.category.thematic: + thematic = asset.category.thematic + + qs = DocumentTag.objects.all() + if thematic: + qs = qs.filter(thematics=thematic) + + # Content-type filter: + # A tag is shown if it has NO related_content_types (applies to all) + # OR if this asset's CT is among its related_content_types. + qs = qs.annotate(ct_count=Count('related_content_types')).filter( + Q(ct_count=0) | Q(related_content_types=asset_ct) + ).distinct().order_by('order', 'name_fr') + + available_tags = list(qs) + available_tags_json = json.dumps([ + {'id': t.pk, 'name': t.name_fr} + for t in available_tags + ]) + + return { + 'asset': asset, + 'documents': documents, + 'available_tags': available_tags, + 'available_tags_json': available_tags_json, + } + + +# --------------------------------------------------------------------------- +# Related children tree +# --------------------------------------------------------------------------- + +def _build_asset_tree(asset, depth, visited): + """ + Recursively collect reverse-FK children that are AbstractAsset subclasses. + Returns a list of group dicts: + [{ 'label': str, 'model_name': str, 'items': [{'asset': obj, 'url': str|None, 'children': [...]}] }] + """ + from django.db.models import ManyToOneRel + from assets.models import AbstractAsset + + if depth == 0: + return [] + + asset_key = (type(asset).__name__, asset.pk) + if asset_key in visited: + return [] + visited = visited | {asset_key} # immutable copy – safe for branching + + groups = [] + for rel in asset._meta.get_fields(): + # Only consider reverse FK (ManyToOneRel), skip OneToOneRel / M2M + if not isinstance(rel, ManyToOneRel): + continue + + related_model = rel.related_model + + # Must be a concrete AbstractAsset subclass + try: + if not issubclass(related_model, AbstractAsset): + continue + except TypeError: + continue + + # Skip self-referential replaced_by / replaces chain + if rel.field.name in ('replaced_by',) and related_model == type(asset): + continue + + accessor_name = rel.get_accessor_name() + manager = getattr(asset, accessor_name, None) + if manager is None: + continue + + try: + qs = list(manager.all()) + except Exception: + continue + + if not qs: + continue + + items = [] + for child in qs: + child_children = _build_asset_tree(child, depth - 1, visited) + url = child.get_absolute_url() if hasattr(child, 'get_absolute_url') else None + items.append({ + 'asset': child, + 'url': url, + 'children': child_children, + }) + + groups.append({ + 'label': str(related_model._meta.verbose_name_plural).capitalize(), + 'model_name': related_model._meta.model_name, + 'field_on_child': rel.field.name, + 'is_parent_group': False, + 'items': items, + }) + + return groups + + +@register.simple_tag +def get_asset_children_tree(asset, max_depth=3): + """ + Returns a nested tree of related child assets (reverse FK AbstractAsset subclasses). + Used in detail_generic.html to display the 'Assets liés' section. + """ + if asset is None or not hasattr(asset, 'pk') or asset.pk is None: + return [] + return _build_asset_tree(asset, depth=max_depth, visited=set()) + + +@register.simple_tag +def get_asset_parents(asset): + """ + Returns parent assets as a list of group dicts in the same format as + _build_asset_tree, so they can be rendered with asset_children_tree.html. + Each FK pointing to an AbstractAsset / AbstractLocation subclass becomes one + group with a single item (no recursive children). + Housekeeping fields (model, category, replaced_by, marked_as_removed_by) are excluded. + """ + from django.db.models import ForeignKey + from assets.models import AbstractAsset + + if asset is None or not hasattr(asset, 'pk') or asset.pk is None: + return [] + + EXCLUDED_FIELDS = { + 'model', 'category', 'replaced_by', 'marked_as_removed_by', + 'controller_maintenance_contract', + } + + groups = [] + for field in asset._meta.get_fields(): + if not isinstance(field, ForeignKey): + continue + if field.name in EXCLUDED_FIELDS: + continue + + related_model = field.related_model + try: + is_parent = issubclass(related_model, AbstractAsset) + except TypeError: + continue + if not is_parent: + continue + + parent_obj = getattr(asset, field.name, None) + if parent_obj is None: + continue + + label = str(field.verbose_name).capitalize() + url = parent_obj.get_absolute_url() if hasattr(parent_obj, 'get_absolute_url') else None + groups.append({ + 'label': label, + 'model_name': related_model._meta.model_name, + 'field_name': field.name, + 'is_parent_group': True, + 'items': [{'asset': parent_obj, 'url': url, 'children': []}], + }) + + return groups + + +@register.filter +def warranty_remaining_months(asset): + """ + Returns the number of remaining warranty months for an asset. + Positive = still under warranty, negative = expired by that many months. + Returns None if installation_date or warranty_duration is missing. + """ + from django.utils import timezone + + installation_date = getattr(asset, 'installation_date', None) + warranty_duration = getattr(asset, 'warranty_duration', None) + + if not installation_date or not warranty_duration: + return None + + now = timezone.now() + # Calculate warranty end date by adding months manually (no dateutil needed) + total_months = (installation_date.month - 1) + warranty_duration + end_year = installation_date.year + total_months // 12 + end_month = total_months % 12 + 1 + days_in_end_month = [31, 29 if (end_year % 4 == 0 and (end_year % 100 != 0 or end_year % 400 == 0)) else 28, + 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][end_month - 1] + end_day = min(installation_date.day, days_in_end_month) + warranty_end = installation_date.replace(year=end_year, month=end_month, day=end_day) + + remaining = (warranty_end.year - now.year) * 12 + (warranty_end.month - now.month) + return remaining diff --git a/streetup/assets/tests.py b/streetup/assets/tests.py new file mode 100644 index 0000000..436e612 --- /dev/null +++ b/streetup/assets/tests.py @@ -0,0 +1,2385 @@ +from django.test import TestCase, tag +from django.contrib.auth.models import User +from django.contrib.gis.geos import MultiPolygon, Polygon +from django.urls import reverse +import json +import os +from unittest import skipUnless + +RUN_DXF_TESTS = os.getenv('RUN_DXF_TESTS', '0') == '1' + +from common.models import Thematic, UserConfig, UserThematics +from assets.models import NatureLocation, TrafficLightIntersection, TrafficLightRadarModel, TrafficLightRadar, AssetCategory + +class NatureLocationsGeoJSONTest(TestCase): + def setUp(self): + # Create a thematic + self.thematic = Thematic.objects.create( + code="nature", + name_fr="Nature", + name_nl="Natuur" + ) + + # Create a user + self.user = User.objects.create_user(username="testuser", password="password123") + self.config = UserConfig.objects.create(user=self.user) + + # Give thematic access to user + UserThematics.objects.create( + user_config=self.config, + thematic=self.thematic, + can_view_assets=True + ) + + # Create a NatureLocation in Brussels (around lat/lon: 50.85, 4.35) + # In SRID 3812, Brussels coordinates are around x=648000, y=670000. + # Let's create a small multipolygon around it. + poly = Polygon(((648000, 670000), (648100, 670000), (648100, 670100), (648000, 670100), (648000, 670000)), srid=3812) + multipoly = MultiPolygon(poly, srid=3812) + + # Build the WGS84 geojson + geom_4326 = multipoly.clone() + geom_4326.transform(4326) + + self.location = NatureLocation.objects.create( + code="LOC-001", + name_fr="Test Location", + geom=multipoly, + geojson=geom_4326.json, + is_highway=False + ) + + def test_nature_locations_geojson_with_bbox_intersection(self): + self.client.force_login(self.user) + + # Coordinates in degrees (WGS84, SRID 4326): + # x=148000, y=170000 in Lambert 2008 (SRID 3812) is roughly lon=4.3508, lat=50.8465 + # Let's verify that a bounding box around it intersects and returns the location + # A polygon in SRID 4326: + bbox = { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[ + [4.34, 50.84], + [4.36, 50.84], + [4.36, 50.86], + [4.34, 50.86], + [4.34, 50.84] + ]] + } + } + + response = self.client.post( + reverse("assets:nature_locations_geojson"), + data=json.dumps({"filter_geom": bbox}), + content_type="application/json" + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["type"], "FeatureCollection") + # Ensure our location is in the results + features = data["features"] + self.assertTrue(any(f["properties"]["code"] == "LOC-001" for f in features)) +from django.test import override_settings + +@override_settings(STORAGES={ + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +}) +class TrafficLightRadarTest(TestCase): + def setUp(self): + # Create a thematic for traffic lights + self.thematic, _ = Thematic.objects.get_or_create( + code="trafficlights", + defaults={ + "name_fr": "Feux de signalisation", + "name_nl": "Verkeerslichten" + } + ) + + self.user = User.objects.create_user(username="testuser", password="password123") + self.config = UserConfig.objects.create(user=self.user, limit_assets_to_contracts=False) + + # Give thematic access to user + UserThematics.objects.create( + user_config=self.config, + thematic=self.thematic, + can_view_assets=True + ) + + # Create category for radar + self.category, _ = AssetCategory.objects.get_or_create( + code="TL_RADAR_PREVENTIF", + defaults={ + "name_fr": "Radar Préventif", + "name_nl": "Preventieve Radar", + "thematic": self.thematic + } + ) + + # Create a spatial point for the intersection + from django.contrib.gis.geos import Point + geom_intersection = Point(648000, 670000, srid=3812) + + # Create intersection with is_radar=True + self.intersection = TrafficLightIntersection.objects.create( + code="ROM001", + name_fr="Test Radar Intersection", + geom=geom_intersection, + lon=4.3508, + lat=50.8465, + is_radar=True + ) + + # Create a model + self.radar_model = TrafficLightRadarModel.objects.create( + code="RAD-MODEL-01", + name_fr="Model 1", + category=self.category + ) + + # Create a radar pole + from assets.models.trafficlights import TrafficLightRadarPole + self.radar_pole = TrafficLightRadarPole.objects.create( + code="RP_ROM001", + intersection=self.intersection, + geom=geom_intersection, + status="active" + ) + + # Create the radar asset + self.radar = TrafficLightRadar.objects.create( + code="RAD_PREV_ROM001", + category=self.category, + model=self.radar_model, + intersection=self.intersection, + pole=self.radar_pole, + geom=geom_intersection, + status="active" + ) + + def test_intersection_is_radar(self): + # Verify fields + self.assertTrue(self.intersection.is_radar) + self.assertEqual(self.radar.intersection, self.intersection) + self.assertEqual(self.intersection.radars.count(), 1) + + def test_geojson_contains_is_radar(self): + self.client.force_login(self.user) + response = self.client.get(reverse("assets:trafficlights_intersections_geojson")) + self.assertEqual(response.status_code, 200) + data = response.json() + + # Verify intersection + features = data["features"] + radar_features = [f for f in features if f["properties"]["code"] == "ROM001"] + self.assertEqual(len(radar_features), 1) + self.assertTrue(radar_features[0]["properties"]["is_radar"]) + + def test_detail_view_includes_radars(self): + self.client.force_login(self.user) + response = self.client.get(reverse("assets:trafficlights_intersections_detail", args=[self.intersection.id])) + self.assertEqual(response.status_code, 200) + + # Check context + self.assertIn("assets_geojson", response.context) + assets_geojson = response.context["assets_geojson"] + self.assertEqual(assets_geojson["type"], "FeatureCollection") + radar_assets = [f for f in assets_geojson["features"] if f["properties"]["asset_type"] == "trafficlightradar"] + self.assertEqual(len(radar_assets), 1) + self.assertEqual(radar_assets[0]["properties"]["code"], "RAD_PREV_ROM001") + + def test_detail_view_includes_radar_poles(self): + self.client.force_login(self.user) + response = self.client.get(reverse("assets:trafficlights_intersections_detail", args=[self.intersection.id])) + self.assertEqual(response.status_code, 200) + + # Check assets list in context + self.assertIn("assets", response.context) + assets = response.context["assets"] + self.assertIn("poles", assets) + # Verify the radar pole is in the poles list + self.assertTrue(any(p.code == "RP_ROM001" for p in assets["poles"])) + + # Check GeoJSON has the correct asset_type and details + self.assertIn("assets_geojson", response.context) + assets_geojson = response.context["assets_geojson"] + radar_pole_assets = [f for f in assets_geojson["features"] if f["properties"]["asset_type"] == "trafficlightradarpole"] + self.assertEqual(len(radar_pole_assets), 1) + self.assertEqual(radar_pole_assets[0]["properties"]["code"], "RP_ROM001") + + def test_detail_view_includes_georeferenced_documents(self): + from django.contrib.contenttypes.models import ContentType + from documents.models import ManagedDocument, DocumentAttachment, DocumentFolder, DocumentFolderAttachment + + # Ensure we have a default folder for the intersection to enable managed documents + folder = DocumentFolder.objects.create(name="Intersection Folder", slug="intersection-folder", created_by=self.user) + ct_tli = ContentType.objects.get_for_model(self.intersection.__class__) + DocumentFolderAttachment.objects.create( + folder=folder, + content_type=ct_tli, + object_id=self.intersection.id, + is_default=True + ) + + # Create a georeferenced document and attach it + doc = ManagedDocument.objects.create( + title="Plan Georef", + georeference_coords=[[4.1, 50.1], [4.2, 50.1], [4.2, 50.2], [4.1, 50.2]], + georeference_page=1, + created_by=self.user + ) + doc.folders.add(folder) + + DocumentAttachment.objects.create( + document=doc, + content_type=ct_tli, + object_id=self.intersection.id + ) + + self.client.force_login(self.user) + response = self.client.get(reverse("assets:trafficlights_intersections_detail", args=[self.intersection.id])) + self.assertEqual(response.status_code, 200) + + self.assertIn("georeferenced_documents_json", response.context) + import json + georef_docs = json.loads(response.context["georeferenced_documents_json"]) + self.assertEqual(len(georef_docs), 1) + self.assertEqual(georef_docs[0]["title"], "Plan Georef") + self.assertEqual(georef_docs[0]["coords"], [[4.1, 50.1], [4.2, 50.1], [4.2, 50.2], [4.1, 50.2]]) + + def test_unlink_parent_from_child(self): + # 1. Create a parent PublicLightingPole + from assets.models.publiclighting import PublicLightingStreet, PublicLightingPoleModel, PublicLightingPole + pl_street = PublicLightingStreet.objects.create(code="PL-ST-01") + pl_model = PublicLightingPoleModel.objects.create(code="PL-MOD-01") + pl_pole = PublicLightingPole.objects.create(code="PL-POLE-01", model=pl_model, street=pl_street) + + # 2. Link parent to our radar child + self.radar.publiclighting_pole = pl_pole + self.radar.save() + + # Check it is linked + self.radar.refresh_from_db() + self.assertEqual(self.radar.publiclighting_pole, pl_pole) + + # 3. Give edit permission to user for trafficlights thematic + UserThematics.objects.filter(user_config=self.config, thematic=self.thematic).update(can_edit_assets=True) + + # 4. Log in the user + self.client.force_login(self.user) + + # 5. Call set-parent endpoint to unlink (parent_id=None/null) + response = self.client.post( + reverse("assets:set_parent_asset", kwargs={"asset_model": "trafficlightradar", "asset_id": self.radar.id}), + data=json.dumps({ + "field_name": "publiclighting_pole", + "parent_model": "publiclightingpole", + "parent_id": None + }), + content_type="application/json" + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data["success"]) + self.assertEqual(data["field_name"], "publiclighting_pole") + + # 6. Verify database state is updated (unlinked) + self.radar.refresh_from_db() + self.assertIsNone(self.radar.publiclighting_pole) + + def test_radar_position_sync_when_support_pole_moves(self): + # Move the radar pole + from django.contrib.gis.geos import Point + new_geom = Point(648100, 670100, srid=3812) + self.radar_pole.geom = new_geom + self.radar_pole.lon = 4.3520 + self.radar_pole.lat = 50.8475 + self.radar_pole.geojson = '{"type":"Point","coordinates":[4.3520,50.8475]}' + self.radar_pole.save() + + # Check if the child radar position updated automatically + self.radar.refresh_from_db() + self.assertEqual(self.radar.geom, new_geom) + self.assertEqual(self.radar.lon, 4.3520) + self.assertEqual(self.radar.lat, 50.8475) + self.assertEqual(self.radar.geojson, '{"type":"Point","coordinates":[4.3520,50.8475]}') + + + +class CategoryAllowedModelTest(TestCase): + def setUp(self): + # Create user & config + self.user = User.objects.create_user(username="testuser_allowed", password="password123") + self.config = UserConfig.objects.create(user=self.user, limit_assets_to_contracts=False) + + # Create thematic + self.thematic, _ = Thematic.objects.get_or_create( + code="trafficlights", + defaults={"name_fr": "Feux", "name_nl": "Verkeerslichten"} + ) + + # Give access + UserThematics.objects.create( + user_config=self.config, + thematic=self.thematic, + can_view_assets=True, + can_edit_assets=True + ) + # Get/create category + self.category, _ = AssetCategory.objects.get_or_create( + code="TL_RADAR_PREVENTIF", + defaults={ + "name_fr": "Radar Préventif", + "name_nl": "Preventieve Radar", + "thematic": self.thematic + } + ) + # Associate content types to category + from django.contrib.contenttypes.models import ContentType + from assets.models.trafficlights import TrafficLightRadar, TrafficLightAccessory + self.ct_radar = ContentType.objects.get_for_model(TrafficLightRadar) + self.ct_accessory = ContentType.objects.get_for_model(TrafficLightAccessory) + self.category.allowed_models.add(self.ct_radar) + self.category.allowed_models.add(self.ct_accessory) + + def test_get_categories_allowed_models(self): + self.client.force_login(self.user) + response = self.client.get(reverse("assets:get_asset_categories", args=[self.thematic.code])) + self.assertEqual(response.status_code, 200) + data = response.json() + + # Verify the radar category returns allowed models + categories = data.get("categories", []) + radar_cat = next((c for c in categories if c["id"] == self.category.id), None) + self.assertIsNotNone(radar_cat) + + allowed_models = radar_cat.get("allowed_models", []) + self.assertEqual(len(allowed_models), 2) + + radar_model = next(m for m in allowed_models if m["model_name"] == "trafficlightradar") + accessory_model = next(m for m in allowed_models if m["model_name"] == "trafficlightaccessory") + + # TrafficLightRadar requires a model template + self.assertTrue(radar_model["is_model_required"]) + # TrafficLightAccessory does not require a model template + self.assertFalse(accessory_model["is_model_required"]) + + +from assets.views.trafficlights import parse_dxf_poles + +@tag('slow', 'dxf') +@skipUnless(RUN_DXF_TESTS, "Skip slow DXF tests by default") +class TrafficLightDXFImportTest(TestCase): + def setUp(self): + # Create a thematic for traffic lights + self.thematic, _ = Thematic.objects.get_or_create( + code="trafficlights", + defaults={ + "name_fr": "Feux de signalisation", + "name_nl": "Verkeerslichten" + } + ) + + self.user = User.objects.create_user(username="testuser", password="password123") + self.config = UserConfig.objects.create(user=self.user, limit_assets_to_contracts=False) + + # Give thematic access to user + UserThematics.objects.create( + user_config=self.config, + thematic=self.thematic, + can_view_assets=True, + can_edit_assets=True + ) + # Create a spatial point for the intersection + from django.contrib.gis.geos import Point + geom_intersection = Point(648000, 670000, srid=3812) + + # Create intersection with code 'SB0239' matching the DXF file name + self.intersection = TrafficLightIntersection.objects.create( + code="SB0239", + name_fr="Carrefour SB0239", + geom=geom_intersection, + lon=4.3508, + lat=50.8465, + status="active" + ) + + # Create category for TrafficLightPole + self.category, _ = AssetCategory.objects.get_or_create( + code="TL_POLE", + defaults={ + "name_fr": "Poteau de signalisation", + "name_nl": "Paal", + "thematic": self.thematic + } + ) + # Create a pole model + from assets.models import TrafficLightPoleModel + self.pole_model = TrafficLightPoleModel.objects.create( + code="POM-01", + name_fr="Poteau Standard", + category=self.category + ) + + # Create the existing active pole asset that Choice A / Choice B should target + from assets.models import TrafficLightPole + self.existing_pole = TrafficLightPole.objects.create( + code="SB0239_T3", # matches label T3 in DXF + category=self.category, + model=self.pole_model, + intersection=self.intersection, + geom=geom_intersection, + lon=4.3508, + lat=50.8465, + status="active" + ) + + def test_parse_dxf_poles_real_file(self): + # Verify parse_dxf_poles works on the actual DXF + import os + from django.conf import settings + external_files_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(external_files_dir, 'SB0239_2026-02-01.dxf') + + if os.path.exists(file_path): + poles = parse_dxf_poles(file_path) + self.assertTrue(len(poles) > 0) + # Ensure coordinates and labels are populated + for pole in poles: + self.assertIn('x_31370', pole) + self.assertIn('y_31370', pole) + self.assertIn('lon', pole) + self.assertIn('lat', pole) + self.assertIn('label', pole) + + def test_import_dxf_views_flow(self): + import os + from django.conf import settings + external_files_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(external_files_dir, 'SB0239_2026-02-01.dxf') + if not os.path.exists(file_path): + self.skipTest("Real DXF file for test not found") + + self.client.force_login(self.user) + + # 1. GET: Select page + url_import = reverse("assets:import_dxf", args=[self.intersection.id]) + response = self.client.get(url_import) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "SB0239_2026-02-01.dxf") + + # 2. POST: Select file redirects to GET page with selected dxf_file + response = self.client.post(url_import, {"dxf_file": "SB0239_2026-02-01.dxf"}) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, f"{url_import}?dxf_file=SB0239_2026-02-01.dxf") + + # 3. POST: Add staged poles via AJAX + url_add = reverse("assets:add_staged_pole", args=[self.intersection.id]) + + # Add pole 1 (replaces existing by code) + response = self.client.post(url_add, { + "code": "SB0239_T3", + "model_id": str(self.pole_model.id), + "lon": "4.35081", + "lat": "50.84651", + "dxf_file": "SB0239_2026-02-01.dxf" + }) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data["success"]) + pole_1_id = data["pole"]["id"] + + # Add pole 2 (new pole) + response = self.client.post(url_add, { + "code": "SB0239_P02", + "model_id": str(self.pole_model.id), + "lon": "4.35085", + "lat": "50.84655", + "dxf_file": "SB0239_2026-02-01.dxf" + }) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data["success"]) + pole_2_id = data["pole"]["id"] + + # Verify staged assets are created in 'to_be_approved' and 'to_be_validated' + from assets.models import TrafficLightPole + staged_poles = TrafficLightPole.objects.filter(intersection=self.intersection, status="to_be_approved") + self.assertEqual(staged_poles.count(), 2) + self.assertTrue(staged_poles.filter(code="SB0239_T3").exists()) + self.assertTrue(staged_poles.filter(code="SB0239_P02").exists()) + for sp in staged_poles: + self.assertEqual(sp.validation_status, "to_be_validated") + self.assertEqual(sp.note, "Créé manuellement sur plan DXF : SB0239_2026-02-01.dxf") + + # Test deleting pole 2 via AJAX (verifying delete view) + url_delete = reverse("assets:delete_staged_pole", args=[self.intersection.id, pole_2_id]) + response = self.client.post(url_delete) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()["success"]) + self.assertEqual(TrafficLightPole.objects.filter(intersection=self.intersection, status="to_be_approved").count(), 1) + + # Re-add pole 2 + response = self.client.post(url_add, { + "code": "SB0239_P02", + "model_id": str(self.pole_model.id), + "lon": "4.35085", + "lat": "50.84655", + "dxf_file": "SB0239_2026-02-01.dxf" + }) + self.assertEqual(response.status_code, 200) + + # 4. POST: Integrate Choice A (replace by code) + url_integrate = reverse("assets:integrate_dxf", args=[self.intersection.id]) + response = self.client.post(url_integrate, {"choice": "choice_a"}) + self.assertEqual(response.status_code, 302) + + # Staged assets should now be active/validated + self.assertEqual(TrafficLightPole.objects.filter(intersection=self.intersection, status="to_be_approved").count(), 0) + active_poles = TrafficLightPole.objects.filter(intersection=self.intersection, status="active") + self.assertEqual(active_poles.count(), 2) + self.assertTrue(active_poles.filter(code="SB0239_T3").exists()) + self.assertTrue(active_poles.filter(code="SB0239_P02").exists()) + + # The old active pole should have been archived/removed and its code changed/uninstallation date set + removed_pole = TrafficLightPole.objects.filter(intersection=self.intersection, status="removed").first() + self.assertIsNotNone(removed_pole) + self.assertIn("SB0239_T3", removed_pole.code) + self.assertIsNotNone(removed_pole.uninstallation_date) + + def test_integrate_choice_b_full_replacement(self): + self.client.force_login(self.user) + # Stage two poles via AJAX + url_add = reverse("assets:add_staged_pole", args=[self.intersection.id]) + self.client.post(url_add, { + "code": "SB0239_T3", + "model_id": str(self.pole_model.id), + "lon": "4.35081", + "lat": "50.84651", + "dxf_file": "SB0239_2026-02-01.dxf" + }) + self.client.post(url_add, { + "code": "SB0239_P02", + "model_id": str(self.pole_model.id), + "lon": "4.35085", + "lat": "50.84655", + "dxf_file": "SB0239_2026-02-01.dxf" + }) + + # POST: Integrate Choice B (replace all) + url_integrate = reverse("assets:integrate_dxf", args=[self.intersection.id]) + response = self.client.post(url_integrate, {"choice": "choice_b"}) + self.assertEqual(response.status_code, 302) + + # Staged assets should now be active + from assets.models import TrafficLightPole + self.assertEqual(TrafficLightPole.objects.filter(intersection=self.intersection, status="to_be_approved").count(), 0) + + # All old active poles should have been removed + removed_poles = TrafficLightPole.objects.filter(intersection=self.intersection, status="removed") + self.assertEqual(removed_poles.count(), 1) + self.assertEqual(removed_poles.first().id, self.existing_pole.id) + + # Staged poles should be active + active_poles = TrafficLightPole.objects.filter(intersection=self.intersection, status="active") + self.assertEqual(active_poles.count(), 2) + + def test_save_dxf_layers(self): + import os + from django.conf import settings + external_files_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(external_files_dir, 'SB0239_2026-02-01.dxf') + if not os.path.exists(file_path): + self.skipTest("Real DXF file for test not found") + + self.client.force_login(self.user) + + # Trigger layout GeoJSON generation and caching + url_geojson = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + self.client.get(f"{url_geojson}?file=SB0239_2026-02-01.dxf") + + # Save a subset of layers (e.g. only 'IGN_Signalisation_Feu_Poteau') + url_save = reverse("assets:save_dxf_layers", args=[self.intersection.id]) + response = self.client.post(url_save, { + "dxf_file": "SB0239_2026-02-01.dxf", + "layers": "IGN_Signalisation_Feu_Poteau" + }) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()["success"]) + + # Querying the dxf-layout-geojson endpoint should now return ONLY features from the saved layer + url_geojson = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + response_geojson = self.client.get(f"{url_geojson}?file=SB0239_2026-02-01.dxf") + self.assertEqual(response_geojson.status_code, 200) + data = response_geojson.json() + self.assertEqual(data["selected_layers"], ["IGN_Signalisation_Feu_Poteau"]) + for feature in data["features"]: + self.assertEqual(feature["properties"]["Layer"], "IGN_Signalisation_Feu_Poteau") + + # Verify that the overlay ManagedDocument was created and georeferenced + from documents.models import ManagedDocument + from django.contrib.contenttypes.models import ContentType + + from assets.models import TrafficLightIntersectionDocument + if getattr(TrafficLightIntersectionDocument, "DOCUMENT_MANAGEMENT_SYNC", False): + from documents.models import DocumentAttachment + ct_tli = ContentType.objects.get_for_model(self.intersection) + dxf_attachment = DocumentAttachment.objects.filter( + content_type=ct_tli, + object_id=self.intersection.id, + document__versions__file__contains="SB0239_2026-02-01" + ).first() + self.assertIsNotNone(dxf_attachment) + dxf_source_id = dxf_attachment.document.id + else: + from assets.models import TrafficLightIntersectionDocument + doc_obj = TrafficLightIntersectionDocument.objects.filter( + intersection=self.intersection, + file__contains="SB0239_2026-02-01" + ).first() + self.assertIsNotNone(doc_obj) + self.assertIsNotNone(doc_obj.managed_document) + dxf_source_id = doc_obj.managed_document.id + + overlay_doc = ManagedDocument.objects.filter( + unique_params__dxf_source_id=dxf_source_id, + is_archived=False + ).first() + self.assertIsNotNone(overlay_doc) + self.assertIsNotNone(overlay_doc.georeference_coords) + + # Verify that the generated overlay file has the correct unsuffixed name and exists on disk + import os + from django.conf import settings + overlay_filename = "sb0239_2026-02-01.overlay.png" + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_filename) + self.assertTrue(os.path.exists(overlay_path)) + + def test_save_dxf_layers_managed_document(self): + import os + from django.conf import settings + from django.core.files.base import ContentFile + from documents.models import ManagedDocument, DocumentVersion, DocumentAttachment + from django.contrib.contenttypes.models import ContentType + + external_files_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + dxf_path = os.path.join(external_files_dir, 'SB0239_2026-02-01.dxf') + if not os.path.exists(dxf_path): + self.skipTest("Real DXF file for test not found") + + self.client.force_login(self.user) + + # Create a ManagedDocument and version mimicking direct upload via Document Management + managed_doc = ManagedDocument.objects.create( + title="Direct Upload", + created_by=self.user + ) + with open(dxf_path, 'rb') as f: + dxf_content = ContentFile(f.read(), name="SB0239_2026-02-01_direct.dxf") + + version = DocumentVersion.objects.create( + document=managed_doc, + file=dxf_content, + uploaded_by=self.user, + status=DocumentVersion.STATUS_APPROVED + ) + + # Attach it to the intersection + ct = ContentType.objects.get_for_model(self.intersection) + DocumentAttachment.objects.create( + document=managed_doc, + content_type=ct, + object_id=self.intersection.id, + attached_by=self.user + ) + + # Trigger saving layers on this managed document using the filename of the version + dxf_filename = os.path.basename(version.file.name) + url_save = reverse("assets:save_dxf_layers", args=[self.intersection.id]) + response = self.client.post(url_save, { + "dxf_file": dxf_filename, + "layers": "IGN_Signalisation_Feu_Poteau" + }) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()["success"]) + + def test_dxf_document_deletion(self): + import os + from django.conf import settings + from documents.models import ManagedDocument, DocumentVersion + from assets.models import TrafficLightIntersectionDocument, TrafficLightPole + + self.user.is_superuser = True + self.user.save() + self.client.force_login(self.user) + + # Create a mock DXF document + doc = TrafficLightIntersectionDocument.objects.create( + intersection=self.intersection, + file="uploaded_test.dxf", + document_type='other', + uploaded_by=self.user + ) + # Create the ManagedDocument and current version associated + m_doc = ManagedDocument.objects.create( + title="uploaded_test.dxf", + created_by=self.user + ) + DocumentVersion.objects.create( + document=m_doc, + file="uploaded_test.dxf", + version_number=1, + uploaded_by=self.user + ) + # Link them + doc.managed_document = m_doc + doc.save() + + # Create a mock overlay document + overlay_doc = ManagedDocument.objects.create( + title="uploaded_test.overlay.png", + created_by=self.user, + unique_params={"format": "overlay_png", "dxf_source_id": m_doc.id} + ) + + # Create a mock imported pole with the dxf filename in note + pole = TrafficLightPole.objects.create( + intersection=self.intersection, + model=self.pole_model, + code="TEST_POLE_DEL", + geom="POINT(0 0)", + status="active", + note="Importé depuis le plan DXF : uploaded_test.dxf", + updated_by=self.user + ) + + # Delete document via client DocumentDeleteView + url_delete = reverse("documents:delete", args=[m_doc.pk]) + + # Confirm delete request with checkboxes checked + response = self.client.post(url_delete, { + "delete_overlay": "on", + "delete_vectors": "on" + }) + self.assertEqual(response.status_code, 302) + + # The document, overlay document, legacy intersection doc, and imported pole should be deleted + self.assertFalse(ManagedDocument.objects.filter(pk=m_doc.pk).exists()) + self.assertFalse(ManagedDocument.objects.filter(pk=overlay_doc.pk).exists()) + self.assertFalse(TrafficLightIntersectionDocument.objects.filter(pk=doc.pk).exists()) + self.assertFalse(TrafficLightPole.objects.filter(pk=pole.pk).exists()) + + def test_import_dxf_via_upload(self): + self.client.force_login(self.user) + import os + from django.conf import settings + from django.core.files.uploadedfile import SimpleUploadedFile + + # Read the real DXF file + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + real_dxf_path = os.path.join(read_only_dxf_dir, 'SB0239_2026-02-01.dxf') + + if not os.path.exists(real_dxf_path): + self.skipTest("Real DXF file for test not found") + + with open(real_dxf_path, 'rb') as f: + dxf_content = f.read() + + # Create SimpleUploadedFile + uploaded_file = SimpleUploadedFile("uploaded_test.dxf", dxf_content, content_type="application/dxf") + + url_import = reverse("assets:import_dxf", args=[self.intersection.id]) + response = self.client.post(url_import, {"dxf_file_upload": uploaded_file}) + self.assertEqual(response.status_code, 302) + + # Extract temp filename from redirect URL + redirect_url = response.url + self.assertIn("?dxf_file=temp_", redirect_url) + temp_filename = redirect_url.split("?dxf_file=")[1] + + # The file should exist in the dxf_temp directory + temp_file_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_temp', temp_filename) + self.assertTrue(os.path.exists(temp_file_path)) + + # Now simulate Step 2: POST to save_dxf_layers + url_save = reverse("assets:save_dxf_layers", args=[self.intersection.id]) + response_save = self.client.post(url_save, { + "dxf_file": temp_filename, + "layers": "IGN_Signalisation_Feu_Poteau" + }) + self.assertEqual(response_save.status_code, 200) + self.assertTrue(response_save.json()["success"]) + + # Verify the document was saved to the database and file exists + from assets.models import TrafficLightIntersectionDocument + if getattr(TrafficLightIntersectionDocument, "DOCUMENT_MANAGEMENT_SYNC", False): + from django.contrib.contenttypes.models import ContentType + from documents.models import ManagedDocument + ct_tli = ContentType.objects.get_for_model(self.intersection) + m_doc = ManagedDocument.objects.filter( + attachments__content_type=ct_tli, + attachments__object_id=self.intersection.id + ).first() + self.assertIsNotNone(m_doc) + latest_v = m_doc.latest_version + self.assertIsNotNone(latest_v) + self.assertTrue(os.path.exists(latest_v.file.path)) + dxf_filename = os.path.basename(latest_v.file.name) + dxf_file_path = latest_v.file.path + else: + from assets.models import TrafficLightIntersectionDocument + doc = TrafficLightIntersectionDocument.objects.filter(intersection=self.intersection).first() + self.assertIsNotNone(doc) + self.assertTrue(os.path.exists(doc.file.path)) + dxf_filename = os.path.basename(doc.file.name) + dxf_file_path = doc.file.path + + # Check overlay image and layout geojson existence + overlay_name = dxf_filename.lower().replace('.dxf', '.overlay.png') + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_name) + self.assertTrue(os.path.exists(overlay_path)) + + geojson_name = dxf_filename.lower().replace('.dxf', '.layout.geojson') + geojson_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts', geojson_name) + self.assertTrue(os.path.exists(geojson_path)) + + # Cleanup saved file after test + try: + os.remove(dxf_file_path) + except Exception: + pass + try: + os.remove(temp_file_path) + except Exception: + pass + + +@tag('slow', 'dxf') +@skipUnless(RUN_DXF_TESTS, "Skip slow DXF tests by default") +class TrafficLightCrossPlanImportTest(TestCase): + def setUp(self): + from assets.models import ( + TrafficLightPoleModel, TrafficLightLanternModel, + TrafficLightDetectorModel, TrafficLightCableModel + ) + # Create a thematic for traffic lights + self.thematic, _ = Thematic.objects.get_or_create( + code="trafficlights", + defaults={ + "name_fr": "Feux de signalisation", + "name_nl": "Verkeerslichten" + } + ) + + # Create a user + self.user = User.objects.create_user(username="testuser", password="password123") + self.config = UserConfig.objects.create(user=self.user) + + # Give view/edit permissions to user + UserThematics.objects.create( + user_config=self.config, + thematic=self.thematic, + can_view_assets=True, + can_edit_assets=True + ) + + # Create intersection SWB01 in DB + self.intersection = TrafficLightIntersection.objects.create( + code="SWB01", + status="active" + ) + + # Setup basic models so fallback works + TrafficLightPoleModel.objects.get_or_create( + code="poteau_droit", + defaults={"name_fr": "Poteau droit, gris 3.1m", "name_nl": "Poteau droit, gris 3.1m"} + ) + TrafficLightLanternModel.objects.get_or_create( + code="3v200", + defaults={"name_fr": "3V200", "name_nl": "3V200", "voltage": 230} + ) + TrafficLightDetectorModel.objects.get_or_create( + code="det_model", + defaults={"name_fr": "Drukknop", "name_nl": "Drukknop"} + ) + TrafficLightCableModel.objects.get_or_create( + code="cab_model", + defaults={"name_fr": "SVAVB 61x1,5mm²", "name_nl": "SVAVB 61x1,5mm²", "number_of_strands": 61} + ) + + def test_import_cross_plan_get(self): + self.client.force_login(self.user) + url = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "assets/trafficlights_import_cross_plan.html") + self.assertEqual(response.context["step"], "select") + + def test_import_cross_plan_post_and_integrate(self): + self.client.force_login(self.user) + import os + from django.conf import settings + from django.core.files.uploadedfile import SimpleUploadedFile + from assets.models.trafficlights import ( + TrafficLightPole, TrafficLightCable, TrafficLightLantern, TrafficLightDetector + ) + + # Path to real Excel file + real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx') + if not os.path.exists(real_excel_path): + self.skipTest("Real Excel file for test not found") + + with open(real_excel_path, 'rb') as f: + excel_content = f.read() + + uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", excel_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + # 1. Test POST import view (Analysis & Comparison Preview) + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["step"], "preview") + self.assertTrue(len(response.context["poles"]) > 0) + self.assertTrue(len(response.context["lanterns"]) > 0) + + # 2. Test POST integrate view (Global replace all action) + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + response_integrate = self.client.post(url_integrate, { + "cross_plan_filename": response.context["cross_plan_filename"], + "global_action": "replace_all" + }) + self.assertEqual(response_integrate.status_code, 302) # Redirects back to detail page + + # Verify created assets in DB + self.assertTrue(TrafficLightPole.objects.filter(intersection=self.intersection, status="active").count() > 0) + self.assertTrue(TrafficLightCable.objects.filter(intersection=self.intersection, status="active").count() > 0) + self.assertTrue(TrafficLightLantern.objects.filter(pole__intersection=self.intersection, status="active").count() > 0) + self.assertTrue(TrafficLightDetector.objects.filter(intersection=self.intersection, status="active").count() > 0) + + def test_prefix_match_and_rename_lantern_unchecked(self): + from assets.models import TrafficLightPole, TrafficLightLantern, TrafficLightPoleModel, TrafficLightLanternModel, AssetCategory + pole_model = TrafficLightPoleModel.objects.first() + lantern_model = TrafficLightLanternModel.objects.first() + category_pole = AssetCategory.objects.filter(code="TL_POLE").first() + category_lantern = AssetCategory.objects.filter(code="TL_LANTERN").first() or AssetCategory.objects.filter(code="TL_LANTERNE").first() + + # Pre-create pole in DB + pole = TrafficLightPole.objects.create( + intersection=self.intersection, + code="SWB01_A01", + model=pole_model, + category=category_pole, + status="active" + ) + # Pre-create lantern with old nomenclature (no phase suffix) + old_lantern = TrafficLightLantern.objects.create( + pole=pole, + code="SWB01_A01_LAN01", + model=lantern_model, + category=category_lantern, + status="active" + ) + + self.client.force_login(self.user) + import os + from django.conf import settings + from django.core.files.uploadedfile import SimpleUploadedFile + + real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx') + if not os.path.exists(real_excel_path): + self.skipTest("Real Excel file for test not found") + + with open(real_excel_path, 'rb') as f: + excel_content = f.read() + + uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", excel_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + # POST to import_cross_plan to analyze + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file}) + self.assertEqual(response.status_code, 200) + + # 1. Post integrate WITHOUT checking the selection checkbox + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + response_integrate = self.client.post(url_integrate, { + "cross_plan_filename": response.context["cross_plan_filename"], + "action_lantern_SWB01_A01_LAN01_T1": "replace", + "db_id_lantern_SWB01_A01_LAN01_T1": str(old_lantern.id) + # Not passing selected_lantern_SWB01_A01_LAN01_T1 + }) + self.assertEqual(response_integrate.status_code, 302) + + # Old lantern must still be active (skipped) + old_lantern.refresh_from_db() + self.assertEqual(old_lantern.status, "active") + + def test_prefix_match_and_rename_lantern_checked(self): + from assets.models import TrafficLightPole, TrafficLightLantern, TrafficLightPoleModel, TrafficLightLanternModel, AssetCategory + pole_model = TrafficLightPoleModel.objects.first() + lantern_model = TrafficLightLanternModel.objects.first() + category_pole = AssetCategory.objects.filter(code="TL_POLE").first() + category_lantern = AssetCategory.objects.filter(code="TL_LANTERN").first() or AssetCategory.objects.filter(code="TL_LANTERNE").first() + + # Pre-create pole in DB + pole = TrafficLightPole.objects.create( + intersection=self.intersection, + code="SWB01_A01", + model=pole_model, + category=category_pole, + status="active" + ) + # Pre-create lantern with old nomenclature (no phase suffix) + old_lantern = TrafficLightLantern.objects.create( + pole=pole, + code="SWB01_A01_LAN01", + model=lantern_model, + category=category_lantern, + status="active" + ) + + self.client.force_login(self.user) + import os + from django.conf import settings + from django.core.files.uploadedfile import SimpleUploadedFile + + real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx') + if not os.path.exists(real_excel_path): + self.skipTest("Real Excel file for test not found") + + with open(real_excel_path, 'rb') as f: + excel_content = f.read() + + uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", excel_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + # POST to import_cross_plan to analyze + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file}) + self.assertEqual(response.status_code, 200) + + # 2. Post integrate WITH checking the selection checkbox + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + response_integrate = self.client.post(url_integrate, { + "cross_plan_filename": response.context["cross_plan_filename"], + "selected_lantern_SWB01_A01_LAN01_T1": "on", + "action_lantern_SWB01_A01_LAN01_T1": "replace", + "db_id_lantern_SWB01_A01_LAN01_T1": str(old_lantern.id) + }) + self.assertEqual(response_integrate.status_code, 302) + + # Old lantern must now be archived + old_lantern.refresh_from_db() + self.assertEqual(old_lantern.status, "archived") + # A new replacement lantern must be active with the updated code + new_active = TrafficLightLantern.objects.filter(pole=pole, code="SWB01_A01_LAN01_T1", status="active").first() + self.assertIsNotNone(new_active) + self.assertEqual(old_lantern.replaced_by, new_active) + + def test_parse_telecom_detectors(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + # Create a mock cross plan workbook + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + + # Row 1: Sleutel + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + + # Cable 2 section start + ws.cell(row=5, column=1, value="Câble 02 TWAVB 10x2x0,8 mm²") + + # Row 6: Richting and Kringen headers + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + + # Row 7: Pole headers + ws.cell(row=7, column=5, value="B01") + ws.cell(row=7, column=6, value="A01") + + # Rows for telecom detectors + # Row 8: RC + ws.cell(row=8, column=1, value="com") + ws.cell(row=8, column=2, value="1") + ws.cell(row=8, column=3, value="RC") + ws.cell(row=8, column=4, value="") + ws.cell(row=8, column=6, value="X") + + # Row 9: RD + ws.cell(row=9, column=1, value="com") + ws.cell(row=9, column=2, value="2") + ws.cell(row=9, column=3, value="RD") + ws.cell(row=9, column=4, value="") + ws.cell(row=9, column=6, value="X") + + # Row 10: Rm T2,B1,B2 + ws.cell(row=10, column=1, value="com") + ws.cell(row=10, column=2, value="3") + ws.cell(row=10, column=3, value="Rm T2,B1,B2") + ws.cell(row=10, column=4, value="") + ws.cell(row=10, column=5, value="X") + + # Save workbook to temp file + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + + # Check parsed detectors + detectors = deduced["detectors"] + self.assertTrue(len(detectors) >= 3) + + # Verify RC + rc_det = next((d for d in detectors if d["code"] == "SWB01_A01_RC"), None) + self.assertIsNotNone(rc_det) + self.assertEqual(rc_det["model_name"], "TM60") + self.assertEqual(rc_det["pole_code"], "SWB01_A01") + + # Verify RD + rd_det = next((d for d in detectors if d["code"] == "SWB01_A01_RD"), None) + self.assertIsNotNone(rd_det) + self.assertEqual(rd_det["model_name"], "TM60") + self.assertEqual(rd_det["pole_code"], "SWB01_A01") + + # Verify Rm T2 + rm_det = next((d for d in detectors if d["code"] == "SWB01_B01_RMT2"), None) + self.assertIsNotNone(rm_det) + self.assertEqual(rm_det["model_name"], "TM60") + self.assertEqual(rm_det["pole_code"], "SWB01_B01") + + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_detector_prefix_match_and_rename(self): + from assets.models import TrafficLightPole, TrafficLightDetector, TrafficLightPoleModel, TrafficLightDetectorModel, AssetCategory + pole_model = TrafficLightPoleModel.objects.first() + detector_model = TrafficLightDetectorModel.objects.first() + category_pole = AssetCategory.objects.filter(code="TL_POLE").first() + category_detector = AssetCategory.objects.filter(code="TL_DETECTOR").first() or AssetCategory.objects.filter(code="TL_DETECTEUR").first() + + # Pre-create pole in DB + pole = TrafficLightPole.objects.create( + intersection=self.intersection, + code="SWB01_B01", + model=pole_model, + category=category_pole, + status="active" + ) + # Pre-create detector with old nomenclature (no pole suffix) + old_detector = TrafficLightDetector.objects.create( + intersection=self.intersection, + pole=pole, + code="SWB01-RA", + model=detector_model, + category=category_detector, + status="active" + ) + + self.client.force_login(self.user) + import os + from django.conf import settings + from django.core.files.uploadedfile import SimpleUploadedFile + + real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx') + if not os.path.exists(real_excel_path): + self.skipTest("Real Excel file for test not found") + + with open(real_excel_path, 'rb') as f: + excel_content = f.read() + + uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", excel_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + # POST to import_cross_plan to analyze + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file}) + self.assertEqual(response.status_code, 200) + + # Check detectors in preview context + preview_detectors = response.context["detectors"] + target_item = next((item for item in preview_detectors if item["code"] == "SWB01_B01_RA"), None) + self.assertIsNotNone(target_item) + self.assertEqual(target_item["status"], "modified") + self.assertEqual(target_item["db_id"], old_detector.id) + self.assertIn("Renommer le code", target_item["details_diff"]) + + # Integrate with checked checkbox + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + response_integrate = self.client.post(url_integrate, { + "cross_plan_filename": response.context["cross_plan_filename"], + "selected_detector_SWB01_B01_RA": "on", + "action_detector_SWB01_B01_RA": "replace", + "db_id_detector_SWB01_B01_RA": str(old_detector.id) + }) + self.assertEqual(response_integrate.status_code, 302) + + # Old detector must now be archived + old_detector.refresh_from_db() + self.assertEqual(old_detector.status, "archived") + # A new replacement detector must be active with the updated code + new_active = TrafficLightDetector.objects.filter(intersection=self.intersection, pole=pole, code="SWB01_B01_RA", status="active").first() + self.assertIsNotNone(new_active) + self.assertEqual(old_detector.replaced_by, new_active) + + def test_multiple_cross_plan_uploads(self): + import openpyxl + import tempfile + from assets.models import TrafficLightPole, TrafficLightLantern, TrafficLightDetector + + # Workbook 1: Lantern A on pole B01 + wb1 = openpyxl.Workbook() + ws1 = wb1.active + ws1.title = "kruisjesplan" + ws1.cell(row=1, column=1, value="Sleutel:") + ws1.cell(row=1, column=2, value="SWB01") + ws1.cell(row=5, column=1, value="Câble 01 SVAVB") + ws1.cell(row=6, column=3, value="richting") + ws1.cell(row=6, column=4, value="kringen") + ws1.cell(row=7, column=5, value="B01") + ws1.cell(row=8, column=1, value="com") + ws1.cell(row=8, column=2, value="1") + ws1.cell(row=8, column=3, value="A") + ws1.cell(row=8, column=4, value="") + ws1.cell(row=8, column=5, value="X") + + # Workbook 2: Detector RC on pole B01 + wb2 = openpyxl.Workbook() + ws2 = wb2.active + ws2.title = "kruisjesplan" + ws2.cell(row=1, column=1, value="Sleutel:") + ws2.cell(row=1, column=2, value="SWB01") + ws2.cell(row=5, column=1, value="Câble 02 TWAVB") + ws2.cell(row=6, column=3, value="richting") + ws2.cell(row=6, column=4, value="kringen") + ws2.cell(row=7, column=5, value="B01") + ws2.cell(row=8, column=1, value="com") + ws2.cell(row=8, column=2, value="1") + ws2.cell(row=8, column=3, value="RC") + ws2.cell(row=8, column=4, value="") + ws2.cell(row=8, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp1: + tmp1_path = tmp1.name + wb1.save(tmp1_path) + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp2: + tmp2_path = tmp2.name + wb2.save(tmp2_path) + + try: + self.client.force_login(self.user) + from django.core.files.uploadedfile import SimpleUploadedFile + + with open(tmp1_path, 'rb') as f1, open(tmp2_path, 'rb') as f2: + file1 = SimpleUploadedFile("plan_lanterns.xlsx", f1.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + file2 = SimpleUploadedFile("plan_detectors.xlsx", f2.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + # Upload both files + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, { + "cross_plan_file_upload": [file1, file2] + }) + self.assertEqual(response.status_code, 200) + + # Check that BOTH lanterns and detectors are in the preview context + preview_lanterns = response.context["lanterns"] + preview_detectors = response.context["detectors"] + + # Lantern A should be present + self.assertTrue(any(l["code"] == "SWB01_B01_LAN01_A" for l in preview_lanterns)) + # Detector RC should be present + self.assertTrue(any(d["code"] == "SWB01_B01_RC" for d in preview_detectors)) + + # Now integrate + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + response_integrate = self.client.post(url_integrate, { + "cross_plan_filename": response.context["cross_plan_filename"], + "selected_pole_SWB01_B01": "on", + "action_pole_SWB01_B01": "add", + "selected_lantern_SWB01_B01_LAN01_A": "on", + "action_lantern_SWB01_B01_LAN01_A": "add", + "selected_detector_SWB01_B01_RC": "on", + "action_detector_SWB01_B01_RC": "add" + }) + self.assertEqual(response_integrate.status_code, 302) + + # Verify that both are created in the database + pole = TrafficLightPole.objects.filter(intersection=self.intersection, code="SWB01_B01", status="active").first() + self.assertIsNotNone(pole) + + lantern = TrafficLightLantern.objects.filter(pole=pole, code="SWB01_B01_LAN01_A", status="active").first() + self.assertIsNotNone(lantern) + + detector = TrafficLightDetector.objects.filter(intersection=self.intersection, pole=pole, code="SWB01_B01_RC", status="active").first() + self.assertIsNotNone(detector) + + finally: + import os + for path in (tmp1_path, tmp2_path): + if os.path.exists(path): + os.remove(path) + + def test_parse_cross_plan_french_and_new_codes(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + # Workbook 1: Using Clé: and code SB403 + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "Plan de croix" + ws.cell(row=1, column=3, value="Clé:") + ws.cell(row=1, column=4, value="SB403") + ws.cell(row=2, column=1, value="Câble 01 SVAVB") + ws.cell(row=3, column=3, value="richting") + ws.cell(row=3, column=4, value="kringen") + ws.cell(row=4, column=5, value="B01") + ws.cell(row=5, column=1, value="com") + ws.cell(row=5, column=2, value="1") + ws.cell(row=5, column=3, value="A") + ws.cell(row=5, column=4, value="") + ws.cell(row=5, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + self.assertEqual(deduced["intersection_code"], "SB403") + self.assertEqual(len(deduced["poles"]), 1) + self.assertEqual(deduced["poles"][0]["code"], "SB403_B01") + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_parse_cross_plan_fallback_regex(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + # Workbook 2: No label, but code SB403 in the first cells + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=4, value="SB403") + ws.cell(row=2, column=1, value="Câble 01 SVAVB") + ws.cell(row=3, column=3, value="richting") + ws.cell(row=3, column=4, value="kringen") + ws.cell(row=4, column=5, value="B01") + ws.cell(row=5, column=1, value="com") + ws.cell(row=5, column=2, value="1") + ws.cell(row=5, column=3, value="A") + ws.cell(row=5, column=4, value="") + ws.cell(row=5, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + self.assertEqual(deduced["intersection_code"], "SB403") + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_parse_cross_plan_pushbuttons(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + # Workbook 3: Testing various pushbutton nomenclatures (PBa1, BPf4, Dka) + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 01 SVAVB") + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + ws.cell(row=7, column=5, value="B01") + + # Row 8: PBa1 + ws.cell(row=8, column=1, value="com") + ws.cell(row=8, column=2, value="1") + ws.cell(row=8, column=3, value="PBa1") + ws.cell(row=8, column=5, value="X") + + # Row 9: BPf4 + ws.cell(row=9, column=1, value="com") + ws.cell(row=9, column=2, value="2") + ws.cell(row=9, column=3, value="BPf4") + ws.cell(row=9, column=5, value="X") + + # Row 10: Dka + ws.cell(row=10, column=1, value="com") + ws.cell(row=10, column=2, value="3") + ws.cell(row=10, column=3, value="Dka") + ws.cell(row=10, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + detectors = deduced["detectors"] + self.assertEqual(len(detectors), 3) + + # Check PBa1 + pb_det = next((d for d in detectors if d["code"] == "SWB01_B01_PBa1"), None) + self.assertIsNotNone(pb_det) + self.assertEqual(pb_det["name"], "bouton poussoir NF (jaune) sur B01 - Phase a") + self.assertEqual(pb_det["model_name"], "Drukknop") + self.assertEqual(pb_det["phases"], ["a"]) + + # Check BPf4 + bp_det = next((d for d in detectors if d["code"] == "SWB01_B01_BPf4"), None) + self.assertIsNotNone(bp_det) + self.assertEqual(bp_det["name"], "bouton poussoir NO (bleu) sur B01 - Phase f") + self.assertEqual(bp_det["model_name"], "Drukknop") + self.assertEqual(bp_det["phases"], ["f"]) + + # Check Dka + dk_det = next((d for d in detectors if d["code"] == "SWB01_B01_DKa"), None) + self.assertIsNotNone(dk_det) + self.assertEqual(dk_det["name"], "Drukknop sur B01 - Phase a") + self.assertEqual(dk_det["model_name"], "Drukknop") + self.assertEqual(dk_det["phases"], ["a"]) + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_cross_plan_custom_names_and_pole_short_code(self): + import openpyxl + import tempfile + from assets.models import TrafficLightPole, TrafficLightDetector + + # Create mock workbook + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 01 SVAVB") + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + ws.cell(row=7, column=5, value="B01") + ws.cell(row=7, column=6, value="B02") + ws.cell(row=8, column=1, value="com") + ws.cell(row=8, column=2, value="1") + ws.cell(row=8, column=3, value="PBa1") + ws.cell(row=8, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + self.client.force_login(self.user) + from django.core.files.uploadedfile import SimpleUploadedFile + + with open(tmp_path, 'rb') as f: + uploaded_file = SimpleUploadedFile("plan_custom_names.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + # 1. Preview + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, { + "cross_plan_file_upload": uploaded_file + }) + self.assertEqual(response.status_code, 200) + + # Check that pole defaults to short code name in preview + preview_poles = response.context["poles"] + b01_prev = next((p for p in preview_poles if p["code"] == "SWB01_B01"), None) + self.assertIsNotNone(b01_prev) + self.assertEqual(b01_prev["name"], "B01") + + b02_prev = next((p for p in preview_poles if p["code"] == "SWB01_B02"), None) + self.assertIsNotNone(b02_prev) + self.assertEqual(b02_prev["name"], "B02") + + # 2. Integrate with customized names + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + response_integrate = self.client.post(url_integrate, { + "cross_plan_filename": response.context["cross_plan_filename"], + "selected_pole_SWB01_B01": "on", + "action_pole_SWB01_B01": "add", + "name_pole_SWB01_B01": "Mon Poteau B01", + + "selected_pole_SWB01_B02": "on", + "action_pole_SWB01_B02": "add", + # no name override for B02, should default to "B02" + + "selected_detector_SWB01_B01_PBa1": "on", + "action_detector_SWB01_B01_PBa1": "add", + "name_detector_SWB01_B01_PBa1": "Mon BP Jaune" + }) + self.assertEqual(response_integrate.status_code, 302) + + # Verify saved assets and names + p1 = TrafficLightPole.objects.filter(intersection=self.intersection, code="SWB01_B01", status="active").first() + self.assertIsNotNone(p1) + self.assertEqual(p1.name_fr, "Mon Poteau B01") + + p2 = TrafficLightPole.objects.filter(intersection=self.intersection, code="SWB01_B02", status="active").first() + self.assertIsNotNone(p2) + self.assertEqual(p2.name_fr, "B02") + + det = TrafficLightDetector.objects.filter(intersection=self.intersection, pole=p1, code="SWB01_B01_PBa1", status="active").first() + self.assertIsNotNone(det) + self.assertEqual(det.name, "Mon BP Jaune") + + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_import_cross_plan_from_existing_document(self): + from documents.models import DocumentTag, ManagedDocument, DocumentVersion, DocumentAttachment + from django.contrib.contenttypes.models import ContentType + from django.core.files.uploadedfile import SimpleUploadedFile + import os + from django.conf import settings + + # 1. Create the DocumentTag 'cross_plan' + tag, _ = DocumentTag.objects.get_or_create( + slug='cross_plan', + defaults={ + 'name_fr': 'Plan de croix', + 'name_nl': 'Kruisjesplan', + 'show_in_asset_detail': True + } + ) + + # 2. Create the ManagedDocument and version with real Excel + real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx') + if not os.path.exists(real_excel_path): + self.skipTest("Real Excel file for test not found") + + with open(real_excel_path, 'rb') as f: + excel_content = f.read() + + uploaded_file = SimpleUploadedFile("18ac47df841-2808f-DOC-23270.xlsx", excel_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + doc = ManagedDocument.objects.create( + title="Plan de croix existant", + created_by=self.user + ) + doc.tags.add(tag) + + version = DocumentVersion.objects.create( + document=doc, + file=uploaded_file, + uploaded_by=self.user, + version_number=1 + ) + + # 3. Attach to intersection + ct_tli = ContentType.objects.get_for_model(self.intersection.__class__) + DocumentAttachment.objects.create( + document=doc, + content_type=ct_tli, + object_id=self.intersection.id + ) + + self.client.force_login(self.user) + + # 4. Check GET request contains the plan choice + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.get(url_import) + self.assertEqual(response.status_code, 200) + self.assertIn("cross_plan_choices", response.context) + choices = response.context["cross_plan_choices"] + self.assertTrue(any(c["id"] == f"managed-{doc.id}" for c in choices)) + + # 5. POST to analyze using existing plan + response_post = self.client.post(url_import, { + "existing_cross_plans": [f"managed-{doc.id}"] + }) + self.assertEqual(response_post.status_code, 200) + self.assertEqual(response_post.context["step"], "preview") + self.assertTrue(len(response_post.context["poles"]) > 0) + + + def test_parse_cross_plan_pedestrian_hp(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + # Workbook: Pedestrian phase aF with HP line + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 01 SVAVB") + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + ws.cell(row=7, column=5, value="B01") + + # Row 8: Pedestrian phase aF + ws.cell(row=8, column=1, value="com") + ws.cell(row=8, column=2, value="1") + ws.cell(row=8, column=3, value="aF") + ws.cell(row=8, column=5, value="X") + + # Row 9: HP + ws.cell(row=9, column=1, value="com") + ws.cell(row=9, column=2, value="2") + ws.cell(row=9, column=3, value="HP") + ws.cell(row=9, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + lanterns = deduced["lanterns"] + self.assertEqual(len(lanterns), 1) + self.assertEqual(lanterns[0]["model_name"], "2V200 piéton+HP") + self.assertEqual(lanterns[0]["code"], "SWB01_B01_LAN01_aF") + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_parse_cross_plan_double_cross_lanterns(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + # Workbook with pole B01 (with arm) and double crosses (XX) for vehicle phase A, pedestrian a, and tram T1 + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 01 SVAVB") + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + ws.cell(row=7, column=5, value="B01") + + # Double crosses for vehicle phase A + ws.cell(row=8, column=3, value="A") + ws.cell(row=8, column=4, value="Y") + ws.cell(row=8, column=5, value="XX") + + # Double crosses for pedestrian phase a + ws.cell(row=9, column=3, value="a") + ws.cell(row=9, column=4, value="g") + ws.cell(row=9, column=5, value="XX") + + # Double crosses for tram phase T1 + ws.cell(row=10, column=3, value="T1") + ws.cell(row=10, column=4, value="Y") + ws.cell(row=10, column=5, value="XX") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + lanterns = deduced["lanterns"] + + # We expect two lanterns for A: SWB01_B01_LAN01_A (3V200) and SWB01_B01_LAN02_A (3V300) + lan01 = next((l for l in lanterns if l["code"] == "SWB01_B01_LAN01_A"), None) + self.assertIsNotNone(lan01) + self.assertEqual(lan01["model_name"], "3V200") + + lan02 = next((l for l in lanterns if l["code"] == "SWB01_B01_LAN02_A"), None) + self.assertIsNotNone(lan02) + self.assertEqual(lan02["model_name"], "3V300") + + # We expect two standard pedestrian lanterns for a: LAN05_a and LAN06_a (2V200) + lan_a1 = next((l for l in lanterns if l["code"] == "SWB01_B01_LAN05_a"), None) + self.assertIsNotNone(lan_a1) + self.assertEqual(lan_a1["model_name"], "2V200") + + lan_a2 = next((l for l in lanterns if l["code"] == "SWB01_B01_LAN06_a"), None) + self.assertIsNotNone(lan_a2) + self.assertEqual(lan_a2["model_name"], "2V200") + + # We expect two tram lanterns for T1: LAN03_T1 (3V200 tram) and LAN04_T1 (3V300 tram) + lan_t1 = next((l for l in lanterns if l["code"] == "SWB01_B01_LAN03_T1"), None) + self.assertIsNotNone(lan_t1) + self.assertEqual(lan_t1["model_name"], "3V200 tram tout droite") + + lan_t2 = next((l for l in lanterns if l["code"] == "SWB01_B01_LAN04_T1"), None) + self.assertIsNotNone(lan_t2) + self.assertEqual(lan_t2["model_name"], "3V300 tram tout droit") + + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_parse_cross_plan_detector_model_mapping(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 02 TWAVB") + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + ws.cell(row=7, column=5, value="B01") + + # Row 8: RC (starts with R, but not Rp) -> should map to TM60 + ws.cell(row=8, column=3, value="RC") + ws.cell(row=8, column=5, value="X") + + # Row 9: RpA (starts with Rp) -> should map to TMA122 + ws.cell(row=9, column=3, value="RpA") + ws.cell(row=9, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + detectors = deduced["detectors"] + self.assertEqual(len(detectors), 2) + + det_rc = next((d for d in detectors if d["code"] == "SWB01_B01_RC"), None) + self.assertIsNotNone(det_rc) + self.assertEqual(det_rc["model_name"], "TM60") + + det_rpa = next((d for d in detectors if d["code"] == "SWB01_B01_RPA"), None) + self.assertIsNotNone(det_rpa) + self.assertEqual(det_rpa["model_name"], "TMA-122 M") + + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + def test_parse_cross_plan_video_detectors(self): + import openpyxl + import tempfile + from assets.utils.cross_plan_parser import parse_cross_plan + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 02 TWAVB") + # Header indicating a Detector section + ws.cell(row=6, column=3, value="Detecteur") + ws.cell(row=6, column=4, value="I/O") + ws.cell(row=7, column=5, value="B01") + + # Row 8: VAF VT VA (starts with V) -> should map to Traficam wide angle + ws.cell(row=8, column=3, value="VAF VT VA") + ws.cell(row=8, column=5, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + deduced = parse_cross_plan(tmp_path) + detectors = deduced["detectors"] + self.assertEqual(len(detectors), 1) + + det_video = next((d for d in detectors if d["code"] == "SWB01_B01_VAFVTVA"), None) + self.assertIsNotNone(det_video) + self.assertEqual(det_video["model_name"], "Traficam wide angle") + + # Verify no lanterns were created since it was in the Detecteur section + self.assertEqual(len(deduced["lanterns"]), 0) + + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + + +class AssetDefaultPositionTest(TestCase): + def test_assign_default_position_hierarchy(self): + from django.contrib.gis.geos import Polygon, MultiPolygon + from assets.models.structures import StructureLocation, Structure, StructureGeoAsset, StructureAssetModel + from assets.views.asset_editing import _assign_default_position + + # Create a parent location with geometry + parent_geom = MultiPolygon( + Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0))), + srid=3812 + ) + parent_loc = StructureLocation.objects.create( + name_fr="Parent Location", + geom=parent_geom, + lon=5.0, + lat=50.0 + ) + + # Create a child location without geometry + child_loc = StructureLocation.objects.create( + name_fr="Child Location", + parent_location=parent_loc + ) + + # Create structure + structure = Structure.objects.create( + name_fr="Structure Test", + main_location=parent_loc + ) + child_loc.structure = structure + child_loc.save() + + # Create model + model = StructureAssetModel.objects.create( + name_fr="Model Test" + ) + + # Create asset without geometry initially + asset = StructureGeoAsset( + code="TEST-001", + model=model, + location=child_loc, + structure=structure + ) + + # Call _assign_default_position + _assign_default_position(asset, child_loc) + + # Assert coordinates/geometry are set from parent location + self.assertIsNotNone(asset.geom) + self.assertEqual(asset.geom.srid, 3812) + self.assertEqual(asset.geom.x, parent_geom.centroid.x) + self.assertEqual(asset.geom.y, parent_geom.centroid.y) + self.assertIsNotNone(asset.lon) + self.assertIsNotNone(asset.lat) + self.assertIsNotNone(asset.geojson) + + +class AssetValidationPermissionTest(TestCase): + def setUp(self): + from django.contrib.auth.models import User + from common.models import UserConfig, Thematic, UserThematics + from assets.models import AssetCategory + from assets.models.structures import StructureGeoAsset, StructureAssetModel + + self.user = User.objects.create_user(username="validator_user", password="password") + self.user_config = UserConfig.objects.create(user=self.user, is_intern=True) + + self.thematic = Thematic.objects.create(code="structures", name_fr="Ouvrages d'art") + self.user_thematic = UserThematics.objects.create( + user_config=self.user_config, + thematic=self.thematic, + can_view_assets=True, + can_edit_assets=True, + can_validate_assets=False # Start with validation disabled + ) + + self.category = AssetCategory.objects.create( + thematic=self.thematic, + code="CAT", + name_fr="Category" + ) + self.model = StructureAssetModel.objects.create(name_fr="Model") + self.asset = StructureGeoAsset.objects.create( + code="TEST-VAL-01", + model=self.model, + category=self.category, + status="active" + ) + + def test_validation_permission(self): + from assets.permissions import can_validate_asset, get_allowed_update_fields_for_asset + + # Without permission + self.assertFalse(can_validate_asset(self.user, self.asset)) + fields = get_allowed_update_fields_for_asset(self.user, self.asset) + self.assertNotIn("validation_status", fields) + + # With permission + self.user_thematic.can_validate_assets = True + self.user_thematic.save() + + self.assertTrue(can_validate_asset(self.user, self.asset)) + fields = get_allowed_update_fields_for_asset(self.user, self.asset) + self.assertIn("validation_status", fields) + + def test_bulk_edit_assets(self): + import json + from django.urls import reverse + + # Login/authenticate the user + self.client.force_login(self.user) + + url = reverse('assets:bulk_edit_assets') + payload = { + "assets": [{"model": "structuregeoasset", "object_id": self.asset.pk}], + "field_name": "validation_status", + "new_value": "validated" + } + + # 1. Without permission + response = self.client.post( + url, + data=json.dumps(payload), + content_type="application/json" + ) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data['success']) + self.assertEqual(data['updated_count'], 0) + self.assertEqual(data['skipped_count'], 1) + + # 2. Enable permission + self.user_thematic.can_validate_assets = True + self.user_thematic.save() + + response = self.client.post( + url, + data=json.dumps(payload), + content_type="application/json" + ) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data['success']) + self.assertEqual(data['updated_count'], 1) + self.assertEqual(data['skipped_count'], 0) + + # Verify database update + self.asset.refresh_from_db() + self.assertEqual(self.asset.validation_status, 'validated') + + +@tag('slow', 'dxf') +@skipUnless(RUN_DXF_TESTS, "Skip slow DXF tests by default") +class TrafficLightDXFLayerSelectionTest(TestCase): + def setUp(self): + from common.models import Thematic, UserConfig, UserThematics + from assets.models import TrafficLightIntersection + self.thematic, _ = Thematic.objects.get_or_create( + code="trafficlights", + defaults={ + "name_fr": "Feux de signalisation", + "name_nl": "Verkeerslichten" + } + ) + self.user = User.objects.create_user(username="testuser", password="password123") + self.config = UserConfig.objects.create(user=self.user, limit_assets_to_contracts=False) + UserThematics.objects.create( + user_config=self.config, + thematic=self.thematic, + can_view_assets=True, + can_edit_assets=True + ) + self.intersection = TrafficLightIntersection.objects.create( + code="SB0239", + name_fr="Carrefour SB0239", + status="active" + ) + + # Create a real temporary DXF file in settings.PRIVATE_MEDIA_ROOT / 'dxf_files' + import os + import ezdxf + from django.conf import settings + + self.dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + os.makedirs(self.dxf_dir, exist_ok=True) + self.dxf_filename = "test_layers.dxf" + self.dxf_path = os.path.join(self.dxf_dir, self.dxf_filename) + + # Create DXF with ezdxf and add some geometry in different layers + doc = ezdxf.new() + doc.layers.new('LAYER_A', dxfattribs={'color': 2}) + doc.layers.new('LAYER_B', dxfattribs={'color': 5}) + msp = doc.modelspace() + + # Add entity to LAYER_A + msp.add_line((0, 0), (10, 10), dxfattribs={'layer': 'LAYER_A'}) + # Add entity to LAYER_B + msp.add_line((10, 10), (20, 20), dxfattribs={'layer': 'LAYER_B'}) + + doc.saveas(self.dxf_path) + + def tearDown(self): + import os + if os.path.exists(self.dxf_path): + try: + os.remove(self.dxf_path) + except Exception: + pass + + # Also clean up the cached layouts created by tests + from django.conf import settings + cache_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts') + if os.path.exists(cache_dir): + import glob + for f in glob.glob(os.path.join(cache_dir, "test_layers.*")): + try: + os.remove(f) + except Exception: + pass + + def test_dxf_layers_endpoint(self): + self.client.force_login(self.user) + url = reverse("assets:dxf_layers", args=[self.intersection.id]) + response = self.client.get(url, {"file": self.dxf_filename}) + self.assertEqual(response.status_code, 200) + + data = response.json() + self.assertIn("layers", data) + layers = data["layers"] + + # Should have LAYER_A and LAYER_B + layer_names = [l["name"] for l in layers] + self.assertIn("LAYER_A", layer_names) + self.assertIn("LAYER_B", layer_names) + + # Colors should match (LAYER_A color ACI 2 is yellow #ffff00, LAYER_B ACI 5 is blue #0000ff) + layer_a = next(l for l in layers if l["name"] == "LAYER_A") + layer_b = next(l for l in layers if l["name"] == "LAYER_B") + self.assertEqual(layer_a["color"], "#ffff00") + self.assertEqual(layer_b["color"], "#0000ff") + + def test_dxf_layout_geojson_with_all_layers(self): + self.client.force_login(self.user) + url = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + + # All layers (default) + response = self.client.get(url, {"file": self.dxf_filename, "layers": "all"}) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIn("features", data) + features = data["features"] + + # Should have features for both LAYER_A and LAYER_B + feature_layers = [f["properties"]["Layer"] for f in features] + self.assertIn("LAYER_A", feature_layers) + self.assertIn("LAYER_B", feature_layers) + + def test_dxf_layout_geojson_with_subset_layers(self): + self.client.force_login(self.user) + url = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + + # Subset: only LAYER_A + response = self.client.get(url, {"file": self.dxf_filename, "layers": "LAYER_A"}) + self.assertEqual(response.status_code, 200) + data = response.json() + features = data["features"] + + feature_layers = [f["properties"]["Layer"] for f in features] + self.assertIn("LAYER_A", feature_layers) + self.assertNotIn("LAYER_B", feature_layers) + + def test_dxf_layout_geojson_with_no_layers(self): + self.client.force_login(self.user) + url = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + + # None layers + response = self.client.get(url, {"file": self.dxf_filename, "layers": "none"}) + self.assertEqual(response.status_code, 200) + data = response.json() + features = data["features"] + self.assertEqual(len(features), 0) + + def test_import_dxf_existing_plan_selection_flow(self): + self.client.force_login(self.user) + + # 1. First trigger layout caching for a subset of layers + url_layout = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + response_layout = self.client.get(url_layout, {"file": self.dxf_filename, "layers": "LAYER_A"}) + self.assertEqual(response_layout.status_code, 200) + + # 2. POST to import_dxf indicating we selected this existing file + url_import = reverse("assets:import_dxf", args=[self.intersection.id]) + response_import = self.client.post(url_import, {"dxf_file": self.dxf_filename}) + + self.assertEqual(response_import.status_code, 200) + + # 3. Verify context variables passed to template + self.assertTrue(response_import.context["is_existing_plan"]) + self.assertTrue(response_import.context["has_cached_layout"]) + self.assertIsNotNone(response_import.context["previous_selection"]) + + # previous_selection should decode back to ['LAYER_A'] + import json + prev_selection = json.loads(response_import.context["previous_selection"]) + self.assertEqual(prev_selection, ["LAYER_A"]) + + def test_layout_cache_overwritten_on_selection_change(self): + self.client.force_login(self.user) + import os + import json + from django.conf import settings + + # 1. First request for LAYER_A only + url_layout = reverse("assets:dxf_layout_geojson", args=[self.intersection.id]) + response_a = self.client.get(url_layout, {"file": self.dxf_filename, "layers": "LAYER_A"}) + self.assertEqual(response_a.status_code, 200) + data_a = response_a.json() + self.assertEqual(data_a["selected_layers"], ["LAYER_A"]) + + # Ensure single cache file exists + geojson_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts') + cache_path = os.path.join(geojson_dir, f"{self.dxf_filename.lower().replace('.dxf', '.layout.geojson')}") + self.assertTrue(os.path.exists(cache_path)) + + # 2. Verify cache contents + with open(cache_path, "r", encoding="utf-8") as f: + content_a = json.load(f) + self.assertEqual(content_a["selected_layers"], ["LAYER_A"]) + + # 3. Second request for LAYER_B only (different selection) + response_b = self.client.get(url_layout, {"file": self.dxf_filename, "layers": "LAYER_B"}) + self.assertEqual(response_b.status_code, 200) + data_b = response_b.json() + self.assertEqual(data_b["selected_layers"], ["LAYER_B"]) + + # 4. Verify cache file was overwritten with new selection + with open(cache_path, "r", encoding="utf-8") as f: + content_b = json.load(f) + self.assertEqual(content_b["selected_layers"], ["LAYER_B"]) + + def test_import_dxf_document_association_and_filter(self): + import os + from assets.models import TrafficLightIntersectionDocument + from django.core.files.uploadedfile import SimpleUploadedFile + self.client.force_login(self.user) + + # 1. Create a dummy DXF file upload associated with the intersection + dummy_dxf = SimpleUploadedFile("carrefour_plan.dxf", b"Dummy DXF content", content_type="application/dxf") + + url_import = reverse("assets:import_dxf", args=[self.intersection.id]) + response_post = self.client.post(url_import, {"dxf_file_upload": dummy_dxf}) + + # Verify document was created + dxf_docs = TrafficLightIntersectionDocument.objects.filter(intersection=self.intersection) + self.assertEqual(dxf_docs.count(), 1) + doc = dxf_docs.first() + self.assertTrue(doc.file.name.endswith(".dxf")) + + # 2. Get request to import_dxf selection step + response_get = self.client.get(url_import) + self.assertEqual(response_get.status_code, 200) + + # Verify the file is listed in the dxf_files list context + self.assertIn(os.path.basename(doc.file.name), response_get.context["dxf_files"]) + + # Clean up database document file + try: + doc.file.delete() + doc.delete() + except Exception: + pass + + +class TrafficLightRadarRelationTests(TestCase): + def setUp(self): + from django.contrib.gis.geos import Point + from assets.models import TrafficLightIntersection, TrafficLightRadarModel, TrafficLightRadarPoleModel, TrafficLightRadarPole, TrafficLightPole, TrafficLightPoleModel + + self.intersection = TrafficLightIntersection.objects.create( + code="INT-TEST", + name_fr="Carrefour Test", + geom=Point(4.35, 50.85, srid=4326) + ) + # Transform intersection geom to 3812 as done in DB + self.intersection.geom.transform(3812) + self.intersection.save() + + self.radar_model = TrafficLightRadarModel.objects.create( + code="RM-001", + name_fr="Modèle Radar Test" + ) + + self.pole_model = TrafficLightRadarPoleModel.objects.create( + code="PM-001", + name_fr="Modèle Poteau Radar Test" + ) + + self.tl_pole_model = TrafficLightPoleModel.objects.create( + code="TPM-001", + name_fr="Modèle Poteau Tricolore Test" + ) + + def test_radar_geom_alignment_with_support(self): + from django.contrib.gis.geos import Point + from assets.models import TrafficLightRadar, TrafficLightRadarPole + + # 1. Create a support pole with specific geometry + support_geom = Point(4.351, 50.851, srid=4326) + support_geom.transform(3812) + + pole = TrafficLightRadarPole.objects.create( + code="POLE-001", + intersection=self.intersection, + model=self.pole_model, + geom=support_geom + ) + + # 2. Create a radar initially at a different position + initial_geom = Point(4.35, 50.85, srid=4326) + initial_geom.transform(3812) + radar = TrafficLightRadar.objects.create( + code="RADAR-001", + intersection=self.intersection, + model=self.radar_model, + geom=initial_geom + ) + + self.assertNotEqual(radar.geom, support_geom) + + # 3. Link the radar to the support pole and save + radar.pole = pole + radar.save() + + # 4. Verify position was aligned to support pole + self.assertEqual(radar.geom, support_geom) + + def test_radar_connection_lines_geojson_helper(self): + from django.contrib.gis.geos import Point + from assets.models import TrafficLightRadar, TrafficLightRadarPole, TrafficLightPole + from assets.views.core import get_connection_lines_geojson + + # 1. Create support pole + pole_geom = Point(4.351, 50.851, srid=4326) + pole_geom.transform(3812) + pole = TrafficLightRadarPole.objects.create( + code="POLE-001", + intersection=self.intersection, + model=self.pole_model, + geom=pole_geom + ) + + # 2. Create power connection pole (trafficlights_pole) at a different location + power_geom = Point(4.352, 50.852, srid=4326) + power_geom.transform(3812) + tl_pole = TrafficLightPole.objects.create( + code="TLPOLE-001", + intersection=self.intersection, + model=self.tl_pole_model, + geom=power_geom + ) + + # 3. Create radar linked to both + radar = TrafficLightRadar.objects.create( + code="RADAR-001", + intersection=self.intersection, + model=self.radar_model, + pole=pole, + power_trafficlights_pole=tl_pole, + geom=pole_geom + ) + + # 4. Call helper + geojson = get_connection_lines_geojson(radar) + self.assertIsNotNone(geojson) + self.assertEqual(geojson["type"], "FeatureCollection") + self.assertEqual(len(geojson["features"]), 1) + + feat = geojson["features"][0] + self.assertEqual(feat["properties"]["relation_type"], "electrical_connection") + self.assertEqual(feat["properties"]["parent_code"], "TLPOLE-001") + + # Coordinates should be in EPSG:4326 (WGS84) + expected_p1 = pole_geom.clone() + expected_p1.transform(4326) + expected_p2 = power_geom.clone() + expected_p2.transform(4326) + + coords = feat["geometry"]["coordinates"] + self.assertAlmostEqual(coords[0][0], expected_p1.x, places=5) + self.assertAlmostEqual(coords[0][1], expected_p1.y, places=5) + self.assertAlmostEqual(coords[1][0], expected_p2.x, places=5) + self.assertAlmostEqual(coords[1][1], expected_p2.y, places=5) + + +class TrafficLightPoleSignPanelsTest(TestCase): + def setUp(self): + from assets.models import TrafficLightIntersection, TrafficLightPoleModel, TrafficLightPole + from assets.models.sign import SignPanelModel + from common.models import Thematic + + self.thematic = Thematic.objects.get_or_create(code="trafficlights", defaults={"name_fr": "Trafic"})[0] + self.sign_thematic = Thematic.objects.get_or_create(code="sign", defaults={"name_fr": "Signalisation"})[0] + + self.intersection = TrafficLightIntersection.objects.create( + code="INT-001", + name_fr="Test Intersection" + ) + self.pole_model = TrafficLightPoleModel.objects.create( + code="TPM-001", + name_fr="Modèle Poteau Tricolore Test" + ) + self.pole = TrafficLightPole.objects.create( + code="POLE-001", + intersection=self.intersection, + model=self.pole_model + ) + self.panel_model = SignPanelModel.objects.create( + code="SPM-001", + name_fr="Modèle Panneau Test" + ) + + def test_sorted_signpanels(self): + from assets.models.sign import SignPanel + + # Create sign panels attached to the traffic light pole + panel2 = SignPanel.objects.create( + code="PANEL-002", + model=self.panel_model, + trafficlight_pole=self.pole + ) + panel1 = SignPanel.objects.create( + code="PANEL-001", + model=self.panel_model, + trafficlight_pole=self.pole + ) + + # Verify that sorted_signpanels returns them sorted by code + panels = list(self.pole.sorted_signpanels) + self.assertEqual(len(panels), 2) + self.assertEqual(panels[0], panel1) + self.assertEqual(panels[1], panel2) + + def test_radar_pole_sorted_signpanels(self): + from assets.models import TrafficLightRadarPole, TrafficLightRadarPoleModel + + radar_pole_model = TrafficLightRadarPoleModel.objects.create( + code="RPM-001", + name_fr="Modèle Radar Pole Test" + ) + radar_pole = TrafficLightRadarPole.objects.create( + code="RPOLE-001", + intersection=self.intersection, + model=radar_pole_model + ) + + # Verify sorted_signpanels returns an empty list for radar poles + self.assertEqual(list(radar_pole.sorted_signpanels), []) + + + + + diff --git a/streetup/assets/urls.py b/streetup/assets/urls.py new file mode 100644 index 0000000..11404f7 --- /dev/null +++ b/streetup/assets/urls.py @@ -0,0 +1,235 @@ +from django.urls import path + +from .views import core, locations, nature, publiclighting, trafficlights, artworks, structures, controlcenters, sign, roads, its, clean, asset_editing, history, location_editing, export, asset_permissions, water + +app_name = "assets" +urlpatterns = [ + path("", core.unified_assets_index, name="index"), + path("export/", export.export_assets, name="export_assets"), + + # ------------------------------------------------ + # Per-instance & per-type permission management + # ------------------------------------------------ + path('permissions///', asset_permissions.asset_permissions_panel, name='asset_permissions_panel'), + path('permissions/access//edit/', asset_permissions.asset_access_edit, name='asset_access_edit'), + path('permissions/access//delete/', asset_permissions.asset_access_delete, name='asset_access_delete'), + path('permissions/type//', asset_permissions.asset_type_permissions_page, name='asset_type_permissions_page'), + path('permissions/type-access//edit/', asset_permissions.asset_type_access_edit, name='asset_type_access_edit'), + path('permissions/type-access//delete/', asset_permissions.asset_type_access_delete, name='asset_type_access_delete'), + + # ------------------------------------------------ + # Generic Location Editing (create / update / delete) + # ------------------------------------------------ + path('locations///form/', location_editing.location_form_html, name='location_form_html_create'), + path('locations////form/', location_editing.location_form_html, name='location_form_html_edit'), + path('locations///create/', location_editing.create_location, name='create_location'), + path('locations////update/', location_editing.update_location, name='update_location'), + path('locations////delete/', location_editing.delete_location, name='delete_location'), + + # Asset editing API endpoints + path('//update/', asset_editing.update_asset, name='update_asset'), + path('//archive/', asset_editing.archive_asset, name='archive_asset'), + path('//replace/', asset_editing.replace_asset, name='replace_asset'), + path('//documents/add/', asset_editing.asset_add_document, name='asset_add_document'), + path('//documents//delete/', asset_editing.asset_delete_document, name='asset_delete_document'), + path('//documents/', asset_editing.asset_document_list_html, name='asset_document_list_html'), + # Linked assets API endpoints + path('//linked-types/', asset_editing.get_linked_asset_types, name='get_linked_asset_types'), + path('//link-child/', asset_editing.link_child_asset, name='link_child_asset'), + path('//unlink-child/', asset_editing.unlink_child_asset, name='unlink_child_asset'), + path('//set-parent/', asset_editing.set_parent_asset, name='set_parent_asset'), + path('//create-child/', asset_editing.create_and_link_child_asset, name='create_and_link_child_asset'), + path('api/search-assets//', asset_editing.search_assets_by_model, name='search_assets_by_model'), + path('api/child-asset-form-fields//', asset_editing.get_child_asset_form_fields, name='get_child_asset_form_fields'), + + # History API endpoints (without thematic) + path('//history/', history.get_asset_history, name='get_asset_history'), + path('//history/status-at-date/', history.get_asset_status_at_date, name='get_asset_status_at_date'), + path('history/global/', history.get_global_history, name='get_global_history'), + + # History API endpoints (with thematic - must come after specific routes) + path('///history/', history.get_asset_history, name='get_asset_history_thematic'), + path('///history/status-at-date/', history.get_asset_status_at_date, name='get_asset_status_at_date_thematic'), + path('/history/global/', history.get_global_history, name='get_global_history_thematic'), + + path('///create-asset/', asset_editing.create_asset, name='create_asset'), + path('///bulk-archive/', asset_editing.bulk_archive_assets, name='bulk_archive_assets'), + path('/categories/', asset_editing.get_asset_categories_for_thematic, name='get_asset_categories'), + path('/category//models/', asset_editing.get_asset_models_for_category, name='get_asset_models'), + path('/asset//available-locations/', asset_editing.get_available_locations_for_asset, name='get_available_locations'), + path('api/create-asset/', asset_editing.create_asset_api, name='create_asset_api'), + path('api/bulk-edit/', asset_editing.bulk_edit_assets, name='bulk_edit_assets'), + path('locations/autocomplete/', locations.locations_autocomplete, name='locations_autocomplete'), + # path('///add-document/', core.add_document, name='assets_add_document'), + path('trafficlights/', trafficlights.trafficlights_index, name='trafficlights_index'), + path('trafficlights/intersections/', trafficlights.trafficlights_intersections, name='trafficlights_intersections'), + path('trafficlights/intersections/get-nearest/', trafficlights.get_nearest_trafficlight_intersection, name='get_nearest_trafficlight_intersection'), + path('trafficlights/intersections/geojson/', trafficlights.trafficlights_intersections_geojson, name='trafficlights_intersections_geojson'), + path('trafficlights/intersections/documents/history/', trafficlights.fetch_document_history, name='trafficlights_intersections_document_history'), + path('trafficlights/intersections/autocomplete/', trafficlights.trafficlights_locations_autocomplete, name='trafficlights_autocomplete_locations'), + path('trafficlights/intersections//', trafficlights.trafficlights_intersections_detail, name='trafficlights_intersections_detail'), + path('trafficlights/intersections//contracts/add/', trafficlights.trafficlights_intersections_contracts_add, name='trafficlights_intersections_contracts_add'), + path('trafficlights/intersections//contracts//remove/', trafficlights.trafficlights_intersections_contracts_remove, name='trafficlights_intersections_contracts_remove'), + path('trafficlights/intersections//document-config/', trafficlights.get_document_config, name='trafficlights_intersections_document_config'), + path('trafficlights/intersections//', trafficlights.trafficlights_intersections_get_by_code, name='trafficlights_interstrafficlights_intersections_get_by_codeections_detail'), + path('trafficlights/intersections//add-document/', trafficlights.trafficlights_intersections_add_document, name='trafficlights_intersections_add_document'), + path('trafficlights/intersections//delete-document/', trafficlights.trafficlights_intersections_delete_document, name='trafficlights_intersections_delete_document'), + path('trafficlights/assets/', trafficlights.trafficlights_assets, name='trafficlights_assets'), + path('trafficlights/assets/geojson/', trafficlights.trafficlights_assets_geojson, name='trafficlights_assets_geojson'), + path('trafficlights/assets/controllers/', trafficlights.trafficlights_controllers, name='trafficlights_controllers'), + path('trafficlights/assets/controllers//', trafficlights.trafficlights_controllers_detail, name='trafficlights_controllers_detail'), + path('trafficlights/assets/autocomplete/', trafficlights.trafficlights_assets_autocomplete, name='trafficlights_autocomplete_assets'), + path('trafficlights///', trafficlights.trafficlights_assets_detail, name='trafficlights_assets_detail'), + + path('publiclighting/', publiclighting.publiclighting_index, name='publiclighting_index'), + path('publiclighting/streets/', publiclighting.publiclighting_streets, name='publiclighting_streets'), + path('publiclighting/streets/geojson/', publiclighting.publiclighting_streets_geojson, name='publiclighting_streets_geojson'), + path('publiclighting/streets/documents/history/', publiclighting.fetch_document_history, name='publiclighting_streets_document_history'), + path('publiclighting/streets/get-nearest/', publiclighting.get_nearest_publiclighting_street, name='get_nearest_publiclighting_street'), + path('publiclighting/streets/autocomplete/', publiclighting.publiclighting_locations_autocomplete, name='publiclighting_autocomplete_locations'), + path('publiclighting/streets//', publiclighting.publiclighting_streets_detail, name='publiclighting_streets_detail'), + path('publiclighting/streets//add-document/', publiclighting.publiclighting_streets_add_document, name='publiclighting_streets_add_document'), + path('publiclighting/streets//delete-document/', publiclighting.publiclighting_streets_delete_document, name='publiclighting_streets_delete_document'), + path('publiclighting/assets/', publiclighting.publiclighting_assets, name='publiclighting_assets'), + path('publiclighting/assets/geojson/', publiclighting.publiclighting_assets_geojson, name='publiclighting_assets_geojson'), + path('publiclighting/assets/autocomplete/', publiclighting.publiclighting_assets_autocomplete, name='publiclighting_autocomplete_assets'), + path('publiclighting///', publiclighting.publiclighting_assets_detail, name='publiclighting_assets_detail'), + + path('artworks/', artworks.artworks_index, name='artworks_index'), + path('artworks/streets/', artworks.artworks_streets, name='artworks_streets'), + path('artworks/streets/geojson/', artworks.artworks_streets_geojson, name='artworks_streets_geojson'), + path('artworks/streets/get-nearest/', artworks.get_nearest_artworks_street, name='get_nearest_artworks_street'), + path('artworks/streets//', artworks.artworks_streets_detail, name='artworks_streets_detail'), + path('artworks/fountains/', artworks.artworks_fountains, name='artworks_fountains'), + path('artworks/fountains/geojson/', artworks.fountains_geojson, name='artworks_fountains_geojson'), + path('artworks/fountains//', artworks.artworks_fountains_detail, name='artworks_fountains_detail'), + path('artworks/monuments/', artworks.artworks_monuments, name='artworks_monuments'), + path('artworks/monuments/geojson/', artworks.monuments_geojson, name='artworks_monuments_geojson'), + path('artworks/monuments//', artworks.artworks_monuments_detail, name='artworks_monuments_detail'), + path('artworks/assets/', artworks.artworks_assets, name='artworks_assets'), + path('artworks/assets///', artworks.artworks_assets_detail_redirect, name='artworks_assets_detail'), + path('artworks/assets/geojson/', artworks.artworks_assets_geojson, name='artworks_assets_geojson'), + path('artworks/assets/autocomplete/', artworks.artworks_assets_autocomplete, name='artworks_autocomplete_assets'), + path('artworks/assets/get-nearest/', artworks.get_nearest_artworks_asset, name='get_nearest_artworks_asset'), + + path('nature/', nature.nature_index, name='nature_index'), + path('nature/locations/', nature.nature_locations, name='nature_locations'), + path('nature/locations/geojson/', nature.nature_locations_geojson, name='nature_locations_geojson'), + path('nature/locations//', nature.nature_locations_detail, name='nature_locations_detail'), + path('nature/locations/get-nearest/', nature.get_nearest_nature_location, name='get_nearest_nature_location'), + path('nature/locations/autocomplete/', nature.nature_locations_autocomplete, name='nature_autocomplete_locations'), + path('nature/assets/', nature.nature_assets, name='nature_assets'), + path('nature/assets/geojson/', nature.nature_assets_geojson, name='nature_assets_geojson'), + path('nature/assets/autocomplete/', nature.nature_assets_autocomplete, name='nature_autocomplete_assets'), + path('nature/assets/bulk-update/', nature.nature_bulk_update_assets, name='nature_bulk_update_assets'), + path('nature/assets/bulk-delete/', nature.nature_bulk_delete_assets, name='nature_bulk_delete_assets'), + path('nature/assets/undo/', nature.nature_undo_action, name='nature_undo_action'), + path('nature/assets/save-geometries/', nature.nature_save_geometries, name='nature_save_geometries'), + + # Green Surfaces Comments API (must be before generic nature///) + path('nature/comments/geojson/', nature.green_surfaces_comments_geojson, name='green_surfaces_comments_geojson'), + path('nature/comments//', nature.green_surfaces_comment_detail, name='green_surfaces_comment_detail'), + path('nature/comments/save/', nature.green_surfaces_comment_save, name='green_surfaces_comment_save'), + path('nature/comments//add-photos/', nature.green_surfaces_comment_add_photos, name='green_surfaces_comment_add_photos'), + path('nature/comments//delete/', nature.green_surfaces_comment_delete, name='green_surfaces_comment_delete'), + + path('nature///', nature.nature_assets_detail, name='nature_assets_detail'), + + path('structures/', structures.structures_index, name='structures_index'), + path('structures/structures/documents/history/', structures.fetch_document_history, name='structures_document_history'), + path('structures/structures/documents/config//', structures.get_document_config, name='structures_document_config'), + path('structures/structures//add-document/', structures.structures_add_document, name='structures_add_document'), + path('structures/structures//delete-document/', structures.structures_delete_document, name='structures_delete_document'), + path('structures/locations/', structures.structures_locations, name='structures_locations'), + path('structures/locations/geojson/', structures.structures_locations_geojson, name='structures_locations_geojson'), + path('structures/locations/get-nearest/', structures.get_nearest_structure_location, name='get_nearest_structure_location'), + # path('structures/locations/autocomplete/', structures.structures_locations_autocomplete, name='structures_autocomplete_locations'), + path('structures/locations//', structures.structures_locations_detail, name='structures_locations_detail'), + path('structures/assets/', structures.structures_assets, name='structures_assets'), + path('structures/assets/geojson/', structures.structures_assets_geojson, name='structures_assets_geojson'), + path('structures/assets/autocomplete/', structures.structures_assets_autocomplete, name='structures_autocomplete_assets'), + path('structures///', structures.structures_assets_detail, name='structures_assets_detail'), + + path('controlcenters/', controlcenters.controlcenters_index, name='controlcenters_index'), + path('controlcenters/locations/', controlcenters.controlcenters_locations, name='controlcenters_locations'), + path('controlcenters/locations/geojson/', controlcenters.controlcenters_locations_geojson, name='controlcenters_locations_geojson'), + path('controlcenters/locations//', controlcenters.controlcenters_locations_detail, name='controlcenters_locations_detail'), + path('controlcenters/assets/', controlcenters.controlcenters_assets, name='controlcenters_assets'), + path('controlcenters/assets/geojson/', controlcenters.controlcenters_assets_geojson, name='controlcenters_assets_geojson'), + path('controlcenters/assets/autocomplete/', controlcenters.controlcenters_assets_autocomplete, name='controlcenters_autocomplete_assets'), + path('controlcenters///', controlcenters.controlcenters_assets_detail, name='controlcenters_assets_detail'), + path('controlcenters/controlcenters//add-document/', controlcenters.controlcenters_add_document, name='controlcenters_add_document'), + path('controlcenters/controlcenters//delete-document/', controlcenters.controlcenters_delete_document, name='controlcenters_delete_document'), + + path('sign/', sign.sign_index, name='sign_index'), + path('sign/streets/', sign.sign_streets, name='sign_streets'), + path('sign/streets/geojson/', sign.sign_streets_geojson, name='sign_streets_geojson'), + path('sign/streets/documents/history/', sign.fetch_document_history, name='sign_streets_document_history'), + path('sign/streets/get-nearest/', sign.get_nearest_sign_street, name='get_nearest_sign_street'), + path('sign/streets//', sign.sign_streets_detail, name='sign_streets_detail'), + path('sign/streets//add-document/', sign.sign_streets_add_document, name='sign_streets_add_document'), + path('sign/streets//delete-document/', sign.sign_streets_delete_document, name='sign_streets_delete_document'), + path('sign/assets/', sign.sign_assets, name='sign_assets'), + path('sign/assets/geojson/', sign.sign_assets_geojson, name='sign_assets_geojson'), + path('sign/assets/autocomplete/', sign.sign_assets_autocomplete, name='sign_autocomplete_assets'), + path('sign///', sign.sign_assets_detail, name='sign_assets_detail'), + + path('roads/', roads.roads_index, name='roads_index'), + path('roads/streets/', roads.roads_streets, name='roads_streets'), + path('roads/streets/geojson/', roads.roads_streets_geojson, name='roads_streets_geojson'), + path('roads/streets/documents/history/', roads.fetch_document_history, name='roads_streets_document_history'), + path('roads/streets/get-nearest/', roads.get_nearest_roads_street, name='get_nearest_roads_street'), + path('roads/streets//', roads.roads_streets_detail, name='roads_streets_detail'), + path('roads/streets//add-document/', roads.roads_streets_add_document, name='roads_streets_add_document'), + path('roads/streets//delete-document/', roads.roads_streets_delete_document, name='roads_streets_delete_document'), + path('roads/assets/', roads.roads_assets, name='roads_assets'), + path('roads/assets/geojson/', roads.roads_assets_geojson, name='roads_assets_geojson'), + path('roads/assets/autocomplete/', roads.roads_assets_autocomplete, name='roads_autocomplete_assets'), + path('roads///', roads.roads_assets_detail, name='roads_assets_detail'), + + 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//', its.its_locations_detail, name='its_locations_detail'), + path('its/locations//add-document/', its.its_location_add_document, name='its_location_add_document'), + path('its/locations//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/assets/autocomplete/', its.its_assets_autocomplete, name='its_autocomplete_assets'), + path('its///', its.its_assets_detail, name='its_assets_detail'), + + path('clean/', clean.clean_index, name='clean_index'), + path('clean/locations/geojson/', clean.clean_locations_geojson, name='clean_locations_geojson'), + path('clean/locations/get-nearest/', clean.get_nearest_clean_location, name='get_nearest_clean_location'), + path('clean/locations//', clean.clean_location_detail, name='clean_location_detail'), + path('clean/assets/geojson/', clean.clean_assets_geojson, name='clean_assets_geojson'), + path('clean/assets/autocomplete/', clean.clean_assets_autocomplete, name='clean_autocomplete_assets'), + path('clean/litterbin//observations/update/', clean.update_litter_bin_observations, name='clean_update_litter_bin_observations'), + path('clean///', clean.clean_assets_detail, name='clean_assets_detail'), + path('water/', water.water_index, name='water_index'), + path('water/locations/geojson/', water.water_locations_geojson, name='water_locations_geojson'), + path('water/locations/get-nearest/', water.get_nearest_water_location, name='get_nearest_water_location'), + path('water/zirw//', water.zirw_detail, name='zirw_detail'), + path('water/zirw//add-document/', water.zirw_add_document, name='zirw_add_document'), + path('water/zirw/document-history/', water.fetch_document_history, name='zirw_document_history'), + path('water/zirw//document-config/', water.get_document_config, name='zirw_document_config'), + path('water/assets/geojson/', water.water_assets_geojson, name='water_assets_geojson'), + path('water/assets/autocomplete/', water.water_assets_autocomplete, name='water_autocomplete_assets'), + path('water///', water.water_assets_detail, name='water_assets_detail'), + + # DXF import URLs + path('trafficlights/intersections//import-dxf/', trafficlights.import_dxf, name='import_dxf'), + path('trafficlights/intersections//stage-dxf/', trafficlights.stage_dxf, name='stage_dxf'), + path('trafficlights/intersections//save-dxf-layers/', trafficlights.save_dxf_layers, name='save_dxf_layers'), + path('trafficlights/intersections//integrate-dxf/', trafficlights.integrate_dxf, name='integrate_dxf'), + path('trafficlights/intersections//dxf-layout-geojson/', trafficlights.dxf_layout_geojson, name='dxf_layout_geojson'), + path('trafficlights/intersections//dxf-layers/', trafficlights.dxf_layers, name='dxf_layers'), + path('trafficlights/intersections//add-staged-pole/', trafficlights.add_staged_pole, name='add_staged_pole'), + path('trafficlights/intersections//delete-staged-pole//', trafficlights.delete_staged_pole, name='delete_staged_pole'), + path('trafficlights/intersections//update-staged-pole-position//', trafficlights.update_staged_pole_position, name='update_staged_pole_position'), + + # Cross plan (Excel) import URLs + path('trafficlights/intersections//import-cross-plan/', trafficlights.import_cross_plan, name='import_cross_plan'), + path('trafficlights/intersections//integrate-cross-plan/', trafficlights.integrate_cross_plan, name='integrate_cross_plan'), + path('trafficlights/intersections//upload-dxf-ajax/', trafficlights.upload_dxf_ajax, name='upload_dxf_ajax'), +] diff --git a/streetup/assets/utils/cross_plan_parser.py b/streetup/assets/utils/cross_plan_parser.py new file mode 100644 index 0000000..9c8d4c1 --- /dev/null +++ b/streetup/assets/utils/cross_plan_parser.py @@ -0,0 +1,468 @@ +import openpyxl +import re +import os +from django.conf import settings + +def parse_cross_plan(excel_path): + """ + Parses a crossroads cross plan (kruisjesplan) Excel file and returns + a list of deduced assets structured for comparison/import. + """ + if not os.path.exists(excel_path): + raise FileNotFoundError(f"Excel file not found at {excel_path}") + + wb = openpyxl.load_workbook(excel_path, data_only=True) + target_sheet = None + is_french = False + for name in wb.sheetnames: + normalized_name = re.sub(r'\s+', ' ', name).strip().lower() + if normalized_name in ("kruisjesplan", "plan croix", "plan de croix"): + target_sheet = name + if normalized_name in ("plan croix", "plan de croix"): + is_french = True + break + + if not target_sheet: + raise ValueError("La feuille 'kruisjesplan' ou 'Plan croix' est introuvable dans le fichier Excel.") + + sheet = wb[target_sheet] + + # 1. Deduce intersection code from the first row (e.g., sleutel: SWB01) + intersection_code = None + for r in range(1, 4): + for c in range(1, 10): + val = sheet.cell(row=r, column=c).value + if val and any(k in str(val).lower() for k in ("sleutel", "clé", "cle", "key")): + if any(k in str(val).lower() for k in ("clé", "cle")): + is_french = True + # Value is in the next cell or adjacent cells + for offset in range(1, 4): + next_val = sheet.cell(row=r, column=c + offset).value + if next_val: + intersection_code = str(next_val).strip() + break + if intersection_code: + break + if intersection_code: + break + + if not intersection_code: + # Fallback search in sheet dimensions + for row in sheet.iter_rows(max_row=5, max_col=10, values_only=True): + for cell in row: + if cell and isinstance(cell, str): + cleaned = cell.strip() + if re.match(r'^[A-Z]{2,4}_?[A-Z]{0,3}\d{2,5}$|^[A-Z]_[A-Z]{1,3}\d{2,5}$', cleaned): + intersection_code = cleaned + break + if intersection_code: + break + + if not intersection_code: + raise ValueError("Impossible de déterminer le code du carrefour (sleutel) dans le fichier Excel.") + + + + # 2. Scan sheet to identify cable sections + # Cable sections start at rows containing 'Câble' or 'Cable' in the first columns + cable_sections = [] + max_row = sheet.max_row + max_col = sheet.max_column + + for r in range(1, max_row + 1): + val = sheet.cell(row=r, column=1).value + if val and isinstance(val, str) and ("câble" in val.lower() or "cable" in val.lower()): + cable_sections.append((r, val.strip())) + + # Parse each cable section + deduced_poles = {} # pole_header -> {code, type} + deduced_connections = [] # list of dicts + + for idx, (start_row, cable_label) in enumerate(cable_sections): + end_row = cable_sections[idx+1][0] - 1 if idx + 1 < len(cable_sections) else max_row + + # We need to find the pole headers and the connection rows in this section. + # The pole header row is a row below start_row that has non-empty values in columns 5+ + # and has column 3 (Richting) / column 4 (Kringen) as labels. + pole_row_num = None + richting_col = 3 + kringen_col = 4 + + for r in range(start_row + 1, end_row + 1): + val_richting = sheet.cell(row=r, column=richting_col).value + val_kringen = sheet.cell(row=r, column=kringen_col).value + # If we find the row with "Richting" and "Kringen" + if val_richting and isinstance(val_richting, str) and "richting" in val_richting.lower(): + # The next row or the row after typically contains the pole list. + # In SWB01, Row 4 is header, Row 5 is poles. + pole_row_num = r + 1 + break + + if not pole_row_num: + # Fallback search: just search for any row in columns 5+ having pole codes + for r in range(start_row + 1, start_row + 10): + if r > end_row: + break + row_vals = [sheet.cell(row=r, column=c).value for c in range(5, min(max_col + 1, 30))] + if any(v and isinstance(v, str) and re.match(r'^[AB]\d{2}$', v.strip()) for v in row_vals): + pole_row_num = r + break + + if not pole_row_num: + continue + + # Read poles for this section + section_poles = {} # col_index -> pole_name + for col in range(5, max_col + 1): + val_pole = sheet.cell(row=pole_row_num, column=col).value + if val_pole: + pole_name = str(val_pole).strip() + if re.match(r'^[AB]\d{2}$', pole_name): + section_poles[col] = pole_name + # Save to global deduced poles + deduced_poles[pole_name] = { + "code": f"{intersection_code}_{pole_name}", + "short_code": pole_name, + "name": pole_name, + "model": "Poteau gris avec bras horizontal de 7.5m" if pole_name.startswith("B") else "Poteau droit, gris 3.1m" + } + + # Parse connections in this section + last_richting = None + is_detector_section = False + if pole_row_num and pole_row_num > 1: + header_val = sheet.cell(row=pole_row_num - 1, column=richting_col).value + if header_val and isinstance(header_val, str) and any(k in header_val.lower() for k in ("detecteur", "détecteur")): + is_detector_section = True + + for r in range(pole_row_num + 1, end_row + 1): + val_richting = sheet.cell(row=r, column=richting_col).value + val_kringen = sheet.cell(row=r, column=kringen_col).value + + # Check if this row is a "Detecteur" section header + if val_richting and isinstance(val_richting, str) and any(k in val_richting.lower() for k in ("detecteur", "détecteur")): + is_detector_section = True + last_richting = None + continue + + # If the row is empty or contains section markers, skip it + if not val_kringen and not val_richting: + # check if the entire row is empty + if all(sheet.cell(row=r, column=c).value is None for c in range(1, max_col + 1)): + continue + + if val_richting: + last_richting = str(val_richting).strip() + + if not last_richting: + continue + + kringen_val = str(val_kringen).strip() if val_kringen else "" + + # Check each pole column for connections (X or XX marker) + for col, pole_name in section_poles.items(): + marker = sheet.cell(row=r, column=col).value + if marker: + marker_str = str(marker).strip().upper() + x_count = marker_str.count('X') + if x_count > 0: + deduced_connections.append({ + "cable_label": cable_label, + "pole": pole_name, + "richting": last_richting, + "kringen": kringen_val, + "crosses": x_count, + "is_detector": is_detector_section + }) + + # 3. Post-process to deduce assets + # A. CABLES + deduced_cables = [] + for idx, (start_row, cable_label) in enumerate(cable_sections): + # e.g., "Câble 01 SVAVB 61x1,5mm²" + m = re.search(r'(?:câble|cable)\s*(\d+)\s*(.*)', cable_label, re.IGNORECASE) + cable_num = int(m.group(1)) if m else (idx + 1) + cable_model_name = m.group(2).strip() if m else "SVAVB 61x1,5mm²" + + # Count number of strands + strands = 61 + if "10x2x0,8" in cable_model_name: + strands = 20 + elif "61x1,5" in cable_model_name: + strands = 61 + elif "14x1,5" in cable_model_name: + strands = 14 + + # Find which poles are associated with this cable_label + connected_poles = sorted(list(set([ + f"{intersection_code}_{conn['pole']}" + for conn in deduced_connections + if conn["cable_label"] == cable_label + ]))) + + deduced_cables.append({ + "code": f"{intersection_code}_CAB{cable_num:02d}", + "name": f"Câble {cable_num:02d}", + "model_name": cable_model_name, + "strands": strands, + "connected_poles": connected_poles + }) + + # B. POLES + # Already populated in deduced_poles + poles_list = sorted(deduced_poles.values(), key=lambda p: p["short_code"]) + + # C. LANTERNS & DETECTORS + # Group connections by pole to identify lanes, pedestrian signals, etc. + pole_phases = {} # pole_name -> set of phases (e.g. {'T1', 'a'}) + pole_has_hp = {} # pole_name -> bool + pole_detectors = {} # pole_name -> list of detectors + + for conn in deduced_connections: + p = conn["pole"] + richting = conn["richting"] + kringen = conn["kringen"] + + # Check if HP is present on this pole + if richting.lower() == "hp": + pole_has_hp[p] = True + continue + + # Detectors + # Check if push button (e.g. Dka, Dkb, PBa1, BPf4...) + if richting.lower().startswith(("dk", "pb", "bp")): + if p not in pole_detectors: + pole_detectors[p] = [] + + # Determine prefix + if richting.lower().startswith("dk"): + prefix = "DK" + elif richting.lower().startswith("pb"): + prefix = "PB" + else: + prefix = "BP" + + # Extract phase and potential index number + phase_and_num = richting[2:] + phase_char = "".join([c for c in phase_and_num if c.isalpha()]) + + det_code = f"{intersection_code}_{p}_{prefix}{phase_and_num}" + if prefix == "BP": + name_val = f"bouton poussoir NO (bleu) sur {p}" + if phase_char: + name_val += f" - Phase {phase_char}" + elif prefix == "PB": + name_val = f"bouton poussoir NF (jaune) sur {p}" + if phase_char: + name_val += f" - Phase {phase_char}" + else: + if is_french: + name_val = f"Bouton Poussoir sur {p}" + if phase_char: + name_val += f" - Phase {phase_char}" + else: + name_val = f"Drukknop sur {p}" + if phase_char: + name_val += f" - Phase {phase_char}" + + if not any(d["code"] == det_code for d in pole_detectors[p]): + pole_detectors[p].append({ + "code": det_code, + "name": name_val, + "model_name": "Drukknop", + "phases": [phase_char] if phase_char else [] + }) + continue + + # Other detectors (e.g. RA, VC, VT1, RC, RD, Rm...) + is_detector = False + det_code = None + det_name = f"{richting} sur {p}" + + normalized_upper = richting.upper().replace(" ", "") + + if conn.get("is_detector", False): + is_detector = True + elif normalized_upper in ("RA", "RAB", "RB", "RC", "RD", "VD", "VC", "VT1", "VT2") or normalized_upper.startswith(("RM", "RP")): + is_detector = True + + if is_detector: + if normalized_upper.startswith("RM"): + # Extract phase/direction (e.g., T2 from Rm T2,B1,B2) + match = re.match(r'^Rm\s+([A-Z0-9]+)', richting, re.IGNORECASE) + if match: + phase = match.group(1) + det_code = f"{intersection_code}_{p}_RM{phase.upper()}" + else: + det_code = f"{intersection_code}_{p}_{normalized_upper}" + else: + det_code = f"{intersection_code}_{p}_{normalized_upper}" + + if p not in pole_detectors: + pole_detectors[p] = [] + + # Map model name + if normalized_upper.startswith("RP"): + model_name = "TMA-122 M" + elif normalized_upper.startswith("R"): + model_name = "TM60" + elif normalized_upper.startswith("V"): + model_name = "Traficam wide angle" + else: + model_name = "Traficam wide angle" + + if not any(d["code"] == det_code for d in pole_detectors[p]): + pole_detectors[p].append({ + "code": det_code, + "name": det_name, + "model_name": model_name, + "phases": [] + }) + continue + + # Standard traffic signals (vehicles, trams, cycles, pedestrians) + # Richting codes are typically: A, B, C, T1, T2, F1, F2, a, b, c... + # Ignore wiring/cabling categories like ALIM, Radar, Com, contact, etc. + normalized = re.sub(r'\s+', ' ', richting).strip() + if re.search(r'(?:alim|com|contact|radar|tension|comm)', normalized, re.IGNORECASE): + continue + + if p not in pole_phases: + pole_phases[p] = set() + pole_phases[p].add(richting) + + # Now generate LANTERNS for each pole + deduced_lanterns = [] + + # helper to sort phases: vehicles first, then trams, then cycles, then pedestrians + def phase_sort_key(ph): + if ph and ph[0].islower(): # Pedestrian + return (3, ph) + elif ph.startswith("T"): # Tram + return (1, ph) + elif ph.startswith("F"): # Cycle + return (2, ph) + else: # Vehicle + return (0, ph) + + for p in sorted(pole_phases.keys()): + phases = sorted(list(pole_phases[p]), key=phase_sort_key) + lantern_idx = 1 + for phase in phases: + is_mast_arm = p.startswith("B") + has_hp = pole_has_hp.get(p, False) + + # Find connections for this pole and phase + phase_connections = [ + conn for conn in deduced_connections + if conn["pole"] == p and conn["richting"] == phase + ] + + # A double lantern is indicated if any connection cell has 2 or more crosses (e.g. 'XX') + has_double_marker = any(conn.get("crosses", 1) >= 2 for conn in phase_connections) + + if is_mast_arm and has_double_marker: + # Determine models for both lanterns + if phase and phase[0].islower(): # Pedestrian + model_name_1 = "2V200 piéton+HP" if has_hp else "2V200" + model_name_2 = "2V200 piéton+HP" if has_hp else "2V200" + elif phase.startswith("T"): # Tram + model_name_1 = "3V200 tram tout droite" + model_name_2 = "3V300 tram tout droit" + elif phase.startswith("F"): # Cycle + model_name_1 = "3V200 cycliste" + model_name_2 = "3V200 cycliste" + else: # Vehicle + model_name_1 = "3V200" + model_name_2 = "3V300" + + # 1. Mast lantern (model_name_1) + lantern_code_1 = f"{intersection_code}_{p}_LAN{lantern_idx:02d}_{phase}" + display_model_1 = model_name_1 + if "tram" in display_model_1.lower(): + m = re.match(r'^(\d+V\d+)\s+tram', display_model_1, re.IGNORECASE) + if m: + display_model_1 = f"{m.group(1)} tram" + lantern_name_1 = f"{display_model_1} sur {p} - Phase {phase}" + deduced_lanterns.append({ + "code": lantern_code_1, + "name": lantern_name_1, + "pole_code": f"{intersection_code}_{p}", + "model_name": model_name_1, + "phase": phase + }) + lantern_idx += 1 + + # 2. Arm lantern (model_name_2) + lantern_code_2 = f"{intersection_code}_{p}_LAN{lantern_idx:02d}_{phase}" + display_model_2 = model_name_2 + if "tram" in display_model_2.lower(): + m = re.match(r'^(\d+V\d+)\s+tram', display_model_2, re.IGNORECASE) + if m: + display_model_2 = f"{m.group(1)} tram" + lantern_name_2 = f"{display_model_2} sur {p} - Phase {phase}" + deduced_lanterns.append({ + "code": lantern_code_2, + "name": lantern_name_2, + "pole_code": f"{intersection_code}_{p}", + "model_name": model_name_2, + "phase": phase + }) + lantern_idx += 1 + else: + # Deduce single model + if phase and phase[0].islower(): # Pedestrian + if has_hp: + model_name = "2V200 piéton+HP" + else: + model_name = "2V200" + elif phase.startswith("T"): # Tram + if is_mast_arm: + model_name = "3V300 tram tout droit" + else: + model_name = "3V200 tram tout droite" + elif phase.startswith("F"): # Cycle + model_name = "3V200 cycliste" + else: # Vehicle + if is_mast_arm: + model_name = "3V300" + else: + model_name = "3V200" + + # Incorporate phase name in the code and name + lantern_code = f"{intersection_code}_{p}_LAN{lantern_idx:02d}_{phase}" + display_model = model_name + if "tram" in display_model.lower(): + m = re.match(r'^(\d+V\d+)\s+tram', display_model, re.IGNORECASE) + if m: + display_model = f"{m.group(1)} tram" + lantern_name = f"{display_model} sur {p} - Phase {phase}" + + deduced_lanterns.append({ + "code": lantern_code, + "name": lantern_name, + "pole_code": f"{intersection_code}_{p}", + "model_name": model_name, + "phase": phase + }) + lantern_idx += 1 + + # Compile all detectors into a single list + deduced_detectors = [] + for p, dets in pole_detectors.items(): + for d in dets: + deduced_detectors.append({ + "code": d["code"], + "name": d["name"], + "pole_code": f"{intersection_code}_{p}", + "model_name": d["model_name"], + "phases": d["phases"] + }) + + return { + "intersection_code": intersection_code, + "poles": poles_list, + "cables": deduced_cables, + "lanterns": deduced_lanterns, + "detectors": deduced_detectors + } diff --git a/streetup/assets/views/__init__.py b/streetup/assets/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/assets/views/artworks.py b/streetup/assets/views/artworks.py new file mode 100644 index 0000000..5505518 --- /dev/null +++ b/streetup/assets/views/artworks.py @@ -0,0 +1,832 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.db.models import Func +from django.db.models.functions import Lower +from django.db.models import Q +from django.db.models import CharField +from django.contrib.gis.geos import Point, GEOSGeometry +from django.contrib.gis.db.models.functions import Distance +from django.views.decorators.http import require_GET, require_POST +from django.utils.translation import gettext as _, get_language +from django.urls import reverse + +import json +import unicodedata + +from common.models import Thematic, UserThematics, Municipality +from common.utils import check_thematic_access + +from assets.models import ArtworkMonument, ArtworkFountain, AssetCategory +from assets.models.artworks import ( + ArtworkStreet, + MONUMENT_TYPE_CHOICES, + FOUNTAIN_TYPE_CHOICES, +) +from interventions.models import Intervention, InterventionAsset +from interventions.views.drafts import InterventionDraftManager + + +@check_thematic_access('artworks') +def artworks_index(request): + thematic = 'artworks' + lang = get_language() + + selected_assets = [] + selected_locations_ids = [] + preventive_element_type = '' + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_municipalities = request.GET.getlist('municipality') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') or '' + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) or [] + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + municipality_order_field = 'name_nl' if lang == 'nl' else 'name_fr' + municipalities = ( + Municipality.objects + .all() + .order_by(municipality_order_field) + ) + municipality_choices = [(m.id, m.name) for m in municipalities] + + # Status choices for filters + status_choices = [ + ('active', _('Actif')), + ('inactive', _('Inactif')), + ('to_be_removed', _('À supprimer')), + ] + + context = { + 'thematics': thematics, + 'municipality_choices': municipality_choices, + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_municipalities': selected_municipalities, + 'status_choices': status_choices, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type, + 'selected_locations_ids': selected_locations_ids, + "selected_assets_json": json.dumps(selected_assets), + 'locations_tab_title': _("Localisations"), + 'locations_assets_title': _("Assets"), + 'default_tab': 'assets', + } + + return render(request, "assets/artworks_index.html", context) + + +def ensure_list(value): + if value is None: + return [] + if isinstance(value, (list, tuple)): + return list(value) + return [value] + + +def _clean_int_list(values): + cleaned = [] + for val in values or []: + try: + cleaned.append(int(val)) + except (TypeError, ValueError): + continue + return cleaned + + +def _extract_lon_lat(obj, fallback_obj=None): + lon = getattr(obj, 'lon', None) + lat = getattr(obj, 'lat', None) + geom = getattr(obj, 'geom', None) + if (lon is None or lat is None) and geom is not None: + geom_4326 = geom.transform(4326, clone=True) + centroid = geom_4326 if geom_4326.geom_type == 'Point' else geom_4326.centroid + lon = centroid.x + lat = centroid.y + if (lon is None or lat is None) and fallback_obj is not None: + fallback_lon, fallback_lat = _extract_lon_lat(fallback_obj) + if lon is None: + lon = fallback_lon + if lat is None: + lat = fallback_lat + return lon, lat + + +def _serialize_geometry(obj): + if getattr(obj, 'geojson', None): + try: + return json.loads(obj.geojson) + except (TypeError, ValueError): + pass + geom = getattr(obj, 'geom', None) + if geom: + geom_4326 = geom.transform(4326, clone=True) + return json.loads(geom_4326.geojson) + return None + + +def _street_to_feature(street): + lon, lat = _extract_lon_lat(street) + return { + "type": "Feature", + "id": street.id, + "geometry": _serialize_geometry(street), + "properties": { + "code": street.code, + "name_fr": street.name_fr, + "name_nl": street.name_nl, + "municipality_name": street.municipality.name if street.municipality else None, + "road_code": street.road_code, + "section_code": street.section_code, + "street_level": street.street_level, + "lon": lon, + "lat": lat, + "detail_url": street.get_absolute_url(), + "element_type": "Voirie œuvres", + "thematic": "artworks", + "is_location": True, + }, + } + + +def _artwork_asset_feature( + obj, + asset_kind, + asset_kind_label, + subtype_label, + detail_url, + fallback_category_id=None, + fallback_category_label=None, +): + from assets.views.core import get_element_type_label + street = getattr(obj, 'street', None) + lon, lat = _extract_lon_lat(obj, fallback_obj=street) + geometry = _serialize_geometry(obj) + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + subtype_code = getattr(obj, 'artwork_type', None) if asset_kind == 'monument' else getattr(obj, 'fountain_type', None) + category_obj = getattr(obj, 'category', None) + category_id = category_obj.id if category_obj else fallback_category_id + category_label = category_obj.get_name() if category_obj else (fallback_category_label or '') + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name_fr": obj.name_fr, + "name_nl": obj.name_nl, + "status": obj.status, + "asset_type": obj._meta.model_name, + "asset_kind": asset_kind, + "asset_kind_label": asset_kind_label, + "asset_subtype": subtype_code, + "asset_subtype_label": subtype_label or subtype_code, + "category": category_label, + "category_id": str(category_id) if category_id is not None else '', + "lon": lon, + "lat": lat, + "detail_url": detail_url, + "element_type": get_element_type_label(f"artwork{asset_kind}"), + "thematic": "artworks", + "is_location": False, + }, + } + + +@check_thematic_access('artworks') +def artworks_streets(request): + """Placeholder view so that list URL exists (redirects to index).""" + return redirect('assets:artworks_index') + + +@check_thematic_access('artworks') +def artworks_streets_detail(request, street_id): + street = get_object_or_404( + ArtworkStreet.objects.select_related('municipality'), + pk=street_id + ) + + assets = street.get_active_assets() or {} + monuments = assets.get('monuments') or ArtworkMonument.objects.none() + fountains = assets.get('fountains') or ArtworkFountain.objects.none() + + context = { + 'street': street, + 'monuments': monuments, + 'fountains': fountains, + 'monuments_count': monuments.count(), + 'fountains_count': fountains.count(), + } + + return render(request, "assets/artworks_street_detail.html", context) + + +@check_thematic_access('artworks') +def artworks_assets(request): + """Placeholder view used as base URL for asset detail redirects.""" + return redirect('assets:artworks_index') + + +@check_thematic_access('artworks') +def artworks_assets_detail_redirect(request, asset_model, asset_id): + model_key = (asset_model or '').lower() + model_map = { + 'artworkmonument': (ArtworkMonument, 'artworks_monuments_detail', 'asset_id'), + 'artworkfountain': (ArtworkFountain, 'artworks_fountains_detail', 'asset_id'), + } + + config = model_map.get(model_key) + if not config: + raise Http404( + _("Type d'asset inconnu: %(asset_model)s") % {"asset_model": asset_model} + ) + + model, url_name, kwarg = config + get_object_or_404(model, pk=asset_id) + return redirect(f'assets:{url_name}', **{kwarg: asset_id}) + + +@check_thematic_access('artworks') +def artworks_streets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + municipalities = ensure_list(data.get('municipality')) + filter_geom_raw = data.get("filter_geom") + filter_geom = None + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as exc: + return JsonResponse({"error": f"Invalid geometry: {exc}"}, status=400) + else: + municipalities = request.GET.getlist('municipality') + filter_geom = None + + streets = ArtworkStreet.objects.select_related('municipality') + + municipality_ids = _clean_int_list(municipalities) + if municipality_ids: + streets = streets.filter(municipality_id__in=municipality_ids) + + if filter_geom: + streets = streets.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + streets = apply_search(streets, data) + + features = [_street_to_feature(street) for street in streets] + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + +@require_GET +@check_thematic_access('artworks') +def get_nearest_artworks_street(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": _("Coordonnées invalides")}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + ArtworkStreet.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) + .order_by('distance') + .first() + ) + + if not nearest: + return JsonResponse({"error": _("Aucune localisation trouvée")}, status=404) + + lon, lat = _extract_lon_lat(nearest) + + return JsonResponse({ + "location_id": nearest.id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": lon, + "lat": lat, + "detail_url": nearest.get_absolute_url(), + }) + + +@check_thematic_access('artworks') +def artworks_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + statuses = ensure_list(data.get('asset_status') or data.get('status')) + locations_ids = _clean_int_list(ensure_list(data.get('locations_ids'))) + categories = _clean_int_list(ensure_list(data.get('category'))) + filter_geom_raw = data.get("filter_geom") + filter_geom = None + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as exc: + return JsonResponse({"error": f"Invalid geometry: {exc}"}, status=400) + else: + statuses = request.GET.getlist('asset_status') or request.GET.getlist('status') + locations_ids = _clean_int_list(request.GET.getlist('locations_ids')) + categories = _clean_int_list(request.GET.getlist('category')) + filter_geom = None + + features = [] + + artwork_categories = list(AssetCategory.objects.filter(thematic__code='artworks')) + default_fountain_category = next( + (c for c in artwork_categories if 'fontain' in strip_accents_lower(c.get_name())), + None, + ) + default_monument_category = next( + ( + c for c in artwork_categories + if any(keyword in strip_accents_lower(c.get_name()) for keyword in ['statue', 'monument', 'sculpt']) + ), + None, + ) + + def build_queryset(model, default_category_id=None): + qs = model.objects.all() + if statuses: + qs = qs.filter(status__in=statuses) + if locations_ids: + qs = qs.filter(street_id__in=locations_ids) + if categories: + category_filter = Q(category_id__in=categories) + if default_category_id and default_category_id in categories: + category_filter |= Q(category__isnull=True) + qs = qs.filter(category_filter) + if filter_geom: + qs = qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + if request.method == 'POST': + from assets.views.core import apply_search + qs = apply_search(qs, data) + return qs + + monument_features = [ + _artwork_asset_feature( + obj, + asset_kind='monument', + asset_kind_label=_('Monument'), + subtype_label=MONUMENT_TYPE_CHOICES.get(obj.artwork_type, obj.artwork_type), + detail_url=reverse('assets:artworks_monuments_detail', kwargs={'asset_id': obj.id}), + fallback_category_id=(default_monument_category.id if default_monument_category else None), + fallback_category_label=(default_monument_category.get_name() if default_monument_category else _('Monument')), + ) + for obj in build_queryset( + ArtworkMonument, + default_category_id=(default_monument_category.id if default_monument_category else None), + ) + ] + + fountain_features = [ + _artwork_asset_feature( + obj, + asset_kind='fountain', + asset_kind_label=_('Fontaine'), + subtype_label=FOUNTAIN_TYPE_CHOICES.get(obj.fountain_type, obj.fountain_type), + detail_url=reverse('assets:artworks_fountains_detail', kwargs={'asset_id': obj.id}), + fallback_category_id=(default_fountain_category.id if default_fountain_category else None), + fallback_category_label=(default_fountain_category.get_name() if default_fountain_category else _('Fontaine')), + ) + for obj in build_queryset( + ArtworkFountain, + default_category_id=(default_fountain_category.id if default_fountain_category else None), + ) + ] + + features.extend(monument_features) + features.extend(fountain_features) + + features.sort(key=lambda feature: feature["properties"].get("code") or "") + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + +def artworks_fountains(request): + pass + + +@check_thematic_access('artworks') +def artworks_fountains_detail(request, asset_id): + from django.contrib.contenttypes.models import ContentType + from assets.views.core import get_asset_edit_context + + fountain = get_object_or_404(ArtworkFountain, id=asset_id) + content_type = ContentType.objects.get_for_model(ArtworkFountain) + + interventions = Intervention.objects.filter( + interventionasset__object_id=asset_id + ).order_by('-creation_time') + + context = { + 'asset': fountain, + 'fountain': fountain, + 'content_type': content_type, + 'interventions': interventions, + } + context.update(get_asset_edit_context(request, fountain, content_type)) + + return render(request, "assets/detail_generic.html", context) + + +def artworks_monuments(request): + pass + + +@check_thematic_access('artworks') +def artworks_monuments_detail(request, asset_id): + from django.contrib.contenttypes.models import ContentType + from assets.views.core import get_asset_edit_context + + monument = get_object_or_404(ArtworkMonument, id=asset_id) + content_type = ContentType.objects.get_for_model(ArtworkMonument) + + interventions = Intervention.objects.filter( + interventionasset__object_id=asset_id + ).order_by('-creation_time') + + context = { + 'asset': monument, + 'monument': monument, + 'content_type': content_type, + 'interventions': interventions, + } + context.update(get_asset_edit_context(request, monument, content_type)) + + return render(request, "assets/detail_generic.html", context) + + +@check_thematic_access('artworks') +def monuments_geojson(request): + from django.contrib.gis.geos import GEOSGeometry + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + statuses = data.get('asset_status', []) + if isinstance(statuses, str): + statuses = [statuses] + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + statuses = request.GET.getlist('asset_status') + filter_geom = None + + monuments = ArtworkMonument.objects.all() + + # Filters + if statuses: + monuments = monuments.filter(status__in=statuses) + + if filter_geom: + monuments = monuments.filter(geom__intersects=filter_geom) + + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name_fr": obj.name_fr, + "name_nl": obj.name_nl, + "asset_type": obj.artwork_type, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": obj.lon, + "lat": obj.lat + }, + } + + + + features = [] + + for a in monuments: + features.append(obj_to_feature(a)) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + +@check_thematic_access('artworks') +def fountains_geojson(request): + from django.contrib.gis.geos import GEOSGeometry + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + statuses = data.get('status', []) + if isinstance(statuses, str): + statuses = [statuses] + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + statuses = request.GET.getlist('status') + filter_geom = None + + fountains = ArtworkFountain.objects.all() + + # Filters + if statuses: + fountains = fountains.filter(status__in=statuses) + + if filter_geom: + fountains = fountains.filter(geom__intersects=filter_geom) + + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name_fr": obj.name_fr, + "name_nl": obj.name_nl, + "asset_type": obj.fountain_type, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": obj.lon, + "lat": obj.lat + }, + } + + + features = [] + + for a in fountains: + features.append(obj_to_feature(a)) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + +def _equipment_type_from_model(asset): + class_name = asset.__class__.__name__ + return class_name + + +@require_GET +def artworks_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + # Normalise la requête (équivalent de unaccent+lower côté Python) + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + def base_qs(model, extra_filter=None): + ann = { + 'code_u': Lower(Unaccent('code')), + 'name_fr_u': Lower(Unaccent('name_fr')), + 'name_nl_u': Lower(Unaccent('name_nl')), + } + q = model.objects.annotate(**ann) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + q = q.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + # Filtrer uniquement les non supprimés + q = q.exclude(status='removed') + + if extra_filter: + q = q.filter(**extra_filter) + + if category_id is not None: + q = q.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + return q + + # Récupère QS pour chaque type + fountains_qs = base_qs(ArtworkFountain)[:100] + monuments_qs = base_qs(ArtworkMonument)[:100] + + # Concatène en mémoire (on priorise ensuite) + items = [] + for a in list(fountains_qs) + list(monuments_qs): + # priorisation locale (recalcule ici car on merge des modèles différents) + code_u = getattr(a, 'code_u', None) + name_fr_u = getattr(a, 'name_fr_u', None) + name_nl_u = getattr(a, 'name_nl_u', None) + + prio = 7 + # Priorisation uniquement si un terme de recherche est fourni + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u and code_u.startswith(qn): + prio = 1 + elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)): + prio = 2 + elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u): + prio = 4 + else: + # Sans terme de recherche, on trie par code + prio = 5 + + equipment_type = _equipment_type_from_model(a) + parts = [a.code, a.get_name()] + equipment_label = " - ".join(p for p in parts if p) + code = getattr(a, 'code', '') or '' + label = equipment_label + lon = a.lon + lat = a.lat + + items.append({ + "prio": prio, + "code": code or "", + "id": getattr(a, 'id', None), + "label": label, + + # attendus par ton autocomplete équipements + "equipment_id": getattr(a, 'id', None), + "equipment_label": equipment_label, + "equipment_type": equipment_type, + + "lon": lon, + "lat": lat, + }) + + # Tri global par priorité puis code, coupe à 100 + items.sort(key=lambda x: (x["prio"], x["code"])) + data = items[:100] + + return JsonResponse(data, safe=False) + + +@require_GET +def get_nearest_artworks_asset(request): + from django.contrib.contenttypes.models import ContentType + + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) # Lambert 2008 → distances en mètres + + # plus proche des fontaines ≤ 50 m + nearest_fountain = ( + ArtworkFountain.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) + .order_by('distance') + .first() + ) + + # plus proche des monuments ≤ 50 m + nearest_monument = ( + ArtworkMonument.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) + .order_by('distance') + .first() + ) + + # garder uniquement ceux trouvés, puis prendre le plus proche + candidates = [obj for obj in (nearest_fountain, nearest_monument) if obj is not None] + if not candidates: + return JsonResponse({"error": _("Aucun équipement trouvé")}, status=404) + + nearest = min(candidates, key=lambda o: o.distance) + + # Récupérer le ContentType de l'asset + ct = ContentType.objects.get_for_model(nearest) + + return JsonResponse({ + "equipment_id": nearest.id, + "equipment_ct": ct.id, + "equipment_label": f"{nearest.get_name()}", + "lon": nearest.lon, + "lat": nearest.lat, + # "distance_m": round(nearest.distance.m, 2), + "equipment_type": nearest.__class__.__name__, + }) \ No newline at end of file diff --git a/streetup/assets/views/asset_editing.py b/streetup/assets/views/asset_editing.py new file mode 100644 index 0000000..5573d3e --- /dev/null +++ b/streetup/assets/views/asset_editing.py @@ -0,0 +1,2378 @@ +# -------------------- +# Asset Editing Views +# -------------------- + +import json +import os +from django.http import JsonResponse, Http404 +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views.decorators.http import require_GET, require_POST +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.geos import Point, GEOSGeometry +from django.db import transaction +from django.db.models import Q +from django.utils import timezone +from django.utils.translation import gettext as _ + +from assets.permissions import ( + can_edit_asset, + get_allowed_update_fields_for_asset, + get_allowed_actions_for_asset, + get_allowed_actions_for_thematic, + get_user_asset_thematic_access, +) +from assets.models import AssetCategory, ASSET_STATUS_CHOICES +from assets.models.core import ASSET_VALIDATION_STATUS_CHOICES +from common.models import Thematic +from interventions.models import Intervention, InterventionAsset +from assets.views.locations import ASSETS_LOCATION_REGISTRY + + +# ----------------------------------------------------------------------- +# Asset Document API +# ----------------------------------------------------------------------- + +@require_POST +def asset_add_document(request, asset_model, asset_id): + """ + Upload a new file, create a ManagedDocument + DocumentVersion and attach + it to the asset via DocumentAttachment. + Expects multipart/form-data with: + - file : the file to upload + - title : document title (optional, defaults to filename) + """ + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse( + {'success': False, 'error': _("Permission refusée")}, status=403 + ) + + uploaded_file = request.FILES.get('file') + if not uploaded_file: + return JsonResponse( + {'success': False, 'error': _("Aucun fichier fourni")}, status=400 + ) + + title = request.POST.get('title', '').strip() + if not title: + title = os.path.splitext(uploaded_file.name)[0] + + from documents.models import ManagedDocument, DocumentVersion, DocumentAttachment + + tag_ids = request.POST.getlist('tag_ids') + + with transaction.atomic(): + doc = ManagedDocument.objects.create( + title=title, + created_by=request.user, + ) + if tag_ids: + from documents.models import DocumentTag + valid_tags = DocumentTag.objects.filter(pk__in=tag_ids) + doc.tags.set(valid_tags) + + version = DocumentVersion( + document=doc, + uploaded_by=request.user, + version_number=1, + ) + version.file.save(uploaded_file.name, uploaded_file, save=True) + + DocumentAttachment.objects.create( + document=doc, + content_type=content_type, + object_id=asset.pk, + attached_by=request.user, + ) + + return JsonResponse({'success': True}) + + +@require_GET +def asset_document_list_html(request, asset_model, asset_id): + """ + Returns the rendered document-viewer HTML for the #documents-list div. + Used after AJAX uploads to refresh the thumbnail grid without a full page reload. + """ + from django.http import HttpResponse + from django.template.loader import render_to_string + from documents.models import DocumentAttachment + + asset, _model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + documents = DocumentAttachment.objects.select_related( + "document", + "document__current_version", + ).filter( + content_type=content_type, + object_id=asset.pk, + ).order_by("-attached_at") + + html = render_to_string( + "assets/components/_documents_inner.html", + {"documents": documents}, + request=request, + ) + return HttpResponse(html) + + +@require_POST +def asset_delete_document(request, asset_model, asset_id, document_id): + """ + Detach (and optionally delete) a document from an asset. + Only removes the DocumentAttachment; the ManagedDocument itself is preserved + unless it has no other attachments, in which case it is deleted too. + """ + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse( + {'success': False, 'error': _("Permission refusée")}, status=403 + ) + + from documents.models import DocumentAttachment + + try: + attachment = DocumentAttachment.objects.get( + document_id=document_id, + content_type=content_type, + object_id=asset.pk, + ) + except DocumentAttachment.DoesNotExist: + return JsonResponse( + {'success': False, 'error': _("Document non trouvé")}, status=404 + ) + + with transaction.atomic(): + doc = attachment.document + attachment.delete() + # Delete the document itself if it is now orphaned + if not doc.attachments.exists(): + doc.delete() + + return JsonResponse({'success': True}) + + +# Constants for code generation +MAX_REPLACEMENT_ATTEMPTS = 100 +MAX_CODE_GENERATION_ATTEMPTS = 1000 + + +def _get_asset_by_ct(content_type_model, asset_id): + """Get asset by content type model name and id.""" + try: + content_type = ContentType.objects.get(app_label='assets', model=content_type_model.lower()) + model_class = content_type.model_class() + return get_object_or_404(model_class, pk=asset_id), model_class, content_type + except ContentType.DoesNotExist: + raise Http404(_("Type d'asset inconnu")) + + +def _get_model_class_for_asset(asset_model_name): + """Get the model class for an asset type.""" + try: + content_type = ContentType.objects.get(app_label='assets', model=asset_model_name.lower()) + return content_type.model_class() + except ContentType.DoesNotExist: + return None + + +def _update_lon_lat_from_geom(asset, geom): + """Update lon/lat fields from geometry (in SRID 3812).""" + # Clone and transform to WGS84 for lon/lat + geom_wgs84 = geom.transform(4326, clone=True) + + if geom_wgs84.geom_type == 'Point': + asset.lon = geom_wgs84.x + asset.lat = geom_wgs84.y + else: + # Use centroid for non-point geometries + centroid = geom_wgs84.centroid + asset.lon = centroid.x + asset.lat = centroid.y + + +def _assign_default_position(new_asset, location, thematic_code=None): + """ + Assign default geometry, lon, lat, and geojson to the asset from its location/hierarchy. + """ + from assets.models import AbstractGeoAsset + if not isinstance(new_asset, AbstractGeoAsset): + return + + # Check if the asset already has a geometry/coordinates assigned + if new_asset.geom is not None or (new_asset.lon is not None and new_asset.lat is not None): + # Already has coordinates/geometry, update geojson if needed + if new_asset.geom is not None and not new_asset.geojson: + try: + geom_4326 = new_asset.geom.transform(4326, clone=True) + new_asset.geojson = geom_4326.geojson + except Exception: + pass + return + + if not location: + return + + # Resolve geom and coordinates by walking up the hierarchy + geom, lon, lat = None, None, None + + # Try the location itself + if hasattr(location, 'geom') and location.geom is not None: + geom = location.geom + if hasattr(location, 'lon') and location.lon is not None: + lon = location.lon + if hasattr(location, 'lat') and location.lat is not None: + lat = location.lat + + # Walk up parent locations if any + current = location + while (geom is None or lon is None or lat is None) and getattr(current, 'parent_location', None) is not None: + current = current.parent_location + if geom is None and hasattr(current, 'geom'): + geom = getattr(current, 'geom', None) + if lon is None and hasattr(current, 'lon'): + lon = getattr(current, 'lon', None) + if lat is None and hasattr(current, 'lat'): + lat = getattr(current, 'lat', None) + + # Fall back to structure's main location if still None (for structures thematic) + if (geom is None or lon is None or lat is None) and hasattr(location, 'structure') and location.structure: + main_loc = getattr(location.structure, 'main_location', None) + if main_loc: + if geom is None and hasattr(main_loc, 'geom'): + geom = getattr(main_loc, 'geom', None) + if lon is None and hasattr(main_loc, 'lon'): + lon = getattr(main_loc, 'lon', None) + if lat is None and hasattr(main_loc, 'lat'): + lat = getattr(main_loc, 'lat', None) + + # Assign resolved values + if geom is not None: + try: + if geom.geom_type == 'Point': + new_asset.geom = geom + else: + new_asset.geom = geom.centroid + except Exception: + pass + + if lon is not None: + new_asset.lon = lon + if lat is not None: + new_asset.lat = lat + + # Ensure lon/lat are set if geom is set, and vice versa + if new_asset.geom is not None and (new_asset.lon is None or new_asset.lat is None): + try: + _update_lon_lat_from_geom(new_asset, new_asset.geom) + except Exception: + pass + + # Populate geojson + if new_asset.geom is not None: + try: + geom_4326 = new_asset.geom.transform(4326, clone=True) + new_asset.geojson = geom_4326.geojson + except Exception: + pass + + +def _update_asset_field(asset, field_name, new_value, model_class): + """ + Update a single field on an asset. + Returns (success, error_message). + """ + if not hasattr(asset, field_name): + return False, _("Champ inconnu: %(field)s") % {"field": field_name} + + try: + # Handle special field types + if field_name in ('installation_date', 'uninstallation_date', 'last_inspection_date'): + # Date field + if new_value: + from django.utils.dateparse import parse_datetime + parsed = parse_datetime(new_value) + if not parsed: + from datetime import datetime + try: + parsed = datetime.strptime(new_value, "%Y-%m-%d") + except ValueError: + return False, _("Format de date invalide pour %(field)s") % {"field": field_name} + setattr(asset, field_name, parsed) + else: + setattr(asset, field_name, None) + + elif field_name == 'model': + # ForeignKey to model + if new_value: + # Get the model's ForeignKey field to find the related model class + field = model_class._meta.get_field('model') + related_model = field.related_model + try: + related_obj = related_model.objects.get(pk=int(new_value)) + asset.model = related_obj + except related_model.DoesNotExist: + return False, _("Modèle non trouvé") + else: + asset.model = None + + elif field_name == 'category': + # ForeignKey to category + if new_value: + try: + category = AssetCategory.objects.get(pk=int(new_value)) + asset.category = category + except AssetCategory.DoesNotExist: + return False, _("Catégorie non trouvée") + else: + asset.category = None + + elif field_name in ('location_id', 'intersection_id'): + # ForeignKey location/intersection — derive the actual field name from the suffix + fk_field_name = field_name[:-3] # 'location' or 'intersection' + if not hasattr(asset, fk_field_name): + # Field doesn't exist on this asset type — silently skip for compatibility + return True, None + if new_value: + try: + field = model_class._meta.get_field(fk_field_name) + related_model = field.related_model + related_obj = related_model.objects.get(pk=int(new_value)) + setattr(asset, fk_field_name, related_obj) + except Exception: + return False, _("Localisation non trouvée") + else: + setattr(asset, fk_field_name, None) + + elif field_name == 'status': + # Validate status + if new_value in dict(ASSET_STATUS_CHOICES): + asset.status = new_value + else: + return False, _("Statut invalide: %(status)s") % {"status": new_value} + + elif field_name == 'validation_status': + # Validate validation status + if new_value in dict(ASSET_VALIDATION_STATUS_CHOICES) or new_value in (None, ''): + asset.validation_status = new_value if new_value else None + else: + return False, _("Statut de validation invalide: %(status)s") % {"status": new_value} + + elif field_name in ('lon', 'lat'): + # Float field with geometry update + if new_value: + try: + float_value = float(new_value) + setattr(asset, field_name, float_value) + # Update geometry if both lon and lat are set + if hasattr(asset, 'geom') and asset.lon is not None and asset.lat is not None: + pt = Point(asset.lon, asset.lat, srid=4326) + pt.transform(3812) + asset.geom = pt + except ValueError: + return False, _("Valeur numérique invalide pour %(field)s") % {"field": field_name} + else: + setattr(asset, field_name, None) + + elif field_name == 'geom': + # Geometry field - expects GeoJSON + if new_value: + try: + if isinstance(new_value, str): + geom_data = json.loads(new_value) + else: + geom_data = new_value + + # Handle GeoJSON Feature or Geometry + if geom_data.get('type') == 'Feature': + geom_json = json.dumps(geom_data.get('geometry', {})) + else: + geom_json = json.dumps(geom_data) + + geom = GEOSGeometry(geom_json, srid=4326) + geom.transform(3812) + asset.geom = geom + + # Update lon/lat from geometry + if hasattr(asset, 'lon') and hasattr(asset, 'lat'): + _update_lon_lat_from_geom(asset, geom) + except Exception as e: + return False, _("Géométrie invalide: %(error)s") % {"error": str(e)} + else: + asset.geom = None + + elif field_name in ('warranty_duration',): + # Integer field + if new_value: + try: + asset.warranty_duration = int(new_value) + except ValueError: + return False, _("Valeur numérique invalide pour %(field)s") % {"field": field_name} + else: + setattr(asset, field_name, None) + + else: + # Use field introspection for proper type coercion + from django.db.models import (FloatField, DecimalField, IntegerField, + BooleanField, DateField, DateTimeField) + try: + field_obj = asset._meta.get_field(field_name) + except Exception: + field_obj = None + + if field_obj and isinstance(field_obj, (FloatField, DecimalField)): + if new_value not in (None, ''): + try: + setattr(asset, field_name, float(str(new_value).replace(',', '.'))) + except (ValueError, TypeError): + return False, _("Valeur numérique invalide pour %(field)s") % {"field": field_name} + else: + setattr(asset, field_name, None) + + elif field_obj and isinstance(field_obj, IntegerField): + if new_value not in (None, ''): + try: + setattr(asset, field_name, int(new_value)) + except (ValueError, TypeError): + return False, _("Valeur numérique invalide pour %(field)s") % {"field": field_name} + else: + setattr(asset, field_name, None) + + elif field_obj and isinstance(field_obj, BooleanField): + if new_value in (None, '') and getattr(field_obj, 'null', False): + setattr(asset, field_name, None) + else: + setattr(asset, field_name, new_value in (True, 'true', 'True', '1', 'on')) + + elif field_obj and isinstance(field_obj, (DateTimeField, DateField)): + if new_value: + from django.utils.dateparse import parse_datetime, parse_date + parsed = parse_datetime(str(new_value)) or parse_date(str(new_value)) + if not parsed: + from datetime import datetime + try: + parsed = datetime.strptime(str(new_value), "%Y-%m-%d") + except ValueError: + return False, _("Format de date invalide pour %(field)s") % {"field": field_name} + setattr(asset, field_name, parsed) + else: + setattr(asset, field_name, None) + + else: + # Char / Text field or fallback + setattr(asset, field_name, new_value if new_value else None) + + return True, None + + except Exception as e: + return False, str(e) + + +@require_POST +def update_asset(request, asset_model, asset_id): + """ + Update asset fields via AJAX. + Expects JSON body with field:value pairs. + """ + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission de modifier cet asset") + }, status=403) + + allowed_fields = get_allowed_update_fields_for_asset(request.user, asset) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({ + 'success': False, + 'error': _("Format JSON invalide") + }, status=400) + + updated_fields = [] + errors = [] + + with transaction.atomic(): + for field_name, new_value in data.items(): + if field_name not in allowed_fields: + errors.append(_("Champ non modifiable: %(field)s") % {"field": field_name}) + continue + + success, error = _update_asset_field(asset, field_name, new_value, model_class) + if success: + updated_fields.append(field_name) + else: + errors.append(error) + + if updated_fields: + # If the user is not internal, automatically set status to 'to_be_approved' + # (unless the asset is already archived/removed, or the user explicitly set the status) + if 'status' not in updated_fields and asset.status not in ('removed',): + try: + from common.models import UserConfig + user_config = UserConfig.objects.get(user=request.user) + if not user_config.is_intern: + asset.status = 'to_be_approved' + updated_fields.append('status') + except UserConfig.DoesNotExist: + pass + + # Set the user for history tracking + asset._history_user = request.user + asset.save() + + if errors and not updated_fields: + return JsonResponse({ + 'success': False, + 'errors': errors, + 'message': _("Aucun champ mis à jour") + }, status=400) + + return JsonResponse({ + 'success': True, + 'updated_fields': updated_fields, + 'errors': errors, + 'message': _("%(count)s champ(s) mis à jour") % {"count": len(updated_fields)} + }) + + +@require_POST +def archive_asset(request, asset_model, asset_id): + """ + Archive an asset (set status to 'removed'). + """ + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission de modifier cet asset") + }, status=403) + + allowed_actions = get_allowed_actions_for_asset(request.user, asset) + if 'archive' not in allowed_actions: + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission d'archiver cet asset") + }, status=403) + + with transaction.atomic(): + asset.status = 'removed' + asset.uninstallation_date = timezone.now() + asset.save(update_fields=['status', 'uninstallation_date']) + + return JsonResponse({ + 'success': True, + 'message': _("Asset archivé avec succès") + }) + + +@require_POST +def replace_asset(request, asset_model, asset_id): + """ + Replace an asset: archive the old one and optionally create a new one. + Expects JSON body with: + - copy_attributes: boolean - whether to copy attributes from old asset + - new_model_id: int - ID of the new asset model (optional, defaults to same) + - new_data: dict - additional data for the new asset + """ + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission de modifier cet asset") + }, status=403) + + allowed_actions = get_allowed_actions_for_asset(request.user, asset) + if 'replace' not in allowed_actions: + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission de remplacer cet asset") + }, status=403) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({ + 'success': False, + 'error': _("Format JSON invalide") + }, status=400) + + copy_attributes = data.get('copy_attributes', True) + new_model_id = data.get('new_model_id') + new_data = data.get('new_data', {}) + intervention_id = data.get('intervention_id') # ID de l'intervention si appelé depuis une intervention + + with transaction.atomic(): + # Create new asset first (before archiving old one) + new_asset = model_class() + + # Copy attributes if requested + if copy_attributes: + for field in model_class._meta.get_fields(): + if field.auto_created or field.name in ('id', 'code', 'replaced_by'): + continue + if hasattr(asset, field.name): + setattr(new_asset, field.name, getattr(asset, field.name)) + + # Set default values for new asset + new_asset.status = 'active' + new_asset.installation_date = timezone.now() + new_asset.uninstallation_date = None + + # Override with new data + for field_name, value in new_data.items(): + if hasattr(new_asset, field_name): + setattr(new_asset, field_name, value) + + # Handle new model if specified + if new_model_id and hasattr(new_asset, 'model'): + try: + model_field = model_class._meta.get_field('model') + related_model = model_field.related_model + new_asset.model = related_model.objects.get(pk=int(new_model_id)) + except Exception: + pass + + # Generate new code for old asset + new_asset.code = asset.code + asset.code = _generate_archive_asset_code(asset) + + # Save new asset first + new_asset.save() + + # Archive old asset and create the replacement link + asset.status = 'removed' + asset.uninstallation_date = timezone.now() + asset.replaced_by = new_asset # Create the replacement link + asset.save(update_fields=['code', 'status', 'uninstallation_date', 'replaced_by']) + + # Si appelé depuis une intervention, créer le lien avec le nouvel asset + if intervention_id: + try: + from interventions.models import Intervention, InterventionAsset + from interventions.views import _create_equipment_operations_for_assets + + intervention = Intervention.objects.get(pk=intervention_id) + InterventionAsset.objects.create( + intervention=intervention, + content_type=content_type, + object_id=new_asset.id + ) + + # Créer les opérations d'équipement pour le nouvel asset + # basées sur les templates d'opération de l'intervention + _create_equipment_operations_for_assets( + intervention, + [(content_type, new_asset)] + ) + except Exception as e: + # Log mais ne fait pas échouer le remplacement + pass + + return JsonResponse({ + 'success': True, + 'message': _("Asset remplacé avec succès"), + 'old_asset_id': asset.id, + 'new_asset_id': new_asset.id, + 'new_asset_code': new_asset.code + }) + + +def _generate_archive_asset_code(old_asset): + """Generate a new unique code for an asset.""" + base_code = old_asset.code + + # new_code = base_code + "YYYY-MM-DD" format + new_code = f"{base_code}_{timezone.now().strftime('%Y%m%d')}" + + # get location from old_asset + location = None + for field_name in ASSETS_LOCATION_REGISTRY: + if hasattr(old_asset, field_name): + location = getattr(old_asset, field_name, None) + if location is not None: + break + + # Ensure uniqueness for archived asset codes in this location + if location and hasattr(location, 'get_direct_child_assets'): + existing_codes = set() + assets_dict = location.get_direct_child_assets() + for key, queryset in assets_dict.items(): + for asset in queryset: + existing_codes.add(asset.code) + + if new_code not in existing_codes: + return new_code + + # Try appending a counter + for i in range(1, MAX_REPLACEMENT_ATTEMPTS): + candidate_code = f"{new_code}-{i:02d}" + if candidate_code not in existing_codes: + return candidate_code + + return new_code + + + +@require_POST +def create_asset(request, thematic_code, location_type, location_id): + """ + Create a new asset at a location. + Expects JSON body with: + - asset_type: string - the asset model name (e.g., 'trafficlightpole') + - model_id: int - ID of the asset model + - category_id: int - ID of the category + - data: dict - additional data for the asset + """ + # Get thematic and check permission + try: + thematic = Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Thématique non trouvée") + }, status=404) + + allowed_actions = get_allowed_actions_for_thematic(request.user, thematic) + if 'create' not in allowed_actions: + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission de créer des assets") + }, status=403) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({ + 'success': False, + 'error': _("Format JSON invalide") + }, status=400) + + asset_type = data.get('asset_type') + model_id = data.get('model_id') + category_id = data.get('category_id') + asset_data = data.get('data', {}) + + # Convert model_id and category_id to int if they are strings + if model_id and str(model_id).strip(): + try: + model_id = int(model_id) + except (ValueError, TypeError): + model_id = None + else: + model_id = None + + if category_id and str(category_id).strip(): + try: + category_id = int(category_id) + except (ValueError, TypeError): + category_id = None + else: + category_id = None + + # Try to resolve category to deduce asset_type + category = None + if category_id: + try: + category = AssetCategory.objects.get(pk=category_id) + except AssetCategory.DoesNotExist: + pass + + # Deduce asset_type from category if not specified + if not asset_type and category: + allowed_cts = category.allowed_models.all() + if allowed_cts.exists(): + asset_type = allowed_cts.first().model + else: + classes = _get_asset_classes_for_category(thematic_code, category) + if classes and classes[0]: + asset_type = classes[0]._meta.model_name + + if not asset_type: + return JsonResponse({ + 'success': False, + 'error': _("Type d'asset requis") + }, status=400) + + # Get asset model class + model_class = _get_model_class_for_asset(asset_type) + if not model_class: + return JsonResponse({ + 'success': False, + 'error': _("Type d'asset invalide: %(type)s") % {"type": asset_type} + }, status=400) + + # Get location + try: + location_ct = ContentType.objects.get(app_label='assets', model=location_type.lower()) + location_model = location_ct.model_class() + location = get_object_or_404(location_model, pk=location_id) + except ContentType.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Type de localisation invalide") + }, status=400) + + with transaction.atomic(): + new_asset = model_class() + + # Get location model name for comparison + location_model_name = location_model._meta.model_name.lower() + + # Find the ForeignKey field that points to the location model + location_set = False + for field in model_class._meta.get_fields(): + if hasattr(field, 'related_model') and field.related_model: + related_model_name = field.related_model._meta.model_name.lower() + if related_model_name == location_model_name: + # This is a ForeignKey to the location model + setattr(new_asset, field.name, location) + location_set = True + break + + if not location_set: + return JsonResponse({ + 'success': False, + 'error': _("Impossible de lier l'asset à cette localisation") + }, status=400) + + # Set model if specified or required + if hasattr(model_class, '_meta'): + try: + model_field = model_class._meta.get_field('model') + is_model_required = not model_field.null and not model_field.blank + + if model_id: + related_model = model_field.related_model + new_asset.model = related_model.objects.get(pk=model_id) + elif is_model_required: + return JsonResponse({ + 'success': False, + 'error': _("Le modèle d'asset est requis pour ce type d'asset") + }, status=400) + except model_class._meta.get_field('model').related_model.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Modèle d'asset non trouvé") + }, status=400) + except Exception: + # No 'model' field on this asset type, that's OK + pass + + # Set category if specified + if category_id: + try: + new_asset.category = AssetCategory.objects.get(pk=int(category_id)) + except AssetCategory.DoesNotExist: + pass + + # Set default values + new_asset.status = 'active' + new_asset.validation_status = 'to_be_validated' + new_asset.installation_date = timezone.now() + + # Generate code + new_asset.code = _generate_asset_code(model_class, location) + + # Set additional data + for field_name, value in asset_data.items(): + if hasattr(new_asset, field_name) and field_name not in ('id', 'code'): + setattr(new_asset, field_name, value) + + # Assign position from location / hierarchy + _assign_default_position(new_asset, location, thematic_code) + + new_asset.save() + + return JsonResponse({ + 'success': True, + 'message': _("Asset créé avec succès"), + 'asset_id': new_asset.id, + 'asset_code': new_asset.code + }) + + +def _generate_asset_code(model_class, location): + """Generate a unique code for a new asset.""" + location_code = getattr(location, 'code', 'AST') + + # Get count of existing assets at this location + model_name = model_class._meta.model_name.upper()[:3] + base_code = f"{location_code}-{model_name}" + + # Find next available number + for i in range(1, MAX_CODE_GENERATION_ATTEMPTS): + new_code = f"{base_code}-{i:03d}" + if not model_class.objects.filter(code=new_code).exists(): + return new_code + + # Fallback + import time + return f"{base_code}-{int(time.time())}" + + +@require_POST +def bulk_archive_assets(request, thematic_code, location_type, location_id): + """ + Archive all active assets at a location. + """ + # Get thematic and check permission + try: + thematic = Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Thématique non trouvée") + }, status=404) + + allowed_actions = get_allowed_actions_for_thematic(request.user, thematic) + if 'bulk_archive' not in allowed_actions: + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission d'archiver en masse") + }, status=403) + + # Get location + try: + location_ct = ContentType.objects.get(app_label='assets', model=location_type.lower()) + location_model = location_ct.model_class() + location = get_object_or_404(location_model, pk=location_id) + except ContentType.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Type de localisation invalide") + }, status=400) + + # Get all active assets for this location + if not hasattr(location, 'get_active_assets'): + return JsonResponse({ + 'success': False, + 'error': _("Cette localisation ne supporte pas la gestion des assets") + }, status=400) + + archived_count = 0 + now = timezone.now() + + with transaction.atomic(): + assets_dict = location.get_active_assets() + + # Flatten all assets into a single list + assets_to_archive = [] + if isinstance(assets_dict, dict): + for val in assets_dict.values(): + if isinstance(val, (list, tuple)) or hasattr(val, '__iter__'): + assets_to_archive.extend(val) + else: + assets_to_archive.append(val) + elif isinstance(assets_dict, (list, tuple)) or hasattr(assets_dict, '__iter__'): + assets_to_archive.extend(assets_dict) + else: + assets_to_archive.append(assets_dict) + + # Archive each asset individually to trigger signals and history logs + for asset in assets_to_archive: + if hasattr(asset, 'status') and asset.status == 'active': + asset._history_user = request.user + asset.status = 'removed' + asset.uninstallation_date = now + asset.save(update_fields=['status', 'uninstallation_date']) + archived_count += 1 + + return JsonResponse({ + 'success': True, + 'message': _("%(count)s asset(s) archivé(s)") % {"count": archived_count}, + 'archived_count': archived_count + }) + + +def _has_field(model_class, field_name): + try: + model_class._meta.get_field(field_name) + return True + except Exception: + return False + + +@require_GET +def get_child_asset_form_fields(request, model_name): + """ + Return field descriptors for building a dynamic creation form for a child asset. + Excludes system-managed fields and the parent FK (field_on_child). + Pre-fills FK fields where the value can be inferred from the parent asset context. + Query params: field_on_child, parent_model, parent_id + """ + from django.db.models import ( + ForeignKey, CharField, TextField, IntegerField, + FloatField, DecimalField, BooleanField, DateTimeField, DateField, + ) + + field_on_child = request.GET.get('field_on_child', '') + parent_model_name = request.GET.get('parent_model', '') + parent_id_str = request.GET.get('parent_id', '') + + child_model_class = None + try: + cts = ContentType.objects.filter(model=model_name.lower()) + if not cts.exists(): + return JsonResponse({'fields': []}) + cts_sorted = sorted(cts, key=lambda ct: (0 if ct.app_label == 'assets' else 1)) + for ct in cts_sorted: + mc = ct.model_class() + if mc is not None: + child_model_class = mc + break + except Exception: + return JsonResponse({'fields': []}) + + if child_model_class is None: + return JsonResponse({'fields': []}) + + # Fields always managed by the system — never shown in the creation form + SYSTEM_FIELDS = { + 'id', 'code', 'status', 'installation_date', 'uninstallation_date', + 'serial_number', 'warranty_duration', 'last_inspection_date', + 'last_preventive_date', 'last_corrective_date', 'last_survey_date', + 'replaced_by', 'marked_as_removed_date', 'marked_as_removed_by', + 'obsolescence', 'note', 'category', 'name_fr', 'name_nl', + 'lon', 'lat', 'geom', 'geojson', 'brand', + } + + # Load parent asset for context-based pre-filling + parent_asset = None + if parent_model_name and parent_id_str: + try: + parent_cts = ContentType.objects.filter(model=parent_model_name.lower()) + parent_cts_sorted = sorted(parent_cts, key=lambda ct: (0 if ct.app_label == 'assets' else 1)) + for pct in parent_cts_sorted: + pmc = pct.model_class() + if pmc is not None: + parent_asset = pmc.objects.get(pk=int(parent_id_str)) + break + except Exception: + pass + + def _prefill_for_fk(field): + """Return the raw PK to pre-select for a child FK field, or None.""" + if parent_asset is None: + return None + # Strategy 1: same field name on parent (e.g. child.intersection → parent.intersection_id) + val = getattr(parent_asset, field.attname, None) + if val is not None: + return val + # Strategy 2: any FK on parent that points to the same related model + for pf in type(parent_asset)._meta.get_fields(): + if (hasattr(pf, 'attname') and hasattr(pf, 'related_model') + and pf.related_model == field.related_model): + val = getattr(parent_asset, pf.attname, None) + if val is not None: + return val + return None + + fields_out = [] + for field in child_model_class._meta.get_fields(): + if not hasattr(field, 'column'): + continue # skip reverse relations / M2M + fname = field.name + if fname in SYSTEM_FIELDS or fname == field_on_child: + continue + required = not getattr(field, 'null', True) and not getattr(field, 'blank', True) + verbose = str(getattr(field, 'verbose_name', fname)).capitalize() + + if isinstance(field, ForeignKey): + order_field = 'code' if _has_field(field.related_model, 'code') else 'pk' + prefill_pk = _prefill_for_fk(field) + + def _obj_label(obj): + lbl = obj.code if hasattr(obj, 'code') else str(obj) + if hasattr(obj, 'get_name'): + n = obj.get_name().strip() + if n and n != lbl: + lbl += ' — ' + n + return lbl + + choices = [] + seen_ids = set() + for obj in field.related_model.objects.order_by(order_field)[:500]: + choices.append({'id': obj.pk, 'label': _obj_label(obj)}) + seen_ids.add(obj.pk) + + # Always include the pre-filled object, even if beyond the :500 limit + if prefill_pk is not None and prefill_pk not in seen_ids: + try: + prefill_obj = field.related_model.objects.get(pk=prefill_pk) + choices.insert(0, {'id': prefill_obj.pk, 'label': _obj_label(prefill_obj)}) + except field.related_model.DoesNotExist: + pass + + fields_out.append({ + 'name': field.attname, # e.g. "intersection_id" + 'label': verbose, + 'type': 'fk', + 'required': required, + 'choices': choices, + 'prefill': prefill_pk, + }) + elif getattr(field, 'choices', None): + fields_out.append({ + 'name': fname, + 'label': verbose, + 'type': 'choice', + 'required': required, + 'choices': [{'id': str(k), 'label': str(v)} for k, v in field.choices], + 'prefill': None, + }) + elif isinstance(field, BooleanField): + fields_out.append({ + 'name': fname, + 'label': verbose, + 'type': 'boolean', + 'required': False, + 'choices': [], + 'prefill': field.default if field.has_default() else False, + }) + elif isinstance(field, DateTimeField): + fields_out.append({ + 'name': fname, + 'label': verbose, + 'type': 'datetime', + 'required': required, + 'choices': [], + 'prefill': None, + }) + elif isinstance(field, (IntegerField, FloatField, DecimalField)): + fields_out.append({ + 'name': fname, + 'label': verbose, + 'type': 'number', + 'required': required, + 'choices': [], + 'prefill': None, + }) + elif isinstance(field, (CharField, TextField)): + fields_out.append({ + 'name': fname, + 'label': verbose, + 'type': 'text', + 'required': required, + 'choices': [], + 'prefill': None, + }) + # GeometryField, ArrayField, etc. are intentionally skipped + + return JsonResponse({'fields': fields_out}) + + +@require_GET +def get_asset_models_for_category(request, thematic_code, category_id): + """ + Get asset models available for a category. + Asset models are like TrafficLightPoleModel, TrafficLightControllerModel, etc. + """ + from django.apps import apps + from assets.models.core import AbstractAssetModel + + try: + category = AssetCategory.objects.get(pk=category_id) + except AssetCategory.DoesNotExist: + return JsonResponse({'models': []}) + + # Find all concrete model classes that inherit from AbstractAssetModel + # and query for instances that have the given category + models_data = [] + + for model in apps.get_models(): + # Skip abstract models + if model._meta.abstract: + continue + + # Check if this model has a category field and inherits from AbstractAssetModel + try: + if not issubclass(model, AbstractAssetModel): + continue + except TypeError: + continue + + # Check if the model has a category field + if not hasattr(model, 'category'): + continue + + # Query for model instances with this category + try: + model_objects = model.objects.filter(category=category) + for obj in model_objects: + models_data.append({ + 'id': obj.id, + 'code': obj.code, + 'name': obj.get_name() if hasattr(obj, 'get_name') else str(obj), + 'model_type': model._meta.model_name + }) + except Exception: + # Skip models that don't have a proper category FK + continue + + return JsonResponse({'models': models_data}) + + +def _is_model_required(model_class): + if not model_class or not hasattr(model_class, '_meta'): + return False + try: + field = model_class._meta.get_field('model') + return field.is_relation and not field.null and not field.blank + except Exception: + return False + + +@require_GET +def get_asset_categories_for_thematic(request, thematic_code): + """ + Get asset categories for a thematic. + """ + try: + thematic = Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + return JsonResponse({'categories': []}) + + categories = AssetCategory.objects.filter(thematic=thematic).order_by('name_fr') + + categories_data = [] + for cat in categories: + allowed_cts = list(cat.allowed_models.all()) + + # Fallback to CATEGORY_ASSET_MAPPING if empty + if not allowed_cts: + asset_class, _ = _get_asset_classes_for_category(thematic_code, cat) + if asset_class: + from django.contrib.contenttypes.models import ContentType + try: + ct = ContentType.objects.get_for_model(asset_class) + allowed_cts = [ct] + except Exception: + pass + + allowed_models_data = [] + for ct in allowed_cts: + model_class = ct.model_class() + if model_class: + allowed_models_data.append({ + 'model_name': ct.model, + 'verbose_name': str(model_class._meta.verbose_name).capitalize(), + 'is_model_required': _is_model_required(model_class) + }) + + categories_data.append({ + 'id': cat.id, + 'code': cat.code, + 'name': cat.get_name(), + 'allowed_models': allowed_models_data + }) + + return JsonResponse({'categories': categories_data}) + + +# Mapping of thematic codes to default asset model classes +# For thematics with multiple asset types, we use category code to determine the right class +THEMATIC_ASSET_MAPPING = { + 'structures': 'StructureGeoAsset', + 'trafficlights': 'TrafficLightPole', # Default, overridden by category + 'publiclighting': 'PublicLightingPole', # Default, overridden by category + 'nature': 'NatureGeoAsset', + 'artworks': 'Fountain', # or Monument depending on category + 'controlcenters': 'ControlCenterGeoAsset', + 'its': 'ITSGeoAsset', + 'sign': 'SignGeoAsset', + 'roads': 'RoadGeoAsset', +} + +# Mapping of category codes to (asset_class, asset_model_class) tuples +# This allows different asset types per category within a thematic +CATEGORY_ASSET_MAPPING = { + # Traffic Lights categories + 'trafficlights_pole': ('TrafficLightPole', 'TrafficLightPoleModel'), + 'trafficlights_cable': ('TrafficLightCable', 'TrafficLightCableModel'), + 'trafficlights_lantern': ('TrafficLightLantern', 'TrafficLightLanternModel'), + 'trafficlights_controller': ('TrafficLightController', 'TrafficLightControllerModel'), + 'trafficlights_detector': ('TrafficLightDetector', 'TrafficLightDetectorModel'), + 'trafficlights_cabinet': ('TrafficLightElectricalCabinet', 'TrafficLightElectricalCabinetModel'), + # Alias with simpler names + 'pole': ('TrafficLightPole', 'TrafficLightPoleModel'), + 'TL_POTEAU': ('TrafficLightPole', 'TrafficLightPoleModel'), + 'cable': ('TrafficLightCable', 'TrafficLightCableModel'), + 'TL_CABLE': ('TrafficLightCable', 'TrafficLightCableModel'), + 'lantern': ('TrafficLightLantern', 'TrafficLightLanternModel'), + 'TL_LANTERNE': ('TrafficLightLantern', 'TrafficLightLanternModel'), + 'controller': ('TrafficLightController', 'TrafficLightControllerModel'), + 'TL_REGELAAR': ('TrafficLightController', 'TrafficLightControllerModel'), + 'detector': ('TrafficLightDetector', 'TrafficLightDetectorModel'), + 'cabinet': ('TrafficLightElectricalCabinet', 'TrafficLightElectricalCabinetModel'), + 'electrical_cabinet': ('TrafficLightElectricalCabinet', 'TrafficLightElectricalCabinetModel'), + 'TL_ARMOIRE': ('TrafficLightElectricalCabinet', 'TrafficLightElectricalCabinetModel'), + 'TL_RADAR_PREVENTIF': ('TrafficLightRadar', 'TrafficLightRadarModel'), + 'TL_RADAR_POLE': ('TrafficLightRadarPole', 'TrafficLightRadarPoleModel'), + 'TL_DETEC_BP': ('TrafficLightDetector', 'TrafficLightDetectorModel'), + 'TL_DETEC_RADAR': ('TrafficLightDetector', 'TrafficLightDetectorModel'), + 'TL_DETEC_VIDEO': ('TrafficLightDetector', 'TrafficLightDetectorModel'), + 'TL_DETEC_OTHER': ('TrafficLightDetector', 'TrafficLightDetectorModel'), + 'TL_BORNIER': ('TrafficLightAccessory', 'TrafficLightAccessoryModel'), + 'TL_DIVERS': ('TrafficLightAccessory', 'TrafficLightAccessoryModel'), + # Public Lighting categories + 'publiclighting_pole': ('PublicLightingPole', 'PublicLightingPoleModel'), + 'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'), + 'publiclighting_luminaire': ('PublicLightingLuminaire', 'PublicLightingLuminaireModel'), + 'LI_lightfixtures': ('PublicLightingLightFixture', 'PublicLightingLightFixtureModel'), +} + + +def _get_asset_classes_for_category(thematic_code, category): + """ + Get the appropriate asset class and model class based on the thematic and category. + Returns (asset_class, asset_model_class) tuple. + """ + if category: + allowed_cts = category.allowed_models.all() + if allowed_cts.exists(): + ct = allowed_cts.first() + asset_class = ct.model_class() + if asset_class: + import importlib + model_class_name = f"{asset_class.__name__}Model" + try: + module = importlib.import_module(asset_class.__module__) + asset_model_class = getattr(module, model_class_name, None) + except Exception: + asset_model_class = None + return asset_class, asset_model_class + + category_code = category.code if category and category.code else None + + # First, try category-specific mapping (case-insensitive) + if category_code: + lower_map = {k.lower(): v for k, v in CATEGORY_ASSET_MAPPING.items()} + + # Try with thematic prefix + prefixed_code = f"{thematic_code}_{category_code}".lower() + if prefixed_code in lower_map: + return _import_asset_classes(thematic_code, lower_map[prefixed_code]) + + # Try category code directly + if category_code.lower() in lower_map: + return _import_asset_classes(thematic_code, lower_map[category_code.lower()]) + + # Fallback to thematic-level default + return _get_asset_model_class_for_thematic(thematic_code) + + +def _import_asset_classes(thematic_code, class_names): + """Import asset classes dynamically based on thematic and class names.""" + asset_class_name, model_class_name = class_names + + # Map thematics to their modules + thematic_module_map = { + 'structures': 'assets.models.structures', + 'trafficlights': 'assets.models.trafficlights', + 'publiclighting': 'assets.models.publiclighting', + 'nature': 'assets.models.nature', + 'controlcenters': 'assets.models.controlcenters', + 'its': 'assets.models.its', + 'artworks': 'assets.models.artworks', + 'sign': 'assets.models.sign', + 'roads': 'assets.models.roads', + } + + module_name = thematic_module_map.get(thematic_code) + if not module_name: + return None, None + + try: + import importlib + module = importlib.import_module(module_name) + asset_class = getattr(module, asset_class_name, None) + model_class = getattr(module, model_class_name, None) + return asset_class, model_class + except (ImportError, AttributeError): + return None, None + + +def _get_asset_model_class_for_thematic(thematic_code): + """Get the appropriate asset model class for a thematic.""" + from django.apps import apps + + # Direct imports for known thematics + if thematic_code == 'structures': + from assets.models.structures import StructureGeoAsset, StructureAssetModel + return StructureGeoAsset, StructureAssetModel + elif thematic_code == 'trafficlights': + from assets.models.trafficlights import TrafficLightPole, TrafficLightPoleModel + return TrafficLightPole, TrafficLightPoleModel + elif thematic_code == 'publiclighting': + from assets.models.publiclighting import PublicLightingPole, PublicLightingPoleModel + return PublicLightingPole, PublicLightingPoleModel + elif thematic_code == 'nature': + from assets.models.nature import NatureGreenSurface, NatureAssetModel + return NatureGreenSurface, NatureAssetModel + elif thematic_code == 'controlcenters': + from assets.models.controlcenters import ControlCenterGeoAsset, ControlCenterAssetModel + return ControlCenterGeoAsset, ControlCenterAssetModel + elif thematic_code == 'its': + from assets.models.its import ITSGeoAsset, ITSAssetModel + return ITSGeoAsset, ITSAssetModel + + return None, None + + +def _get_asset_model_instance(thematic_code, model_id, category=None): + """Get the asset model instance for a given thematic, model ID, and optionally category.""" + # Try category-specific lookup first + if category: + _, asset_model_class = _get_asset_classes_for_category(thematic_code, category) + else: + _, asset_model_class = _get_asset_model_class_for_thematic(thematic_code) + + if asset_model_class and model_id: + try: + return asset_model_class.objects.get(pk=model_id) + except asset_model_class.DoesNotExist: + return None + return None + + +@require_POST +def create_asset_api(request): + """ + API endpoint to create a new asset and optionally link it to an intervention. + Expects JSON body with: + - thematic_code: string - the thematic code (e.g., 'structures') + - category_id: int - ID of the category + - model_id: int - ID of the asset model + - code: string - unique code for the asset + - name_fr: string (optional) - French name + - name_nl: string (optional) - Dutch name + - location_id: int (optional) - ID of the location + - structure_id: int (optional) - ID of the structure (for structures thematic) + - intervention_id: int (optional) - ID of the intervention to link to + """ + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({ + 'success': False, + 'error': _("Format JSON invalide") + }, status=400) + + thematic_code = data.get('thematic_code') + category_id = data.get('category_id') + model_id = data.get('model_id') + code = data.get('code', '').strip() + name_fr = data.get('name_fr', '').strip() + name_nl = data.get('name_nl', '').strip() + location_id = data.get('location_id') + structure_id = data.get('structure_id') + intervention_id = data.get('intervention_id') + + # Convert string IDs to integers + try: + if category_id: + category_id = int(category_id) + if model_id: + model_id = int(model_id) + if location_id: + location_id = int(location_id) + if structure_id: + structure_id = int(structure_id) + if intervention_id: + intervention_id = int(intervention_id) + except (ValueError, TypeError): + return JsonResponse({ + 'success': False, + 'error': _("Format d'ID invalide") + }, status=400) + + # Validate required fields + if not thematic_code: + return JsonResponse({ + 'success': False, + 'error': _("Thématique requise") + }, status=400) + + if not category_id: + return JsonResponse({ + 'success': False, + 'error': _("Catégorie requise") + }, status=400) + + if not code: + return JsonResponse({ + 'success': False, + 'error': _("Code requis") + }, status=400) + + # Get thematic and check permission + try: + thematic = Thematic.objects.get(code=thematic_code) + except Thematic.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Thématique non trouvée") + }, status=404) + + allowed_actions = get_allowed_actions_for_thematic(request.user, thematic) + if 'create' not in allowed_actions: + return JsonResponse({ + 'success': False, + 'error': _("Vous n'avez pas la permission de créer des équipements") + }, status=403) + + # Get category first (needed to determine the correct asset class) + try: + category = AssetCategory.objects.get(pk=category_id) + except AssetCategory.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Catégorie non trouvée") + }, status=400) + + # Get the appropriate asset class based on thematic AND category + asset_class, asset_model_class = _get_asset_classes_for_category(thematic_code, category) + if not asset_class: + # Fallback to thematic-level default + asset_class, asset_model_class = _get_asset_model_class_for_thematic(thematic_code) + + if not asset_class: + return JsonResponse({ + 'success': False, + 'error': _("Type d'asset non supporté pour cette thématique/catégorie") + }, status=400) + + # Check if code already exists + if asset_class.objects.filter(code=code).exists(): + return JsonResponse({ + 'success': False, + 'error': _("Un équipement avec ce code existe déjà") + }, status=400) + + # Get asset model using category-aware function + asset_model_instance = None + if model_id: + asset_model_instance = _get_asset_model_instance(thematic_code, model_id, category) + if not asset_model_instance: + return JsonResponse({ + 'success': False, + 'error': _("Modèle d'équipement non trouvé") + }, status=400) + elif _is_model_required(asset_class): + return JsonResponse({ + 'success': False, + 'error': _("Modèle requis") + }, status=400) + + # If creating from an intervention without explicit location, get the first location from intervention + intervention = None + if intervention_id: + try: + intervention = Intervention.objects.get(pk=intervention_id) + + # If no location_id specified, try to get first location from intervention + if not location_id and not structure_id: + from interventions.models import InterventionLocation + first_location_link = intervention.locations.first() + + if first_location_link: + # Set location_id or structure_id based on what we find + location_id = first_location_link.object_id + # We'll use the location's content type to determine what to set + except Intervention.DoesNotExist: + intervention = None + + with transaction.atomic(): + # Create the new asset + new_asset = asset_class() + new_asset.code = code + + # Only set name fields if the model has them + if hasattr(new_asset, 'name_fr'): + new_asset.name_fr = name_fr if name_fr else None + if hasattr(new_asset, 'name_nl'): + new_asset.name_nl = name_nl if name_nl else None + + # Only set category if the model has it + if hasattr(new_asset, 'category'): + new_asset.category = category + + new_asset.model = asset_model_instance + new_asset.status = 'active' + new_asset.validation_status = 'to_be_validated' + new_asset.installation_date = timezone.now() + + # Handle location based on thematic + if thematic_code == 'structures': + if location_id: + from assets.models.structures import StructureLocation + try: + location = StructureLocation.objects.get(pk=location_id) + new_asset.location = location + if not structure_id and getattr(location, 'structure', None): + new_asset.structure = location.structure + except StructureLocation.DoesNotExist: + pass + + # Handle structure (for structures) + if structure_id: + from assets.models.structures import Structure + try: + structure = Structure.objects.get(pk=structure_id) + new_asset.structure = structure + except Structure.DoesNotExist: + pass + + # Handle intersection (for trafficlights) + if thematic_code == 'trafficlights': + intersection_id = data.get('intersection_id') or location_id + + if not intersection_id: + return JsonResponse({ + 'success': False, + 'error': _("Une localisation est requise pour créer cet asset.") + }, status=400) + + from assets.models.trafficlights import TrafficLightIntersection + try: + intersection = TrafficLightIntersection.objects.get(pk=intersection_id) + new_asset.intersection = intersection + except TrafficLightIntersection.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Localisation introuvable") + }, status=400) + + # Handle pole (for trafficlight lanterns and detectors) + pole_id = data.get('pole_id') + if pole_id: + from assets.models.trafficlights import TrafficLightPole + try: + pole = TrafficLightPole.objects.get(pk=pole_id) + if hasattr(new_asset, 'pole'): + new_asset.pole = pole + except TrafficLightPole.DoesNotExist: + pass + + # Assign position from location / hierarchy + location = getattr(new_asset, 'location', None) or getattr(new_asset, 'intersection', None) + _assign_default_position(new_asset, location, thematic_code) + + new_asset.save() + + # Link to intervention if specified + intervention_linked = False + operations_created = 0 + if intervention: + try: + content_type = ContentType.objects.get_for_model(asset_class) + + # Create the link + InterventionAsset.objects.create( + intervention=intervention, + content_type=content_type, + object_id=new_asset.id + ) + intervention_linked = True + + # For preventive interventions, don't create operations automatically + # Let the user choose which templates to apply via the modal + # This is handled by the JavaScript frontend + + except Exception as e: + # Log the error but don't fail the asset creation + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error linking asset to intervention or creating operations: {str(e)}", exc_info=True) + + # Prepare response message + success_message = _("Équipement créé avec succès") + if intervention_linked and operations_created > 0: + success_message += _(" • %(count)d opération(s) ajoutée(s)") % {'count': operations_created} + + response_data = { + 'success': True, + 'message': success_message, + 'asset_id': new_asset.id, + 'asset_code': new_asset.code, + 'intervention_linked': intervention_linked, + 'operations_created': operations_created, + 'needs_template_selection': intervention_linked and intervention and intervention.maintain_type == 'preventive' + } + + # Add redirect URL + if intervention_id and intervention_linked: + response_data['redirect_url'] = reverse('interventions:intervention_detail', kwargs={'intervention_id': intervention_id}) + elif hasattr(new_asset, 'get_absolute_url'): + response_data['redirect_url'] = new_asset.get_absolute_url() + + return JsonResponse(response_data) + + +@require_GET +def get_available_locations_for_asset(request, thematic_code, asset_id): + """ + Get available locations for an asset based on its thematic. + Used for changing the location of an asset in edit mode. + """ + from django.contrib.contenttypes.models import ContentType + + query = (request.GET.get('q') or '').strip() + try: + limit = int(request.GET.get('limit', 100)) + except (TypeError, ValueError): + limit = 100 + limit = max(1, min(limit, 500)) + + # Get the base asset class for the thematic to find all possible content types + asset_class, _ = _get_asset_model_class_for_thematic(thematic_code) + if not asset_class: + return JsonResponse({'locations': []}) + + # Try to find the asset in any of the possible concrete classes + asset = None + + if thematic_code == 'structures': + from assets.models.structures import StructureGeoAsset + try: + asset = StructureGeoAsset.objects.get(pk=asset_id) + except StructureGeoAsset.DoesNotExist: + return JsonResponse({'locations': []}) + + elif thematic_code == 'trafficlights': + from assets.models.trafficlights import ( + TrafficLightPole, TrafficLightCable, TrafficLightLantern, + TrafficLightController, TrafficLightDetector, TrafficLightElectricalCabinet + ) + # Try each possible class + for cls in [TrafficLightPole, TrafficLightCable, TrafficLightLantern, + TrafficLightController, TrafficLightDetector, TrafficLightElectricalCabinet]: + try: + asset = cls.objects.get(pk=asset_id) + break + except cls.DoesNotExist: + continue + + if not asset: + return JsonResponse({'locations': []}) + + else: + # For other thematics, try the base class + try: + asset = asset_class.objects.get(pk=asset_id) + except asset_class.DoesNotExist: + return JsonResponse({'locations': []}) + + locations = [] + has_more = False + + if thematic_code == 'structures': + from assets.models.structures import StructureLocation + # Get locations from the same structure when available; otherwise + # fall back to all structure locations so unlinked assets can be associated. + structure = getattr(asset, 'structure', None) + if not structure and getattr(asset, 'location', None): + structure = getattr(asset.location, 'structure', None) + + if structure: + structure_locations_qs = StructureLocation.objects.filter(structure=structure) + else: + structure_locations_qs = StructureLocation.objects.all() + + if query: + structure_locations_qs = structure_locations_qs.filter( + Q(code__icontains=query) | + Q(name_fr__icontains=query) | + Q(name_nl__icontains=query) + ) + + structure_locations = list(structure_locations_qs.order_by('code')[:limit + 1]) + has_more = len(structure_locations) > limit + + for loc in structure_locations[:limit]: + locations.append({ + 'id': loc.id, + 'code': loc.code, + 'name': loc.get_name() if hasattr(loc, 'get_name') else str(loc) + }) + + elif thematic_code == 'trafficlights': + from assets.models.trafficlights import TrafficLightIntersection + # Get all intersections (or filtered if necessary) + intersections_qs = TrafficLightIntersection.objects.all() + if query: + intersections_qs = intersections_qs.filter( + Q(code__icontains=query) | + Q(name_fr__icontains=query) | + Q(name_nl__icontains=query) + ) + + intersections = list(intersections_qs.order_by('code')[:limit + 1]) + has_more = len(intersections) > limit + + for intersection in intersections[:limit]: + locations.append({ + 'id': intersection.id, + 'code': intersection.code, + 'name': intersection.get_name() if hasattr(intersection, 'get_name') else str(intersection) + }) + + # Add similar logic for other thematics as needed + + return JsonResponse({'locations': locations, 'has_more': has_more}) + + +# --------------------------------------------------------------------------- +# Linked Assets API +# --------------------------------------------------------------------------- + +@require_GET +def get_linked_asset_types(request, asset_model, asset_id): + """ + Returns metadata about the child asset types linkable to this asset + (reverse FK relations) and the parent FK fields it has. + Used to populate the "link / create asset" modal. + """ + from django.db.models import ManyToOneRel, ForeignKey + from assets.models import AbstractAsset + + asset, model_class, _ct = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse({'success': False, 'error': _("Permission refusée")}, status=403) + + EXCLUDED_CHILD_FIELDS = {'replaced_by'} + EXCLUDED_PARENT_FIELDS = { + 'model', 'category', 'replaced_by', 'marked_as_removed_by', + 'controller_maintenance_contract', + } + + # Child asset types (reverse FK relations) + child_types = [] + for rel in type(asset)._meta.get_fields(): + if not isinstance(rel, ManyToOneRel): + continue + related_model = rel.related_model + try: + if not issubclass(related_model, AbstractAsset): + continue + except TypeError: + continue + if rel.field.name in EXCLUDED_CHILD_FIELDS and related_model == type(asset): + continue + child_types.append({ + 'model_name': related_model._meta.model_name, + 'verbose_name': str(related_model._meta.verbose_name).capitalize(), + 'verbose_name_plural': str(related_model._meta.verbose_name_plural).capitalize(), + 'field_on_child': rel.field.name, + }) + + # Parent FK fields on this asset + parent_types = [] + for field in type(asset)._meta.get_fields(): + if not isinstance(field, ForeignKey): + continue + if field.name in EXCLUDED_PARENT_FIELDS: + continue + related_model = field.related_model + try: + if not issubclass(related_model, AbstractAsset): + continue + except TypeError: + continue + current_parent = getattr(asset, field.name, None) + parent_types.append({ + 'field_name': field.name, + 'model_name': related_model._meta.model_name, + 'verbose_name': str(field.verbose_name).capitalize(), + 'current_id': current_parent.pk if current_parent else None, + 'current_code': current_parent.code if current_parent else None, + }) + + return JsonResponse({ + 'success': True, + 'child_types': child_types, + 'parent_types': parent_types, + }) + + +@require_GET +def search_assets_by_model(request, model_name): + """ + Autocomplete search for assets of a given model type. + Query params: + q – search string (matches code / name_fr / name_nl) + limit – max results (default 10, max 50) + field_on_child – FK field name on the child model (used to optionallyfilter: only unlinked or already linked to parent_id) + parent_id – current parent ID + """ + from assets.models import AbstractAsset + from django.db.models import Q + + model_class = None + try: + cts = ContentType.objects.filter(model=model_name.lower()) + if not cts.exists(): + return JsonResponse({'results': []}) + # Prefer the ContentType from the 'assets' app, then fall back to any matching one + cts_sorted = sorted(cts, key=lambda ct: (0 if ct.app_label == 'assets' else 1)) + for ct in cts_sorted: + mc = ct.model_class() + try: + if mc is not None and issubclass(mc, AbstractAsset): + model_class = mc + break + except TypeError: + continue + except Exception: + return JsonResponse({'results': []}) + + if model_class is None: + return JsonResponse({'results': []}) + + q = request.GET.get('q', '').strip() + limit = min(int(request.GET.get('limit', 10)), 50) + field_on_child = request.GET.get('field_on_child', '') + parent_id = request.GET.get('parent_id', '') + + qs = model_class.objects.all() + + if q: + qs = qs.filter( + Q(code__icontains=q) | Q(name_fr__icontains=q) | Q(name_nl__icontains=q) + ) + + # Show: assets where FK is NULL OR already linked to this parent + if field_on_child and parent_id: + try: + pid = int(parent_id) + qs = qs.filter( + Q(**{f'{field_on_child}__isnull': True}) | + Q(**{field_on_child: pid}) + ) + except (ValueError, TypeError): + pass + + qs = qs[:limit] + + results = [] + for a in qs: + results.append({ + 'id': a.pk, + 'code': a.code, + 'name': getattr(a, 'name_fr', None) or getattr(a, 'name_nl', None) or '', + 'status': getattr(a, 'status', ''), + }) + + return JsonResponse({'results': results}) + + +@require_POST +def link_child_asset(request, parent_model, parent_id): + """ + Link an existing child asset to this parent by setting the FK on the child. + Body JSON: {child_model, child_id, field_on_child} + """ + from assets.models import AbstractAsset + + parent_asset, parent_model_class, _ct = _get_asset_by_ct(parent_model, parent_id) + + if not can_edit_asset(request.user, parent_asset): + return JsonResponse({'success': False, 'error': _("Permission refusée")}, status=403) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Format JSON invalide")}, status=400) + + child_model_name = data.get('child_model', '') + child_id = data.get('child_id') + field_on_child = data.get('field_on_child', '') + + try: + child_ct = ContentType.objects.get(app_label='assets', model=child_model_name.lower()) + child_model_class = child_ct.model_class() + except ContentType.DoesNotExist: + return JsonResponse({'success': False, 'error': _("Type d'asset inconnu")}, status=400) + + child_asset = get_object_or_404(child_model_class, pk=child_id) + + # Validate that field_on_child is a FK pointing to parent's model + try: + field_obj = child_model_class._meta.get_field(field_on_child) + if field_obj.related_model != parent_model_class: + return JsonResponse({'success': False, 'error': _("Champ de liaison invalide")}, status=400) + except Exception: + return JsonResponse({'success': False, 'error': _("Champ de liaison inconnu")}, status=400) + + with transaction.atomic(): + setattr(child_asset, field_on_child, parent_asset) + child_asset._history_user = request.user + child_asset.save() + + child_url = child_asset.get_absolute_url() if hasattr(child_asset, 'get_absolute_url') else None + return JsonResponse({ + 'success': True, + 'message': _("Asset lié avec succès"), + 'child_id': child_asset.pk, + 'child_code': child_asset.code, + 'child_name': child_asset.get_name() if hasattr(child_asset, 'get_name') else '', + 'child_url': child_url, + 'child_model_name': child_model_name, + 'field_on_child': field_on_child, + }) + + +@require_POST +def unlink_child_asset(request, parent_model, parent_id): + """ + Unlink a child asset from this parent (clear the FK on the child). + Body JSON: {child_model, child_id, field_on_child} + """ + parent_asset, parent_model_class, _ct = _get_asset_by_ct(parent_model, parent_id) + + if not can_edit_asset(request.user, parent_asset): + return JsonResponse({'success': False, 'error': _("Permission refusée")}, status=403) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Format JSON invalide")}, status=400) + + child_model_name = data.get('child_model', '') + child_id = data.get('child_id') + field_on_child = data.get('field_on_child', '') + + try: + child_ct = ContentType.objects.get(app_label='assets', model=child_model_name.lower()) + child_model_class = child_ct.model_class() + except ContentType.DoesNotExist: + return JsonResponse({'success': False, 'error': _("Type d'asset inconnu")}, status=400) + + child_asset = get_object_or_404(child_model_class, pk=child_id) + + # Verify child currently points to this parent + current_parent_id = getattr(child_asset, field_on_child + '_id', None) + if current_parent_id != parent_asset.pk: + return JsonResponse( + {'success': False, 'error': _("Cet asset n'est pas lié à ce parent")}, status=400 + ) + + # Validate field is nullable + try: + field_obj = child_model_class._meta.get_field(field_on_child) + if not field_obj.null: + return JsonResponse( + {'success': False, 'error': _("Ce champ est obligatoire et ne peut pas être vidé")}, + status=400 + ) + except Exception: + return JsonResponse({'success': False, 'error': _("Champ de liaison inconnu")}, status=400) + + with transaction.atomic(): + setattr(child_asset, field_on_child, None) + child_asset._history_user = request.user + child_asset.save() + + return JsonResponse({'success': True, 'message': _("Asset délié avec succès")}) + + +@require_POST +def set_parent_asset(request, asset_model, asset_id): + """ + Set (or clear) a parent FK field on the current asset. + Body JSON: {field_name, parent_model, parent_id} + - If parent_id is null/empty → clears the field (sets it to None). + - Otherwise → sets the FK to the given parent asset. + """ + from assets.models import AbstractAsset + + asset, model_class, _ct = _get_asset_by_ct(asset_model, asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse({'success': False, 'error': _("Permission refusée")}, status=403) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Format JSON invalide")}, status=400) + + field_name = data.get('field_name', '') + parent_model_name = data.get('parent_model', '') + new_parent_id = data.get('parent_id') # may be None to clear + + # Validate that field_name is indeed a FK on this asset pointing to an AbstractAsset subclass + try: + from django.db.models import ForeignKey + field_obj = model_class._meta.get_field(field_name) + if not isinstance(field_obj, ForeignKey): + return JsonResponse({'success': False, 'error': _("Champ de liaison invalide")}, status=400) + related_model = field_obj.related_model + if not issubclass(related_model, AbstractAsset): + return JsonResponse({'success': False, 'error': _("Champ de liaison invalide")}, status=400) + except Exception: + return JsonResponse({'success': False, 'error': _("Champ de liaison inconnu")}, status=400) + + if new_parent_id: + # Resolve parent asset + try: + parent_ct = ContentType.objects.get(app_label='assets', model=parent_model_name.lower()) + parent_model_class = parent_ct.model_class() + except ContentType.DoesNotExist: + return JsonResponse({'success': False, 'error': _("Type d'asset inconnu")}, status=400) + + # Make sure the field actually points to this parent model + if field_obj.related_model != parent_model_class: + return JsonResponse({'success': False, 'error': _("Champ de liaison invalide")}, status=400) + + parent_asset = get_object_or_404(parent_model_class, pk=new_parent_id) + + with transaction.atomic(): + setattr(asset, field_name, parent_asset) + asset._history_user = request.user + asset.save() + + parent_url = parent_asset.get_absolute_url() if hasattr(parent_asset, 'get_absolute_url') else None + return JsonResponse({ + 'success': True, + 'message': _("Parent lié avec succès"), + 'parent_id': parent_asset.pk, + 'parent_code': parent_asset.code, + 'parent_name': parent_asset.get_name() if hasattr(parent_asset, 'get_name') else '', + 'parent_url': parent_url, + 'field_name': field_name, + }) + else: + # Clear the field + if not field_obj.null: + return JsonResponse( + {'success': False, 'error': _("Ce champ est obligatoire et ne peut pas être vidé")}, + status=400 + ) + with transaction.atomic(): + setattr(asset, field_name, None) + asset._history_user = request.user + asset.save() + + return JsonResponse({'success': True, 'message': _("Parent délié avec succès"), 'field_name': field_name}) + + +@require_POST +def create_and_link_child_asset(request, parent_model, parent_id): + """ + Create a new child asset and link it to this parent. + Body JSON: {child_model, field_on_child, category_id (opt), model_id (opt), data (opt)} + """ + parent_asset, parent_model_class, _ct = _get_asset_by_ct(parent_model, parent_id) + + if not can_edit_asset(request.user, parent_asset): + return JsonResponse({'success': False, 'error': _("Permission refusée")}, status=403) + + # Check create permission at thematic level + thematic = None + if hasattr(parent_asset, 'category') and parent_asset.category and parent_asset.category.thematic: + thematic = parent_asset.category.thematic + elif (hasattr(parent_asset, 'model') and parent_asset.model + and getattr(parent_asset.model, 'category', None) + and parent_asset.model.category.thematic): + thematic = parent_asset.model.category.thematic + + if thematic: + thematic_actions = get_allowed_actions_for_thematic(request.user, thematic) + if 'create' not in thematic_actions: + return JsonResponse( + {'success': False, 'error': _("Vous n'avez pas la permission de créer des assets")}, + status=403 + ) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Format JSON invalide")}, status=400) + + child_model_name = data.get('child_model', '') + field_on_child = data.get('field_on_child', '') + category_id = data.get('category_id') + model_id = data.get('model_id') + extra_data = data.get('data', {}) + + try: + child_ct = ContentType.objects.get(app_label='assets', model=child_model_name.lower()) + child_model_class = child_ct.model_class() + except ContentType.DoesNotExist: + return JsonResponse({'success': False, 'error': _("Type d'asset inconnu")}, status=400) + + # Validate field + try: + field_obj = child_model_class._meta.get_field(field_on_child) + if field_obj.related_model != parent_model_class: + return JsonResponse({'success': False, 'error': _("Champ de liaison invalide")}, status=400) + except Exception: + return JsonResponse({'success': False, 'error': _("Champ de liaison inconnu")}, status=400) + + from django.db import IntegrityError as DBIntegrityError + from django.db.models import ForeignKey as DjangoForeignKey + + with transaction.atomic(): + new_child = child_model_class() + + # Set parent FK + setattr(new_child, field_on_child, parent_asset) + + # Defaults set before extra_data so user values can override if needed + new_child.status = 'active' + new_child.installation_date = timezone.now() + + # Set category if provided at top level (backward compat) + if category_id: + try: + new_child.category_id = int(category_id) + except (ValueError, TypeError): + pass + + # Apply all extra form data (includes model_id, intersection_id, etc.) + # field_name_id fields (FK raw IDs) are set directly to avoid DB lookups + SKIP_KEYS = {'id', 'code'} + for k, v in extra_data.items(): + if k in SKIP_KEYS or v is None or v == '': + continue + if not hasattr(new_child, k): + continue + # Coerce FK _id values to int + if k.endswith('_id'): + try: + v = int(v) + except (ValueError, TypeError): + continue + setattr(new_child, k, v) + + # Backward-compat: top-level model_id (used by old frontend) + if model_id and not extra_data.get('model_id'): + try: + mf = child_model_class._meta.get_field('model') + new_child.model_id = int(model_id) + except Exception: + pass + + # Generate code using parent as "location" + new_child.code = _generate_asset_code(child_model_class, parent_asset) + + new_child._history_user = request.user + try: + new_child.save() + except DBIntegrityError as exc: + # Parse the violated column from the DB error to give a user-friendly message + import re as _re + m = _re.search(r'colonne\s+[«\"]([\w]+)[»\"]', str(exc)) \ + or _re.search(r'column\s+"([\w]+)"', str(exc)) + if m: + col = m.group(1) + # Try to get verbose name from the model field + try: + bad_field = child_model_class._meta.get_field(col.removesuffix('_id')) + col = str(getattr(bad_field, 'verbose_name', col)).capitalize() + except Exception: + pass + msg = _("Le champ « %(field)s » est obligatoire.") % {'field': col} + else: + msg = _("Impossible de créer l'asset : un champ obligatoire est manquant.") + return JsonResponse({'success': False, 'error': msg}, status=400) + + child_url = new_child.get_absolute_url() if hasattr(new_child, 'get_absolute_url') else None + return JsonResponse({ + 'success': True, + 'message': _("Asset créé et lié avec succès"), + 'child_id': new_child.pk, + 'child_code': new_child.code, + 'child_name': new_child.get_name() if hasattr(new_child, 'get_name') else '', + 'child_url': child_url, + 'child_model_name': child_model_name, + 'field_on_child': field_on_child, + }) + + +@require_POST +def bulk_edit_assets(request): + """ + Bulk update status or validation_status of assets. + Expects JSON body: + { + "assets": [{"model": "...", "object_id": 123}, ...], + "field_name": "status" | "validation_status", + "new_value": "..." + } + """ + from django.db import transaction + from assets.permissions import can_edit_asset, can_validate_asset + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Format JSON invalide")}, status=400) + + assets_data = data.get('assets', []) + field_name = data.get('field_name') + new_value = data.get('new_value') + + if field_name not in ('status', 'validation_status'): + return JsonResponse({'success': False, 'error': _("Champ non modifiable en masse")}, status=400) + + updated_count = 0 + skipped_count = 0 + errors = [] + + with transaction.atomic(): + for item in assets_data: + model_name = item.get('model') + object_id = item.get('object_id') + if not model_name or not object_id: + continue + + try: + ct = ContentType.objects.get(app_label='assets', model=model_name) + asset = ct.get_object_for_this_type(pk=object_id) + except Exception: + skipped_count += 1 + continue + + # Permission check + if field_name == 'status': + has_perm = can_edit_asset(request.user, asset) + elif field_name == 'validation_status': + has_perm = can_validate_asset(request.user, asset) + else: + has_perm = False + + if not has_perm: + skipped_count += 1 + continue + + success, error = _update_asset_field(asset, field_name, new_value, ct.model_class()) + if success: + asset._history_user = request.user + asset.save(update_fields=[field_name]) + updated_count += 1 + else: + errors.append(error) + skipped_count += 1 + + return JsonResponse({ + 'success': True, + 'updated_count': updated_count, + 'skipped_count': skipped_count, + 'errors': errors, + 'message': _("%(updated)s équipement(s) mis à jour, %(skipped)s ignoré(s).") % { + 'updated': updated_count, + 'skipped': skipped_count + } + }) + diff --git a/streetup/assets/views/asset_permissions.py b/streetup/assets/views/asset_permissions.py new file mode 100644 index 0000000..1c8be6c --- /dev/null +++ b/streetup/assets/views/asset_permissions.py @@ -0,0 +1,394 @@ +# ----------------------------------------------------------------------- +# Asset / Location Permission Management Views +# ----------------------------------------------------------------------- +# +# These views provide an in-app interface to manage per-instance user +# permissions (UserAssetAccess) and per-type configuration +# (AssetTypePermissionConfig). +# +# Access is restricted to admin-role users. +# ----------------------------------------------------------------------- + +import json + +from django.contrib.contenttypes.models import ContentType +from django.db.models import ForeignKey, OneToOneField, ManyToManyField, AutoField +from django.contrib.gis.db.models import GeometryField +from django.http import JsonResponse, Http404 +from django.shortcuts import get_object_or_404, render, redirect +from django.views.decorators.http import require_POST, require_GET +from django.utils.translation import gettext as _ +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required + +from common.models import UserConfig, UserAssetAccess, UserAssetTypeAccess, AssetTypePermissionConfig + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _is_admin(user): + """Return True if the user has the 'admin' role.""" + try: + return UserConfig.objects.get(user=user).roles.filter(name='admin').exists() + except UserConfig.DoesNotExist: + return False + + +def _admin_required(view_fn): + """Simple decorator: only admin-role users may access these views.""" + from functools import wraps + + @wraps(view_fn) + @login_required + def _wrapped(request, *args, **kwargs): + if not _is_admin(request.user): + return JsonResponse({'error': _("Accès refusé — rôle admin requis.")}, status=403) + return view_fn(request, *args, **kwargs) + + return _wrapped + + +def _get_editable_field_choices(model_class): + """ + Return a list of (field_name, verbose_label) for fields that can be + restricted via editable_fields: + - only concrete (non-FK, non-M2M, non-AutoField, non-Geometry) fields + of the model itself and its abstract parents. + """ + # Gather fields from the full MRO (includes abstract parents) + excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField) + seen = set() + choices = [] + for f in model_class._meta.get_fields(): + if f.name in seen: + continue + if not hasattr(f, 'column'): # reverse relations, M2M descriptors + continue + if isinstance(f, excluded_types): + continue + seen.add(f.name) + label = f.verbose_name if hasattr(f, 'verbose_name') else f.name + choices.append((f.name, f"{label} ({f.name})")) + return choices + + +# --------------------------------------------------------------------------- +# Per-object permission list + create (panel rendered inside asset detail) +# --------------------------------------------------------------------------- + +@_admin_required +def asset_permissions_panel(request, content_type_id, object_id): + """ + GET → renders the permissions panel partial for a given object. + POST → creates a new UserAssetAccess. + """ + ct = get_object_or_404(ContentType, pk=content_type_id) + model_class = ct.model_class() + if model_class is None: + raise Http404 + + obj = get_object_or_404(model_class, pk=object_id) + + # Type-level config (ensure it exists) + type_config, _ = AssetTypePermissionConfig.objects.get_or_create(content_type=ct) + + if request.method == 'POST': + user_id = request.POST.get('user_id') + if not user_id: + return JsonResponse({'success': False, 'error': _("Utilisateur requis.")}, status=400) + + try: + target_user = User.objects.get(pk=user_id) + target_config = UserConfig.objects.get(user=target_user) + except (User.DoesNotExist, UserConfig.DoesNotExist): + return JsonResponse({'success': False, 'error': _("Utilisateur introuvable.")}, status=404) + + editable_fields_raw = request.POST.getlist('editable_fields') + visible_fields_raw = request.POST.getlist('visible_fields') + + access, created = UserAssetAccess.objects.get_or_create( + user_config=target_config, + content_type=ct, + object_id=object_id, + defaults={ + 'can_view': request.POST.get('can_view') == 'on', + 'can_edit': request.POST.get('can_edit') == 'on', + 'can_delete': request.POST.get('can_delete') == 'on', + 'editable_fields': editable_fields_raw or None, + 'visible_fields': visible_fields_raw or None, + } + ) + if not created: + access.can_view = request.POST.get('can_view') == 'on' + access.can_edit = request.POST.get('can_edit') == 'on' + access.can_delete = request.POST.get('can_delete') == 'on' + access.editable_fields = editable_fields_raw or None + access.visible_fields = visible_fields_raw or None + access.save() + + # Re-render the panel (HTMX-friendly) + return _render_panel(request, ct, object_id, obj, type_config, model_class) + + # GET + return _render_panel(request, ct, object_id, obj, type_config, model_class) + + +def _render_panel(request, ct, object_id, obj, type_config, model_class): + accesses = ( + UserAssetAccess.objects + .filter(content_type=ct, object_id=object_id) + .select_related('user_config__user') + ) + type_user_accesses = ( + UserAssetTypeAccess.objects + .filter(content_type=ct) + .select_related('user_config__user') + .order_by('user_config__user__username') + ) + all_user_configs = ( + UserConfig.objects + .select_related('user') + .order_by('user__username') + ) + field_choices = _get_editable_field_choices(model_class) + + return render( + request, + 'assets/permissions/panel.html', + { + 'ct': ct, + 'object_id': object_id, + 'obj': obj, + 'type_config': type_config, + 'accesses': accesses, + 'type_user_accesses': type_user_accesses, + 'all_user_configs': all_user_configs, + 'field_choices': field_choices, + }, + ) + + +# --------------------------------------------------------------------------- +# Edit a single UserAssetAccess +# --------------------------------------------------------------------------- + +@_admin_required +def asset_access_edit(request, access_id): + """ + GET → renders an edit form for the given UserAssetAccess. + POST → saves the changes and re-renders the permissions panel. + """ + access = get_object_or_404( + UserAssetAccess.objects.select_related('user_config__user', 'content_type'), + pk=access_id, + ) + ct = access.content_type + model_class = ct.model_class() + field_choices = _get_editable_field_choices(model_class) if model_class else [] + + if request.method == 'POST': + access.can_view = request.POST.get('can_view') == 'on' + access.can_edit = request.POST.get('can_edit') == 'on' + access.can_delete = request.POST.get('can_delete') == 'on' + editable_fields_raw = request.POST.getlist('editable_fields') + access.editable_fields = editable_fields_raw or None + visible_fields_raw = request.POST.getlist('visible_fields') + access.visible_fields = visible_fields_raw or None + access.save() + + # Redirect back to the parent panel + return redirect( + 'assets:asset_permissions_panel', + content_type_id=ct.pk, + object_id=access.object_id, + ) + + return render( + request, + 'assets/permissions/access_edit.html', + { + 'access': access, + 'field_choices': field_choices, + }, + ) + + +# --------------------------------------------------------------------------- +# Delete a single UserAssetAccess +# --------------------------------------------------------------------------- + +@_admin_required +@require_POST +def asset_access_delete(request, access_id): + access = get_object_or_404(UserAssetAccess, pk=access_id) + ct_id = access.content_type_id + obj_id = access.object_id + access.delete() + return redirect( + 'assets:asset_permissions_panel', + content_type_id=ct_id, + object_id=obj_id, + ) + + +# --------------------------------------------------------------------------- +# Edit / delete a single UserAssetTypeAccess +# --------------------------------------------------------------------------- + +@_admin_required +def asset_type_access_edit(request, type_access_id): + """ + GET → renders an edit form for the given UserAssetTypeAccess. + POST → saves changes and redirects to the type page. + """ + type_access = get_object_or_404( + UserAssetTypeAccess.objects.select_related('user_config__user', 'content_type'), + pk=type_access_id, + ) + ct = type_access.content_type + model_class = ct.model_class() + field_choices = _get_editable_field_choices(model_class) if model_class else [] + + if request.method == 'POST': + type_access.can_view = request.POST.get('can_view') == 'on' + type_access.can_edit = request.POST.get('can_edit') == 'on' + type_access.can_delete = request.POST.get('can_delete') == 'on' + editable_fields_raw = request.POST.getlist('editable_fields') + type_access.editable_fields = editable_fields_raw or None + visible_fields_raw = request.POST.getlist('visible_fields') + type_access.visible_fields = visible_fields_raw or None + type_access.save() + return redirect('assets:asset_type_permissions_page', content_type_id=ct.pk) + + return render( + request, + 'assets/permissions/type_access_edit.html', + { + 'type_access': type_access, + 'field_choices': field_choices, + }, + ) + + +@_admin_required +@require_POST +def asset_type_access_delete(request, type_access_id): + type_access = get_object_or_404(UserAssetTypeAccess, pk=type_access_id) + ct_id = type_access.content_type_id + type_access.delete() + return redirect('assets:asset_type_permissions_page', content_type_id=ct_id) + + +# --------------------------------------------------------------------------- +# Per-type configuration (AssetTypePermissionConfig) +# --------------------------------------------------------------------------- + +@_admin_required +def asset_type_permissions_page(request, content_type_id): + """ + Dedicated page for a model type: + - Toggle requires_explicit_permissions + - Manage UserAssetTypeAccess rows (per-user, all objects of this type) + - List all UserAssetAccess rows for every instance of this type + """ + ct = get_object_or_404(ContentType, pk=content_type_id) + model_class = ct.model_class() + + type_config, _ = AssetTypePermissionConfig.objects.get_or_create(content_type=ct) + + if request.method == 'POST': + action = request.POST.get('action', 'type_config') + + if action == 'type_config': + type_config.requires_explicit_permissions = ( + request.POST.get('requires_explicit_permissions') == 'on' + ) + type_config.save() + + elif action == 'add_type_user_access': + user_id = request.POST.get('user_id') + if user_id: + try: + target_user = User.objects.get(pk=user_id) + target_config = UserConfig.objects.get(user=target_user) + editable_fields_raw = request.POST.getlist('editable_fields') + visible_fields_raw = request.POST.getlist('visible_fields') + access, created = UserAssetTypeAccess.objects.get_or_create( + user_config=target_config, + content_type=ct, + defaults={ + 'can_view': request.POST.get('can_view') == 'on', + 'can_edit': request.POST.get('can_edit') == 'on', + 'can_delete': request.POST.get('can_delete') == 'on', + 'editable_fields': editable_fields_raw or None, + 'visible_fields': visible_fields_raw or None, + }, + ) + if not created: + access.can_view = request.POST.get('can_view') == 'on' + access.can_edit = request.POST.get('can_edit') == 'on' + access.can_delete = request.POST.get('can_delete') == 'on' + access.editable_fields = editable_fields_raw or None + access.visible_fields = visible_fields_raw or None + access.save() + except (User.DoesNotExist, UserConfig.DoesNotExist): + pass + + return redirect('assets:asset_type_permissions_page', content_type_id=ct.pk) + + # All UserAssetAccess rows for this content type + accesses = ( + UserAssetAccess.objects + .filter(content_type=ct) + .select_related('user_config__user') + .order_by('user_config__user__username', 'object_id') + ) + + # Resolve objects (best-effort; skip if model is unavailable) + resolved = [] + if model_class: + obj_ids = list(accesses.values_list('object_id', flat=True).distinct()) + objs = {o.pk: o for o in model_class.objects.filter(pk__in=obj_ids)} + for a in accesses: + resolved.append({ + 'access': a, + 'obj': objs.get(a.object_id), + }) + else: + for a in accesses: + resolved.append({'access': a, 'obj': None}) + + # UserAssetTypeAccess rows for this content type + type_user_accesses = ( + UserAssetTypeAccess.objects + .filter(content_type=ct) + .select_related('user_config__user') + .order_by('user_config__user__username') + ) + + field_choices = _get_editable_field_choices(model_class) if model_class else [] + + all_user_configs = ( + UserConfig.objects + .select_related('user') + .order_by('user__username') + ) + + return render( + request, + 'assets/permissions/type_page.html', + { + 'ct': ct, + 'type_config': type_config, + 'resolved_accesses': resolved, + 'type_user_accesses': type_user_accesses, + 'field_choices': field_choices, + 'all_user_configs': all_user_configs, + 'model_verbose_name': ( + model_class._meta.verbose_name_plural + if model_class else ct.model + ), + }, + ) diff --git a/streetup/assets/views/clean.py b/streetup/assets/views/clean.py new file mode 100644 index 0000000..796cd27 --- /dev/null +++ b/streetup/assets/views/clean.py @@ -0,0 +1,632 @@ +from django.shortcuts import render, get_object_or_404 +from django.http import JsonResponse, Http404 +from django.views.decorators.http import require_POST +from django.db import transaction +from django.utils import timezone +from django.utils.dateparse import parse_datetime + +from django.contrib.gis.geos import GEOSGeometry, Point +from django.contrib.gis.db.models.functions import Distance +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import get_language, gettext as _ +from django.db.models import F, Q +from django.db.models.functions import Lower +from django.urls import reverse + +import json +import unicodedata +from collections import defaultdict + +from common.utils import check_thematic_access +from common.models import UserThematics +from assets.models import CleanLocation, CleanLitterBin, CleanLitterBinModel, CleanLitterBinObservation, CleanGlassContainer, CleanGlassContainerModel, AssetCategory +from assets.permissions import can_edit_asset + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + + +MAX_ASSET_FEATURES = 5000 + +# Ordered list of (keyword_variants, canonical_color_name). +# The canonical name must match the keys used in the front-end 'match' expression. +_COLOR_KEYWORDS = [ + (['anthracite'], 'gris anthracite'), + (['bordeaux'], 'bordeaux'), + (['violet', 'violette'], 'violet'), + (['beige'], 'beige'), + (['marron', 'brun', 'brune'], 'marron'), + (['orange'], 'orange'), + (['jaune'], 'jaune'), + (['rouge'], 'rouge'), + (['bleu', 'bleue'], 'bleu'), + (['vert', 'verte'], 'vert'), + (['noir', 'noire'], 'noir'), + (['blanc', 'blanche'], 'blanc'), + (['gris', 'grise'], 'gris'), +] + + +def _color_from_text(text: str | None) -> str: + """ + Return a canonical lowercase colour name by scanning *text* for colour + keywords (word-boundary aware). Returns '' if nothing is recognised. + """ + import re + import unicodedata + + if not text: + return '' + # Normalise to ASCII-folded lowercase so accents don't matter + normalised = unicodedata.normalize('NFD', text.lower()) + normalised = ''.join(c for c in normalised if unicodedata.category(c) != 'Mn') + for variants, canonical in _COLOR_KEYWORDS: + for kw in variants: + if re.search(r'(? 0 + + location_id = request.GET.get('location_id') + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + def base_qs(model_class): + qs = model_class.objects.annotate( + code_u=Lower('code'), + name_fr_u=Lower('name_fr'), + name_nl_u=Lower('name_nl'), + ).filter(status='active') + + if location_id: + qs = qs.filter(location_id=location_id) + if category_id is not None: + qs = qs.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + if has_search_term: + qs = qs.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + return qs.select_related('location', 'model')[:100] + + items = [] + for model_class in (CleanLitterBin, CleanGlassContainer): + for a in base_qs(model_class): + code_u = getattr(a, 'code_u', '') or '' + name_fr_u = getattr(a, 'name_fr_u', '') or '' + name_nl_u = getattr(a, 'name_nl_u', '') or '' + + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u.startswith(qn): + prio = 1 + elif name_fr_u.startswith(qn) or name_nl_u.startswith(qn): + prio = 2 + elif qn in code_u or qn in name_fr_u or qn in name_nl_u: + prio = 4 + else: + prio = 7 + else: + prio = 5 + + location = a.location + label = " - ".join(p for p in [a.code, a.get_name()] if p) + equipment_ct_id = ContentType.objects.get_for_model(a).id + location_ct_id = ContentType.objects.get_for_model(location).id if location else None + + items.append({ + "prio": prio, + "code": a.code or "", + "id": a.id, + "label": label, + "equipment_id": a.id, + "equipment_label": label, + "equipment_type": a.model.get_name() if a.model else "", + "equipment_ct": equipment_ct_id, + "location_id": location.id if location else None, + "location_label": f"{location.code} - {location.get_name()}" if location else None, + "location_ct": location_ct_id, + "lon": a.lon, + "lat": a.lat, + }) + + items.sort(key=lambda x: (x["prio"], x["code"])) + return JsonResponse(items[:100], safe=False) + + +# --------------------- +# Street detail +# --------------------- + +@check_thematic_access('clean') +def clean_location_detail(request, location_id): + location = get_object_or_404(CleanLocation, pk=location_id) + + ct = ContentType.objects.get_for_model(CleanLocation) + interventions = [ + obj.intervention + for obj in InterventionLocation.objects.filter(content_type=ct, object_id=location.id).select_related('intervention').order_by('-intervention__creation_time') + ] + + assets = location.get_active_assets() + all_assets = location.get_all_assets() + + geom = None + if location.geom: + geom = json.loads(location.geom.transform(4326, clone=True).geojson) + + return render(request, "assets/clean_location_detail.html", { + "location": location, + "interventions": interventions, + "assets": assets, + "all_assets": all_assets, + "geom": geom, + }) + + +# --------------------- +# Index +# --------------------- + +@check_thematic_access('clean') +def clean_index(request): + thematic = 'clean' + lang = get_language() + + statuses = request.GET.getlist('status') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + + preventive_draft_id = request.GET.get('preventive_draft_id') + preventive_element_type = '' + selected_assets = [] + selected_locations_ids = [] + + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements', '') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + categories = sorted( + AssetCategory.objects.filter(thematic__code='clean'), + key=lambda m: m.get_name().lower() + ) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = sorted( + list(CleanLitterBinModel.objects.all()) + list(CleanGlassContainerModel.objects.all()), + key=lambda m: (m.get_name() or '').lower() + ) + model_choices = [(m.id, m.get_name()) for m in models] + + category_model_map = defaultdict(list) + for model in CleanLitterBinModel.objects.all(): + if model.category_id: + category_model_map[str(model.category_id)].append({"key": str(model.id), "label": model.get_name()}) + for model in CleanGlassContainerModel.objects.all(): + if model.category_id: + category_model_map[str(model.category_id)].append({"key": str(model.id), "label": model.get_name()}) + + context = { + 'thematics': thematics, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': dict(category_model_map), + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type, + 'selected_locations_ids': selected_locations_ids, + 'selected_assets_json': json.dumps(selected_assets), + 'locations_tab_title': _("Locations"), + 'locations_assets_title': _("Assets"), + } + + return render(request, "assets/clean_index.html", context) + + +# --------------------- +# Locations GeoJSON +# --------------------- + +@check_thematic_access('clean') +def clean_locations_geojson(request): + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + filter_geom = None + + qs = CleanLocation.objects.all() + + if filter_geom: + qs = qs.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + qs = apply_search(qs, data) + + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": "Zone propreté", + "thematic": "clean", + "is_location": True, + } + } + + features = [obj_to_feature(location) for location in qs] + + return JsonResponse({"type": "FeatureCollection", "features": features}, safe=False) + + +# --------------------- +# Assets GeoJSON +# --------------------- + +@check_thematic_access('clean') +def clean_assets_geojson(request): + lang = get_language() if get_language() in ('fr', 'nl') else 'fr' + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + locations_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + categories = data.get('category') or [] + models = data.get('model') or [] + if isinstance(categories, str): + categories = [categories] + if isinstance(models, str): + models = [models] + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + location_id = request.GET.get('location') + locations_ids = [location_id] if location_id else [] + statuses = request.GET.getlist('asset_status') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + filter_geom = None + + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not locations_ids and not categories and not models and not filter_geom and not _search_q: + return JsonResponse({"type": "FeatureCollection", "features": []}) + + def build_qs(model_class, asset_type_str): + qs = model_class.objects.all() + if locations_ids: + qs = qs.filter(location__id__in=locations_ids) + if categories: + qs = qs.filter(category__id__in=categories) + if models: + qs = qs.filter(model__id__in=models) + if filter_geom: + filter_geom_3812 = filter_geom.transform(3812, clone=True) + qs = qs.filter( + Q(geom__intersects=filter_geom_3812) | + Q(geom__isnull=True, location__geom__intersects=filter_geom_3812) + ) + if statuses: + qs = qs.filter(status__in=statuses) + else: + qs = qs.exclude(status='removed') + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + qs = apply_search(qs, data) + qs = ( + qs.annotate( + name=F(f'name_{lang}'), + category_name=F(f'category__name_{lang}'), + model_name=F(f'model__name_{lang}'), + location_lon=F('location__lon'), + location_lat=F('location__lat'), + ) + .values('id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'geojson', + 'category_name', 'model_name', 'category_id', 'model_id', 'color', + 'location_lon', 'location_lat') + .order_by('code') + )[:MAX_ASSET_FEATURES + 1] + return qs + + from assets.views.core import get_element_type_label + def obj_to_feature(obj, asset_type): + lon = obj['lon'] + lat = obj['lat'] + if lon is None: + lon = obj.get('location_lon') + if lat is None: + lat = obj.get('location_lat') + + if obj['geojson']: + geometry = json.loads(obj['geojson']) + elif lon is not None and lat is not None: + geometry = {"type": "Point", "coordinates": [lon, lat]} + else: + geometry = None + detail_url = reverse('assets:clean_assets_detail', kwargs={'asset_model': asset_type, 'asset_id': obj['id']}) + return { + "type": "Feature", + "id": obj['id'], + "geometry": geometry, + "properties": { + "code": obj['code'], + "name": obj['name'], + "category": obj['category_name'], + "model": obj['model_name'], + "category_id": obj.get('category_id'), + "model_id": obj.get('model_id'), + "status": obj['status'], + "validation_status": obj.get('validation_status'), + "lon": lon, + "lat": lat, + "color": obj.get('color') or '', + "display_color": _color_from_text(obj.get('color')) or _color_from_text(obj.get('model_name')), + "asset_type": asset_type, + "detail_url": detail_url, + "element_type": get_element_type_label(asset_type), + "thematic": "clean", + "is_location": False, + } + } + + litter_bin_qs = build_qs(CleanLitterBin, 'cleanlitterbin') + glass_container_qs = build_qs(CleanGlassContainer, 'cleanglasscontainer') + + total = litter_bin_qs.count() + glass_container_qs.count() + if total > MAX_ASSET_FEATURES: + return JsonResponse( + {"error": f"Too many objects to display (> {MAX_ASSET_FEATURES}). Please narrow your selection."}, + status=413 + ) + + features = [] + for obj in litter_bin_qs: + features.append(obj_to_feature(obj, 'cleanlitterbin')) + for obj in glass_container_qs: + features.append(obj_to_feature(obj, 'cleanglasscontainer')) + + return JsonResponse({"type": "FeatureCollection", "features": features}, safe=False) + + +# --------------------- +# Asset detail +# --------------------- + +@check_thematic_access('clean') +def clean_assets_detail(request, asset_model, asset_id): + try: + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Unknown model: %(asset_model)s") % {'asset_model': asset_model}) + + asset = get_object_or_404(model_class, id=asset_id) + + from django.template.loader import select_template + template = select_template([ + f"assets/{asset_model}_detail.html", + "assets/detail_generic.html", + ]) + + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + context['thematic_code'] = 'clean' + + return render(request, template.template.name, context) + + +# --------------------- +# Observations API +# --------------------- + +@require_POST +@check_thematic_access('clean') +def update_litter_bin_observations(request, asset_id): + """ + Create / update / delete observations for a litter bin. + Expects JSON: { "observations": [ {id?, ...fields}, ... ] } + Observations currently in DB but absent from the payload are deleted. + """ + asset = get_object_or_404(CleanLitterBin, pk=asset_id) + + if not can_edit_asset(request.user, asset): + return JsonResponse({'success': False, 'error': _( + "Vous n'avez pas la permission de modifier cet asset" + )}, status=403) + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Format JSON invalide")}, status=400) + + observations_data = data.get('observations', []) + + BOOL_FIELDS = [ + 'sol_pente_trop_forte', 'sol_instable', 'sol_reflechissant', 'sol_glissant', + 'sol_fentes', 'sol_entrave', 'sol_dalles_podo', + 'acc_approche_2m', 'acc_voirie_a_traverser', 'acc_poubelle_isolee', + 'acc_gene_flux_routier', 'acc_gene_flux_pieton', 'acc_chemin_logique', + 'acc_contraste', 'acc_indications_tri', + ] + FLOAT_FIELDS = ['sol_pente_transversale', 'sol_pente_longitudinale'] + TEXT_FIELDS = [ + 'observer_note', 'sol_remarque', + 'acc_type_quartier', 'acc_densite_flux_pieton', + 'acc_agent_vehicule', 'acc_agent_pied', + 'acc_point_interet_autre', 'acc_point_interet_remarque', 'acc_remarque', + ] + JSON_FIELDS = ['acc_point_interet', 'acc_pertinence_placement', 'acc_entrave'] + + with transaction.atomic(): + submitted_ids = [] + + for obs_data in observations_data: + obs_id = obs_data.get('id') + if obs_id: + try: + obs = CleanLitterBinObservation.objects.get(pk=obs_id, litter_bin=asset) + except CleanLitterBinObservation.DoesNotExist: + obs = CleanLitterBinObservation(litter_bin=asset) + else: + obs = CleanLitterBinObservation(litter_bin=asset) + + # datetime + dt_str = obs_data.get('observation_datetime') or '' + if dt_str: + dt = parse_datetime(dt_str) + if dt and timezone.is_naive(dt): + dt = timezone.make_aware(dt) + obs.observation_datetime = dt + else: + obs.observation_datetime = None + + # boolean fields + for f in BOOL_FIELDS: + if f in obs_data: + setattr(obs, f, bool(obs_data[f])) + + # float fields + for f in FLOAT_FIELDS: + val = obs_data.get(f, '') + if val == '' or val is None: + setattr(obs, f, None) + else: + try: + setattr(obs, f, float(val)) + except (ValueError, TypeError): + setattr(obs, f, None) + + # text / choice fields + for f in TEXT_FIELDS: + val = obs_data.get(f, '') + setattr(obs, f, str(val).strip() if val else None) + + # JSON list fields + for f in JSON_FIELDS: + if f in obs_data: + raw = obs_data[f] + setattr(obs, f, raw if isinstance(raw, list) else []) + + obs.save() + submitted_ids.append(obs.pk) + + # Delete any observation not in the submitted payload + CleanLitterBinObservation.objects.filter( + litter_bin=asset + ).exclude(pk__in=submitted_ids).delete() + + return JsonResponse({'success': True}) + + +# --------------------- +# Get nearest location +# --------------------- + +def get_nearest_clean_location(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Invalid coordinates"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + CleanLocation.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # within 50 metres + .order_by('distance') + .first() + ) + + if nearest: + location_ct_id = ContentType.objects.get_for_model(nearest).id + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, + "lat": nearest.lat, + }) + else: + return JsonResponse({"error": _("No location found nearby")}) diff --git a/streetup/assets/views/controlcenters.py b/streetup/assets/views/controlcenters.py new file mode 100644 index 0000000..a422564 --- /dev/null +++ b/streetup/assets/views/controlcenters.py @@ -0,0 +1,794 @@ + +# --------------------- +# Control Centers Views +# --------------------- + +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.views.decorators.http import require_GET, require_POST +from django.contrib.gis.db.models.functions import Transform +from django.contrib.gis.geos import GEOSGeometry +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q, Case, When, IntegerField, CharField +from django.db.models.functions import Lower +from django.db.models import Func, Prefetch +from django.db import connection +from django.template.loader import render_to_string +from django.utils.translation import get_language, gettext as _ + + +import unicodedata +import json +from collections import defaultdict + +from common.utils import check_thematic_access + +from common.models import Thematic, UserConfig, UserContractAccess, UserThematics +from assets.models import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, AssetCategory + +from assets.forms import ControlCenterDocumentForm + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + + +@check_thematic_access('controlcenters') +def controlcenters_index(request): + thematic = 'controlcenters' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_is_main_location = request.GET.get('is_main_location') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + categories = list(AssetCategory.objects.filter(thematic__code = 'controlcenters')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(ControlCenterAssetModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + category_model_map = defaultdict(list) + models = ControlCenterAssetModel.objects.all() + for model in models: + if model.category: + display_name = model.get_name() + category_model_map[str(model.category_id)].append({ + "key": str(model.id), + "label": display_name, + }) + + context = { + 'thematics': thematics, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': dict(category_model_map), + 'status_choices': ControlCenterGeoAsset._meta.get_field('status').choices, + 'selected_thematic': selected_thematic, + 'selected_is_main_location': selected_is_main_location, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'selected_statuses': statuses, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + "locations_tab_title": _("Localisations"), + "locations_assets_title": _("Assets"), + } + + return render(request, "assets/controlcenters_index.html", context) + + +def controlcenters_locations(request): + pass + +def controlcenters_assets(request): + pass + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +@check_thematic_access('controlcenters') +def controlcenters_locations_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + is_main_location = data.get('is_main_location') + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + is_main_location = request.GET.get('is_main_location') + filter_geom = None + + # --------- Point d'entrée du queryset + controlcenters_locations = ControlCenterLocation.objects.select_related('control_center').all() + + + # --------- Filtre par localisation principale + if is_main_location: + val = is_main_location.lower() + if val == 'true': + controlcenters_locations = controlcenters_locations.filter(is_main_location=True) + elif val == 'false': + controlcenters_locations = controlcenters_locations.filter(is_main_location=False) + + # --------- Filtre géométrique + if filter_geom: + controlcenters_locations = controlcenters_locations.filter(geom__intersects=filter_geom) + + # --------- Limitation pour utilisateurs externes + user = request.user + if user.is_authenticated: + try: + user_config = user.config # OneToOne UserConfig + except UserConfig.DoesNotExist: + user_config = None + + # externe = pas interne ; on tient aussi compte du flag limit_assets_to_contracts + 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: + # contrats accessibles à l'utilisateur (idéalement avec droit "assets") + accessible_contract_ids = UserContractAccess.objects.filter( + user_config=user_config, + can_view_assets=True # champ prévu dans la table d'accès contrat + ).values_list('contract_id', flat=True) + + # Ne garder que les locations dont le centre de contrôle a un contrat actif lié à ces contrats + controlcenters_locations = controlcenters_locations.filter( + control_center__controlcenters_contracts__status='active', # ControlCenterContract.status + control_center__controlcenters_contracts__contract_id__in=accessible_contract_ids # FK contract + ).distinct() + else: + # Non authentifié : ne rien exposer (option sûre) + controlcenters_locations = controlcenters_locations.none() + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + controlcenters_locations = apply_search(controlcenters_locations, data) + + # --------- Conversion en GeoJSON + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None # AbstractLocation.geojson # :contentReference[oaicite:16]{index=16} + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "control_center": obj.control_center.get_name() if obj.control_center else "-", + "name": obj.get_name(), + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": "Centre de contrôle", + "thematic": "controlcenters", + "is_location": True, + } + } + + features = [] + for location in controlcenters_locations.order_by('control_center__code', 'code'): + features.append(obj_to_feature(location)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('controlcenters') +def controlcenters_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + controlcenter_location_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + controlcenter_location_id = request.GET.get('location') + controlcenter_location_ids = [controlcenter_location_id] if controlcenter_location_id else [] + statuses = request.GET.getlist('asset_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + filter_geom = None + + # Pas de paramètres = pas de carte (comportement existant conservé) + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not controlcenter_location_ids and not categories and not models and not filter_geom and not _search_q: + geojson_data = {"type": "FeatureCollection", "features": []} + return JsonResponse(geojson_data) + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.lon + lat = obj.lat + + if lon is None or lat is None: + location = getattr(obj, 'location', None) + if location is not None: + if lon is None: + lon = getattr(location, 'lon', None) + if lat is None: + lat = getattr(location, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "category": obj.category.get_name() if obj.category else None, + "model": obj.model.get_name() if obj.model else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "controlcenters", + "is_location": False, + }, + } + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + # --------- Query de base + optimisations + controlcenter_asset_qs = ( + ControlCenterGeoAsset.objects + .select_related('location', 'location__control_center', 'category', 'model') + .all() + ) + + # --------- Filtres métier existants + if controlcenter_location_ids: + controlcenter_asset_qs = controlcenter_asset_qs.filter(location__id__in=controlcenter_location_ids) + + if models: + controlcenter_asset_qs = controlcenter_asset_qs.filter(model__id__in=models) + + if categories: + controlcenter_asset_qs = controlcenter_asset_qs.filter(category__id__in=categories) + + if filter_geom: + controlcenter_asset_qs = controlcenter_asset_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, location__geom__intersects=filter_geom) + ) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + controlcenter_asset_qs = apply_search(controlcenter_asset_qs, data) + + # --------- Limitation pour utilisateurs externes + user = request.user + if user.is_authenticated: + try: + user_config = user.config # OneToOne + 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) + + # Ne garder que les assets qui ont un contrat actif + # auquel l'utilisateur a accès + controlcenter_asset_qs = controlcenter_asset_qs.filter( + controlcenters_assets_contracts__status='active', + controlcenters_assets_contracts__contract_id__in=accessible_contract_ids + ).distinct() + else: + # Non authentifié : ne rien exposer (plus sûr) + controlcenter_asset_qs = controlcenter_asset_qs.none() + + # --------- Cap de sécurité + controlcenter_asset_qs = controlcenter_asset_qs[0:MAX_ASSET_FEATURES+1] + if len(controlcenter_asset_qs) >= MAX_ASSET_FEATURES: + return JsonResponse( + { + "error": _("Trop d'objets à afficher (> %(max)d)") % {"max": MAX_ASSET_FEATURES} + }, + status=413, + ) + + # --------- Construction GeoJSON + features = [] + feature_count = 0 + for qs, asset_type in [ + (controlcenter_asset_qs, "controlcentergeoasset"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": _("Trop d'objets à afficher (> %(max)d). Limitez votre sélection.") % {"max": MAX_ASSET_FEATURES} + }, status=413) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + +@check_thematic_access('controlcenters') +def controlcenters_locations_detail(request, location_id): + # Préfetch (réduction N+1) : ASSETS + enfants (2 niveaux) + location = ( + ControlCenterLocation.objects + .prefetch_related( + Prefetch( + 'assets', + queryset=ControlCenterGeoAsset.objects.filter(status='active').order_by('code') + ), + 'child_locations__assets', + 'child_locations__child_locations', + ) + .select_related('control_center') + .get(pk=location_id) + ) + + # 1) Construire la liste des ids (location + enfants) + def collect_ids(loc): + ids = [loc.id] + for child in loc.child_locations.all().order_by('code'): + ids.extend(collect_ids(child)) + return ids + + all_loc_ids = collect_ids(location) + + # 2) Récupérer en bloc tous les liens GFK pour ces ids + ct_sl = ContentType.objects.get_for_model(ControlCenterLocation) + links = ( + InterventionLocation.objects + .filter(content_type=ct_sl, object_id__in=all_loc_ids) + .select_related('intervention') + ) + + # 3) Indexer par location_id → [interventions...] + by_loc = defaultdict(list) + for link in links: + by_loc[link.object_id].append(link.intervention) + + # 4) Arbre + def build_tree(loc): + return { + 'loc': loc, + 'assets': list(loc.assets.all().order_by('code')), + 'interventions': by_loc.get(loc.id, []), + 'children': [build_tree(child) for child in loc.child_locations.all().order_by('code')], + } + + tree = build_tree(location) + + # 5) Aplatir les interventions de l’arbre (sans doublons) + tri global desc. par code + def flatten_interventions(node): + seen = set() + out = [] + def walk(n): + for itv in n['interventions']: + if itv.pk not in seen: + seen.add(itv.pk) + out.append(itv) + for c in n['children']: + walk(c) + walk(node) + return out + + interventions = sorted( + flatten_interventions(tree), + key=lambda itv: itv.creation_time, + reverse=True + ) + + # 6) Assets + documents + assets = location.get_active_assets() + + other_docs = location.control_center.documents.filter(document_type='other', status='active').order_by('-id') + + # 7) Géométrie 4326 (main_location fallback) + if location.geom: + location_geom_4326 = location.geom.transform(4326, clone=True) + elif location.control_center.main_location and location.control_center.main_location.geom: + location_geom_4326 = location.control_center.main_location.geom.transform(4326, clone=True) + else: + location_geom_4326 = None + + geom = json.loads(location_geom_4326.geojson) if location_geom_4326 else None + + return render(request, "assets/controlcenters_location_detail.html", { + "location": location, + "interventions": interventions, # toutes les interventions de l’arbre + "assets": assets, + "other_documents": other_docs, + "add_document_form": ControlCenterDocumentForm(), + "geom": geom, + "location_tree": tree, # contient interventions par nœud + }) + + + +@check_thematic_access('controlcenters') +def controlcenters_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle '%(model)s' inconnu.") % {"model": asset_model}) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"controlcenters/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for controlcenters + context['thematic_code'] = 'controlcenters' + + return render(request, template.template.name, context) + + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + + +def get_descendant_location_ids(root_location_id: int) -> list[int]: + """ + Récupère l'id de la localisation racine + tous ses descendants (FK parent_location) via un CTE récursif. + Nécessite PostgreSQL. + """ + # Récupérer dynamiquement le nom de la table depuis le modèle Django + table_name = ControlCenterLocation._meta.db_table + + with connection.cursor() as cur: + cur.execute(f""" + WITH RECURSIVE loc_tree AS ( + SELECT id + FROM {table_name} + WHERE id = %s + UNION ALL + SELECT sl.id + FROM {table_name} sl + JOIN loc_tree lt ON sl.parent_location_id = lt.id + ) + SELECT id FROM loc_tree; + """, [root_location_id]) + rows = cur.fetchall() + return [r[0] for r in rows] + + +@require_GET +def controlcenters_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + # CT ids (calculés une fois) + equipment_ct_id = ContentType.objects.get_for_model(ControlCenterGeoAsset).id + location_ct_id = ContentType.objects.get_for_model(ControlCenterLocation).id + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + # filtres optionnels + location_id = request.GET.get('location_id') + controlcenter_id = request.GET.get('controlcenter_id') + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # si location fournie, calcule l'ensemble {location + descendants} + location_ids = None + if location_id: + try: + location_ids = get_descendant_location_ids(int(location_id)) + except Exception: + # en cas d'ID invalide ou autre, on ignore le filtre + location_ids = None + + # Base queryset accent-insensible + actifs uniquement + qs = ( + ControlCenterGeoAsset.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + ) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + qs = qs.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + qs = qs.exclude(status='removed') # <-- exclure les supprimés + qs = qs.select_related( + 'location', + 'location__control_center', + 'control_center', + 'control_center__main_location', + 'model', + 'category', + ) + + if location_ids: + qs = qs.filter(location_id__in=location_ids) + if controlcenter_id: + qs = qs.filter(control_center_id=controlcenter_id) + if category_id is not None: + qs = qs.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + # Priorité : exact > code startswith > nom startswith > contient + if has_search_term: + priority = Case( + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + default=7, + output_field=IntegerField(), + ) + qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100] + else: + # Sans terme de recherche, tri par code uniquement + qs = qs.order_by('code')[:100] + + data = [] + for a in qs: + # equipment_type depuis model/category + equipment_type = None + if getattr(a, 'model', None): + equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None) + if not equipment_type and getattr(a, 'category', None): + equipment_type = getattr(a.category, 'name', None) + + # coordonnées: asset > geom(4326) > location > controlcenter.main_location + lon = a.lon + lat = a.lat + + if (lon is None or lat is None) and getattr(a, 'geom', None): + try: + g = a.geom + if g.srid and g.srid != 4326: + g = a.geom.transform(4326, clone=True) + coords = getattr(g, 'coords', None) + if coords: + if lon is None: lon = coords[0] + if lat is None: lat = coords[1] + except Exception: + pass + + if (lon is None or lat is None) and getattr(a, 'location', None): + if lon is None and getattr(a.location, 'lon', None) is not None: + lon = a.location.lon + if lat is None and getattr(a.location, 'lat', None) is not None: + lat = a.location.lat + + main_loc = (a.control_center.main_location if getattr(a, 'control_center', None) and getattr(a.control_center, 'main_location', None) + else (a.location.control_center.main_location + if getattr(a, 'location', None) and getattr(a.location, 'control_center', None) + and getattr(a.location.control_center, 'main_location', None) + else None)) + if (lon is None or lat is None) and main_loc: + if lon is None: lon = getattr(main_loc, 'lon', None) + if lat is None: lat = getattr(main_loc, 'lat', None) + + equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name() + label = f"{a.code} - {equipment_label}" if a.code else equipment_label + + location_label = ( + f"{a.location.get_name()} ({a.location.code})" + if getattr(a, "location", None) else None + ) + struct = a.control_center or (a.location.control_center if getattr(a, "location", None) else None) + controlcenter_id_val = struct.id if struct else None + controlcenter_label = (f"{struct.get_name()} ({struct.code})" if struct else None) + + data.append({ + "id": a.id, + "label": label, + + "equipment_id": a.id, + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": a.location.id if getattr(a, "location", None) else None, + "location_label": location_label, + "location_ct": location_ct_id, + + "parent_ref_id": controlcenter_id_val, + "parent_ref_label": controlcenter_label, + + "lon": lon, + "lat": lat, + }) + + return JsonResponse(data, safe=False) + + + + +@check_thematic_access('controlcenters', edit_permission_required=True) +@require_POST +def controlcenters_add_document(request, controlcenter_id): + controlcenter = get_object_or_404(ControlCenter, pk=controlcenter_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + for i in range(count): + file = files[i] + document_type = request.POST.get(f'document_type_{i}', 'other') + + ControlCenterDocument.objects.create( + control_center=controlcenter, + file=file, + document_type=document_type, + uploaded_by = request.user + ) + return redirect('assets:controlcenters_locations_detail', location_id=controlcenter.main_location.id) + + +@check_thematic_access('controlcenters', edit_permission_required=True) +@require_POST +def controlcenters_delete_document(request, document_id): + document = get_object_or_404(ControlCenterDocument, id=document_id) + controlcenter_location_id = document.control_center.main_location.id # pour rediriger vers la bonne page + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + #messages.success(request, "Le document a été supprimé avec succès.") + return redirect('assets:controlcenters_locations_detail', location_id=controlcenter_location_id) \ No newline at end of file diff --git a/streetup/assets/views/core.py b/streetup/assets/views/core.py new file mode 100644 index 0000000..9c9e5c2 --- /dev/null +++ b/streetup/assets/views/core.py @@ -0,0 +1,859 @@ +import json +import re +from datetime import datetime +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.http import Http404 +from django.shortcuts import redirect, render, get_object_or_404 +from django.template.loader import select_template +from django.urls import reverse +from django.utils.translation import gettext as _ + +from common.models import UserConfig, Thematic +from common.utils import check_thematic_access +from assets.models import AssetCategory, ASSET_STATUS_CHOICES +from assets.models.core import ASSET_VALIDATION_STATUS_CHOICES +from assets.permissions import ( + get_allowed_update_fields_for_asset, + get_allowed_actions_for_asset, +) + + +def get_connection_lines_geojson(asset): + """ + Returns a GeoJSON FeatureCollection containing LineStrings representing + only electrical connections. For a TrafficLightRadar, this means connections to + power_publiclighting_pole and/or power_trafficlights_pole when they are set and have geoms. + """ + import json + from django.contrib.gis.geos import LineString + from assets.models.trafficlights import TrafficLightRadar + + if not isinstance(asset, TrafficLightRadar): + return None + + if not asset.geom: + return None + + features = [] + + # We check both power sources + power_sources = [ + getattr(asset, 'power_publiclighting_pole', None), + getattr(asset, 'power_trafficlights_pole', None) + ] + + for ps in power_sources: + if ps and getattr(ps, 'geom', None): + p1 = asset.geom.clone() + p1.transform(4326) + p2 = ps.geom.clone() + p2.transform(4326) + + # Avoid zero-length lines if by any chance they are at the exact same point + if p1.distance(p2) >= 0.00001: + line = LineString(p1, p2) + features.append({ + "type": "Feature", + "geometry": json.loads(line.json), + "properties": { + "parent_code": ps.code, + "relation_type": "electrical_connection" + } + }) + + if not features: + return None + + return { + "type": "FeatureCollection", + "features": features + } + + +def get_asset_edit_context(request, asset, content_type): + """ + Get context variables for asset editing in templates. + Includes the complete replacement chain based on the replaced_by field. + """ + allowed_update_fields = get_allowed_update_fields_for_asset(request.user, asset) + allowed_actions = get_allowed_actions_for_asset(request.user, asset) + + # Format coordinates: convert to float then to string with proper decimal separator (dot) + lon_display = None + lat_display = None + if hasattr(asset, 'lon') and asset.lon is not None: + try: + # Convert to float to normalize, then back to string + lon_value = float(str(asset.lon).replace(',', '.')) + lon_display = str(lon_value) + except (ValueError, TypeError): + lon_display = None + if hasattr(asset, 'lat') and asset.lat is not None: + try: + # Convert to float to normalize, then back to string + lat_value = float(str(asset.lat).replace(',', '.')) + lat_display = str(lat_value) + except (ValueError, TypeError): + lat_display = None + + # Get status choices + status_choices = list(ASSET_STATUS_CHOICES.items()) + validation_status_choices = list(ASSET_VALIDATION_STATUS_CHOICES.items()) + + # Get categories for the same thematic + # Priority: asset.category.thematic > asset.model.category.thematic > URL path + categories = [] + _cat_thematic = None + if hasattr(asset, 'category') and asset.category and asset.category.thematic: + _cat_thematic = asset.category.thematic + elif hasattr(asset, 'model') and asset.model and getattr(asset.model, 'category', None) and asset.model.category.thematic: + _cat_thematic = asset.model.category.thematic + else: + # Fallback: extract thematic code from URL path (/assets//...) + url_match = re.match(r'^/assets/([^/]+)/', request.path) + if url_match: + try: + _cat_thematic = Thematic.objects.get(code=url_match.group(1)) + except Thematic.DoesNotExist: + pass + if _cat_thematic: + from django.contrib.contenttypes.models import ContentType + from django.db.models import Q + asset_content_type = ContentType.objects.get_for_model(asset) + categories = list(AssetCategory.objects.filter( + thematic=_cat_thematic + ).filter( + Q(allowed_models=asset_content_type) | Q(id=getattr(asset, 'category_id', None)) + ).distinct().order_by('name_fr')) + + # Get asset models for replacement + asset_models = [] + if hasattr(asset, 'model') and asset.model: + model_field = asset._meta.get_field('model') + related_model = model_field.related_model + asset_models = list(related_model.objects.all().order_by('code')) + + # Get the original asset (first in the chain) and the complete replacement chain + asset_replacements = [] + asset_base_code = None + if hasattr(asset, 'get_original_asset'): + try: + original_asset = asset.get_original_asset() + asset_replacements = original_asset.get_replacement_chain() + asset_base_code = getattr(original_asset, 'code', None) + except Exception: + asset_replacements = [asset] + asset_base_code = getattr(asset, 'code', None) + else: + asset_replacements = [asset] + asset_base_code = getattr(asset, 'code', None) + + # Get geometry JSON for JavaScript (transform to WGS84 for MapLibre) + asset_geometry_json = None + if hasattr(asset, 'geom') and asset.geom: + try: + geom_wgs84 = asset.geom.clone() + geom_wgs84.transform(4326) + asset_geometry_json = geom_wgs84.json + except Exception: + pass + + # Get thematic code for URL construction + # Priority: asset.category > asset.model.category > None + thematic_code = None + thematic = None + _thematic_source = None + if hasattr(asset, 'category') and asset.category and asset.category.thematic: + _thematic_source = asset.category.thematic + elif hasattr(asset, 'model') and asset.model and getattr(asset.model, 'category', None) and asset.model.category.thematic: + _thematic_source = asset.model.category.thematic + if _thematic_source: + thematic_code = _thematic_source.code + thematic = _thematic_source + + # Verbose name of the concrete model (e.g. "Traffic light pole") + asset_verbose_name = str(asset._meta.verbose_name).capitalize() + + # Is the current user an admin? (used to display the permissions panel) + is_admin = False + try: + from common.models import UserConfig as _UC + is_admin = _UC.objects.get(user=request.user).roles.filter(name='admin').exists() + except Exception: + pass + + return { + 'allowed_update_fields': allowed_update_fields, + 'allowed_actions': allowed_actions, + 'status_choices': status_choices, + 'validation_status_choices': validation_status_choices, + 'categories': categories, + 'asset_models': asset_models, + 'asset_model': content_type.model, + 'asset_geometry_json': asset_geometry_json, + 'thematic_code': thematic_code, + 'thematic': thematic, + 'asset_replacements': asset_replacements, + 'asset_base_code': asset_base_code, + 'lon_display': lon_display, + 'lat_display': lat_display, + 'asset_verbose_name': asset_verbose_name, + 'is_admin': is_admin, + 'connection_lines_geojson': get_connection_lines_geojson(asset), + } + + +def index(request): + + user_config = UserConfig.objects.filter(user=request.user).first() + default_thematic = user_config.default_thematic.code if user_config and user_config.default_thematic else 'trafficlights' + + # Redirection based on default_thematic + if default_thematic == 'trafficlights': + return redirect('assets:trafficlights_index') + elif default_thematic == 'publiclighting': + return redirect('assets:publiclighting_index') + elif default_thematic == 'artworks': + return redirect('assets:artworks_index') + elif default_thematic == 'structures': + return redirect('assets:structures_index') + elif default_thematic == 'nature': + return redirect('assets:nature_index') + elif default_thematic == 'water': + return redirect('assets:water_index') + elif default_thematic == 'roads': + return redirect('assets:roads_index') + elif default_thematic == 'sign': + return redirect('assets:sign_index') + elif default_thematic == 'controlcenters': + return redirect('assets:controlcenters_index') + # Add more conditions for other thematics + else: + return redirect('assets:trafficlights_index') # Fallback URL if no match is found + + +def locations_geojson(request): + pass + + +def mobile_asset_detail(request, thematic_code, asset_model, asset_id): + """Mobile-friendly asset detail using the generic template with the mobile base.""" + try: + content_type = ContentType.objects.get(app_label='assets', model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + asset = get_object_or_404(model_class, id=asset_id) + + # Permission gate based on the provided thematic code + permission_check = check_thematic_access(thematic_code) + result = permission_check(lambda req, *a, **kw: None)(request) + if result is not None: + return result + + template = select_template([ + f"assets/{asset_model}_detail_mobile.html", + "assets/detail_generic.html", + ]) + + # Build back URL from referer or intervention context + back_url = request.GET.get('back') + if not back_url: + referer = request.META.get('HTTP_REFERER', '') + if referer: + # If coming from an intervention, build the back URL + if '/mobile/interventions/' in referer: + import re + match = re.search(r'/interventions/(\d+)', referer) + if match: + back_url = reverse('mobile:intervention_detail_mobile', args=[match.group(1)]) + + context = { + "asset": asset, + "content_type": content_type, + "base_template": "mobile/mobile_base.html", + "mobile_view": True, + "back_url": back_url, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + return render(request, template.template.name, context) + + + +def filter_queryset_by_geom(qs, filter_geom, include_no_geom, geom_field='geom', location_field='location'): + """ + Filtre un queryset d'assets selon une géométrie (intersects) et prend en compte l'option include_no_geom. + """ + if not filter_geom: + return qs + + from django.db.models import Q + + # Standard intersects query + q_intersects = Q(**{f"{geom_field}__intersects": filter_geom}) + + if location_field: + q_intersects |= Q(**{ + f"{geom_field}__isnull": True, + f"{location_field}__geom__intersects": filter_geom + }) + + if include_no_geom: + q_no_geom = Q(**{f"{geom_field}__isnull": True}) + if location_field: + q_no_geom &= ( + Q(**{f"{location_field}__isnull": True}) | + Q(**{f"{location_field}__geom__isnull": True}) + ) + return qs.filter(q_intersects | q_no_geom) + + return qs.filter(q_intersects) + + +# --------------------- +# Recherche plein texte +# --------------------- + +def apply_search(qs, data, fields=None): + """ + Filtre un queryset par recherche plein texte (AND par défaut, ou OR). + Paramètres POST attendus : `q` (texte) et `search_mode` ('and'/'or'). + Par défaut, cherche dans `code`, `name_fr` et `name_nl`. + """ + q = (data.get('q') or '').strip() + mode = (data.get('search_mode') or 'and').lower() + if not q: + return qs + if fields is None: + fields = ['code', 'name_fr', 'name_nl'] + words = q.split() + + def _word_q(w): + combined = Q() + for field in fields: + combined |= Q(**{f'{field}__icontains': w}) + return combined + + combined = Q() + if mode == 'and': + for w in words: + combined &= _word_q(w) + else: + for w in words: + combined |= _word_q(w) + return qs.filter(combined) + + +# ----------------------------------------------- +# Libellés lisibles par type d'élément +# ----------------------------------------------- + +ELEMENT_TYPE_LABELS = { + # --- Localisations --- + 'roadstreet': 'Voirie', + 'signstreet': 'Voirie signalisation', + 'publiclightingstreet': 'Rue EP', + 'trafficlightintersection': 'Carrefour', + 'naturelocation': 'Section verte', + 'naturerwiz': 'Zone RWIZ', + 'structurelocation': "Ouvrage d'art", + 'controlcenterlocation': 'Centre de contrôle', + 'itslocation': 'Zone ITS', + 'cleanlocation': 'Zone propreté', + 'artworkstreet': 'Voirie œuvres', + # --- Voiries --- + 'roadway': 'Chaussée', + 'roadsidewalk': 'Trottoir', + 'roadfurniture': 'Mobilier voirie', + 'roaddrain': 'Drain', + 'roadconcreteblockset': 'Blocs béton', + # --- Éclairage public --- + 'publiclightingpole': 'Candélabre', + 'publiclightinglightfixture': 'Luminaire', + 'publiclightingelectricalcabinet': 'Armoire électrique EP', + # --- Signalisation --- + 'signpole': 'Poteau signalisation', + 'signpanel': 'Panneau', + 'signparkingspot': 'Place de parking', + # --- Tricolores --- + 'trafficlightpole': 'Poteau tricolore', + 'trafficlightcable': 'Câble TL', + 'trafficlightlantern': 'Lanterne TL', + 'trafficlightcontroller': 'Contrôleur TL', + 'trafficlightcontrollerhardware': 'Hardware contrôleur', + 'trafficlightdetector': 'Détecteur TL', + 'trafficlightnetworkhardware': 'Hardware réseau TL', + 'trafficlightvoltagehardware': 'Hardware tension TL', + 'trafficlightelectricalcabinet': 'Armoire électrique TL', + 'trafficlightelectricalcabinetcontent': 'Contenu armoire TL', + 'trafficlightaccessory': 'Accessoire TL', + # --- Nature --- + 'naturegreensurface': 'Surface verte', + 'naturetree': 'Arbre', + 'natureshrub': 'Arbuste', + 'naturebench': 'Banc', + 'naturerwiasset': 'Ouvrage RWI', + # --- Ouvrages d'art --- + 'structuregeoasset': "Composant d'ouvrage", + # --- Œuvres d'art --- + 'artworkmonument': 'Monument', + 'artworkfountain': 'Fontaine', + # --- Centres de contrôle --- + 'controlcentergeoasset': 'Équipement CdC', + # --- ITS --- + 'itsgeoasset': 'Équipement ITS', + # --- Propreté --- + 'cleanlitterbin': 'Corbeille', + 'cleanglasscontainer': 'Conteneur verre', +} + + +def get_element_type_label(model_name): + """Returns the French display label for an asset/location model name (lowercase).""" + if not model_name: + return '' + return ELEMENT_TYPE_LABELS.get((model_name or '').lower(), model_name or '') + + +def build_thematic_config(thematics): + """ + Returns a dict mapping thematic code → {name, icon, locations_geojson_url, assets_geojson_url}. + Used to populate the unified assets page JS config. + """ + _THEMATIC_URLS = { + 'roads': { + 'locations_geojson_url': reverse('assets:roads_streets_geojson'), + 'assets_geojson_url': reverse('assets:roads_assets_geojson'), + }, + 'publiclighting': { + 'locations_geojson_url': reverse('assets:publiclighting_streets_geojson'), + 'assets_geojson_url': reverse('assets:publiclighting_assets_geojson'), + }, + 'sign': { + 'locations_geojson_url': reverse('assets:sign_streets_geojson'), + 'assets_geojson_url': reverse('assets:sign_assets_geojson'), + }, + 'trafficlights': { + 'locations_geojson_url': reverse('assets:trafficlights_intersections_geojson'), + 'assets_geojson_url': reverse('assets:trafficlights_assets_geojson'), + }, + 'nature': { + 'locations_geojson_url': reverse('assets:nature_locations_geojson'), + 'assets_geojson_url': reverse('assets:nature_assets_geojson'), + }, + 'water': { + 'locations_geojson_url': reverse('assets:water_locations_geojson'), + 'assets_geojson_url': reverse('assets:water_assets_geojson'), + }, + 'structures': { + 'locations_geojson_url': reverse('assets:structures_locations_geojson'), + 'assets_geojson_url': reverse('assets:structures_assets_geojson'), + }, + 'artworks': { + 'locations_geojson_url': reverse('assets:artworks_streets_geojson'), + 'assets_geojson_url': reverse('assets:artworks_assets_geojson'), + }, + 'controlcenters': { + 'locations_geojson_url': reverse('assets:controlcenters_locations_geojson'), + 'assets_geojson_url': reverse('assets:controlcenters_assets_geojson'), + }, + 'its': { + 'locations_geojson_url': reverse('assets:its_locations_geojson'), + 'assets_geojson_url': reverse('assets:its_assets_geojson'), + }, + 'clean': { + 'locations_geojson_url': reverse('assets:clean_locations_geojson'), + 'assets_geojson_url': reverse('assets:clean_assets_geojson'), + }, + } + result = {} + for thematic in thematics: + code = thematic.code + if code in _THEMATIC_URLS: + entry = dict(_THEMATIC_URLS[code]) + entry['name'] = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic) + entry['icon'] = thematic.icon if hasattr(thematic, 'icon') else '' + result[code] = entry + return result + + +def build_conditional_filter_config(accessible_thematics, user=None): + """ + Returns a dict mapping thematic code -> conditional filters definition + used by the unified assets page. + """ + accessible_codes = {t.code for t in accessible_thematics} + config = {} + + # Shared lists used by multiple thematics + from common.models import Municipality + municipalities = Municipality.objects.all().order_by('name_fr') + municipality_options = [ + {'value': str(m.id), 'label': m.name} + for m in municipalities + ] + + # Traffic lights specific choices + from assets.models import TrafficLightIntersection, SignPanelType, TrafficLightContract, Structure + from assets.models.roads import QUALITY_LEVEL_CHOICES + from contracts.models import Contract + + voltage_options = [ + {'value': str(key), 'label': str(value)} + for key, value in TrafficLightIntersection._meta.get_field('voltage_type').choices + ] + + trafficlight_status_options = [ + {'value': str(key), 'label': str(value)} + for key, value in TrafficLightIntersection._meta.get_field('status').choices + ] + if not any(option['value'] == 'archived' for option in trafficlight_status_options): + trafficlight_status_options.append({'value': 'archived', 'label': str(_('Archivé'))}) + + quality_level_options = [ + {'value': str(key), 'label': str(value)} + for key, value in QUALITY_LEVEL_CHOICES + ] + + structure_type_options = [ + {'value': str(key), 'label': str(value)} + for key, value in Structure._meta.get_field('structure_type').choices + ] + + PANEL_CLASSES = { + 'danger': _("Signaux de danger"), + 'priority': _("Signaux de priorité"), + 'prohibition': _("Signaux d'interdiction"), + 'obligation': _("Signaux d'obligation"), + 'parking': _("Arrêt / Stationnement"), + 'indication': _("Signaux d'indication"), + 'zone': _("Signaux de zone"), + 'additional': _("Signaux additionnels"), + 'other': _("Autres") + } + + def get_panel_class_key(code): + code_upper = (code or '').upper() + if code_upper.startswith('A'): + return 'danger' + elif code_upper.startswith('B'): + return 'priority' + elif code_upper.startswith('C'): + return 'prohibition' + elif code_upper.startswith('D'): + return 'obligation' + elif code_upper.startswith('E'): + return 'parking' + elif code_upper.startswith('F'): + return 'indication' + elif code_upper.startswith('Z'): + return 'zone' + else: + return 'additional' + + panel_type_options = [] + for pt in SignPanelType.objects.all().order_by('code'): + code = (pt.code or '').strip() + if not code: + continue + + group_key = get_panel_class_key(code) + group_label = str(PANEL_CLASSES.get(group_key, PANEL_CLASSES['additional'])) + + panel_type_options.append({ + 'value': str(pt.id), + 'label': code, + 'code': code, + 'name': pt.get_name(), + 'group': group_key, + 'group_label': group_label, + 'image_url': None, + }) + + controller_maintenance_provider_options = [] + if user is not None: + user_config = getattr(user, 'config', None) + is_intern = bool(user_config and user_config.is_intern) + if is_intern: + contracts = Contract.objects.filter( + id__in=TrafficLightContract.objects.filter( + status='active', + maintenance_type='controller' + ).values_list('contract_id', flat=True).distinct() + ).select_related('company').order_by('contract_number') + controller_maintenance_provider_options = [ + {'value': str(contract.id), 'label': f"{contract.contract_number} - {contract.company.name}"} + for contract in contracts + ] + + # Roads / public lighting / sign / artworks: municipality on locations + for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks']: + if thematic_code in accessible_codes: + config.setdefault(thematic_code, []).append({ + 'key': 'municipality', + 'label': _('Commune'), + 'type': 'multi', + 'options': municipality_options, + }) + + # Traffic lights + if 'trafficlights' in accessible_codes: + config['trafficlights'] = [ + { + 'key': 'status', + 'label': _('Statut des carrefours'), + 'type': 'multi', + 'options': trafficlight_status_options, + }, + { + 'key': 'voltage', + 'label': _('Voltage'), + 'type': 'multi', + 'options': voltage_options, + }, + { + 'key': 'connected', + 'label': _('Connexion à la centrale'), + 'type': 'single', + 'options': [ + {'value': 'yes', 'label': _('Oui')}, + {'value': 'no', 'label': _('Non')}, + ], + }, + { + 'key': 'asset_begin_date', + 'label': _('Date de mise en service (à partir de)'), + 'type': 'date', + }, + ] + if controller_maintenance_provider_options: + config['trafficlights'].append({ + 'key': 'controller_maintenance_provider', + 'label': _('Prestataire maintenance contrôleurs'), + 'type': 'select', + 'options': controller_maintenance_provider_options, + }) + + # Structures / ITS / control centers: main location flag + for thematic_code in ['structures', 'its', 'controlcenters']: + if thematic_code in accessible_codes: + config[thematic_code] = [{ + 'key': 'is_main_location', + 'label': _('Localisation principale'), + 'type': 'single', + 'options': [ + {'value': 'true', 'label': _('Oui')}, + {'value': 'false', 'label': _('Non')}, + ], + }] + + # Structures: structure type (same option set as dedicated page) + if 'structures' in accessible_codes: + config.setdefault('structures', []).insert(0, { + 'key': 'structure_type', + 'label': _("Type d'ouvrage"), + 'type': 'multi', + 'options': structure_type_options, + }) + + # Nature + if 'nature' in accessible_codes: + config['nature'] = [ + { + 'key': 'location_type', + 'label': _('Type de localisation'), + 'type': 'single', + 'options': [ + {'value': '', 'label': _('Tous')}, + {'value': 'naturelocation', 'label': _('Voiries')}, + ], + }, + { + 'key': 'highway', + 'label': _('Ring et autoroutes'), + 'type': 'checkbox', + 'value': '1', + }, + ] + + # Water + if 'water' in accessible_codes: + config['water'] = [ + { + 'key': 'location_type', + 'label': _('Type de localisation'), + 'type': 'single', + 'options': [ + {'value': '', 'label': _('Tous')}, + {'value': 'naturerwiz', 'label': _('GIEP')}, + ], + }, + ] + + # Roads assets specific filter + if 'roads' in accessible_codes: + config.setdefault('roads', []).append({ + 'key': 'quality_level', + 'label': _('Niveau de qualité'), + 'type': 'multi', + 'options': quality_level_options, + }) + + # Sign assets specific filter + if 'sign' in accessible_codes: + config.setdefault('sign', []).append({ + 'key': 'panel_type', + 'label': _('Type de panneau'), + 'type': 'multi', + 'options': panel_type_options, + }) + + return config + + +def build_unified_add_modal_config(accessible_thematics, user=None): + """ + Returns the thematic choices allowed for the unified add modal. + + Asset creation uses thematic edit rights. Location creation is limited to + thematics that also expose a generic location form. + """ + from assets.forms import LOCATION_FORM_REGISTRY + from assets.permissions import ( + can_edit_location_for_thematic, + get_allowed_actions_for_thematic, + ) + + location_type_by_thematic = { + entry['thematic_code']: { + 'location_type': location_type, + 'label': entry.get('label', location_type), + } + for location_type, entry in LOCATION_FORM_REGISTRY.items() + } + + asset_thematics = [] + location_thematics = [] + + for thematic in accessible_thematics: + thematic_name = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic) + thematic_icon = thematic.icon if hasattr(thematic, 'icon') else '' + + if 'create' in get_allowed_actions_for_thematic(user, thematic): + asset_entry = { + 'code': thematic.code, + 'name': thematic_name, + 'icon': thematic_icon, + } + location_meta = location_type_by_thematic.get(thematic.code) + if location_meta: + asset_entry['location_type'] = location_meta['location_type'] + asset_thematics.append(asset_entry) + + location_meta = location_type_by_thematic.get(thematic.code) + if location_meta and can_edit_location_for_thematic(user, thematic.code): + location_thematics.append({ + 'code': thematic.code, + 'name': thematic_name, + 'icon': thematic_icon, + 'location_type': location_meta['location_type'], + 'location_label': location_meta['label'], + }) + + return { + 'asset_thematics': asset_thematics, + 'location_thematics': location_thematics, + } + + +def unified_assets_index(request): + """Unified asset/location index page combining all accessible thematics.""" + from django.utils.translation import get_language + from common.models import UserThematics + from interventions.views.drafts import InterventionDraftManager + + lang = get_language() or 'fr' + + # Thématiques accessibles à l'utilisateur + thematic_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + accessible_thematics = [tc.thematic for tc in thematic_qs] + + # Mode intervention préventive + preventive_draft_id = request.GET.get('preventive_draft_id') + locked_thematic_code = request.GET.get('thematic_code', '') + preventive_element_type = '' + selected_assets = [] + selected_locations_ids = [] + + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) if draft_data else {} + preventive_element_type = form_data.get('type_of_selected_elements', 'locations') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) or [] + + # Thématiques présélectionnées (GET ou fixées par le mode intervention) + selected_thematics = request.GET.getlist('thematic') or ( + [locked_thematic_code] if locked_thematic_code else [] + ) + selected_categories = request.GET.getlist('category') + + # Config JS par thématique (URLs GeoJSON, nom, icône) + thematic_config = build_thematic_config(accessible_thematics) + conditional_filter_config = build_conditional_filter_config(accessible_thematics, request.user) + add_modal_config = build_unified_add_modal_config(accessible_thematics, request.user) + + # Catégories groupées par thématique + thematic_categories = {} + for thematic in accessible_thematics: + cats = list(AssetCategory.objects.filter(thematic=thematic).order_by('name_fr')) + thematic_categories[thematic.code] = [ + {'id': c.id, 'name': c.get_name()} for c in cats + ] + + link_assets_url = ( + reverse( + 'interventions:intervention_preventive_link_elements', + kwargs={'thematic_code': locked_thematic_code}, + ) + if locked_thematic_code else '' + ) + + user_roles = request.user.config.roles.values_list('name', flat=True) if hasattr(request.user, 'config') else [] + is_admin = 'admin' in user_roles or request.user.is_superuser + has_bulk_edit_status_permission = is_admin or UserThematics.objects.filter(user_config=request.user.config, can_edit_assets=True).exists() + has_bulk_edit_validation_status_permission = is_admin or UserThematics.objects.filter(user_config=request.user.config, can_validate_assets=True).exists() + + context = { + 'accessible_thematics': accessible_thematics, + 'has_bulk_edit_status_permission': has_bulk_edit_status_permission, + 'has_bulk_edit_validation_status_permission': has_bulk_edit_validation_status_permission, + 'thematic_config_json': json.dumps(thematic_config), + 'conditional_filter_config_json': json.dumps(conditional_filter_config), + 'thematic_categories_json': json.dumps(thematic_categories), + 'asset_status_choices': list(ASSET_STATUS_CHOICES.items()), + 'asset_validation_status_choices': list(ASSET_VALIDATION_STATUS_CHOICES.items()), + 'add_modal_config_json': json.dumps(add_modal_config), + 'selected_thematics': selected_thematics, + 'selected_categories_json': json.dumps(selected_categories), + 'preventive_draft_id': preventive_draft_id, + 'locked_thematic_code': locked_thematic_code, + 'preventive_element_type': preventive_element_type, + 'selected_assets_json': json.dumps(selected_assets), + 'selected_locations_ids': selected_locations_ids, + 'link_assets_url': link_assets_url, + 'assets_index_prefs_json': json.dumps(request.user.config.assets_index_preferences or {}), + 'default_thematic_code': (request.user.config.default_thematic.code if request.user.config.default_thematic_id else ''), + } + return render(request, 'assets/unified_index.html', context) diff --git a/streetup/assets/views/export.py b/streetup/assets/views/export.py new file mode 100644 index 0000000..e7fc0ec --- /dev/null +++ b/streetup/assets/views/export.py @@ -0,0 +1,204 @@ +""" +Export views for assets. +""" +import csv +import json +import re +from io import BytesIO + +from django.contrib.auth.decorators import login_required +from django.contrib.contenttypes.models import ContentType +from django.http import HttpResponse, HttpResponseBadRequest +from django.utils.dateformat import format as dj_format_date +from django.utils.timezone import localtime, now +from django.utils.translation import gettext as _ +from django.views.decorators.http import require_POST + +from assets.models import AbstractLocation +from assets.permissions import can_view_asset + + +def _get_asset_location(asset): + """Return the parent location of an asset by inspecting FK fields.""" + for field in asset._meta.get_fields(): + related = getattr(field, 'related_model', None) + if related and issubclass(related, AbstractLocation): + try: + loc = getattr(asset, field.name) + if loc is not None: + return loc + except Exception: + pass + return None + + +def _get_asset_model_display(asset): + """Return the display name of the asset's model (subtype), if any.""" + try: + model_val = getattr(asset, 'model', None) + if model_val and hasattr(model_val, 'get_name'): + return model_val.get_name() + except Exception: + pass + return '' + + +def _fmt_date(dt): + if dt is None: + return '' + try: + return dj_format_date(dt, 'Y-m-d') + except Exception: + return str(dt) + + +def _get_row(model_name, asset): + location = _get_asset_location(asset) + loc_code = location.code if location else '' + loc_name = location.get_name() if location else '' + status_display = ( + asset.get_status_display() + if hasattr(asset, 'get_status_display') + else (getattr(asset, 'status', '') or '') + ) + obsolescence_display = ( + asset.get_obsolescence_display() + if hasattr(asset, 'get_obsolescence_display') + else (getattr(asset, 'obsolescence', '') or '') + ) + warranty = getattr(asset, 'warranty_duration', None) + return [ + model_name, + asset.id, + asset.code or '', + asset.get_name() if hasattr(asset, 'get_name') else '', + asset.category.get_name() if getattr(asset, 'category', None) else '', + _get_asset_model_display(asset), + status_display, + obsolescence_display, + loc_code, + loc_name, + _fmt_date(getattr(asset, 'installation_date', None)), + _fmt_date(getattr(asset, 'uninstallation_date', None)), + getattr(asset, 'serial_number', '') or '', + warranty if warranty is not None else '', + getattr(asset, 'brand', '') or '', + getattr(asset, 'note', '') or '', + getattr(asset, 'lon', '') if getattr(asset, 'lon', None) is not None else '', + getattr(asset, 'lat', '') if getattr(asset, 'lat', None) is not None else '', + ] + + +EXPORT_HEADERS = [ + _("Type"), + _("ID"), + _("Code"), + _("Nom"), + _("Catégorie"), + _("Modèle"), + _("Statut"), + _("Obsolescence"), + _("Code localisation"), + _("Nom localisation"), + _("Date d'installation"), + _("Date de déinstallation"), + _("Numéro de série"), + _("Garantie (mois)"), + _("Marque"), + _("Note"), + _("Longitude"), + _("Latitude"), +] + + +@login_required +@require_POST +def export_assets(request): + """Export selected assets as CSV or XLSX. + + POST params: + - ids: JSON string of asset keys (e.g. ``'["streetlamp:1","pole:2"]'``) + - format: ``'csv'`` (default) or ``'xlsx'`` + + Only assets that the current user is allowed to view are included. + """ + ids_param = request.POST.get('ids', '[]') + fmt = (request.POST.get('format') or 'csv').lower() + + try: + keys = json.loads(ids_param) + if not isinstance(keys, list): + raise ValueError + except (ValueError, TypeError, json.JSONDecodeError): + return HttpResponseBadRequest(_("Paramètre 'ids' invalide.")) + + if not keys: + return HttpResponseBadRequest(_("Aucun asset sélectionné.")) + + # Group keys by model name + groups: dict[str, list[int]] = {} + for key in keys: + try: + model_name, asset_id = str(key).rsplit(':', 1) + groups.setdefault(model_name.lower(), []).append(int(asset_id)) + except (ValueError, TypeError): + continue + + # Fetch assets, filtering by user view permission + items: list[tuple[str, object]] = [] + for model_name, asset_ids in groups.items(): + try: + ct = ContentType.objects.get(model=model_name) + model_class = ct.model_class() + if model_class is None: + continue + for asset in model_class.objects.filter(pk__in=asset_ids): + if can_view_asset(request.user, asset): + items.append((model_name, asset)) + except ContentType.DoesNotExist: + continue + + timestamp = localtime(now()).strftime('%Y%m%d_%H%M%S') + headers = [str(h) for h in EXPORT_HEADERS] + + if fmt == 'xlsx': + try: + from openpyxl import Workbook + except ImportError: + return HttpResponseBadRequest(_("Export XLSX indisponible (dépendance manquante).")) + + _illegal_re = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]') + + def _sanitize(value): + return _illegal_re.sub('', value) if isinstance(value, str) else value + + wb = Workbook() + ws = wb.active + ws.title = 'Assets' + ws.append(headers) + for model_name, asset in items: + ws.append([_sanitize(v) for v in _get_row(model_name, asset)]) + + output = BytesIO() + wb.save(output) + output.seek(0) + filename = f"assets_{timestamp}.xlsx" + resp = HttpResponse( + output.read(), + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ) + resp['Content-Disposition'] = f'attachment; filename="{filename}"' + return resp + + # Default: CSV with semicolon separator (better EU Excel support) + filename = f"assets_{timestamp}.csv" + resp = HttpResponse(content_type='text/csv; charset=utf-8') + resp['Content-Disposition'] = f'attachment; filename="{filename}"' + # UTF-8 BOM for Excel auto-detection + resp.write('\ufeff') + writer = csv.writer(resp, delimiter=';') + writer.writerow(headers) + for model_name, asset in items: + writer.writerow(_get_row(model_name, asset)) + + return resp diff --git a/streetup/assets/views/history.py b/streetup/assets/views/history.py new file mode 100644 index 0000000..8c03e2c --- /dev/null +++ b/streetup/assets/views/history.py @@ -0,0 +1,427 @@ +""" +API views for asset history. +""" +from django.http import JsonResponse +from django.views.decorators.http import require_GET +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ +from django.utils.dateparse import parse_datetime, parse_date +from django.utils import timezone +from django.contrib.auth.decorators import login_required +from functools import wraps +from assets.models import AssetHistory +from assets.views.asset_editing import _get_asset_by_ct +from assets.permissions import get_user_asset_thematic_access, _get_asset_thematic +from common.models import UserConfig +from datetime import datetime + + +def _check_user_thematic_access(user, asset, require_edit=False): + """ + Check if user has access to view an asset's history. + Returns (has_access, error_message) tuple. + """ + if not user.is_authenticated: + return False, JsonResponse({ + 'success': False, + 'error': _("Authentication required") + }, status=401) + + # Get asset's thematic + thematic = _get_asset_thematic(asset) + if not thematic: + return False, JsonResponse({ + 'success': False, + 'error': _("Cannot determine asset's thematic") + }, status=400) + + # Check user's permissions for this thematic + can_view, can_edit = get_user_asset_thematic_access(user, thematic) + + if require_edit and not can_edit: + return False, JsonResponse({ + 'success': False, + 'error': _("You do not have permission to edit assets for this thematic") + }, status=403) + + if not can_view: + return False, JsonResponse({ + 'success': False, + 'error': _("You do not have permission to view assets for this thematic") + }, status=403) + + return True, None + + +def _get_user_accessible_thematics(user): + """ + Get all thematics accessible to a user. + Returns list of Thematic objects. + """ + try: + user_config = UserConfig.objects.get(user=user) + user_thematics = user_config.userthematics.all() + return [ut.thematic for ut in user_thematics if ut.can_view_assets] + except UserConfig.DoesNotExist: + return [] + + +def _short_name(user): + """Return 'First L.' style short name for user.""" + if not user: + return "" + first = getattr(user, "first_name", "") or "" + last = getattr(user, "last_name", "") or "" + initial = (last[0].upper() + ".") if last else "" + return f"{first} {initial}".strip() + + +def history_view_required(require_edit=False): + """ + Decorator to check if user has permission to access history for a specific asset. + """ + def decorator(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + if not request.user.is_authenticated: + return JsonResponse({ + 'success': False, + 'error': _("Authentication required") + }, status=401) + + # The view will handle specific asset access checks + return view_func(request, *args, **kwargs) + return wrapper + return decorator + + +@require_GET +@history_view_required() +def get_asset_history(request, asset_model, asset_id, thematic_code=None): + """ + Get the history of changes for a specific asset. + Returns JSON with list of changes ordered by date (newest first). + Optional thematic_code parameter for URL structure compatibility. + + Permissions: User must have view_assets permission for the asset's thematic. + """ + # Check authentication first + if not request.user.is_authenticated: + return JsonResponse({ + 'success': False, + 'error': _("Authentication required") + }, status=401) + + try: + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + + # Verify asset belongs to specified thematic if provided + if thematic_code and hasattr(asset, 'thematic'): + if not asset.thematic or asset.thematic.code != thematic_code: + return JsonResponse({ + 'success': False, + 'error': f"Asset does not belong to thematic '{thematic_code}'" + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=404) + + # Check user permissions for this asset + has_access, error_response = _check_user_thematic_access(request.user, asset, require_edit=False) + if not has_access: + return error_response + + # Get history for this asset + history = AssetHistory.objects.filter( + content_type=content_type, + object_id=asset_id + ).select_related('user').order_by('-timestamp') + + # Format data for response + history_data = [] + for entry in history: + history_data.append({ + 'id': entry.id, + 'timestamp': entry.timestamp.isoformat(), + 'user': entry.user.get_full_name() if entry.user else _("Système"), + 'user_short_name': _short_name(entry.user) if entry.user else _("Système"), + 'user_username': entry.user.username if entry.user else None, + 'field_name': entry.field_name, + 'field_display_name': entry.get_field_display_name(), + 'old_value': entry.old_value, + 'new_value': entry.new_value, + 'old_value_display': entry.get_display_old_value(), + 'new_value_display': entry.get_display_new_value(), + 'action_type': entry.action_type, + 'action_type_display': entry.get_action_type_display(), + }) + + return JsonResponse({ + 'success': True, + 'history': history_data, + 'count': len(history_data) + }) + + +@require_GET +@history_view_required() +def get_global_history(request, thematic_code=None): + """ + Get recent history across all assets (for timeline views). + Supports pagination and filtering. + Optional thematic_code parameter to filter by thematic. + + Permissions: + - If thematic_code is specified, user must have view_assets permission for that thematic. + - If no thematic_code, user can only see history for thematics they have access to. + """ + # Get query parameters + limit = int(request.GET.get('limit', 50)) + offset = int(request.GET.get('offset', 0)) + thematic = thematic_code or request.GET.get('thematic', None) + action_type = request.GET.get('action_type', None) + user_id = request.GET.get('user_id', None) + + # Ensure limits are reasonable + limit = min(limit, 5000) # Cap at 5000 to prevent huge queries + offset = max(offset, 0) + + # Get user's accessible thematics + accessible_thematics = _get_user_accessible_thematics(request.user) + if not accessible_thematics: + return JsonResponse({ + 'success': False, + 'error': _("You do not have access to any thematics") + }, status=403) + + # Base query - only include content types from accessible thematics + from common.models import Thematic + accessible_thematic_codes = [t.code for t in accessible_thematics] + + # Get all asset categories for accessible thematics + asset_content_type_ids = [] + for thematic_obj in accessible_thematics: + categories = thematic_obj.asset_categories.all() + for category in categories: + if hasattr(category, 'asset_models'): + content_types = ContentType.objects.filter( + model__in=[m.lower() for m in category.asset_models] + ) + asset_content_type_ids.extend([ct.id for ct in content_types]) + + query = AssetHistory.objects.filter(content_type_id__in=asset_content_type_ids) + + # Apply additional filters + if thematic: + # User is requesting a specific thematic - verify they have access + try: + thematic_obj = Thematic.objects.get(code=thematic) + if thematic_obj not in accessible_thematics: + return JsonResponse({ + 'success': False, + 'error': _("You do not have permission to view this thematic") + }, status=403) + + categories = thematic_obj.asset_categories.all() + thematic_content_type_ids = [] + for category in categories: + if hasattr(category, 'asset_models'): + content_types = ContentType.objects.filter( + model__in=[m.lower() for m in category.asset_models] + ) + thematic_content_type_ids.extend([ct.id for ct in content_types]) + + if thematic_content_type_ids: + query = query.filter(content_type_id__in=thematic_content_type_ids) + except Thematic.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': _("Thematic not found") + }, status=404) + + if action_type: + query = query.filter(action_type=action_type) + + if user_id: + query = query.filter(user_id=user_id) + + # Get total count + total_count = query.count() + + # Apply pagination + history = query.select_related('user', 'content_type').order_by('-timestamp')[offset:offset + limit] + + # Format data + history_data = [] + for entry in history: + # Get asset info + asset_info = None + try: + asset = entry.asset + if asset: + asset_info = { + 'code': asset.code, + 'name': asset.get_name() if hasattr(asset, 'get_name') else str(asset), + 'model': entry.content_type.model, + 'id': asset.id, + } + except Exception: + # Asset may have been deleted; fallback to identifiers only + asset_info = { + 'model': entry.content_type.model, + 'id': entry.object_id, + } + + history_data.append({ + 'id': entry.id, + 'timestamp': entry.timestamp.isoformat(), + 'user': entry.user.get_full_name() if entry.user else _("Système"), + 'user_short_name': _short_name(entry.user) if entry.user else _("Système"), + 'user_username': entry.user.username if entry.user else None, + 'field_name': entry.field_name, + 'field_display_name': entry.get_field_display_name(), + 'old_value_display': entry.get_display_old_value(), + 'new_value_display': entry.get_display_new_value(), + 'action_type': entry.action_type, + 'action_type_display': entry.get_action_type_display(), + 'asset': asset_info, + }) + + return JsonResponse({ + 'success': True, + 'history': history_data, + 'count': len(history_data), + 'total': total_count, + 'has_more': (offset + limit) < total_count + }) + + +@require_GET +@history_view_required() +def get_asset_status_at_date(request, asset_model, asset_id, thematic_code=None): + """ + Get the status of an asset at a specific date. + Query params: + - date (ISO format, e.g., 2025-01-23 or 2025-01-23T15:30:00) + - app_label (optional, to disambiguate model names, e.g., 'assets' or 'sign') + URL params: + - thematic_code (optional, for URL structure, validates asset belongs to thematic) + + Permissions: User must have view_assets permission for the asset's thematic. + """ + # Check authentication first + if not request.user.is_authenticated: + return JsonResponse({ + 'success': False, + 'error': _("Authentication required") + }, status=401) + + # Parse date parameter + date_str = request.GET.get('date') + if not date_str: + return JsonResponse({ + 'success': False, + 'error': _("Paramètre 'date' manquant") + }, status=400) + + try: + # Try to parse as datetime first, then as date + try: + target_date = parse_datetime(date_str) + if not target_date: + parsed_date = parse_date(date_str) + if not parsed_date: + raise ValueError("Invalid date format") + target_date = datetime.combine(parsed_date, datetime.min.time()) + target_date = timezone.make_aware(target_date) + except: + return JsonResponse({ + 'success': False, + 'error': _("Format de date invalide. Utilisez ISO 8601.") + }, status=400) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=400) + + # Get optional app_label to disambiguate if multiple models with same name exist + app_label = request.GET.get('app_label') + + try: + # Try to resolve the asset model (handle multiple ContentType matches) + if app_label: + content_type = ContentType.objects.get(app_label=app_label, model=asset_model.lower()) + else: + # First try to get via _get_asset_by_ct (backward compatible) + try: + asset, model_class, content_type = _get_asset_by_ct(asset_model, asset_id) + except Exception: + # If fails, try to find the model + content_types = list(ContentType.objects.filter(model=asset_model.lower())) + if not content_types: + raise ContentType.DoesNotExist(f"Model {asset_model} not found") + if len(content_types) > 1: + # Multiple matches - return error with options + return JsonResponse({ + 'success': False, + 'error': f"Multiple models found: {', '.join([f'{ct.app_label}.{ct.model}' for ct in content_types])}. Specify app_label parameter.", + 'options': [{'app_label': ct.app_label, 'model': ct.model} for ct in content_types] + }, status=400) + content_type = content_types[0] + except ContentType.DoesNotExist as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=404) + + # Get the asset to verify it exists + try: + asset_model_class = content_type.model_class() + asset = asset_model_class.objects.get(pk=asset_id) + + # Verify asset belongs to specified thematic if provided + if thematic_code and hasattr(asset, 'thematic'): + if not asset.thematic or asset.thematic.code != thematic_code: + return JsonResponse({ + 'success': False, + 'error': f"Asset does not belong to thematic '{thematic_code}'" + }, status=404) + except asset_model_class.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': f"{asset_model} with id {asset_id} not found" + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=404) + + # Check user permissions for this asset + has_access, error_response = _check_user_thematic_access(request.user, asset, require_edit=False) + if not has_access: + return error_response + + # Get status at date + status = AssetHistory.objects.get_asset_status_at_date(asset, target_date) + + from assets.models.core import ASSET_STATUS_CHOICES + status_display_dict = dict(ASSET_STATUS_CHOICES) + + return JsonResponse({ + 'success': True, + 'asset_id': asset.id, + 'asset_code': asset.code, + 'asset_model': f"{content_type.app_label}.{content_type.model}", + 'date': target_date.isoformat(), + 'status': status, + 'status_display': status_display_dict.get(status, _('Inexistant')) if status else _('Inexistant') + }) diff --git a/streetup/assets/views/its.py b/streetup/assets/views/its.py new file mode 100644 index 0000000..100aef0 --- /dev/null +++ b/streetup/assets/views/its.py @@ -0,0 +1,863 @@ +# --------------------- +# ITS Views +# --------------------- + +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.urls import reverse +from django.views.decorators.http import require_GET, require_POST +from django.contrib.gis.db.models.functions import Transform +from django.contrib.gis.geos import GEOSGeometry, Point +from django.contrib.gis.db.models.functions import Distance +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q, Case, When, IntegerField, CharField +from django.db.models.functions import Lower +from django.db.models import Func, Prefetch +from django.db import connection +from django.template.loader import render_to_string +from django.utils.translation import get_language, gettext as _ + + +import unicodedata +import json +from collections import defaultdict + +from common.utils import check_thematic_access + +from common.models import Thematic, UserConfig, UserContractAccess, UserThematics +from assets.models import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory +from assets.permissions import can_edit_location_for_thematic, get_allowed_actions_for_thematic + +from assets.forms import ITSLocationDocumentForm + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + + + +@check_thematic_access('its') +def its_index(request): + thematic = 'its' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_is_main_location = request.GET.get('is_main_location') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + categories = list(AssetCategory.objects.filter(thematic__code = 'its')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(ITSAssetModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + category_model_map = defaultdict(list) + models = ITSAssetModel.objects.all() + for model in models: + if model.category: + display_name = model.get_name() + category_model_map[str(model.category_id)].append({ + "key": str(model.id), + "label": display_name, + }) + + context = { + 'thematics': thematics, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': dict(category_model_map), + 'status_choices': ITSGeoAsset._meta.get_field('status').choices, + 'selected_thematic': selected_thematic, + 'selected_is_main_location': selected_is_main_location, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'selected_statuses': statuses, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Localisations"), + 'locations_assets_title': "Assets", + 'can_create_location': can_edit_location_for_thematic(request.user, 'its'), + 'location_type': 'itslocation', + 'thematic_code': 'its', + 'can_create_asset': 'create' in get_allowed_actions_for_thematic( + request.user, Thematic.objects.filter(code='its').first() + ), + 'its_categories': categories, + } + + return render(request, "assets/its_index.html", context) + + + +def its_locations(request): + pass + +def its_assets(request): + pass + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +@check_thematic_access('its') +def its_locations_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + is_main_location = data.get('is_main_location') + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + is_main_location = request.GET.get('is_main_location') + filter_geom = None + + # --------- Point d'entrée du queryset + its_locations = ITSLocation.objects.all() + + + # --------- Filtre par localisation principale + if is_main_location: + val = is_main_location.lower() + if val == 'true': + its_locations = its_locations.filter(is_main_location=True) + elif val == 'false': + its_locations = its_locations.filter(is_main_location=False) + + # --------- Filtre géométrique + if filter_geom: + its_locations = its_locations.filter(geom__intersects=filter_geom) + + # --------- Limitation pour utilisateurs externes + user = request.user + if user.is_authenticated: + try: + user_config = user.config # OneToOne UserConfig + except UserConfig.DoesNotExist: + user_config = None + + # externe = pas interne ; on tient aussi compte du flag limit_assets_to_contracts + 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: + # contrats accessibles à l'utilisateur (idéalement avec droit "assets") + accessible_contract_ids = UserContractAccess.objects.filter( + user_config=user_config, + can_view_assets=True # champ prévu dans la table d'accès contrat + ).values_list('contract_id', flat=True) + + # Ne garder que les locations dont le centre de contrôle a un contrat actif lié à ces contrats + its_locations = its_locations.filter( + its_contracts__status='active', # ITSContract.status + its_contracts__contract_id__in=accessible_contract_ids # FK contract + ).distinct() + else: + # Non authentifié : ne rien exposer (option sûre) + its_locations = its_locations.none() + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + its_locations = apply_search(its_locations, data) + + # --------- Conversion en GeoJSON + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None # AbstractLocation.geojson # :contentReference[oaicite:16]{index=16} + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": "Zone ITS", + "thematic": "its", + "is_location": True, + } + } + + features = [] + for location in its_locations.order_by('code'): + features.append(obj_to_feature(location)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('its') +def its_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + its_location_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + its_location_id = request.GET.get('location') + its_location_ids = [its_location_id] if its_location_id else [] + statuses = request.GET.getlist('asset_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + filter_geom = None + + # Pas de paramètres = pas de carte (comportement existant conservé) + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not its_location_ids and not categories and not models and not filter_geom and not _search_q: + geojson_data = {"type": "FeatureCollection", "features": []} + return JsonResponse(geojson_data) + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.lon + lat = obj.lat + + if lon is None or lat is None: + location = getattr(obj, 'location', None) + if location is not None: + if lon is None: + lon = getattr(location, 'lon', None) + if lat is None: + lat = getattr(location, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "category": obj.category.get_name() if obj.category else None, + "model": obj.model.get_name() if obj.model else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "its", + "is_location": False, + }, + } + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + # --------- Query de base + optimisations + its_asset_qs = ( + ITSGeoAsset.objects + .select_related('location', 'category', 'model') + .all() + ) + + # --------- Filtres métier existants + if its_location_ids: + its_asset_qs = its_asset_qs.filter(location__id__in=its_location_ids) + + if models: + its_asset_qs = its_asset_qs.filter(model__id__in=models) + + if categories: + its_asset_qs = its_asset_qs.filter(category__id__in=categories) + + if filter_geom: + its_asset_qs = its_asset_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, location__geom__intersects=filter_geom) | + Q(geom__isnull=True, location__isnull=True) + ) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + its_asset_qs = apply_search(its_asset_qs, data) + + # --------- Limitation pour utilisateurs externes + user = request.user + if user.is_authenticated: + try: + user_config = user.config # OneToOne + 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) + + # Ne garder que les assets qui ont un contrat actif + # auquel l'utilisateur a accès + its_asset_qs = its_asset_qs.filter( + its_assets_contracts__status='active', + its_assets_contracts__contract_id__in=accessible_contract_ids + ).distinct() + else: + # Non authentifié : ne rien exposer (plus sûr) + its_asset_qs = its_asset_qs.none() + + # --------- Cap de sécurité + its_asset_qs = its_asset_qs[0:MAX_ASSET_FEATURES+1] + if len(its_asset_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + # --------- Construction GeoJSON + features = [] + feature_count = 0 + for qs, asset_type in [ + (its_asset_qs, "itsgeoasset"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + +@check_thematic_access('its') +def its_locations_detail(request, location_id): + # Préfetch (réduction N+1) : ASSETS + enfants (2 niveaux) + location = ( + ITSLocation.objects + .prefetch_related( + Prefetch( + 'assets', + queryset=ITSGeoAsset.objects.filter(status='active').order_by('code') + ), + 'child_locations__assets', + 'child_locations__child_locations', + ) + .get(pk=location_id) + ) + + # 1) Construire la liste des ids (location + enfants) + def collect_ids(loc): + ids = [loc.id] + for child in loc.child_locations.all().order_by('code'): + ids.extend(collect_ids(child)) + return ids + + all_loc_ids = collect_ids(location) + + # 2) Récupérer en bloc tous les liens GFK pour ces ids + ct_sl = ContentType.objects.get_for_model(ITSLocation) + links = ( + InterventionLocation.objects + .filter(content_type=ct_sl, object_id__in=all_loc_ids) + .select_related('intervention') + ) + + # 3) Indexer par location_id → [interventions...] + by_loc = defaultdict(list) + for link in links: + by_loc[link.object_id].append(link.intervention) + + # 4) Arbre + def build_tree(loc): + return { + 'loc': loc, + 'assets': list(loc.assets.all().order_by('code')), + 'interventions': by_loc.get(loc.id, []), + 'children': [build_tree(child) for child in loc.child_locations.all().order_by('code')], + } + + tree = build_tree(location) + + # 5) Aplatir les interventions de l’arbre (sans doublons) + tri global desc. par code + def flatten_interventions(node): + seen = set() + out = [] + def walk(n): + for itv in n['interventions']: + if itv.pk not in seen: + seen.add(itv.pk) + out.append(itv) + for c in n['children']: + walk(c) + walk(node) + return out + + interventions = sorted( + flatten_interventions(tree), + key=lambda itv: itv.creation_time, + reverse=True + ) + + # 6) Assets + documents + assets = location.get_active_assets() + + docs = list(location.documents.filter(status='active').order_by('-id')) + for doc in docs: + doc._delete_url = reverse('assets:its_location_delete_document', args=[doc.id]) + + # 7) Géométrie 4326 (main_location fallback) + if location.geom: + location_geom_4326 = location.geom.transform(4326, clone=True) + else: + location_geom_4326 = None + + geom = json.loads(location_geom_4326.geojson) if location_geom_4326 else None + + return render(request, "assets/its_location_detail.html", { + "location": location, + "interventions": interventions, # toutes les interventions de l’arbre + "assets": assets, + "documents": docs, + "add_document_form": ITSLocationDocumentForm(), + "geom": geom, + "location_tree": tree, # contient interventions par nœud + }) + + + +@check_thematic_access('its') +def its_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(f"Modèle '{asset_model}' inconnu.") + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"its/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for its + context['thematic_code'] = 'its' + + return render(request, template.template.name, context) + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + + +def get_descendant_location_ids(root_location_id: int) -> list[int]: + """ + Récupère l'id de la localisation racine + tous ses descendants (FK parent_location) via un CTE récursif. + Nécessite PostgreSQL. + """ + # Récupérer dynamiquement le nom de la table depuis le modèle Django + table_name = ITSLocation._meta.db_table + + with connection.cursor() as cur: + cur.execute(f""" + WITH RECURSIVE loc_tree AS ( + SELECT id + FROM {table_name} + WHERE id = %s + UNION ALL + SELECT sl.id + FROM {table_name} sl + JOIN loc_tree lt ON sl.parent_location_id = lt.id + ) + SELECT id FROM loc_tree; + """, [root_location_id]) + rows = cur.fetchall() + return [r[0] for r in rows] + + +@require_GET +def its_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + # CT ids (calculés une fois) + equipment_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id + location_ct_id = ContentType.objects.get_for_model(ITSLocation).id + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + # filtres optionnels + location_id = request.GET.get('location_id') + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # si location fournie, calcule l'ensemble {location + descendants} + location_ids = None + if location_id: + try: + location_ids = get_descendant_location_ids(int(location_id)) + except Exception: + # en cas d'ID invalide ou autre, on ignore le filtre + location_ids = None + + # Base queryset accent-insensible + actifs uniquement + qs = ( + ITSGeoAsset.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + ) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + qs = qs.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + qs = qs.exclude(status='removed') # <-- exclure les supprimés + qs = qs.select_related( + 'location', + 'model', + 'category', + ) + + if location_ids: + qs = qs.filter(location_id__in=location_ids) + if category_id is not None: + qs = qs.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + + # Priorité : exact > code startswith > nom startswith > contient + if has_search_term: + priority = Case( + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + default=7, + output_field=IntegerField(), + ) + qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100] + else: + # Sans terme de recherche, tri par code uniquement + qs = qs.order_by('code')[:100] + + data = [] + for a in qs: + # equipment_type depuis model/category + equipment_type = None + if getattr(a, 'model', None): + equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None) + if not equipment_type and getattr(a, 'category', None): + equipment_type = getattr(a.category, 'name', None) + + # coordonnées: asset > geom(4326) > location + lon = a.lon + lat = a.lat + + if (lon is None or lat is None) and getattr(a, 'geom', None): + try: + g = a.geom + if g.srid and g.srid != 4326: + g = a.geom.transform(4326, clone=True) + coords = getattr(g, 'coords', None) + if coords: + if lon is None: lon = coords[0] + if lat is None: lat = coords[1] + except Exception: + pass + + if (lon is None or lat is None) and getattr(a, 'location', None): + if lon is None and getattr(a.location, 'lon', None) is not None: + lon = a.location.lon + if lat is None and getattr(a.location, 'lat', None) is not None: + lat = a.location.lat + + + equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name() + label = f"{a.code} - {equipment_label}" if a.code else equipment_label + + location_label = ( + f"{a.location.get_name()} ({a.location.code})" + if getattr(a, "location", None) else None + ) + + data.append({ + "id": a.id, + "label": label, + + "equipment_id": a.id, + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": a.location.id if getattr(a, "location", None) else None, + "location_label": location_label, + "location_ct": location_ct_id, + + "lon": lon, + "lat": lat, + }) + + return JsonResponse(data, safe=False) + + + +@check_thematic_access('its', edit_permission_required=True) +@require_POST +def its_location_add_document(request, location_id): + location = get_object_or_404(ITSLocation, pk=location_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + for i in range(count): + file = files[i] + document_type = request.POST.get(f'document_type_{i}', 'other') + + ITSLocationDocument.objects.create( + location=location, + file=file, + document_type=document_type, + uploaded_by = request.user + ) + return redirect('assets:its_locations_detail', location_id=location.id) + + +@check_thematic_access('its', edit_permission_required=True) +@require_POST +def its_location_delete_document(request, document_id): + document = get_object_or_404(ITSLocationDocument, id=document_id) + location_id = document.location.id + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({'success': True}) + return redirect('assets:its_locations_detail', location_id=location_id) + + + + + +def _compute_depth(loc, cache): + """Nombre d'ancêtres via parent_location (racine => profondeur 0).""" + if loc is None: + return -1 + if loc.id in cache: + return cache[loc.id] + depth = 0 + cur = loc + # NB: select_related('parent_location') limite les accès DB au 1er parent, + # mais on mémorise au fur et à mesure pour éviter les recalculs. + while cur.parent_location_id: + if cur.parent_location_id in cache: + depth += (cache[cur.parent_location_id] + 1) + break + depth += 1 + cur = cur.parent_location + cache[loc.id] = depth + return depth + +def _pick_deepest(candidates): + """Retourne l'objet avec profondeur max; tie-break sur .distance si présent.""" + if not candidates: + return None + depth_cache = {} + best = None + best_key = None # (depth, -distance) car on veut depth max et distance min + for obj in candidates: + d = _compute_depth(obj, depth_cache) + # distance peut ne pas exister (cas 'covers'): on met None => tie-break neutre + dist_val = getattr(obj, 'distance', None) + # On inverse la distance pour que 'min' devienne un tie-break correct + key = (d, -float(dist_val.m) if dist_val is not None else 0.0) + if best is None or key > best_key: + best, best_key = obj, key + return best + +@require_GET +def get_nearest_its_location(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + # 1) Candidats qui couvrent le point (frontière incluse) + covering_qs = ( + ITSLocation.objects + .filter(geom__isnull=False, geom__covers=user_point) + .select_related('parent_location') # aide pour la 1ère montée de parent + ) + covering = list(covering_qs) + chosen = _pick_deepest(covering) + + # 2) Fallback: candidats dans un rayon de 50 m + if chosen is None: + nearby_qs = ( + ITSLocation.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) + .select_related('parent_location') + ) + nearby = list(nearby_qs) + chosen = _pick_deepest(nearby) + + if not chosen: + return JsonResponse({"error": _("Aucune localisation trouvée")}, status=404) + + location_ct_id = ContentType.objects.get_for_model(chosen).id + return JsonResponse({ + "location_id": chosen.id, + "location_ct": location_ct_id, + "location_label": f"{chosen.code} - {chosen.get_name()}", + "lon": chosen.lon, + "lat": chosen.lat, + }) + \ No newline at end of file diff --git a/streetup/assets/views/location_editing.py b/streetup/assets/views/location_editing.py new file mode 100644 index 0000000..7b7ade4 --- /dev/null +++ b/streetup/assets/views/location_editing.py @@ -0,0 +1,221 @@ +# -------------------- +# Location Editing Views (generic: create / update / delete) +# -------------------- +""" +Generic CRUD views for editable location types. +New location types can be supported by adding an entry to +assets.forms.LOCATION_FORM_REGISTRY. + +URL conventions (see assets/urls.py): + GET locations///form/ + → Returns an HTML fragment with an empty form (for create). + GET locations////form/ + → Returns an HTML fragment pre-filled with the existing location data. + POST locations///create/ + → Creates a new location; returns JSON with {success, redirect_url, errors}. + POST locations////update/ + → Updates the location; returns JSON with {success, errors}. + POST locations////delete/ + → Deletes the location; returns JSON with {success, redirect_url}. +""" + +from django.http import JsonResponse, Http404 +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from django.views.decorators.http import require_GET, require_POST +from django.utils.translation import gettext as _ +from django.contrib.gis.geos import Point, GEOSGeometry, MultiPolygon, Polygon + +from assets.permissions import can_edit_location_for_thematic +from assets.forms import LOCATION_FORM_REGISTRY + + +def _get_registry_entry(thematic_code, location_type): + """Return registry entry or raise Http404.""" + entry = LOCATION_FORM_REGISTRY.get(location_type) + if not entry: + raise Http404(f"Location type '{location_type}' not found in registry.") + if entry.get('thematic_code') != thematic_code: + raise Http404(f"Location type '{location_type}' does not belong to thematic '{thematic_code}'.") + return entry + + +def _check_permission(request, thematic_code): + """Return 403 JSON response if the user cannot edit locations, otherwise None.""" + if not request.user.is_authenticated: + return JsonResponse({'success': False, 'error': _("Authentification requise.")}, status=401) + if not can_edit_location_for_thematic(request.user, thematic_code): + return JsonResponse({'success': False, 'error': _("Permission refusée.")}, status=403) + return None + + +@require_GET +def location_form_html(request, thematic_code, location_type, location_id=None): + """ + Returns an HTML fragment containing the form to create or edit a location. + Used by the frontend to populate a Bootstrap modal dynamically. + """ + if not request.user.is_authenticated or not can_edit_location_for_thematic(request.user, thematic_code): + return JsonResponse({'error': _("Permission refusée.")}, status=403) + + entry = _get_registry_entry(thematic_code, location_type) + FormClass = entry['form_class'] + model_class = FormClass._meta.model + + instance = None + if location_id: + instance = get_object_or_404(model_class, pk=location_id) + + form = FormClass(instance=instance) + + html = render_to_string( + 'assets/includes/location_form_fragment.html', + { + 'form': form, + 'location_type': location_type, + 'thematic_code': thematic_code, + 'instance': instance, + 'entry_label': entry.get('label', location_type), + }, + request=request, + ) + return JsonResponse({'html': html}) + + +@require_POST +def create_location(request, thematic_code, location_type): + """Create a new location of the given type for the given thematic.""" + denied = _check_permission(request, thematic_code) + if denied: + return denied + + entry = _get_registry_entry(thematic_code, location_type) + FormClass = entry['form_class'] + + form = FormClass(request.POST) + if form.is_valid(): + instance = form.save(commit=False) + # Sync geom from lat/lon if provided and geom is a PointField + _sync_geom(instance, form.cleaned_data) + instance.save() + redirect_url = instance.get_absolute_url() if hasattr(instance, 'get_absolute_url') else None + return JsonResponse({ + 'success': True, + 'redirect_url': redirect_url, + 'id': instance.pk, + 'label': str(instance), + }) + else: + return JsonResponse({'success': False, 'errors': form.errors}, status=400) + + +@require_POST +def update_location(request, thematic_code, location_type, location_id): + """Update an existing location.""" + denied = _check_permission(request, thematic_code) + if denied: + return denied + + entry = _get_registry_entry(thematic_code, location_type) + FormClass = entry['form_class'] + model_class = FormClass._meta.model + + instance = get_object_or_404(model_class, pk=location_id) + form = FormClass(request.POST, instance=instance) + if form.is_valid(): + updated = form.save(commit=False) + _sync_geom(updated, form.cleaned_data) + updated.save() + return JsonResponse({'success': True, 'label': str(updated)}) + else: + return JsonResponse({'success': False, 'errors': form.errors}, status=400) + + +@require_POST +def delete_location(request, thematic_code, location_type, location_id): + """Delete an existing location.""" + denied = _check_permission(request, thematic_code) + if denied: + return denied + + entry = _get_registry_entry(thematic_code, location_type) + FormClass = entry['form_class'] + model_class = FormClass._meta.model + + instance = get_object_or_404(model_class, pk=location_id) + + # Build listing redirect URL before deleting + redirect_url = _get_listing_url(thematic_code) + instance.delete() + return JsonResponse({'success': True, 'redirect_url': redirect_url}) + + +def _sync_geom(instance, cleaned_data): + """ + Synchronise the geometry field of *instance* from the form's cleaned data. + + - Point / MultiPoint fields: built from lat / lon (WGS84). + - Polygon / MultiPolygon fields: built from a GeoJSON string stored in + geom_geojson (drawn by the polygon picker on the frontend). + + The geometry is reprojected to the field's native SRID when needed. + """ + if not hasattr(instance, 'geom'): + return + + from django.contrib.gis.db.models import PointField, MultiPointField + field = type(instance)._meta.get_field('geom') + geom_type = getattr(field, 'geom_type', '').upper() + + if geom_type in ('POINT', 'MULTIPOINT') or isinstance(field, (PointField, MultiPointField)): + lat = cleaned_data.get('lat') + lon = cleaned_data.get('lon') + if lat is not None and lon is not None: + srid = getattr(field, 'srid', 4326) + if srid == 4326: + instance.geom = Point(lon, lat, srid=4326) + else: + pt = Point(lon, lat, srid=4326) + pt.transform(srid) + instance.geom = pt + + else: + # Polygon / MultiPolygon (and any other geometry): read GeoJSON drawn + # by the polygon picker widget. + geojson_str = (cleaned_data.get('geom_geojson') or '').strip() + if not geojson_str: + return + try: + geom = GEOSGeometry(geojson_str, srid=4326) + # Promote a simple Polygon to MultiPolygon if the field expects it + if geom_type == 'MULTIPOLYGON' and isinstance(geom, Polygon): + geom = MultiPolygon(geom, srid=4326) + srid = getattr(field, 'srid', 4326) + if srid != 4326: + geom.transform(srid) + instance.geom = geom + except Exception: + pass + + +def _get_listing_url(thematic_code): + """Return the URL of the listing page for a given thematic, used after deletion.""" + from django.urls import reverse, NoReverseMatch + listing_url_names = { + 'trafficlights': 'assets:trafficlights_index', + 'publiclighting': 'assets:publiclighting_index', + 'sign': 'assets:sign_index', + 'roads': 'assets:roads_index', + 'structures': 'assets:structures_index', + 'nature': 'assets:nature_index', + 'controlcenters': 'assets:controlcenters_index', + 'its': 'assets:its_index', + 'clean': 'assets:clean_index', + } + url_name = listing_url_names.get(thematic_code) + if url_name: + try: + return reverse(url_name) + except NoReverseMatch: + pass + return '/' diff --git a/streetup/assets/views/locations.py b/streetup/assets/views/locations.py new file mode 100644 index 0000000..71a5fa5 --- /dev/null +++ b/streetup/assets/views/locations.py @@ -0,0 +1,304 @@ +# locations_registry.py +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import FieldDoesNotExist +from django.db.models import Q, Case, When, IntegerField, Value, CharField +from django.db.models.functions import Lower, Coalesce, Concat +from django.utils.translation import get_language, gettext as _ +from django.views.decorators.http import require_GET +from django.http import JsonResponse + +from common.utils import strip_accents_lower, Unaccent + +from typing import Callable, Optional + +from assets.models import StructureLocation +from assets.models import TrafficLightIntersection +from assets.models import PublicLightingStreet +from assets.models import NatureLocation, NatureRWIZ +from assets.models import ControlCenterLocation +from assets.models import SignStreet +from assets.models import RoadStreet +from assets.models import CleanLocation + + +LOCATION_REGISTRY = { + "structures_location": {"thematic": "structures", "model": StructureLocation, "label": _("Ouvrages"), "parent": {"attr": "structure"},}, + "trafficlights_intersection": {"thematic": "trafficlights", "model": TrafficLightIntersection,"label": _("Feux")}, + "publiclighting_street":{"thematic":"publiclighting", "model": PublicLightingStreet, "label": _("Éclairage")}, + "green_section": {"thematic": "nature", "model": NatureLocation, "label": _("Nature")}, + "naturerwiz": {"thematic": "water", "model": NatureRWIZ, "label": _("GIEP")}, + "controlcenters_location": {"thematic": "controlcenters", "model": ControlCenterLocation, "label": _("Centres de contrôle"), "parent": {"attr": "control_center"},}, + "sign_street":{"thematic":"sign", "model": SignStreet, "label": _("Signalisation")}, + "roads_street":{"thematic":"roads", "model": RoadStreet, "label": _("Voirie")}, + "clean_location":{"thematic":"clean", "model": CleanLocation, "label": _("Propreté")}, +} + +ASSETS_LOCATION_REGISTRY = ['location', 'intersection', 'street'] + + +def registry_items_for_thematics(thematics: list[str] | None): + items = LOCATION_REGISTRY.items() + if thematics: + items = [(k,v) for (k,v) in items if v["thematic"] in thematics] + return items + +def content_type_for_model(Model): + return ContentType.objects.get_for_model(Model) + + + +SEARCH_LIMIT_PER_TYPE = 25 +SEARCH_LIMIT_TOTAL = 50 + + +def base_search_qs(Model, qn: str, is_nl: bool): + qs = ( + Model.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + .filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + ) + + prio = Case( + # code exact / startswith d'abord + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + # puis startswith/contains dans la langue active avant l'autre + *(( + When(name_nl_u__startswith=qn, then=2), + When(name_fr_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_nl_u__contains=qn, then=5), + When(name_fr_u__contains=qn, then=6), + ) if is_nl else ( + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + )), + default=7, + output_field=IntegerField(), + ) + return qs.annotate(_prio=prio) + + +def search_locations( + raw_query: str, + thematics: list[str] | None = None, + label_builder: Optional[Callable[[str | None, str | None, dict], str]] = None, + prioritize: str | None = None, +): + """ + Retourne une liste d'items prêts pour l'UI: + { + "type": "...", "type_label": "...", + "ct": , "obj": , + "name": , "code": , + "label": " ( - )", + "lon": ..., "lat": ... + } + """ + qn = strip_accents_lower((raw_query or "").strip()) + if len(qn) < 2: + return [] + + lang = (get_language() or "fr").split("-")[0] + is_nl = (lang == "nl") + + # Label par défaut: "name_lang (code)" — surchargable via label_builder + def _default_label_builder(code: str | None, name_lang: str | None, ctx: dict) -> str: + name_lang = (name_lang or "").strip() + code = (code or "").strip() + return f"{name_lang} ({code})" if name_lang and code else (name_lang or code) + + build_label = label_builder or _default_label_builder + + items = registry_items_for_thematics(thematics) + if not items: + return [] + + ancestor_coords_cache: dict[tuple[str, int], tuple[float | None, float | None]] = {} + + def _coords_from_ancestors(Model, location_id: int | None) -> tuple[float | None, float | None]: + """Retourne (lon, lat) d'un parent (récursif) en cachant les lectures DB.""" + if not location_id: + return (None, None) + + cache_key = (Model._meta.label_lower, location_id) + if cache_key in ancestor_coords_cache: + return ancestor_coords_cache[cache_key] + + data = ( + Model.objects + .filter(pk=location_id) + .values("lon", "lat", "parent_location_id") + .first() + ) + if not data: + ancestor_coords_cache[cache_key] = (None, None) + return (None, None) + + lon, lat = data["lon"], data["lat"] + parent_id = data.get("parent_location_id") + if (lon is None or lat is None) and parent_id: + parent_lon, parent_lat = _coords_from_ancestors(Model, parent_id) + if lon is None: + lon = parent_lon + if lat is None: + lat = parent_lat + + ancestor_coords_cache[cache_key] = (lon, lat) + return lon, lat + + out = [] + for type_key, meta in items: + Model = meta["model"] + ct = content_type_for_model(Model) + + + # --- champs dynamiques à récupérer --- + fields = ["id", "code", "name_fr", "name_nl", "lon", "lat"] + parent_attr = (meta.get("parent") or {}).get("attr") + if parent_attr: + fields += [ + f"{parent_attr}_id", + f"{parent_attr}__name_fr", + f"{parent_attr}__name_nl", + f"{parent_attr}__code", + ] + + has_parent_location = False + try: + Model._meta.get_field("parent_location") + has_parent_location = True + fields.append("parent_location_id") + except FieldDoesNotExist: + has_parent_location = False + + rows = ( + base_search_qs(Model, qn, is_nl) + .order_by("_prio", "code") + .values(*fields)[:SEARCH_LIMIT_PER_TYPE] + ) + for r in rows: + name_lang = r["name_nl"] if is_nl else r["name_fr"] + if not name_lang: + name_lang = r["name_fr"] or r["name_nl"] or "" + + # --- parent de référence (optionnel) --- + parent_ref_id = None + parent_ref_label = None + if parent_attr: + pid = r.get(f"{parent_attr}_id") + if pid: + # nom du parent dans la langue active (+ fallback) + pname = r.get(f"{parent_attr}__name_nl") if is_nl else r.get(f"{parent_attr}__name_fr") + if not pname: + pname = r.get(f"{parent_attr}__name_fr") or r.get(f"{parent_attr}__name_nl") or "" + pcode = r.get(f"{parent_attr}__code") + parent_ref_id = pid + parent_ref_label = f"{pname} ({pcode})" if pcode else pname + + lon = r["lon"] + lat = r["lat"] + if (lon is None or lat is None) and has_parent_location: + ancestor_lon, ancestor_lat = _coords_from_ancestors(Model, r.get("parent_location_id")) + if lon is None: + lon = ancestor_lon + if lat is None: + lat = ancestor_lat + + ctx = { + "type": type_key, + "type_label": meta["label"], + "ct": ct.id, + "obj": r["id"], + "lon": lon, + "lat": lat, + } + + out.append({ + "type": type_key, + "type_label": meta["label"], + "ct": ct.id, + "obj": r["id"], + "name": name_lang, + "code": r["code"], + "label": build_label(r["code"], name_lang, ctx), + "lon": lon, + "lat": lat, + "thematic": meta.get("thematic"), + "parent_ref_id": parent_ref_id, + "parent_ref_label": parent_ref_label, + }) + + # Tri global léger par label (les per-type sont déjà ordonnés par priorité) + prioritize_type = None + if prioritize: + p_lower = prioritize.lower().strip() + if p_lower in LOCATION_REGISTRY: + prioritize_type = p_lower + elif "rwi" in p_lower or "rwiz" in p_lower or "giep" in p_lower: + prioritize_type = "naturerwiz" + else: + for key, meta in LOCATION_REGISTRY.items(): + if p_lower in key.lower() or p_lower in meta["model"].__name__.lower(): + prioritize_type = key + break + + if prioritize_type: + out.sort(key=lambda r: (0 if r["type"] == prioritize_type else 1, r["label"] or "")) + else: + out.sort(key=lambda r: (r["label"] or "")) + return out[:SEARCH_LIMIT_TOTAL] + + + + +def _parse_thematics_param(request): + thematics = request.GET.getlist("thematic") + parsed_thematics = [] + for t in thematics: + for item in t.split(","): + item_stripped = item.strip() + if item_stripped: + parsed_thematics.append(item_stripped) + return parsed_thematics or None + + +@require_GET +def locations_autocomplete(request): + q = (request.GET.get("q") or "").strip() + thematics = _parse_thematics_param(request) + prioritize = request.GET.get("prioritize") + + def lbl(code, name, ctx): + base = f"{name} ({code})" if name and code else (name or code or "") + # return f"{base} — {ctx['type_label']}" if base else ctx["type_label"] + return base + + results = search_locations(q, thematics=thematics, label_builder=lbl, prioritize=prioritize) + + payload = [{ + "label": d["label"], # ex. "Schuman (INT-001) — Feux" + "value": {"ct": d["ct"], "obj": d["obj"]}, # jQuery UI attend {label, value} + "type": d["type"], + "type_label": d["type_label"], + "lon": d["lon"], "lat": d["lat"], + "name": d.get("name"), "code": d.get("code"), + # (facultatif) utile pour purger côté UI quand les thématiques changent : + "thematic": d.get("thematic"), + # ↓ parent de référence envoyé au front + "parent_ref_id": d.get("parent_ref_id"), + "parent_ref_label": d.get("parent_ref_label"), + } for d in results] + + return JsonResponse(payload, safe=False) \ No newline at end of file diff --git a/streetup/assets/views/nature.py b/streetup/assets/views/nature.py new file mode 100644 index 0000000..f13a73b --- /dev/null +++ b/streetup/assets/views/nature.py @@ -0,0 +1,1340 @@ +from django.shortcuts import render, get_object_or_404 +from django.http import JsonResponse, Http404 +from django.contrib.gis.db.models.functions import Transform +from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon +from django.contrib.gis.db.models.functions import Distance +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import get_language, gettext as _ +from django.db.models import F, Func, Q, Case, When, CharField, IntegerField, Value +from django.db.models import Max +from django.db.models.functions import Lower +from django.views.decorators.http import require_GET +from django.urls import reverse +import uuid +import datetime + +import json +from collections import defaultdict +import unicodedata + +from common.utils import check_thematic_access, measure_time + +from common.models import UserThematics +from assets.models import NatureLocation, NatureGreenSurface, NatureAssetModel, NatureTree, NatureShrub, NatureBench, AssetCategory, NatureRWIZ, NatureRWIAsset +from assets.models import GreenSurfacesComment, GreenSurfacesCommentPhoto + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + +@check_thematic_access('nature') +def nature_index(request): + thematic = 'nature' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + highway = request.GET.get('highway') == "1" + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + selected_location_type = request.GET.get('location_type', '') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + + categories = list(AssetCategory.objects.filter(thematic__code = 'nature')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(NatureAssetModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + category_model_map = defaultdict(list) + models = NatureAssetModel.objects.all() + for model in models: + if model.category: + display_name = model.get_name() + category_model_map[str(model.category_id)].append({ + "key": str(model.id), + "label": display_name, + }) + + + context = { + 'thematics': thematics, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': dict(category_model_map), + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'highway': highway, + 'selected_location_type': selected_location_type, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Voiries"), + 'locations_assets_title': "Assets", + } + + return render(request, "assets/nature_index.html", context) + + + +def nature_locations(request): + pass + + +@check_thematic_access('nature') +def nature_locations_detail(request, location_id): + section = get_object_or_404(NatureLocation, id=location_id) + content_type = ContentType.objects.get_for_model(section) + + interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=content_type, object_id=section.id).select_related('intervention').order_by('-intervention__creation_time')] + + context = { + 'section': section, + 'interventions': interventions, + } + + return render(request, "assets/nature_locations_detail.html", context) + + + + +@check_thematic_access('nature') +def nature_locations_geojson(request): + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + highway = data.get('highway') == "1" + location_type = data.get('location_type', '') + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + highway = request.GET.get('highway') == "1" + location_type = request.GET.get('location_type', '') + filter_geom = None + + + include_naturelocation = location_type in ('', 'naturelocation') + + nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none() + + if highway: + nature_locations = nature_locations.filter(is_highway=highway) + + if filter_geom: + nature_locations = nature_locations.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + nature_locations = apply_search(nature_locations, data) + + from assets.views.core import get_element_type_label + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "highway": getattr(obj, 'is_highway', False), + "location_type": obj.__class__.__name__, + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(obj.__class__.__name__.lower()), + "thematic": "nature", + "is_location": True, + } + } + + features = [] + + for section in nature_locations: + features.append(obj_to_feature(section)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + + +def nature_assets(request): + pass + + +@check_thematic_access('nature') +def nature_assets_detail(request, asset_model, asset_id): + + try: + # Récupérer le ContentType pour le modèle fourni + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"assets/{asset_model}_detail.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for nature + context['thematic_code'] = 'nature' + + return render(request, template.template.name, context) + + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('nature') +def nature_assets_geojson(request): + lang = get_language() if get_language() in ('fr', 'nl') else 'fr' + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + sections_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + highway = data.get('highway') == "1" + location_type = data.get('location_type', '') + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + section_id = request.GET.get('location') + sections_ids = [section_id] if section_id else [] + statuses = request.GET.getlist('asset_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + highway = request.GET.get('highway') == "1" + location_type = request.GET.get('location_type', '') + filter_geom = None + + + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not sections_ids and not categories and not models and not filter_geom and not _search_q and not highway and not location_type: # return empty geojson if no param with section_id + geojson_data = { + "type": "FeatureCollection", + "features": [], + } + return JsonResponse(geojson_data) + + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.get('geojson'): + try: + geometry = json.loads(obj['geojson']) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.get('lon') + lat = obj.get('lat') + + if lon is None or lat is None: + if lon is None: + lon = obj.get('location_lon') + if lat is None: + lat = obj.get('location_lat') + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + detail_url = reverse('assets:nature_assets_detail', kwargs={'asset_model': asset_type, 'asset_id': obj['id']}) if asset_type else None + return { + "type": "Feature", + "id": obj['id'], + "geometry": geometry, + "properties": { + "code": obj['code'], + "name": obj['name'], + "category": obj['category_name'], + "model": obj['model_name'], + "category_id": obj.get('category_id'), + "model_id": obj.get('model_id'), + "status": obj['status'], + "validation_status": obj.get('validation_status'), + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "area": obj.get('surface_area', ''), + "color": obj['color'] if obj['color'] else '#ccc', + "detail_url": detail_url, + "bulb_types": obj.get('bulb_types') or [], + "element_type": get_element_type_label(asset_type or ''), + "thematic": "nature", + "is_location": False, + "structure_type": obj.get('structure_type', ''), + } + } + + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + + naturegreensurface_qs = ( + NatureGreenSurface.objects.all() + ) + naturetree_qs = ( + NatureTree.objects.all() + ) + natureshrub_qs = ( + NatureShrub.objects.all() + ) + naturebench_qs = ( + NatureBench.objects.all() + ) + + include_naturelocation = location_type in ('', 'naturelocation') + + if not include_naturelocation: + naturegreensurface_qs = naturegreensurface_qs.none() + naturetree_qs = naturetree_qs.none() + natureshrub_qs = natureshrub_qs.none() + naturebench_qs = naturebench_qs.none() + + + if sections_ids: + naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids) + naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids) + natureshrub_qs = natureshrub_qs.filter(location__id__in=sections_ids) + naturebench_qs = naturebench_qs.filter(location__id__in=sections_ids) + + if highway: + naturegreensurface_qs = naturegreensurface_qs.filter(location__is_highway=True) + naturetree_qs = naturetree_qs.filter(location__is_highway=True) + natureshrub_qs = natureshrub_qs.filter(location__is_highway=True) + naturebench_qs = naturebench_qs.filter(location__is_highway=True) + if models: + naturegreensurface_qs = naturegreensurface_qs.filter(model__id__in=models) + naturetree_qs = naturetree_qs.filter(model__id__in=models) + natureshrub_qs = natureshrub_qs.filter(model__id__in=models) + naturebench_qs = naturebench_qs.filter(model__id__in=models) + if categories: + naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories) + naturetree_qs = naturetree_qs.filter(category__id__in=categories) + natureshrub_qs = natureshrub_qs.filter(category__id__in=categories) + naturebench_qs = naturebench_qs.filter(category__id__in=categories) + if filter_geom: + filter_geom_3812 = filter_geom.transform(3812, clone=True) + naturegreensurface_qs = naturegreensurface_qs.filter( + Q(geom__intersects=filter_geom_3812) | + Q(geom__isnull=True, location__geom__intersects=filter_geom_3812) + ) + naturetree_qs = naturetree_qs.filter( + Q(geom__intersects=filter_geom_3812) | + Q(geom__isnull=True, location__geom__intersects=filter_geom_3812) + ) + natureshrub_qs = natureshrub_qs.filter( + Q(geom__intersects=filter_geom_3812) | + Q(geom__isnull=True, location__geom__intersects=filter_geom_3812) + ) + naturebench_qs = naturebench_qs.filter( + Q(geom__intersects=filter_geom_3812) | + Q(geom__isnull=True, location__geom__intersects=filter_geom_3812) + ) + + if statuses: + naturegreensurface_qs = naturegreensurface_qs.filter(status__in=statuses) + naturetree_qs = naturetree_qs.filter(status__in=statuses) + natureshrub_qs = natureshrub_qs.filter(status__in=statuses) + naturebench_qs = naturebench_qs.filter(status__in=statuses) + else: + # Default behavior: exclude removed assets + naturegreensurface_qs = naturegreensurface_qs.exclude(status='removed') + naturetree_qs = naturetree_qs.exclude(status='removed') + natureshrub_qs = natureshrub_qs.exclude(status='removed') + naturebench_qs = naturebench_qs.exclude(status='removed') + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + naturegreensurface_qs = apply_search(naturegreensurface_qs, data) + naturetree_qs = apply_search(naturetree_qs, data) + natureshrub_qs = apply_search(natureshrub_qs, data) + naturebench_qs = apply_search(naturebench_qs, data) + + if (naturegreensurface_qs.count() + naturetree_qs.count() + natureshrub_qs.count() + naturebench_qs.count()) > MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + naturegreensurface_qs = (naturegreensurface_qs.annotate( + name=F(f'name_{lang}'), + category_name=F(f'category__name_{lang}'), + model_name=F(f'model__name_{lang}'), + color = F('model__color'), + location_lon=F('location__lon'), + location_lat=F('location__lat'), + ) + .values( # on limite strictement les colonnes + 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'surface_area', + 'category_name', 'model_name', 'color', 'geojson', + 'category_id', 'model_id', 'bulb_types', 'location_lon', 'location_lat' + ) + .order_by('code') + )[:MAX_ASSET_FEATURES + 1] + + + naturetree_qs = (naturetree_qs.annotate( + name=F(f'name_{lang}'), + category_name=F(f'category__name_{lang}'), + model_name=F(f'model__name_{lang}'), + color = F('model__color'), + location_lon=F('location__lon'), + location_lat=F('location__lat'), + ) + .values( # on limite strictement les colonnes + 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', + 'category_name', 'model_name', 'color', 'geojson', + 'category_id', 'model_id', 'location_lon', 'location_lat' + ) + .order_by('code') + )[:MAX_ASSET_FEATURES + 1] + + natureshrub_qs = (natureshrub_qs.annotate( + name=F(f'name_{lang}'), + category_name=F(f'category__name_{lang}'), + model_name=F(f'model__name_{lang}'), + color=F('model__color'), + location_lon=F('location__lon'), + location_lat=F('location__lat'), + ) + .values( + 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', + 'category_name', 'model_name', 'color', 'geojson', + 'category_id', 'model_id', 'location_lon', 'location_lat' + ) + .order_by('code') + )[:MAX_ASSET_FEATURES + 1] + + naturebench_qs = (naturebench_qs.annotate( + name=F(f'name_{lang}'), + category_name=F(f'category__name_{lang}'), + model_name=F(f'model__name_{lang}'), + color=F('model__color'), + location_lon=F('location__lon'), + location_lat=F('location__lat'), + ) + .values( + 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', + 'category_name', 'model_name', 'color', 'geojson', + 'category_id', 'model_id', 'location_lon', 'location_lat' + ) + .order_by('code') + )[:MAX_ASSET_FEATURES + 1] + + + features = [] + feature_count = 0 + + for qs, asset_type in [ + (naturegreensurface_qs, "naturegreensurface"), + (naturetree_qs, "naturetree"), + (natureshrub_qs, "natureshrub"), + (naturebench_qs, "naturebench"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + + +@require_GET +def nature_locations_autocomplete(request): + raw_query = (request.GET.get('q') or '').strip() + if len(raw_query) < 2: + return JsonResponse([], safe=False) + + # Normalise la requête (équivalent de unaccent+lower côté Python) + qn = strip_accents_lower(raw_query) + + # Prépare des colonnes "recherchables" unaccentées + minuscules + qs = ( + NatureLocation.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + .filter( + Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + ) + + # Priorité : exact > startswith > nom startswith > contient + priority = Case( + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + default=7, + output_field=IntegerField(), + ) + + qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50] + + data = [{ + "label": f"{loc.code} - {loc.get_name()}", + "location_id": loc.id, + "location_type": loc.__class__.__name__, + "location_label": f"{loc.code} - {loc.get_name()}", + "lon": loc.lon, + "lat": loc.lat + } for loc in list(qs)] + + data.sort(key=lambda x: x["label"]) + + return JsonResponse(data, safe=False) + + + + +@require_GET +def nature_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + location_id = request.GET.get('location_id') # = id de la rue + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # Prépare filtre "dans cette rue uniquement" si location_id fourni + location_filter = {} + if location_id: + location_filter = {'location_id': location_id} + + + def base_qs(model, extra_filter=None): + ann = { + 'code_u': Lower(Unaccent('code')), + 'name_fr_u': Lower(Unaccent('name_fr')), + 'name_nl_u': Lower(Unaccent('name_nl')), + } + q = model.objects.annotate(**ann) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + q = q.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + # Filtrer uniquement les non supprimés + q = q.exclude(status='removed') + + if extra_filter: + q = q.filter(**extra_filter) + + if category_id is not None: + q = q.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + q = q.select_related('location', 'model') + + return q + + # Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python) + trees_qs = base_qs(NatureTree, extra_filter=location_filter)[:100] + greensurfaces_qs = base_qs(NatureGreenSurface, extra_filter=location_filter)[:100] + + # Concatène en mémoire (on priorise ensuite) + items = [] + for a in list(trees_qs) + list(greensurfaces_qs): + # priorisation locale (recalcule ici car on merge des modèles différents) + code_u = getattr(a, 'code_u', None) + name_fr_u = getattr(a, 'name_fr_u', None) + name_nl_u = getattr(a, 'name_nl_u', None) + + prio = 7 + # Priorisation uniquement si un terme de recherche est fourni + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u and code_u.startswith(qn): + prio = 1 + elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)): + prio = 2 + elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u): + prio = 4 + else: + # Sans terme de recherche, on trie par code + prio = 5 + + location = a.location if hasattr(a, 'location') else None + equipment_type = a.model.get_name() if hasattr(a, 'model') else None + parts = [a.code, a.get_name()] + equipment_label = " - ".join(p for p in parts if p) + code = getattr(a, 'code', '') or '' + label = equipment_label + lon, lat = [getattr(a, 'lon', None), getattr(a, 'lat', None)] + + location_id_val = location.id if location else None + location_label = (f"{location.code} - {location.get_name()}" if location else None) + + equipment_ct_id = ContentType.objects.get_for_model(a).id + location_ct_id = ContentType.objects.get_for_model(location).id if location else None + + items.append({ + "prio": prio, + "code": code or "", + "id": getattr(a, 'id', None), + "label": label, + + # attendus par ton autocomplete équipements + "equipment_id": getattr(a, 'id', None), + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": location_id_val, + "location_label": location_label, + "location_ct": location_ct_id, + + "lon": lon, + "lat": lat, + }) + + # Tri global par priorité puis code, coupe à 100 + items.sort(key=lambda x: (x["prio"], x["code"])) + data = items[:100] + + return JsonResponse(data, safe=False) + + + +@require_GET +def get_nearest_nature_location(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + NatureLocation.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by('distance') + .first() + ) + + location_ct_id = ContentType.objects.get_for_model(nearest).id + + if nearest: + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, "lat": nearest.lat + }) + else: + return JsonResponse({"error": _("Aucune section trouvée")}) + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + +@check_thematic_access('nature') +def nature_bulk_update_assets(request): + """Bulk update category and/or model for selected nature assets.""" + if request.method != 'POST': + return JsonResponse({'error': 'POST method required'}, status=405) + + try: + data = json.loads(request.body) + asset_ids = data.get('asset_ids', []) + category_id = data.get('category_id') + model_id = data.get('model_id') + bulb_types = data.get('bulb_types') # list or None + + if not asset_ids: + return JsonResponse({'error': _('Aucun asset sélectionné')}, status=400) + + updated_count = 0 + + # Update green surfaces + green_surfaces = NatureGreenSurface.objects.filter(id__in=asset_ids) + if category_id: + green_surfaces.update(category_id=category_id) + if model_id: + green_surfaces.update(model_id=model_id) + if bulb_types is not None: + green_surfaces.update(bulb_types=bulb_types) + updated_count += green_surfaces.count() + + # Update trees (if any in selection) + trees = NatureTree.objects.filter(id__in=asset_ids) + if category_id: + trees.update(category_id=category_id) + if model_id: + trees.update(model_id=model_id) + updated_count += trees.count() + + return JsonResponse({ + 'success': True, + 'updated_count': updated_count + }) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@check_thematic_access('nature') +def nature_bulk_delete_assets(request): + """Bulk delete (archive) selected nature assets.""" + if request.method != 'POST': + return JsonResponse({'error': 'POST method required'}, status=405) + + try: + data = json.loads(request.body) + asset_ids = data.get('asset_ids', []) + + if not asset_ids: + return JsonResponse({'error': _('Aucun asset sélectionné')}, status=400) + + deleted_count = 0 + + # Archive green surfaces + green_surfaces = NatureGreenSurface.objects.filter(id__in=asset_ids) + green_surfaces.update(status='removed') + deleted_count += green_surfaces.count() + + # Archive trees + trees = NatureTree.objects.filter(id__in=asset_ids) + trees.update(status='removed') + deleted_count += trees.count() + + return JsonResponse({ + 'success': True, + 'deleted_count': deleted_count + }) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@check_thematic_access('nature') +def nature_undo_action(request): + """ + Undo the last action (simplified version). + + NOTE: This is a basic implementation for demonstration purposes. + In a production environment, you would need to: + 1. Store the previous state of modified assets before changes + 2. Implement a proper undo stack with complete state management + 3. Handle more complex undo scenarios (geometry changes, creations, etc.) + """ + if request.method != 'POST': + return JsonResponse({'error': 'POST method required'}, status=405) + + try: + data = json.loads(request.body) + action = data.get('action') + + # Simplified undo - currently only supports undoing delete actions + if action == 'delete': + asset_ids = data.get('assets', []) + # Restore archived assets + NatureGreenSurface.objects.filter(id__in=asset_ids).update(status='active') + NatureTree.objects.filter(id__in=asset_ids).update(status='active') + elif action == 'bulk-edit': + # For bulk edit, you'd need to restore the old values + # This would require storing them before the edit + pass + + return JsonResponse({'success': True}) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@check_thematic_access('nature') +def nature_save_geometries(request): + """ + Save geometry modifications for nature assets. + + Expects a list of modifications with asset_id, asset_type, and geometry. + Updates the geom field for each asset with the new geometry. + """ + if request.method != 'POST': + return JsonResponse({'error': 'POST method required'}, status=405) + + try: + data = json.loads(request.body) + modifications = data.get('modifications', []) + deletions = data.get('deletions', []) # New: handle deletions for merged assets + + if not modifications and not deletions: + return JsonResponse({'error': _('Aucune modification fournie')}, status=400) + + updated_count = 0 + deleted_count = 0 + id_mapping = {} + + # Handle deletions first (for merged assets) + for deletion in deletions: + asset_id = deletion.get('asset_id') + asset_type = deletion.get('asset_type') + + if not all([asset_id, asset_type]): + continue + + try: + if asset_type == 'naturegreensurface': + asset = NatureGreenSurface.objects.filter(id=asset_id).first() + if asset: + asset.delete() + deleted_count += 1 + elif asset_type == 'naturetree': + asset = NatureTree.objects.filter(id=asset_id).first() + if asset: + asset.delete() + deleted_count += 1 + except Exception as e: + print(f"Error deleting asset {asset_id}: {str(e)}") + continue + + # Handle modifications + for mod in modifications: + asset_id = mod.get('asset_id') + asset_type = mod.get('asset_type') + geometry_geojson = mod.get('geometry') + category_id = mod.get('category_id') + model_id = mod.get('model_id') + bulb_types = mod.get('bulb_types') # list or None + + if not all([asset_id, asset_type, geometry_geojson]): + continue + + # Check if this is a new asset + is_new = str(asset_id).startswith('new_') + + # Convert GeoJSON to GEOS geometry + try: + # The geometry comes as GeoJSON, convert to GEOS + geom = GEOSGeometry(json.dumps(geometry_geojson), srid=4326) + # Transform to the storage SRID (3812) + geom.transform(3812) + + # Update the appropriate model + if asset_type == 'naturegreensurface': + asset = None + if is_new: + # Generate a unique code for the new asset + code = f"SURF-{uuid.uuid4().hex[:8].upper()}" + asset = NatureGreenSurface(code=code, status='active') + else: + asset = NatureGreenSurface.objects.filter(id=asset_id).first() + + if asset: + # Force multipolygon for green surfaces + if geom.geom_type == 'Polygon': + geom = MultiPolygon(geom) + asset.geom = geom + # Update centroid coordinates + centroid = geom.centroid + centroid.transform(4326) + asset.lon = centroid.x + asset.lat = centroid.y + geom_4326 = geom.transform(4326, clone=True) + asset.geojson = geom_4326.geojson + # Recalculate area for polygons + if hasattr(geom, 'area'): + asset.surface_area = geom.area # in square meters + + # Update attributes if provided + if category_id is not None: + if category_id: + try: + category = AssetCategory.objects.get(id=category_id) + asset.category = category + except AssetCategory.DoesNotExist: + pass + else: + if not is_new: # Don't clear category on creation if not provided (though defaults are null) + asset.category = None + + if model_id is not None: + if model_id: + try: + model = NatureAssetModel.objects.get(id=model_id) + asset.model = model + # If model has a category and no category was provided, use model's category + if not asset.category and model.category: + asset.category = model.category + except NatureAssetModel.DoesNotExist: + pass + else: + if not is_new: + asset.model = None + + if bulb_types is not None: + asset.bulb_types = bulb_types + + asset.save() + updated_count += 1 + if is_new: + id_mapping[asset_id] = asset.id + + elif asset_type == 'naturetree': + asset = NatureTree.objects.filter(id=asset_id).first() + if asset: + asset.geom = geom + # Update coordinates + centroid = geom.centroid + centroid.transform(4326) + asset.lon = centroid.x + asset.lat = centroid.y + asset.save() + updated_count += 1 + + except Exception as e: + # Log the error but continue with other modifications + print(f"Error updating geometry for asset {asset_id}: {str(e)}") + continue + + return JsonResponse({ + 'success': True, + 'updated_count': updated_count, + 'deleted_count': deleted_count, + 'id_mapping': id_mapping + }) + + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +# ============================================================================ +# Green Surfaces Comments API +# ============================================================================ + +@check_thematic_access('nature') +def green_surfaces_comments_geojson(request): + """ + Retrieve geolocated comments as GeoJSON. + Optionally filter by geographic bounds. + """ + if request.method == 'POST': + try: + data = json.loads(request.body) + filter_geom_data = data.get('filter_geom') + + comments = GreenSurfacesComment.objects.select_related('created_by').prefetch_related('photos') + + # Apply geographic filter if provided + if filter_geom_data: + filter_geom = GEOSGeometry(json.dumps(filter_geom_data['geometry'])) + filter_geom.srid = 4326 + filter_geom.transform(3812) + comments = comments.filter(geom__intersects=filter_geom) + + # Build GeoJSON features + features = [] + for comment in comments: + if comment.geom: + geom_4326 = comment.geom.transform(4326, clone=True) + feature = { + 'type': 'Feature', + 'id': comment.id, + 'geometry': json.loads(geom_4326.geojson), + 'properties': { + 'id': comment.id, + 'comment': comment.comment, + 'status': comment.status, + 'address': comment.address or '', + 'created_by': comment.created_by.username if comment.created_by else '', + 'created_at': comment.created_at.isoformat(), + 'photo_count': comment.photos.count(), + } + } + features.append(feature) + + return JsonResponse({ + 'type': 'FeatureCollection', + 'features': features + }) + + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + return JsonResponse({'error': 'Method not allowed'}, status=405) + + +@check_thematic_access('nature') +def green_surfaces_comment_detail(request, comment_id): + """ + Retrieve detailed information about a specific comment including photos. + """ + from common.private_files.helpers import build_private_url + + try: + comment = GreenSurfacesComment.objects.select_related('created_by').prefetch_related('photos').get(id=comment_id) + + photos = [] + for photo in comment.photos.all(): + photos.append({ + 'id': photo.id, + 'url': build_private_url('assets', 'GreenSurfacesCommentPhoto', photo.pk, 'photo'), + 'heading': photo.heading, + 'order': photo.order, + 'created_at': photo.created_at.isoformat(), + }) + + # Check if user can edit (owner or staff with edit permission) + can_edit = comment.created_by == request.user or request.user.is_staff + + data = { + 'id': comment.id, + 'comment': comment.comment, + 'status': comment.status, + 'latitude': comment.latitude, + 'longitude': comment.longitude, + 'address': comment.address or '', + 'created_by': comment.created_by.username if comment.created_by else '', + 'created_at': comment.created_at.isoformat(), + 'updated_at': comment.updated_at.isoformat(), + 'photos': photos, + 'can_edit': can_edit, + } + + return JsonResponse({'success': True, 'data': data}) + + except GreenSurfacesComment.DoesNotExist: + return JsonResponse({'error': 'Comment not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@check_thematic_access('nature') +def green_surfaces_comment_save(request): + """ + Create or update a geolocated comment with photos. + """ + if request.method != 'POST': + return JsonResponse({'error': 'Method not allowed'}, status=405) + + try: + comment_id = request.POST.get('comment_id') + comment_text = request.POST.get('comment', '').strip() + status = request.POST.get('status', 'memo') # Default to 'memo' + latitude = request.POST.get('latitude') + longitude = request.POST.get('longitude') + address = request.POST.get('address', '') + heading = request.POST.get('heading') # Device compass heading when photos were taken + photos_to_delete = request.POST.getlist('photos_to_delete') # Photos to delete during edit + + # Validate status + valid_statuses = ['memo', 'attention', 'todo', 'ok', 'problem'] + if status not in valid_statuses: + status = 'memo' + + # Comment text is now optional + + if not latitude or not longitude: + return JsonResponse({'error': 'Location is required'}, status=400) + + try: + latitude = float(latitude) + longitude = float(longitude) + except ValueError: + return JsonResponse({'error': 'Invalid coordinates'}, status=400) + + # Create or update comment + if comment_id: + comment = GreenSurfacesComment.objects.get(id=comment_id) + + # Check permission for editing + if comment.created_by != request.user and not request.user.is_staff: + return JsonResponse({'error': 'Permission denied'}, status=403) + + comment.comment = comment_text + comment.status = status + comment.latitude = latitude + comment.longitude = longitude + comment.address = address + else: + comment = GreenSurfacesComment( + comment=comment_text, + status=status, + latitude=latitude, + longitude=longitude, + address=address, + created_by=request.user + ) + + # Create geometry + point = Point(longitude, latitude, srid=4326) + point.transform(3812) + comment.geom = point + comment.save() + + # Delete photos if editing + if comment_id and photos_to_delete: + GreenSurfacesCommentPhoto.objects.filter( + id__in=photos_to_delete, + comment=comment + ).delete() + + # Handle photo uploads + photos = request.FILES.getlist('photos') + photo_headings = request.POST.getlist('photo_headings') # Individual headings for each photo + + # Get current max order for photos + max_order = comment.photos.aggregate(Max('order'))['order__max'] or 0 + + for i, photo_file in enumerate(photos): + # Use individual photo heading if provided, otherwise use global heading + photo_heading = None + if i < len(photo_headings) and photo_headings[i]: + try: + photo_heading = float(photo_headings[i]) + except ValueError: + pass + elif heading: + try: + photo_heading = float(heading) + except ValueError: + pass + + GreenSurfacesCommentPhoto.objects.create( + comment=comment, + photo=photo_file, + heading=photo_heading, + order=max_order + i + 1 + ) + + return JsonResponse({ + 'success': True, + 'comment_id': comment.id, + 'message': _('Commentaire enregistré avec succès') + }) + + except GreenSurfacesComment.DoesNotExist: + return JsonResponse({'error': 'Comment not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@check_thematic_access('nature') +def green_surfaces_comment_delete(request, comment_id): + """ + Delete a geolocated comment and all its associated photos. + """ + if request.method != 'POST': + return JsonResponse({'error': 'Method not allowed'}, status=405) + + try: + comment = GreenSurfacesComment.objects.get(id=comment_id) + + # Check if user owns the comment or has admin rights + if comment.created_by != request.user and not request.user.is_staff: + return JsonResponse({'error': 'Permission denied'}, status=403) + + comment.delete() # Cascade delete will remove photos + + return JsonResponse({ + 'success': True, + 'message': _('Commentaire supprimé avec succès') + }) + + except GreenSurfacesComment.DoesNotExist: + return JsonResponse({'error': 'Comment not found'}, status=404) + + +@check_thematic_access('nature') +def green_surfaces_comment_add_photos(request, comment_id): + """ + Add photos to an existing comment. + """ + if request.method != 'POST': + return JsonResponse({'error': 'Method not allowed'}, status=405) + + try: + comment = GreenSurfacesComment.objects.get(id=comment_id) + + # Check if user owns the comment or has admin rights + if comment.created_by != request.user and not request.user.is_staff: + return JsonResponse({'error': 'Permission denied'}, status=403) + + # Handle photo uploads + photos = request.FILES.getlist('photos') + photo_headings = request.POST.getlist('photo_headings') + + if not photos: + return JsonResponse({'error': 'No photos provided'}, status=400) + + # Get current max order + max_order = comment.photos.aggregate(Max('order'))['order__max'] or 0 + + # Save each photo with heading + for i, photo in enumerate(photos): + heading_str = photo_headings[i] if i < len(photo_headings) else '' + heading = None + if heading_str and heading_str.strip(): + try: + heading = float(heading_str) + except ValueError: + pass + + GreenSurfacesCommentPhoto.objects.create( + comment=comment, + photo=photo, + heading=heading, + order=max_order + i + 1 + ) + + return JsonResponse({ + 'success': True, + 'message': _('Photos ajoutées avec succès'), + 'comment_id': comment.id + }) + + except GreenSurfacesComment.DoesNotExist: + return JsonResponse({'error': 'Comment not found'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/streetup/assets/views/publiclighting.py b/streetup/assets/views/publiclighting.py new file mode 100644 index 0000000..fb62dc6 --- /dev/null +++ b/streetup/assets/views/publiclighting.py @@ -0,0 +1,828 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.db.models.functions import Transform +from django.contrib.gis.geos import Point, GEOSGeometry +from django.contrib.gis.db.models.functions import Distance +from django.views.decorators.http import require_GET, require_POST +from django.template.loader import render_to_string +from django.db.models import Value, OuterRef, Subquery, Q, Case, When +from django.db.models import IntegerField, CharField, BooleanField +from django.db.models import Func +from django.db.models.functions import Lower +from django.utils.translation import gettext as _, get_language + +import json +import unicodedata + +from common.models import UserThematics, Municipality +from common.utils import check_thematic_access + +from assets.models import PublicLightingStreet, PublicLightingStreetDocument, PublicLightingPole, PublicLightingLightFixture, AssetCategory, PublicLightingPoleModel, PublicLightingLightFixtureModel, PublicLightingElectricalCabinet, PublicLightingElectricalCabinetModel +from assets.forms import StreetDocumentForm + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + + +@check_thematic_access('publiclighting') +def publiclighting_index(request): + thematic = 'publiclighting' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_municipalities = request.GET.getlist('municipality') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + municipalities = list(Municipality.objects.all()) + municipalities.sort(key=lambda m: m.name.lower()) + municipality_choices = [(m.id, m.name) for m in municipalities] + + categories = list(AssetCategory.objects.filter(thematic__code = 'publiclighting')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(PublicLightingPoleModel.objects.all()) + models += list(PublicLightingLightFixtureModel.objects.all()) + models += list(PublicLightingElectricalCabinetModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + context = { + 'thematics': thematics, + 'municipality_choices':municipality_choices, + 'category_choices':category_choices, + 'model_choices':model_choices, + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_municipalities': selected_municipalities, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Voiries"), + 'locations_assets_title': "Assets", + } + + return render(request, "assets/publiclighting_index.html", context) + + + +def publiclighting_streets(request): + pass + +@check_thematic_access('publiclighting') +def publiclighting_streets_detail(request, street_id): + street = get_object_or_404(PublicLightingStreet, pk=street_id) + + ct_pls = ContentType.objects.get_for_model(PublicLightingStreet) + interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=ct_pls, object_id=street.id).select_related('intervention').order_by('-intervention__creation_time')] + + assets = street.get_active_assets() + + as_built_doc = street.documents.filter(document_type='as_built').filter(status='active').order_by('-id').first() + other_docs = street.documents.filter(document_type='other').filter(status='active').order_by('-id') + + street_geom_4326 = street.geom.transform(4326, clone=True) + geom = json.loads(street_geom_4326.geojson) + + return render(request, "assets/publiclighting_street_detail.html", + { + "street":street, + "interventions":interventions, + "assets":assets, + 'main_documents': [as_built_doc,], + "other_documents": other_docs, + 'add_document_form': StreetDocumentForm(), + "geom": geom + }) + + +@check_thematic_access('publiclighting', edit_permission_required=True) +@require_POST +def publiclighting_streets_add_document(request, street_id): + street = get_object_or_404(PublicLightingStreet, pk=street_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + for i in range(count): + file = files[i] + document_type = request.POST.get(f'document_type_{i}', 'other') + + PublicLightingStreetDocument.objects.create( + street=street, + file=file, + document_type=document_type, + uploaded_by = request.user + ) + return redirect('assets:publiclighting_streets_detail', street_id=street_id) + +@check_thematic_access('publiclighting', edit_permission_required=True) +@require_POST +def publiclighting_streets_delete_document(request, document_id): + document = get_object_or_404(PublicLightingStreetDocument, id=document_id) + street_id = document.street.id # pour rediriger vers la bonne page + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + #messages.success(request, "Le document a été supprimé avec succès.") + return redirect('assets:publiclighting_streets_detail', street_id=street_id) + + +@check_thematic_access('publiclighting') +def fetch_document_history(request): + street_id = request.GET.get('street_id') + document_type = request.GET.get('document_type') + + documents = PublicLightingStreetDocument.objects.filter( + street_id=street_id, + document_type=document_type + ).order_by('-id') + + html = render_to_string('assets/document_history_list.html', {'documents': documents}) + return JsonResponse({'html': html}) + + + +@check_thematic_access('publiclighting') +def publiclighting_streets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + municipalities = ensure_list(data.get('municipality')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + municipalities = request.GET.getlist('municipality') + filter_geom = None + + + publiclighting_streets = PublicLightingStreet.objects.all() + + if municipalities: + publiclighting_streets = publiclighting_streets.filter(municipality__in=municipalities) + + if filter_geom: + publiclighting_streets = publiclighting_streets.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + publiclighting_streets = apply_search(publiclighting_streets, data) + + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "municipality": obj.municipality.name if obj.municipality else None, + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": "Rue EP", + "thematic": "publiclighting", + "is_location": True, + } + } + + features = [] + + for street in publiclighting_streets: + features.append(obj_to_feature(street)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + + +def publiclighting_assets(request): + pass + + +@check_thematic_access('publiclighting') +def publiclighting_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni (ex: 'trafficlightpole') + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"publiclighting/assets/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for publiclighting + context['thematic_code'] = 'publiclighting' + + return render(request, template.template.name, context) + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('publiclighting') +def publiclighting_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + street_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + municipalities = ensure_list(data.get('municipality')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + street_id = request.GET.get('location') + street_ids = [street_id] if street_id else [] + statuses = request.GET.getlist('asset_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + municipalities = request.GET.getlist('municipality', []) + filter_geom = None + + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not street_ids and not categories and not models and not municipalities and not filter_geom and not _search_q: # return empty geojson if no param + geojson_data = { + "type": "FeatureCollection", + "features": [], + } + return JsonResponse(geojson_data) + + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.lon + lat = obj.lat + + if lon is None or lat is None: + street = getattr(obj, 'street', None) + if street is not None: + if lon is None: + lon = getattr(street, 'lon', None) + if lat is None: + lat = getattr(street, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "category": obj.category.get_name() if obj.category else None, + "model": obj.model.get_name() if obj.model else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "publiclighting", + "is_location": False, + }, + } + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + features = [] + feature_count = 0 + + pole_qs = PublicLightingPole.objects.select_related('street', 'category', 'model').all() + + if street_ids: + pole_qs = pole_qs.filter(street__id__in=street_ids) + + if municipalities: + pole_qs = pole_qs.filter(street__municipality_id__in=municipalities) + + if models: + pole_qs = pole_qs.filter(model__id__in=models) + + if categories: + pole_qs = pole_qs.filter(category__id__in=categories) + + if filter_geom: + pole_qs = pole_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + pole_qs = apply_search(pole_qs, data) + electrical_cabinet_pre_qs = PublicLightingElectricalCabinet.objects.select_related('street', 'category', 'model').all() + if street_ids: + electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(street__id__in=street_ids) + if municipalities: + electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(street__municipality_id__in=municipalities) + if models: + electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(model__id__in=models) + if categories: + electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(category__id__in=categories) + if filter_geom: + electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + electrical_cabinet_pre_qs = apply_search(electrical_cabinet_pre_qs, data) + + pole_qs = pole_qs[0:MAX_ASSET_FEATURES+1] + + if len(pole_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + lightfixture_qs = PublicLightingLightFixture.objects.select_related('street', 'category', 'model').all() + if street_ids: + lightfixture_qs = lightfixture_qs.filter(street__id__in=street_ids) + if municipalities: + lightfixture_qs = lightfixture_qs.filter(street__municipality_id__in=municipalities) + if models: + lightfixture_qs = lightfixture_qs.filter(model__id__in=models) + if categories: + lightfixture_qs = lightfixture_qs.filter(category__id__in=categories) + if filter_geom: + lightfixture_qs = lightfixture_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + if request.method == 'POST': + from assets.views.core import apply_search + lightfixture_qs = apply_search(lightfixture_qs, data) + lightfixture_qs = lightfixture_qs[0:MAX_ASSET_FEATURES+1] + if len(lightfixture_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + if request.method == 'POST' and (data.get('q') or '').strip(): + electrical_cabinet_qs = electrical_cabinet_pre_qs[0:MAX_ASSET_FEATURES+1] + else: + electrical_cabinet_qs = PublicLightingElectricalCabinet.objects.select_related('street', 'category', 'model').all() + if street_ids: + electrical_cabinet_qs = electrical_cabinet_qs.filter(street__id__in=street_ids) + if municipalities: + electrical_cabinet_qs = electrical_cabinet_qs.filter(street__municipality_id__in=municipalities) + if models: + electrical_cabinet_qs = electrical_cabinet_qs.filter(model__id__in=models) + if categories: + electrical_cabinet_qs = electrical_cabinet_qs.filter(category__id__in=categories) + if filter_geom: + electrical_cabinet_qs = electrical_cabinet_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + electrical_cabinet_qs = electrical_cabinet_qs[0:MAX_ASSET_FEATURES+1] + if len(electrical_cabinet_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + for qs, asset_type in [ + (pole_qs, "publiclightingpole"), + (lightfixture_qs, "publiclightinglightfixture"), + (electrical_cabinet_qs, "publiclightingelectricalcabinet") + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + +@require_GET +def get_nearest_publiclighting_street(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + PublicLightingStreet.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by('distance') + .first() + ) + + location_ct_id = ContentType.objects.get_for_model(nearest).id + + if nearest: + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, "lat": nearest.lat + }) + else: + return JsonResponse({"error": _("Aucune rue trouvée")}) + + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + +@require_GET +def publiclighting_locations_autocomplete(request): + raw_query = (request.GET.get('q') or '').strip() + if len(raw_query) < 2: + return JsonResponse([], safe=False) + + # Normalise la requête (équivalent de unaccent+lower côté Python) + qn = strip_accents_lower(raw_query) + + # Prépare des colonnes "recherchables" unaccentées + minuscules + qs = ( + PublicLightingStreet.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + .filter( + Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + ) + + # Priorité : exact > startswith > nom startswith > contient + priority = Case( + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + default=7, + output_field=IntegerField(), + ) + + qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50] + + data = [{ + "label": f"{loc.code} - {loc.get_name()}", + "location_id": loc.id, + "location_label": f"{loc.code} - {loc.get_name()}", + "lon": loc.lon, + "lat": loc.lat + } for loc in qs] + + return JsonResponse(data, safe=False) + + + + +# --- helpers spécifiques publiclighting --- +def _equipment_type_from_model(asset): + m = getattr(asset, 'model', None) + return (getattr(m, 'name', None) or getattr(m, 'code', None)) if m else None + +def _street_of(asset): + """ + Renvoie la rue associée à un asset, directement ou via le pole (lantern/detector). + - Pole/Controller/Cable: asset.street + - Lantern/Detector: asset.pole.street si pole présent + """ + # Lantern or Detector via pole + if hasattr(asset, 'pole') and getattr(asset, 'pole', None): + return getattr(asset.pole, 'street', None) + # Others have direct FK 'intersection' + return getattr(asset, 'street', None) + +def _coords_of(asset): + """ + Détermine lon/lat: + 1) asset.lon/lat (si présents) + 2) si PointField: coords du point (transformé en 4326 si nécessaire) + si LineString: centroid en 4326 + 3) via pole.geom (lantern/detector) + 4) via intersection.geom (point) + """ + # 1) lon/lat directs + lon = getattr(asset, 'lon', None) + lat = getattr(asset, 'lat', None) + if lon is not None and lat is not None: + return lon, lat + + # 2) géométrie sur l'asset (poles point, cables line) + geom = getattr(asset, 'geom', None) + if geom: + try: + g = geom + if getattr(g, 'srid', None) and g.srid != 4326: + g = geom.transform(4326, clone=True) + # point + if hasattr(g, 'coords'): + c = g.coords + if isinstance(c, (list, tuple)) and len(c) >= 2: + return c[0], c[1] + # line/poly: centroid + centroid = getattr(g, 'centroid', None) + if centroid: + if getattr(centroid, 'srid', None) and centroid.srid != 4326: + centroid = centroid.transform(4326, clone=True) + cc = getattr(centroid, 'coords', None) + if cc and len(cc) >= 2: + return cc[0], cc[1] + except Exception: + pass + + # 3) pole.geom (lantern/detector) + pole = getattr(asset, 'pole', None) + if pole and getattr(pole, 'geom', None): + try: + g = pole.geom + if getattr(g, 'srid', None) and g.srid != 4326: + g = g.transform(4326, clone=True) + if hasattr(g, 'coords'): + c = g.coords + if c and len(c) >= 2: + return c[0], c[1] + except Exception: + pass + + # 4) intersection.geom + street = _street_of(asset) + if street and getattr(street, 'geom', None): + try: + g = street.geom + if getattr(g, 'srid', None) and g.srid != 4326: + g = g.transform(4326, clone=True) + if hasattr(g, 'coords'): + c = g.coords + if c and len(c) >= 2: + return c[0], c[1] + except Exception: + pass + + return None, None + +@require_GET +def publiclighting_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + location_id = request.GET.get('location_id') # = id de l'intersection + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # Prépare filtre "dans cette rue uniquement" si location_id fourni + street_filter = {} + pole_street_filter = {} + if location_id: + street_filter = {'street_id': location_id} + pole_street_filter = {'pole__street_id': location_id} + + + def base_qs(model, extra_filter=None, via_pole=False): + ann = { + 'code_u': Lower(Unaccent('code')), + 'name_fr_u': Lower(Unaccent('name_fr')), + 'name_nl_u': Lower(Unaccent('name_nl')), + } + q = model.objects.annotate(**ann) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + q = q.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + # Filtrer uniquement les non supprimés + q = q.exclude(status='removed') + + if extra_filter: + q = q.filter(**extra_filter) + + if category_id is not None: + q = q.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + # select_related utile selon le modèle + if via_pole: + q = q.select_related('pole', 'pole__street', 'model') + else: + q = q.select_related('street', 'model') + + return q + + # Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python) + poles_qs = base_qs(PublicLightingPole, extra_filter=street_filter)[:100] + lanterns_qs = base_qs(PublicLightingLightFixture, extra_filter=pole_street_filter, via_pole=True)[:100] + + # Concatène en mémoire (on priorise ensuite) + items = [] + for a in list(poles_qs) + list(lanterns_qs): + # priorisation locale (recalcule ici car on merge des modèles différents) + code_u = getattr(a, 'code_u', None) + name_fr_u = getattr(a, 'name_fr_u', None) + name_nl_u = getattr(a, 'name_nl_u', None) + + prio = 7 + # Priorisation uniquement si un terme de recherche est fourni + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u and code_u.startswith(qn): + prio = 1 + elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)): + prio = 2 + elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u): + prio = 4 + else: + # Sans terme de recherche, on trie par code + prio = 5 + + street = _street_of(a) + equipment_type = _equipment_type_from_model(a) + parts = [a.code, a.get_name()] + equipment_label = " - ".join(p for p in parts if p) + code = getattr(a, 'code', '') or '' + label = equipment_label + lon, lat = _coords_of(a) + + location_id_val = street.id if street else None + location_label = (f"{street.code} - {street.get_name()}" if street else None) + + equipment_ct_id = ContentType.objects.get_for_model(a).id + location_ct_id = ContentType.objects.get_for_model(street).id if street else None + + items.append({ + "prio": prio, + "code": code or "", + "id": getattr(a, 'id', None), + "label": label, + + # attendus par ton autocomplete équipements + "equipment_id": getattr(a, 'id', None), + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": location_id_val, + "location_label": location_label, + "location_ct": location_ct_id, + + "lon": lon, + "lat": lat, + }) + + # Tri global par priorité puis code, coupe à 100 + items.sort(key=lambda x: (x["prio"], x["code"])) + data = items[:100] + + return JsonResponse(data, safe=False) \ No newline at end of file diff --git a/streetup/assets/views/roads.py b/streetup/assets/views/roads.py new file mode 100644 index 0000000..0f440a0 --- /dev/null +++ b/streetup/assets/views/roads.py @@ -0,0 +1,772 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.urls import reverse +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.db.models.functions import Transform +from django.contrib.gis.geos import Point, GEOSGeometry +from django.contrib.gis.db.models.functions import Distance +from django.views.decorators.http import require_GET, require_POST +from django.template.loader import render_to_string +from django.db.models import Value, OuterRef, Subquery, Q, Case, When +from django.db.models import IntegerField, CharField, BooleanField +from django.db.models import Func, F +from django.db.models.functions import Lower +from django.utils.translation import gettext as _, get_language + +import json +import unicodedata + +from common.models import Thematic, UserConfig, UserContractAccess, UserThematics, Municipality +from common.utils import check_thematic_access + +from assets.models import RoadStreet, RoadStreetDocument, Roadway, RoadSidewalk, RoadFurniture, RoadFurnitureModel, AssetCategory +from assets.forms import RoadStreetDocumentForm +from assets.models.roads import QUALITY_LEVEL_CHOICES, RoadDrain, RoadConcreteBlockSet + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + + +@check_thematic_access('roads') +def roads_index(request): + thematic = 'roads' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_municipalities = request.GET.getlist('municipality') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + selected_levels = request.GET.getlist('quality_level') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + municipalities = list(Municipality.objects.all()) + municipalities.sort(key=lambda m: m.name.lower()) + municipality_choices = [(m.id, m.name) for m in municipalities] + + categories = list(AssetCategory.objects.filter(thematic__code = 'roads')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(RoadFurnitureModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + quality_levels = QUALITY_LEVEL_CHOICES + quality_levels_choices = [(level[0], level[1]) for level in quality_levels] + + context = { + 'thematics': thematics, + 'municipality_choices':municipality_choices, + 'category_choices':category_choices, + 'model_choices':model_choices, + 'quality_levels_choices':quality_levels_choices, + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_municipalities': selected_municipalities, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'selected_levels': selected_levels, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Voiries"), + 'locations_assets_title': "Assets", + } + + return render(request, "assets/roads_index.html", context) + + + +def roads_streets(request): + pass + +def roads_assets(request): + pass + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +@check_thematic_access('roads') +def roads_streets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + municipalities = ensure_list(data.get('municipality')) + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + municipalities = request.GET.getlist('municipality') + filter_geom = None + + # --------- Point d'entrée du queryset + qs = RoadStreet.objects.all() + + # --------- Filtre par communes + if municipalities: + qs = qs.filter(municipality_id__in=municipalities) + + # --------- Filtre géométrique + if filter_geom: + qs = qs.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + qs = apply_search(qs, data) + + lang = get_language() or "fr" + name_field = 'name_nl' if lang == 'nl' else 'name_fr' + name_expr = F(name_field) + + qs = ( + qs.annotate( + name=name_expr, + municipality_name=F('municipality__name'), + ) + .values('id', 'code', 'lon', 'lat', 'name', 'municipality_name', 'geojson') + .order_by('name') + ) + + # --------- Conversion en GeoJSON + def obj_to_feature(r): + geom = json.loads(r['geojson']) if r['geojson'] else None + return { + "type": "Feature", + "id": r['id'], + "geometry": geom, + "properties": { + "code": r['code'], + "municipality": r['municipality_name'] or "-", + "name": r['name'], + "lon": r['lon'], + "lat": r['lat'], + "detail_url": reverse('assets:roads_streets_detail', kwargs={'street_id': r['id']}), + "element_type": "Voirie", + "thematic": "roads", + "is_location": True, + } + } + + + features = [] + for location in qs: + features.append(obj_to_feature(location)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('roads') +def roads_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + roads_location_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + municipalities = ensure_list(data.get('municipality')) + quality_levels = ensure_list(data.get('quality_level')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + roads_location_id = request.GET.get('location') + roads_location_ids = [roads_location_id] if roads_location_id else [] + statuses = request.GET.getlist('asset_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + municipalities = request.GET.getlist('municipality', []) + quality_levels = request.GET.getlist('quality_level', []) + filter_geom = None + + # Pas de paramètres = pas de carte (comportement existant conservé) + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not roads_location_ids and not categories and not models and not municipalities and not filter_geom and not quality_levels and not _search_q: + geojson_data = {"type": "FeatureCollection", "features": []} + return JsonResponse(geojson_data) + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.lon + lat = obj.lat + + if lon is None or lat is None: + street = getattr(obj, 'street', None) + if street is not None: + if lon is None: + lon = getattr(street, 'lon', None) + if lat is None: + lat = getattr(street, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + model = getattr(obj, 'model', None) + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.code, + "category": obj.category.get_name() if obj.category else None, + "model": model.get_name() if model else None, + "quality_level": obj.get_quality_level_display() if hasattr(obj, 'get_quality_level_display') else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "area": obj.area if hasattr(obj, 'area') else None, + "length": obj.length if hasattr(obj, 'length') else None, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "roads", + "is_location": False, + }, + } + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + # --------- Query de base + optimisations + roadways_qs = ( + Roadway.objects + .select_related('street', 'category', 'model') + .all() + ) + sidewalks_qs = ( + RoadSidewalk.objects + .select_related('street', 'category', 'model') + .all() + ) + furnitures_qs = ( + RoadFurniture.objects + .select_related('street', 'category', 'model') + .all() + ) + drains_qs = ( + RoadDrain.objects + .select_related('street', 'model') + .all() + ) + concrete_block_sets_qs = ( + RoadConcreteBlockSet.objects + .select_related('category') + .all() + ) + + # --------- Filtres métier existants + if roads_location_ids: + roadways_qs = roadways_qs.filter(street__id__in=roads_location_ids) + sidewalks_qs = sidewalks_qs.filter(street__id__in=roads_location_ids) + furnitures_qs = furnitures_qs.filter(street__id__in=roads_location_ids) + drains_qs = drains_qs.filter(street__id__in=roads_location_ids) + concrete_block_sets_qs = concrete_block_sets_qs.none() + + if municipalities: + roadways_qs = roadways_qs.filter(street__municipality_id__in=municipalities) + sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=municipalities) + furnitures_qs = furnitures_qs.filter(street__municipality_id__in=municipalities) + drains_qs = drains_qs.filter(street__municipality_id__in=municipalities) + concrete_block_sets_qs = concrete_block_sets_qs.none() + + if models: + roadways_qs = roadways_qs.filter(model__id__in=models) + sidewalks_qs = sidewalks_qs.filter(model__id__in=models) + furnitures_qs = furnitures_qs.filter(model__id__in=models) + drains_qs = drains_qs.filter(model__id__in=models) + concrete_block_sets_qs = concrete_block_sets_qs.none() + + if categories: + roadways_qs = roadways_qs.filter(category__id__in=categories) + sidewalks_qs = sidewalks_qs.filter(category__id__in=categories) + furnitures_qs = furnitures_qs.filter(category__id__in=categories) + drains_qs = drains_qs.filter(category__id__in=categories) + concrete_block_sets_qs = concrete_block_sets_qs.filter(category__id__in=categories) + + if filter_geom: + roadways_qs = roadways_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + sidewalks_qs = sidewalks_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + furnitures_qs = furnitures_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + drains_qs = drains_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + concrete_block_sets_qs = concrete_block_sets_qs.filter(geom__intersects=filter_geom) + + if quality_levels: + roadways_qs = roadways_qs.filter(quality_level__in=quality_levels) + sidewalks_qs = sidewalks_qs.filter(quality_level__in=quality_levels) + furnitures_qs = furnitures_qs.none() # pas de quality_level sur furnitures + drains_qs = drains_qs.none() # pas de quality_level sur les avaloirs + concrete_block_sets_qs = concrete_block_sets_qs.none() # pas de quality_level sur les blocs béton + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + roadways_qs = apply_search(roadways_qs, data) + sidewalks_qs = apply_search(sidewalks_qs, data) + furnitures_qs = apply_search(furnitures_qs, data) + drains_qs = apply_search(drains_qs, data) + concrete_block_sets_qs = apply_search(concrete_block_sets_qs, data, fields=['code', 'name_fr', 'name_nl', 'address']) + + # --------- Cap de sécurité + roadways_qs = roadways_qs[0:MAX_ASSET_FEATURES+1] + if len(roadways_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + sidewalks_qs = sidewalks_qs[0:MAX_ASSET_FEATURES+1] + if len(sidewalks_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + furnitures_qs = furnitures_qs[0:MAX_ASSET_FEATURES+1] + if len(furnitures_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + drains_qs = drains_qs[0:MAX_ASSET_FEATURES+1] + if len(drains_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + concrete_block_sets_qs = concrete_block_sets_qs[0:MAX_ASSET_FEATURES+1] + if len(concrete_block_sets_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + + # --------- Construction GeoJSON + features = [] + feature_count = 0 + + for qs, asset_type in [ + (roadways_qs, "roadway"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + for qs, asset_type in [ + (sidewalks_qs, "sidewalk"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + for qs, asset_type in [ + (furnitures_qs, "streetfurniture"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + for qs, asset_type in [ + (drains_qs, "drain"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + for qs, asset_type in [ + (concrete_block_sets_qs, "roadconcreteblockset"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + + +@check_thematic_access('roads') +def roads_streets_detail(request, street_id): + street = get_object_or_404(RoadStreet, pk=street_id) + + ct_roads_street = ContentType.objects.get_for_model(RoadStreet) + interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=ct_roads_street, object_id=street.id).select_related('intervention').order_by('-intervention__creation_time')] + + assets = street.get_active_assets() + + as_built_doc = street.documents.filter(document_type='as_built').filter(status='active').order_by('-id').first() + other_docs = street.documents.filter(document_type='other').filter(status='active').order_by('-id') + + street_geom_4326 = street.geom.transform(4326, clone=True) + geom = json.loads(street_geom_4326.geojson) + + return render(request, "assets/roads_street_detail.html", + { + "street":street, + "interventions":interventions, + "assets":assets, + 'main_documents': [as_built_doc,], + "other_documents": other_docs, + 'add_document_form': RoadStreetDocumentForm(), + "geom": geom + }) + + + +@check_thematic_access('roads') +def roads_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"roads/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for roads + context['thematic_code'] = 'roads' + + return render(request, template.template.name, context) + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + + + +@check_thematic_access('roads', edit_permission_required=True) +@require_POST +def roads_streets_add_document(request, street_id): + roadsstreet = get_object_or_404(RoadStreet, pk=street_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + for i in range(count): + file = files[i] + document_type = request.POST.get(f'document_type_{i}', 'other') + + RoadStreetDocument.objects.create( + street=roadsstreet, + file=file, + document_type=document_type, + uploaded_by = request.user + ) + return redirect('assets:roads_streets_detail', street_id=street_id) + +@check_thematic_access('roads', edit_permission_required=True) +@require_POST +def roads_streets_delete_document(request, document_id): + document = get_object_or_404(RoadStreetDocument, id=document_id) + street_id = document.street.id # pour rediriger vers la bonne page + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + #messages.success(request, "Le document a été supprimé avec succès.") + return redirect('assets:roads_streets_detail', structure_id=street_id) + + +@check_thematic_access('roads') +def fetch_document_history(request): + street_id = request.GET.get('street_id') + document_type = request.GET.get('document_type') + + documents = RoadStreetDocument.objects.filter( + street_id=street_id, + document_type=document_type + ).order_by('-id') + + html = render_to_string('assets/document_history_list.html', {'documents': documents}) + return JsonResponse({'html': html}) + + + + +@require_GET +def get_nearest_roads_street(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + RoadStreet.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by('distance') + .first() + ) + + location_ct_id = ContentType.objects.get_for_model(nearest).id + + if nearest: + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, "lat": nearest.lat + }) + else: + return JsonResponse({"error": _("Aucune rue trouvée")}) + + +@require_GET +def roads_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + location_id = request.GET.get('location_id') # = id de la rue + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # Prépare filtre "dans cette rue uniquement" si location_id fourni + street_filter = {} + if location_id: + street_filter = {'street_id': location_id} + + + def base_qs(model, extra_filter=None): + ann = { + 'code_u': Lower(Unaccent('code')), + 'name_fr_u': Lower(Unaccent('name_fr')), + 'name_nl_u': Lower(Unaccent('name_nl')), + } + q = model.objects.annotate(**ann) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + q = q.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + # Filtrer uniquement les non supprimés + q = q.exclude(status='removed') + + if extra_filter: + q = q.filter(**extra_filter) + + if category_id is not None: + q = q.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + q = q.select_related('street', 'model') + + return q + + # Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python) + roadways_qs = base_qs(Roadway, extra_filter=street_filter)[:100] + sidewalks_qs = base_qs(RoadSidewalk, extra_filter=street_filter)[:100] + furnitures_qs = base_qs(RoadFurniture, extra_filter=street_filter)[:100] + + # Concatène en mémoire (on priorise ensuite) + items = [] + for a in list(roadways_qs) + list(sidewalks_qs) + list(furnitures_qs): + # priorisation locale (recalcule ici car on merge des modèles différents) + code_u = getattr(a, 'code_u', None) + name_fr_u = getattr(a, 'name_fr_u', None) + name_nl_u = getattr(a, 'name_nl_u', None) + + prio = 7 + # Priorisation uniquement si un terme de recherche est fourni + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u and code_u.startswith(qn): + prio = 1 + elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)): + prio = 2 + elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u): + prio = 4 + else: + # Sans terme de recherche, on trie par code + prio = 5 + code = getattr(a, 'code', '') or '' + label = equipment_label + lon, lat = [getattr(a, 'lon', None), getattr(a, 'lat', None)] + + location_id_val = street.id if street else None + location_label = (f"{street.code} - {street.get_name()}" if street else None) + + equipment_ct_id = ContentType.objects.get_for_model(a).id + location_ct_id = ContentType.objects.get_for_model(street).id if street else None + + items.append({ + "prio": prio, + "code": code or "", + "id": getattr(a, 'id', None), + "label": label, + + # attendus par ton autocomplete équipements + "equipment_id": getattr(a, 'id', None), + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": location_id_val, + "location_label": location_label, + "location_ct": location_ct_id, + + "lon": lon, + "lat": lat, + }) + + # Tri global par priorité puis code, coupe à 100 + items.sort(key=lambda x: (x["prio"], x["code"])) + data = items[:100] + + return JsonResponse(data, safe=False) \ No newline at end of file diff --git a/streetup/assets/views/sign.py b/streetup/assets/views/sign.py new file mode 100644 index 0000000..ce9512c --- /dev/null +++ b/streetup/assets/views/sign.py @@ -0,0 +1,1031 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.db.models.functions import Transform +from django.contrib.gis.geos import Point, GEOSGeometry +from django.contrib.gis.db.models.functions import Distance +from django.views.decorators.http import require_GET, require_POST +from django.template.loader import render_to_string +from django.db.models import Value, OuterRef, Subquery, Q, Case, When +from django.db.models import IntegerField, CharField, BooleanField +from django.db.models import Func, F +from django.db.models.functions import Lower +from django.utils.translation import gettext as _, get_language +from django.urls import reverse +import json +import unicodedata +import math +import re +import xml.etree.ElementTree as ET +try: + from PIL import Image +except ImportError: + Image = None + +from common.models import Thematic, UserConfig, UserContractAccess, UserThematics, Municipality +from common.utils import check_thematic_access + +from assets.models import SignStreet, SignPole, SignPanel, SignPoleModel, SignPanelModel, SignPanelType, SignStreetDocument, AssetCategory +from assets.forms import SignStreetDocumentForm + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + + +@check_thematic_access('sign') +def sign_index(request): + thematic = 'sign' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_municipalities = request.GET.getlist('municipality') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + selected_panel_types = request.GET.getlist('panel_type') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + municipalities = list(Municipality.objects.all()) + municipalities.sort(key=lambda m: m.name.lower()) + municipality_choices = [(m.id, m.name) for m in municipalities] + + categories = list(AssetCategory.objects.filter(thematic__code = 'sign')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(SignPoleModel.objects.all()) + models += list(SignPanelModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + panel_types = list(SignPanelType.objects.all().order_by('code')) + panel_type_choices = [(m.id, m.code) for m in panel_types] + + context = { + 'thematics': thematics, + 'municipality_choices':municipality_choices, + 'category_choices':category_choices, + 'model_choices':model_choices, + 'panel_type_choices':panel_type_choices, + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_municipalities': selected_municipalities, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'selected_panel_types': selected_panel_types, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Voiries"), + 'locations_assets_title': "Assets", + } + + return render(request, "assets/sign_index.html", context) + + + +def sign_streets(request): + pass + +def sign_assets(request): + pass + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +@check_thematic_access('sign') +def sign_streets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + municipalities = ensure_list(data.get('municipality')) + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + municipalities = request.GET.getlist('municipality') + filter_geom = None + + # --------- Point d'entrée du queryset + qs = SignStreet.objects.all() + + # --------- Filtre par communes + if municipalities: + qs = qs.filter(municipality_id__in=municipalities) + + # --------- Filtre géométrique + if filter_geom: + qs = qs.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + qs = apply_search(qs, data) + + lang = get_language() or "fr" + name_field = 'name_nl' if lang == 'nl' else 'name_fr' + name_expr = F(name_field) + + qs = ( + qs.annotate( + name=name_expr, + municipality_name=F('municipality__name'), + ) + .values('id', 'code', 'lon', 'lat', 'name', 'municipality_name', 'geojson') + .order_by('name') + ) + + # --------- Conversion en GeoJSON + def obj_to_feature(r): + geom = json.loads(r['geojson']) if r['geojson'] else None + return { + "type": "Feature", + "id": r['id'], + "geometry": geom, + "properties": { + "code": r['code'], + "municipality": r['municipality_name'] or "-", + "name": r['name'], + "lon": r['lon'], + "lat": r['lat'], + "detail_url": reverse('assets:sign_streets_detail', kwargs={'street_id': r['id']}), + "element_type": "Voirie signalisation", + "thematic": "sign", + "is_location": True, + } + } + + + features = [] + for location in qs: + features.append(obj_to_feature(location)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + +MAX_ASSET_FEATURES = 5000 # limite imposée +PANEL_VERTICAL_GAP_PX = 65 +MIN_PANEL_BASE_UP_OFFSET_PX = 40 +ORIENTATION_ARROW_MARGIN_PX = 40 +DEFAULT_PANEL_ICON_HEIGHT_PX = PANEL_VERTICAL_GAP_PX +TEXT_LINE_HEIGHT_PX = 20 +TEXT_VERTICAL_PADDING_PX = 14 +# Height boost for lines containing inline symbols (PEDESTRIAN, CAR, BIKE) that render at 1.5x font size +SYMBOL_LINE_EXTRA_HEIGHT_PX = 12 +# Height of embedded icon in Zone panels (Z*) - matches signIconHeight in JS (64px) + spacing (6px) +ZONE_EMBEDDED_ICON_HEIGHT_PX = 70 +PANEL_HEIGHT_ADJUST_PX = { +# "XC": 80, # XC needs a taller rendering envelope +# "TYPEXIVF": -20, # TYPEXIVF was too tall; pull it back slightly +# "ZE9A": -40, +} +STANDARD_PANEL_STACK_GAP_PX = 4 +TEXT_PANEL_BASE_WIDTH_PX = 72 +ZONE_TEXT_PANEL_BASE_WIDTH_PX = 64 + +@check_thematic_access('sign') +def sign_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + sign_location_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + municipalities = ensure_list(data.get('municipality')) + panel_types = ensure_list(data.get('panel_type')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + sign_location_id = request.GET.get('location') + sign_location_ids = [sign_location_id] if sign_location_id else [] + statuses = request.GET.getlist('asset_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + municipalities = request.GET.getlist('municipality', []) + panel_types = request.GET.getlist('panel_type', []) + filter_geom = None + + # Pas de paramètres = pas de carte (comportement existant conservé) + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not sign_location_ids and not categories and not models and not municipalities and not filter_geom and not panel_types and not _search_q: + geojson_data = {"type": "FeatureCollection", "features": []} + return JsonResponse(geojson_data) + + # Estimate render heights and offsets for panels to avoid per-type magic numbers + fallback_text_lines = { + "TYPEXIVF": 5, + "TYPE VIIDE": 2, + "TYPEVIIDE": 2, + "TYPEVI": 2, + "TYPEVE": 2, + "XC": 3, + "ICR": 2, + "FXX": 2, + "F35": 2, + } + + def normalize_panel_text(text): + if not text: + return "" + # Support multiple line break formats (in priority order): + # 1. Literal \n in plain text (two chars: backslash + n) -> convert to newline + # This handles cases like "Line 1\nLine 2" stored as literal backslash-n + text = text.replace("\\n", "\n") + # 2. HTML tags:
,
,
+ text = text.replace("
", "\n").replace("
", "\n").replace("
", "\n") + # 3. Carriage returns and other line breaks (Windows/Unix line endings) + text = text.replace("\r\n", "\n").replace("\r", "\n") + return text + + def can_panel_render_text(panel_type_code, panel_text): + if not panel_type_code: + return False + code = panel_type_code.upper() + if code.startswith("Z"): + return True + if code in {"F34A", "FXX", "F33C", "F35", "TYPEVE", "TYPE VIIDE", "TYPEVIIDE", "XC", "TYPEXIVF"}: + return bool(panel_text) or code in {"TYPEVE", "TYPEXIVF", "FXX", "TYPE VIIDE", "TYPEVIIDE", "F35"} + if code == "ICR": + return True + if code.startswith("TYPE"): + return bool(panel_text) + return False + + def count_panel_text_lines(panel_type_code, panel_text, can_render_text): + if not can_render_text: + return 0 + normalized = normalize_panel_text(panel_text) + lines = [line for line in normalized.splitlines() if line.strip()] + code = (panel_type_code or "").upper() + fallback = fallback_text_lines.get(code, 0) + # For TYPE* series, treat '|' as a line break (bilingual separation) + if code.startswith("TYPE"): + normalized = normalized.replace('|', '\n') + lines = [line for line in normalized.splitlines() if line.strip()] + # Z* panels always include a "ZONE" header line + if code.startswith("Z"): + # For Zone panels, treat '|' as a line break + normalized = normalized.replace('|', '\n') + lines = [line.strip() for line in normalized.splitlines() if line.strip()] + # Remove any provided "ZONE" lines to avoid double counting (header is added separately in UI) + filtered = [l for l in lines if l.upper() != 'ZONE'] + # Also remove a leading underlying sign code line matching the remainder (e.g., E9B) + remainder = code[1:].replace("’", "").replace("'", "") + filtered = [l for l in filtered if l.upper() != remainder.upper()] + # Count: 1 header line (ZONE) + remaining text lines + return 1 + len(filtered) + if not lines and fallback: + return fallback + if not lines and panel_text: + return 1 + return max(len(lines), 1 if fallback and can_render_text else len(lines)) + + def get_image_aspect_ratio(signpanel_type): + """Extract aspect ratio (height/width) from SignPanelType image. + Supports raster images and SVG files. Returns 1.0 on failure.""" + if not signpanel_type or not signpanel_type.sgv_picture: + return 1.0 + + def parse_svg_numeric(value): + if not value: + return None + match = re.match(r"\s*([0-9]*\.?[0-9]+)", str(value)) + if not match: + return None + try: + return float(match.group(1)) + except ValueError: + return None + + try: + img_path = signpanel_type.sgv_picture.path + if img_path.lower().endswith('.svg'): + tree = ET.parse(img_path) + root = tree.getroot() + width = parse_svg_numeric(root.attrib.get('width')) + height = parse_svg_numeric(root.attrib.get('height')) + if (not width or not height) and root.attrib.get('viewBox'): + view_box = re.split(r"[\s,]+", root.attrib.get('viewBox').strip()) + if len(view_box) == 4: + try: + width = float(view_box[2]) + height = float(view_box[3]) + except ValueError: + width = width or None + height = height or None + if width and height and width > 0: + return height / width + elif Image: + with Image.open(img_path) as img: + width, height = img.size + if width > 0: + return height / width + except Exception: + pass + return 1.0 + + def estimate_panel_height(panel_type_code, panel_text, can_render_text, scale_factor=1.0, aspect_ratio=1.0): + code = (panel_type_code or "").upper() + if not can_render_text: + if code == "FXX" and not panel_text: + return max((DEFAULT_PANEL_ICON_HEIGHT_PX - 10) * scale_factor, 40 * scale_factor) + return DEFAULT_PANEL_ICON_HEIGHT_PX * scale_factor * aspect_ratio + + line_count = count_panel_text_lines(panel_type_code, panel_text, can_render_text) + text_height = line_count * TEXT_LINE_HEIGHT_PX + (TEXT_VERTICAL_PADDING_PX if line_count else 0) + + # Add extra height for lines with inline symbols (rendered at 1.5x font size in canvas) + symbol_lines = 0 + if panel_text: + normalized = normalize_panel_text(panel_text) + # Count lines containing symbol markers + for line in normalized.splitlines(): + if '\u0001PEDESTRIAN\u0002' in line or '\u0001CAR\u0002' in line or '\u0001BIKE\u0002' in line: + symbol_lines += 1 + + # Z* panels include an embedded sign icon (64px + 6px spacing) + additional_image_height = 0 + if code.startswith("Z"): + additional_image_height = ZONE_EMBEDDED_ICON_HEIGHT_PX + + adjusted_height = text_height + (symbol_lines * SYMBOL_LINE_EXTRA_HEIGHT_PX) + additional_image_height + PANEL_HEIGHT_ADJUST_PX.get(code, 0) + return max(DEFAULT_PANEL_ICON_HEIGHT_PX, adjusted_height) * scale_factor + + def estimate_panel_width(panel_type_code, can_render_text, scale_factor=1.0): + code = (panel_type_code or "").upper() + if can_render_text: + base_width = ZONE_TEXT_PANEL_BASE_WIDTH_PX if code.startswith("Z") else TEXT_PANEL_BASE_WIDTH_PX + return base_width * scale_factor + return DEFAULT_PANEL_ICON_HEIGHT_PX * scale_factor + + def get_panel_dimensions_cm(panel): + """Return panel width/height in cm with resilient fallbacks. + + Prefer explicit panel attributes when available, then type defaults, then model dimensions, then defaults. + """ + if hasattr(panel, 'get_width') and hasattr(panel, 'get_height'): + return panel.get_width(), panel.get_height() + + model = getattr(panel, 'model', None) + + width = getattr(panel, 'width', None) + if width in (None, 0) and model is not None: + width = getattr(model, 'width', None) or getattr(model, 'diameter', None) + + if not width or width <= 0: + width = 50.0 + + height = getattr(panel, 'height', None) + if height in (None, 0) and model is not None: + height = getattr(model, 'height', None) + + if not height or height <= 0: + height = width + + return float(width), float(height) + + def has_explicit_panel_height(panel): + if getattr(panel, 'height', None) not in (None, 0): + return True + model = getattr(panel, 'model', None) + if model is not None and getattr(model, 'height', None) not in (None, 0): + return True + if getattr(panel, 'signpanel_type', None) is not None: + if getattr(panel.signpanel_type, 'default_height', None) not in (None, 0) or getattr(panel.signpanel_type, 'default_diameter', None) not in (None, 0): + return True + return False + + def get_panel_scale_factor(panel_width_cm): + if panel_width_cm and panel_width_cm > 0: + return panel_width_cm / 50.0 + return 1.0 + + + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.lon + lat = obj.lat + + if lon is None or lat is None: + street = getattr(obj, 'street', None) + if street is not None: + if lon is None: + lon = getattr(street, 'lon', None) + if lat is None: + lat = getattr(street, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + panel_type_code = obj.signpanel_type.code.strip() if hasattr(obj, 'signpanel_type') and obj.signpanel_type and obj.signpanel_type.code else None + # Normalize panel_type_code to match file naming (case-insensitive) + icon_code = panel_type_code + if panel_type_code: + upper_code = panel_type_code.upper() + if upper_code == "XA": + icon_code = "GXA" # use existing SVG icon + elif upper_code == "XB": + icon_code = "GXB" + elif upper_code == "XD": + icon_code = "GXD" + # For Z* types, use the underlying code (e.g., ZE9A -> E9A) for icon mapping + if panel_type_code and panel_type_code.upper().startswith("Z") and len(panel_type_code) > 1: + icon_code = panel_type_code[1:] + panel_icon_image = f"road-sign-{icon_code.lower()}" if icon_code else None + panel_icon_image_scaled = f"{panel_icon_image}-x12" if panel_icon_image else None + + feature = { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.code, + "category": obj.category.get_name() if obj.category else None, + "model": obj.model.get_name() if obj.model else None, + "panel_type": panel_type_code, + "panel_text": normalize_panel_text(obj.signpanel_text or ("ICR" if (panel_type_code and panel_type_code.upper() == "ICR") else "")) if hasattr(obj, 'signpanel_text') else None, + "vertical_order": obj.vertical_order if hasattr(obj, 'vertical_order') else None, + "orientation": obj.orientation if hasattr(obj, 'orientation') else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "panel_icon_image": panel_icon_image, + "panel_icon_image_scaled": panel_icon_image_scaled, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "sign", + "is_location": False, + }, + } + + if asset_type == "signpanel": + panel_meta = panel_render_meta.get(obj.id, {}) + scale_factor = panel_meta.get("scale_factor", 1.0) + panel_width = panel_meta.get("width", 50.0) + panel_height = panel_meta.get("height", 50.0) + render_width = panel_meta.get("render_width", 72) + render_height = panel_meta.get("render_height", DEFAULT_PANEL_ICON_HEIGHT_PX) + vertical_order = obj.vertical_order if hasattr(obj, "vertical_order") else None + feature["properties"].update({ + "pole_id": getattr(obj, "pole_id", None), + }) + + orientation_value = obj.orientation if hasattr(obj, "orientation") and obj.orientation is not None else None + show_orientation_arrow = bool( + orientation_value is not None and + (vertical_order is None or vertical_order == 1) + ) + + arrow_vertical_shift = -MIN_PANEL_BASE_UP_OFFSET_PX + ORIENTATION_ARROW_MARGIN_PX + + if orientation_value is not None: + offset_x = 0 + offset_y = arrow_vertical_shift + else: + offset_x = 0 + offset_y = arrow_vertical_shift + + # Determine text rendering capabilities and sprite id + can_render_text = panel_meta.get("can_render_text", False) + + # Generate appropriate sprite ID based on panel type + if panel_type_code and panel_type_code.upper() == "TYPEVE": + sprite_prefix = "panel-text-typeve" + elif panel_type_code and panel_type_code.upper() in ("TYPE VIIDE", "TYPEVIIDE"): + sprite_prefix = "panel-text-typeviide" + elif panel_type_code and panel_type_code.upper() == "XC": + sprite_prefix = "panel-text-xc" + elif panel_type_code and panel_type_code.upper() == "ICR": + sprite_prefix = "panel-text-icr" + elif panel_type_code and panel_type_code.upper() == "TYPEXIVF": + sprite_prefix = "panel-text-typexivf" + elif panel_type_code and panel_type_code.upper().startswith("Z"): + sprite_prefix = "panel-text-zone" + elif panel_type_code and panel_type_code.upper().startswith("TYPE"): + sprite_prefix = "panel-text-type" + elif panel_type_code and (panel_type_code.upper() == "F33C" or panel_type_code.upper() == "F35"): + sprite_prefix = "panel-text-f33c" + elif panel_type_code and panel_type_code.upper() == "FXX": + sprite_prefix = "panel-text-fxx" + else: + sprite_prefix = "panel-text-f34a" + sprite_id = f"{sprite_prefix}-{obj.id}" if can_render_text else None + + + aspect_ratio = panel_meta.get("aspect_ratio", 1.0) + feature["properties"].update({ + "panel_orientation_offset": [offset_x, offset_y], + "show_orientation_arrow": show_orientation_arrow, + "panel_text_can_render": can_render_text, + "panel_text_image_id": sprite_id, + "panel_hide_icon": can_render_text, + "panel_vertical_order": vertical_order, + "panel_render_width": render_width, + "panel_render_height": render_height, + "panel_scale_factor": scale_factor, + "panel_width_cm": panel_width, + "panel_height_cm": panel_height, + "panel_aspect_ratio": aspect_ratio, + }) + + return feature + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + # --------- Query de base + optimisations + signpole_qs = ( + SignPole.objects + .select_related('street', 'category', 'model') + .all() + ) + signpanel_qs = ( + SignPanel.objects + .select_related('street', 'category', 'model', 'signpanel_type') + .all() + ) + + # --------- Filtres métier existants + if sign_location_ids: + signpole_qs = signpole_qs.filter(street__id__in=sign_location_ids) + signpanel_qs = signpanel_qs.filter(street__id__in=sign_location_ids) + + if municipalities: + signpole_qs = signpole_qs.filter(street__municipality_id__in=municipalities) + signpanel_qs = signpanel_qs.filter(street__municipality_id__in=municipalities) + + if models: + signpole_qs = signpole_qs.filter(model__id__in=models) + signpanel_qs = signpanel_qs.filter(model__id__in=models) + + if categories: + signpole_qs = signpole_qs.filter(category__id__in=categories) + signpanel_qs = signpanel_qs.filter(category__id__in=categories) + + if filter_geom: + signpole_qs = signpole_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + signpanel_qs = signpanel_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, street__geom__intersects=filter_geom) + ) + + if panel_types: + signpole_qs = signpole_qs.filter(id__in=[]) # Aucun pole n'a de type de panneau + signpanel_qs = signpanel_qs.filter(signpanel_type__id__in=panel_types) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + signpole_qs = apply_search(signpole_qs, data) + signpanel_qs = apply_search(signpanel_qs, data) + + # --------- Cap de sécurité + signpole_qs = signpole_qs[0:MAX_ASSET_FEATURES+1] + if len(signpole_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + signpanel_qs = signpanel_qs[0:MAX_ASSET_FEATURES+1] + if len(signpanel_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + signpanel_list = list(signpanel_qs) + panel_render_meta = {} + for panel in signpanel_list: + panel_type_code = panel.signpanel_type.code.strip() if getattr(panel, "signpanel_type", None) and panel.signpanel_type.code else None + panel_text_val = getattr(panel, "signpanel_text", None) + panel_can_render_text = can_panel_render_text(panel_type_code, panel_text_val) + panel_width_cm, panel_height_cm = get_panel_dimensions_cm(panel) + scale_factor = get_panel_scale_factor(panel_width_cm) + # Calculate aspect ratio from image if available + aspect_ratio = get_image_aspect_ratio(panel.signpanel_type) if not panel_can_render_text else 1.0 + if not has_explicit_panel_height(panel) and not panel_can_render_text: + effective_height_cm = panel_width_cm * aspect_ratio + else: + effective_height_cm = panel_height_cm + render_height = estimate_panel_height(panel_type_code, panel_text_val, panel_can_render_text, scale_factor, aspect_ratio) + render_width = estimate_panel_width(panel_type_code, panel_can_render_text, scale_factor) + panel_render_meta[panel.id] = { + "render_height": render_height, + "render_width": render_width, + "can_render_text": panel_can_render_text, + "panel_text": panel_text_val, + "scale_factor": scale_factor, + "width": panel_width_cm, + "height": panel_height_cm, + "effective_width_cm": panel_width_cm, + "effective_height_cm": effective_height_cm, + "aspect_ratio": aspect_ratio, + } + + # --------- Construction GeoJSON + features = [] + feature_count = 0 + + for qs, asset_type in [ + (signpanel_list, "signpanel"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + for qs, asset_type in [ + (signpole_qs, "signpole"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + + +@check_thematic_access('sign') +def sign_streets_detail(request, street_id): + street = get_object_or_404(SignStreet, pk=street_id) + + ct_sign_street = ContentType.objects.get_for_model(SignStreet) + interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=ct_sign_street, object_id=street.id).select_related('intervention').order_by('-intervention__creation_time')] + + assets = street.get_active_assets() + + as_built_doc = street.documents.filter(document_type='as_built').filter(status='active').order_by('-id').first() + other_docs = street.documents.filter(document_type='other').filter(status='active').order_by('-id') + + street_geom_4326 = street.geom.transform(4326, clone=True) + geom = json.loads(street_geom_4326.geojson) + + return render(request, "assets/sign_street_detail.html", + { + "street":street, + "interventions":interventions, + "assets":assets, + 'main_documents': [as_built_doc,], + "other_documents": other_docs, + 'add_document_form': SignStreetDocumentForm(), + "geom": geom + }) + + + +@check_thematic_access('sign') +def sign_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni (spécifier l'app_label 'assets' pour éviter les conflits) + content_type = ContentType.objects.get(app_label='assets', model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"sign/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for sign + context['thematic_code'] = 'sign' + + return render(request, template.template.name, context) + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + + + +@check_thematic_access('sign', edit_permission_required=True) +@require_POST +def sign_streets_add_document(request, street_id): + signstreet = get_object_or_404(SignStreet, pk=street_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + for i in range(count): + file = files[i] + document_type = request.POST.get(f'document_type_{i}', 'other') + + SignStreetDocument.objects.create( + street=signstreet, + file=file, + document_type=document_type, + uploaded_by = request.user + ) + return redirect('assets:sign_streets_detail', street_id=street_id) + +@check_thematic_access('sign', edit_permission_required=True) +@require_POST +def sign_streets_delete_document(request, document_id): + document = get_object_or_404(SignStreetDocument, id=document_id) + street_id = document.street.id # pour rediriger vers la bonne page + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + #messages.success(request, "Le document a été supprimé avec succès.") + return redirect('assets:sign_streets_detail', structure_id=street_id) + + +@check_thematic_access('sign') +def fetch_document_history(request): + street_id = request.GET.get('street_id') + document_type = request.GET.get('document_type') + + documents = SignStreetDocument.objects.filter( + street_id=street_id, + document_type=document_type + ).order_by('-id') + + html = render_to_string('assets/document_history_list.html', {'documents': documents}) + return JsonResponse({'html': html}) + + + + +@require_GET +def get_nearest_sign_street(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + SignStreet.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by('distance') + .first() + ) + + location_ct_id = ContentType.objects.get_for_model(nearest).id + + if nearest: + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, "lat": nearest.lat + }) + else: + return JsonResponse({"error": _("Aucune rue trouvée")}) + + + + +@require_GET +def sign_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + location_id = request.GET.get('location_id') # = id de la rue + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # Prépare filtre "dans cette rue uniquement" si location_id fourni + street_filter = {} + if location_id: + street_filter = {'street_id': location_id} + + + def base_qs(model, extra_filter=None): + ann = { + 'code_u': Lower(Unaccent('code')), + 'name_fr_u': Lower(Unaccent('name_fr')), + 'name_nl_u': Lower(Unaccent('name_nl')), + } + q = model.objects.annotate(**ann) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + q = q.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + # Filtrer uniquement les non supprimés + q = q.exclude(status='removed') + + if extra_filter: + q = q.filter(**extra_filter) + + if category_id is not None: + q = q.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + q = q.select_related('street', 'model') + + return q + + # Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python) + poles_qs = base_qs(SignPole, extra_filter=street_filter)[:100] + panels_qs = base_qs(SignPanel, extra_filter=street_filter)[:100] + + # Concatène en mémoire (on priorise ensuite) + items = [] + for a in list(poles_qs) + list(panels_qs): + # priorisation locale (recalcule ici car on merge des modèles différents) + code_u = getattr(a, 'code_u', None) + name_fr_u = getattr(a, 'name_fr_u', None) + name_nl_u = getattr(a, 'name_nl_u', None) + + prio = 7 + # Priorisation uniquement si un terme de recherche est fourni + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u and code_u.startswith(qn): + prio = 1 + elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)): + prio = 2 + elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u): + prio = 4 + else: + # Sans terme de recherche, on trie par code + prio = 5 + + street = a.street if hasattr(a, 'street') else None + equipment_type = a.model if hasattr(a, 'model') else None + parts = [a.code, a.get_name()] + equipment_label = " - ".join(p for p in parts if p) + code = getattr(a, 'code', '') or '' + label = equipment_label + lon, lat = [getattr(a, 'lon', None), getattr(a, 'lat', None)] + + location_id_val = street.id if street else None + location_label = (f"{street.code} - {street.get_name()}" if street else None) + + equipment_ct_id = ContentType.objects.get_for_model(a).id + location_ct_id = ContentType.objects.get_for_model(street).id if street else None + + items.append({ + "prio": prio, + "code": code or "", + "id": getattr(a, 'id', None), + "label": label, + + # attendus par ton autocomplete équipements + "equipment_id": getattr(a, 'id', None), + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": location_id_val, + "location_label": location_label, + "location_ct": location_ct_id, + + "lon": lon, + "lat": lat, + }) + + # Tri global par priorité puis code, coupe à 100 + items.sort(key=lambda x: (x["prio"], x["code"])) + data = items[:100] + + return JsonResponse(data, safe=False) \ No newline at end of file diff --git a/streetup/assets/views/structures.py b/streetup/assets/views/structures.py new file mode 100644 index 0000000..b6764ba --- /dev/null +++ b/streetup/assets/views/structures.py @@ -0,0 +1,1181 @@ +# ---------------- +# Structures Views +# ---------------- + +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.views.decorators.http import require_GET, require_POST +from django.contrib.gis.db.models.functions import Transform, Distance +from django.contrib.gis.geos import GEOSGeometry, Point +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q, Case, When, IntegerField, CharField +from django.db.models.functions import Lower +from django.db.models import Func, Prefetch +from django.db import connection +from django.template.loader import render_to_string +from django.utils.translation import get_language, gettext as _ + + +import unicodedata +import json +from collections import defaultdict + +from common.utils import check_thematic_access + +from common.models import Thematic, UserConfig, UserContractAccess, UserThematics +from assets.models import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel, StructureDocument, StructureContract, AssetCategory + +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager + +from assets.forms import StructureDocumentForm + +from documents.models import DocumentTag, DocumentFolder +from documents.services import DocumentManagementService + +@check_thematic_access('structures') +def structures_index(request): + thematic = 'structures' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_structure_types = request.GET.getlist('structure_type') + selected_is_main_location = request.GET.get('is_main_location') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + categories = list(AssetCategory.objects.filter(thematic__code = 'structures')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + models = list(StructureAssetModel.objects.all()) + models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in models] + + category_model_map = defaultdict(list) + models = StructureAssetModel.objects.all() + for model in models: + if model.category: + display_name = model.get_name() + category_model_map[str(model.category_id)].append({ + "key": str(model.id), + "label": display_name, + }) + + context = { + 'thematics': thematics, + 'structure_type_choices': Structure._meta.get_field('structure_type').choices, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': dict(category_model_map), + 'status_choices': StructureGeoAsset._meta.get_field('status').choices, + 'selected_thematic': selected_thematic, + 'selected_structure_types': selected_structure_types, + 'selected_is_main_location': selected_is_main_location, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'selected_statuses': statuses, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Localisations"), + 'locations_assets_title': "Assets", + } + + return render(request, "assets/structures_index.html", context) + + +def structures_locations(request): + pass + +def structures_assets(request): + pass + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +@check_thematic_access('structures') +def structures_locations_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + structure_types = ensure_list(data.get('structure_type')) + is_main_location = data.get('is_main_location') + + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + structure_types = request.GET.getlist('structure_type') + is_main_location = request.GET.get('is_main_location') + filter_geom = None + + # --------- Point d'entrée du queryset + structure_locations = StructureLocation.objects.select_related('structure').all() + + # --------- Filtre par type de structure + if structure_types: + structure_locations = structure_locations.filter(structure__structure_type__in=structure_types) + + # --------- Filtre par localisation principale + if is_main_location: + val = is_main_location.lower() + if val == 'true': + structure_locations = structure_locations.filter(is_main_location=True) + elif val == 'false': + structure_locations = structure_locations.filter(is_main_location=False) + + # --------- Filtre géométrique + if filter_geom: + structure_locations = structure_locations.filter(geom__intersects=filter_geom) + + # --------- Limitation pour utilisateurs externes + user = request.user + if user.is_authenticated: + try: + user_config = user.config # OneToOne UserConfig + except UserConfig.DoesNotExist: + user_config = None + + # externe = pas interne ; on tient aussi compte du flag limit_assets_to_contracts + 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: + # contrats accessibles à l'utilisateur (idéalement avec droit "assets") + accessible_contract_ids = UserContractAccess.objects.filter( + user_config=user_config, + can_view_assets=True # champ prévu dans la table d'accès contrat + ).values_list('contract_id', flat=True) + + # Ne garder que les locations dont la structure a un contrat actif lié à ces contrats + structure_locations = structure_locations.filter( + structure__contracts__status='active', # StructureContract.status + structure__contracts__contract_id__in=accessible_contract_ids # FK contract + ).distinct() + else: + # Non authentifié : ne rien exposer (option sûre) + structure_locations = structure_locations.none() + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + structure_locations = apply_search(structure_locations, data) + + # --------- Conversion en GeoJSON + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None # AbstractLocation.geojson # :contentReference[oaicite:16]{index=16} + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "structure": obj.structure.code if obj.structure else "-", + "name": obj.get_name(), + "structure_type": obj.structure.get_structure_type_display() if (obj.structure and obj.structure.structure_type) else None, + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": "Ouvrage d'art", + "thematic": "structures", + "is_location": True, + } + } + + features = [] + for location in structure_locations.order_by('structure__code', 'code'): + features.append(obj_to_feature(location)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('structures') +def structures_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + structure_location_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + begin_date = data.get('asset_begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + structure_types = ensure_list(data.get('structure_type')) + is_main_location = data.get('is_main_location') + include_no_geom = data.get('include_no_geom', False) + validation_statuses = ensure_list(data.get('asset_validation_status')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + structure_location_id = request.GET.get('location') + structure_location_ids = [structure_location_id] if structure_location_id else [] + statuses = request.GET.getlist('asset_status') + validation_statuses = request.GET.getlist('asset_validation_status') + begin_date = request.GET.get('asset_begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + structure_types = request.GET.getlist('structure_type') + is_main_location = request.GET.get('is_main_location') + include_no_geom = request.GET.get('include_no_geom') == 'true' + filter_geom = None + + # Pas de paramètres = pas de carte (comportement existant conservé) + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not structure_location_ids and not categories and not models and not filter_geom and not _search_q and not structure_types and not is_main_location: + geojson_data = {"type": "FeatureCollection", "features": []} + return JsonResponse(geojson_data) + + from assets.views.core import get_element_type_label + + def build_geometry_and_coords(obj): + geometry = None + if obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.lon + lat = obj.lat + + if lon is None or lat is None: + location = getattr(obj, 'location', None) + if location is not None: + if lon is None: + lon = getattr(location, 'lon', None) + if lat is None: + lat = getattr(location, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "category": obj.category.get_name() if obj.category else None, + "model": obj.model.get_name() if obj.model else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "structures", + "is_location": False, + }, + } + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + # --------- Query de base + optimisations + structure_asset_qs = ( + StructureGeoAsset.objects + .select_related('location', 'location__structure', 'category', 'model') + .all() + ) + + # --------- Filtres métier existants + if structure_location_ids: + # Étendre pour inclure tous les descendants des localisations sélectionnées + all_location_ids = [] + for loc_id in structure_location_ids: + try: + descendant_ids = get_descendant_location_ids(int(loc_id)) + all_location_ids.extend(descendant_ids) + except Exception: + # En cas d'erreur, on ajoute juste l'ID original + all_location_ids.append(loc_id) + # Dédupliquer la liste + all_location_ids = list(set(all_location_ids)) + structure_asset_qs = structure_asset_qs.filter(location__id__in=all_location_ids) + + # Filtres de localisation appliqués aux assets (mode unifié et page dédiée). + if structure_types: + structure_asset_qs = structure_asset_qs.filter(location__structure__structure_type__in=structure_types) + + if is_main_location: + val = is_main_location.lower() + if val == 'true': + structure_asset_qs = structure_asset_qs.filter(location__is_main_location=True) + elif val == 'false': + structure_asset_qs = structure_asset_qs.filter(location__is_main_location=False) + + if models: + structure_asset_qs = structure_asset_qs.filter(model__id__in=models) + + if categories: + structure_asset_qs = structure_asset_qs.filter(category__id__in=categories) + + if validation_statuses: + structure_asset_qs = structure_asset_qs.filter(validation_status__in=validation_statuses) + + if filter_geom: + from assets.views.core import filter_queryset_by_geom + structure_asset_qs = filter_queryset_by_geom( + structure_asset_qs, + filter_geom, + include_no_geom, + geom_field='geom', + location_field='location' + ) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + structure_asset_qs = apply_search(structure_asset_qs, data) + + # --------- Limitation pour utilisateurs externes + user = request.user + if user.is_authenticated: + try: + user_config = user.config # OneToOne + 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) + + # Ne garder que les assets dont la structure (via la location) a un contrat actif + # auquel l'utilisateur a accès + structure_asset_qs = structure_asset_qs.filter( + location__structure__contracts__status='active', + location__structure__contracts__contract_id__in=accessible_contract_ids + ).distinct() + else: + # Non authentifié : ne rien exposer (plus sûr) + structure_asset_qs = structure_asset_qs.none() + + # --------- Cap de sécurité + structure_asset_qs = structure_asset_qs[0:MAX_ASSET_FEATURES+1] + if len(structure_asset_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + # --------- Construction GeoJSON + features = [] + feature_count = 0 + for qs, asset_type in [ + (structure_asset_qs, "structuregeoasset"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + return JsonResponse(geojson_data, safe=False) + + + + +@check_thematic_access('structures') +def structures_locations_detail(request, location_id): + show_inactive_assets = (request.GET.get('include_inactive') or '').lower() in ('1', 'true', 'yes', 'on') + + toggle_params = request.GET.copy() + if show_inactive_assets: + toggle_params.pop('include_inactive', None) + else: + toggle_params['include_inactive'] = '1' + toggle_query = toggle_params.urlencode() + toggle_inactive_url = f"{request.path}?{toggle_query}" if toggle_query else request.path + + asset_tree_qs = StructureGeoAsset.objects.all().order_by('code') + if not show_inactive_assets: + asset_tree_qs = asset_tree_qs.filter(status='active') + + # Préfetch (réduction N+1) : ASSETS + enfants (2 niveaux) + location = ( + StructureLocation.objects + .prefetch_related( + Prefetch( + 'assets', + queryset=asset_tree_qs + ), + Prefetch('child_locations__assets', queryset=asset_tree_qs), + Prefetch('child_locations__child_locations__assets', queryset=asset_tree_qs), + 'child_locations__child_locations', + ) + .select_related('structure') + .get(pk=location_id) + ) + + # 1) Construire la liste des ids (location + enfants) + def collect_ids(loc): + ids = [loc.id] + for child in loc.child_locations.all(): + ids.extend(collect_ids(child)) + return ids + + all_loc_ids = collect_ids(location) + + # 2) Récupérer en bloc tous les liens GFK pour ces ids + ct_sl = ContentType.objects.get_for_model(StructureLocation) + links = ( + InterventionLocation.objects + .filter(content_type=ct_sl, object_id__in=all_loc_ids) + .select_related('intervention') + ) + + # 3) Indexer par location_id → [interventions...] + by_loc = defaultdict(list) + for link in links: + by_loc[link.object_id].append(link.intervention) + + # 4) Arbre + def build_tree(loc): + return { + 'loc': loc, + 'assets': list(loc.assets.all()), + 'interventions': by_loc.get(loc.id, []), + 'children': [build_tree(child) for child in loc.child_locations.all()], + } + + tree = build_tree(location) + + # 5) Aplatir les interventions de l’arbre (sans doublons) + tri global desc. par code + def flatten_interventions(node): + seen = set() + out = [] + def walk(n): + for itv in n['interventions']: + if itv.pk not in seen: + seen.add(itv.pk) + out.append(itv) + for c in n['children']: + walk(c) + walk(node) + return out + + interventions = sorted( + flatten_interventions(tree), + key=lambda itv: itv.creation_time, + reverse=True + ) + + # 6) Assets + documents + assets = location.get_all_assets() if show_inactive_assets else location.get_active_assets() + + # --- NOUVELLE GESTION DOCUMENTAIRE --- + # Récupérer les documents depuis le système de gestion documentaire + doc_data = DocumentManagementService.get_documents_for_asset(location.structure) + managed_main_documents = doc_data.get("main_documents", []) + managed_other_documents = doc_data.get("other_documents", []) + document_folder = doc_data.get("folder") + + # Fallback vers l'ancien système si pas de répertoire configuré + if document_folder is None: + if location.structure: + # Ancien système: récupérer depuis StructureDocument + as_built_doc = location.structure.documents.filter(document_type='as_built', status='active').order_by('-id').first() + cme_doc = location.structure.documents.filter(document_type='cme', status='active').order_by('-id').first() + pis_doc = location.structure.documents.filter(document_type='pis', status='active').order_by('-id').first() + inspection_gc_doc = location.structure.documents.filter(document_type='inspection_ce', status='active').order_by('-id').first() + inspection_em_doc = location.structure.documents.filter(document_type='inspection_em', status='active').order_by('-id').first() + other_docs = location.structure.documents.filter(document_type='other', status='active').order_by('-id') + + main_documents = [] + if as_built_doc: + main_documents.append(as_built_doc) + if cme_doc: + main_documents.append(cme_doc) + if pis_doc: + main_documents.append(pis_doc) + if inspection_gc_doc: + main_documents.append(inspection_gc_doc) + if inspection_em_doc: + main_documents.append(inspection_em_doc) + else: + main_documents = [] + other_docs = [] + + use_managed_documents = False + else: + main_documents = managed_main_documents + other_docs = managed_other_documents + use_managed_documents = True + # --- FIN NOUVELLE GESTION DOCUMENTAIRE --- + + user = request.user + user_config = getattr(user, "config", None) + + is_delete_document_allowed = False + can_add_unique_tag_documents = False + can_create_assets = False + if user_config and user_config.is_intern: + if user_config.roles.filter(name__in=["admin", "manager", "controller"]).exists(): + is_delete_document_allowed = True + can_create_assets = True + can_add_unique_tag_documents = True + elif user_config: + can_add_unique_tag_documents = user_config.can_add_unique_tag_documents + if user_config.roles.filter(name__in=["external_manager", "technician"]).exists(): + can_create_assets = True + + # Récupérer les tags disponibles pour la thématique "structures" + available_tags = DocumentManagementService.get_available_tags_for_thematic('structures') + + # Récupérer les sous-répertoires disponibles pour le choix de l'utilisateur + available_folders = DocumentManagementService.get_subfolders_for_object(location.structure) if location.structure else [] + + # 7) Géométrie 4326 (main_location fallback) + if location.geom: + location_geom_4326 = location.geom.transform(4326, clone=True) + elif location.structure and location.structure.main_location: + location_geom_4326 = location.structure.main_location.geom.transform(4326, clone=True) + else: + location_geom_4326 = None + + geom = json.loads(location_geom_4326.geojson) if location_geom_4326 else None + + return render(request, "assets/structures_location_detail.html", { + "location": location, + "interventions": interventions, # toutes les interventions de l'arbre + "assets": assets, + "main_documents": main_documents, + "other_documents": other_docs, + "add_document_form": StructureDocumentForm(), + "geom": geom, + "location_tree": tree, # contient interventions par nœud + "show_inactive_assets": show_inactive_assets, + "toggle_inactive_url": toggle_inactive_url, + "is_delete_document_allowed": is_delete_document_allowed, + "can_add_unique_tag_documents": can_add_unique_tag_documents, + "can_create_assets": can_create_assets, + "available_tags": available_tags, + "available_folders": available_folders, + "document_folder": document_folder, + "use_managed_documents": use_managed_documents if 'use_managed_documents' in dir() else False, + }) + + + +@check_thematic_access('structures') +def structures_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"structures/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for structures + context['thematic_code'] = 'structures' + + return render(request, template.template.name, context) + + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + +# @require_GET +# def structures_locations_autocomplete(request): +# raw_query = (request.GET.get('q') or '').strip() +# if len(raw_query) < 2: +# return JsonResponse([], safe=False) + +# # Normalise la requête (équivalent de unaccent+lower côté Python) +# qn = strip_accents_lower(raw_query) + +# # Prépare des colonnes "recherchables" unaccentées + minuscules +# qs = ( +# StructureLocation.objects +# .annotate( +# code_u=Lower(Unaccent('code')), +# name_fr_u=Lower(Unaccent('name_fr')), +# name_nl_u=Lower(Unaccent('name_nl')), +# ) +# .filter( +# Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower +# Q(name_fr_u__contains=qn) | +# Q(name_nl_u__contains=qn) +# ) +# ) + +# # Priorité : exact > startswith > nom startswith > contient +# priority = Case( +# When(code_u=qn, then=0), +# When(code_u__startswith=qn, then=1), +# When(name_fr_u__startswith=qn, then=2), +# When(name_nl_u__startswith=qn, then=3), +# When(code_u__contains=qn, then=4), +# When(name_fr_u__contains=qn, then=5), +# When(name_nl_u__contains=qn, then=6), +# default=7, +# output_field=IntegerField(), +# ) + +# qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50] + +# data = [{ +# "label": f"{loc.code} - {loc.get_name()}", +# "location_id": loc.id, +# "location_label": f"{loc.get_name()} ({loc.code})", +# "lon": ( +# loc.lon if loc.lon is not None +# else (loc.structure.main_location.lon +# if getattr(loc, "structure", None) and getattr(loc.structure, "main_location", None) +# else None) +# ), +# "lat": ( +# loc.lat if loc.lat is not None +# else (loc.structure.main_location.lat +# if getattr(loc, "structure", None) and getattr(loc.structure, "main_location", None) +# else None) +# ), +# "structure_id": loc.structure.id if getattr(loc, "structure", None) else None, +# "structure_label": ( +# f"{loc.structure.get_name()} ({loc.structure.code})" +# if getattr(loc, "structure", None) else None +# ), +# } for loc in qs] + +# return JsonResponse(data, safe=False) + + + +def get_descendant_location_ids(root_location_id: int) -> list[int]: + """ + Récupère l'id de la localisation racine + tous ses descendants (FK parent_location) via un CTE récursif. + Nécessite PostgreSQL. + """ + # Récupérer dynamiquement le nom de la table depuis le modèle Django + table_name = StructureLocation._meta.db_table + + with connection.cursor() as cur: + cur.execute(f""" + WITH RECURSIVE loc_tree AS ( + SELECT id + FROM {table_name} + WHERE id = %s + UNION ALL + SELECT sl.id + FROM {table_name} sl + JOIN loc_tree lt ON sl.parent_location_id = lt.id + ) + SELECT id FROM loc_tree; + """, [root_location_id]) + rows = cur.fetchall() + return [r[0] for r in rows] + +@require_GET +def structures_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + # CT ids (calculés une fois) + equipment_ct_id = ContentType.objects.get_for_model(StructureGeoAsset).id + location_ct_id = ContentType.objects.get_for_model(StructureLocation).id + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + # filtres optionnels + # Supporte à la fois location_id (singulier) et location_ids (pluriel via getlist) + location_id = request.GET.get('location_id') + location_ids_param = request.GET.getlist('location_ids') or request.GET.getlist('location_id') + structure_id = request.GET.get('structure_id') + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # si location(s) fournie(s), calcule l'ensemble {locations + tous les descendants} + location_ids = None + input_location_ids = [] + + # Collecter tous les IDs de localisations fournis + if location_id: + input_location_ids.append(location_id) + if location_ids_param: + input_location_ids.extend(location_ids_param) + + # Dédupliquer + input_location_ids = list(set(input_location_ids)) + + if input_location_ids: + all_location_ids = [] + for loc_id in input_location_ids: + try: + descendant_ids = get_descendant_location_ids(int(loc_id)) + all_location_ids.extend(descendant_ids) + except Exception: + # en cas d'ID invalide ou autre, on ignore cet ID + pass + # Dédupliquer la liste finale + location_ids = list(set(all_location_ids)) if all_location_ids else None + + # Base queryset accent-insensible + actifs uniquement + qs = ( + StructureGeoAsset.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + ) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + qs = qs.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + qs = qs.exclude(status='removed') # <-- exclure les supprimés + qs = qs.select_related( + 'location', + 'location__structure', + 'structure', + 'structure__main_location', + 'model', + 'category', + ) + + if location_ids: + qs = qs.filter(location_id__in=location_ids) + if structure_id: + qs = qs.filter(structure_id=structure_id) + if category_id is not None: + qs = qs.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + # Priorité : exact > code startswith > nom startswith > contient + if has_search_term: + priority = Case( + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + default=7, + output_field=IntegerField(), + ) + qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100] + else: + # Sans terme de recherche, tri par code uniquement + qs = qs.order_by('code')[:100] + + data = [] + for a in qs: + # equipment_type depuis model/category + equipment_type = None + if getattr(a, 'model', None): + equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None) + if not equipment_type and getattr(a, 'category', None): + equipment_type = getattr(a.category, 'name', None) + + # coordonnées: asset > geom(4326) > location > structure.main_location + lon = a.lon + lat = a.lat + + if (lon is None or lat is None) and getattr(a, 'geom', None): + try: + g = a.geom + if g.srid and g.srid != 4326: + g = a.geom.transform(4326, clone=True) + coords = getattr(g, 'coords', None) + if coords: + if lon is None: lon = coords[0] + if lat is None: lat = coords[1] + except Exception: + pass + + if (lon is None or lat is None) and getattr(a, 'location', None): + if lon is None and getattr(a.location, 'lon', None) is not None: + lon = a.location.lon + if lat is None and getattr(a.location, 'lat', None) is not None: + lat = a.location.lat + + main_loc = (a.structure.main_location if getattr(a, 'structure', None) and getattr(a.structure, 'main_location', None) + else (a.location.structure.main_location + if getattr(a, 'location', None) and getattr(a.location, 'structure', None) + and getattr(a.location.structure, 'main_location', None) + else None)) + if (lon is None or lat is None) and main_loc: + if lon is None: lon = getattr(main_loc, 'lon', None) + if lat is None: lat = getattr(main_loc, 'lat', None) + + equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name() + # label = f"{a.code} - {equipment_label}" if a.code else equipment_label + label = equipment_label + + location_label = ( + f"{a.location.get_name()} ({a.location.code})" + if getattr(a, "location", None) else None + ) + struct = a.structure or (a.location.structure if getattr(a, "location", None) else None) + structure_id_val = struct.id if struct else None + structure_label = (f"{struct.get_name()} ({struct.code})" if struct else None) + + data.append({ + "id": a.id, + "label": label, + + "equipment_id": a.id, + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": a.location.id if getattr(a, "location", None) else None, + "location_label": location_label, + "location_ct": location_ct_id, + + "parent_ref_id": structure_id_val, + "parent_ref_label": structure_label, + + "lon": lon, + "lat": lat, + }) + + return JsonResponse(data, safe=False) + + + + + + + +@check_thematic_access('structures', edit_permission_required=True) +@require_POST +def structures_add_document(request, structure_id): + structure = get_object_or_404(Structure, pk=structure_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + user = request.user + user_config = getattr(user, "config", None) + + # Vérifier si le nouveau système de gestion documentaire est activé + document_folder = DocumentManagementService.get_default_folder_for_object(structure) + + for i in range(count): + file = files[i] + tag_slug = request.POST.get(f'tag_{i}', '') + folder_id = request.POST.get(f'folder_{i}', '') + + # Nouveau système de gestion documentaire + if document_folder is not None and tag_slug: + try: + tag = DocumentTag.objects.get(slug=tag_slug) + except DocumentTag.DoesNotExist: + tag = None + + # Vérifier les permissions pour les tags uniques + if tag and tag.is_unique: + can_add = DocumentManagementService.can_user_add_unique_tag_document(user, tag) + + if not can_add: + # L'utilisateur n'a pas les permissions, on skip ce fichier + continue + + # Déterminer le répertoire cible + target_folder = None + if folder_id: + try: + target_folder = DocumentFolder.objects.get(pk=int(folder_id)) + except (DocumentFolder.DoesNotExist, ValueError): + target_folder = document_folder + else: + target_folder = document_folder + + # Ajouter le document via le service + DocumentManagementService.add_document( + file=file, + tag=tag, + folder=document_folder, + user=user, + title=file.name if hasattr(file, 'name') else None, + content_object=structure, + target_folder=target_folder + ) + else: + # Fallback vers l'ancien système (StructureDocument) + document_type = request.POST.get(f'document_type_{i}', 'other') + + StructureDocument.objects.create( + structure=structure, + file=file, + document_type=document_type, + uploaded_by=user + ) + + return redirect('assets:structures_locations_detail', location_id=structure.main_location.id) + +@check_thematic_access('structures', edit_permission_required=True) +@require_POST +def structures_delete_document(request, document_id): + document = get_object_or_404(StructureDocument, id=document_id) + structure_id = document.structure.id # pour rediriger vers la bonne page + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + #messages.success(request, "Le document a été supprimé avec succès.") + return redirect('assets:structures_locations_detail', location_id=document.structure.main_location.id) + + +@check_thematic_access('structures') +def fetch_document_history(request): + structure_id = request.GET.get('structure_id') + document_type = request.GET.get('document_type') + tag_slug = request.GET.get('tag_slug') + + user = request.user + user_config = getattr(user, "config", None) + + is_delete_document_allowed = False + if user_config and user_config.is_intern: + if user_config.roles.filter(name__in=["admin", "manager"]).exists(): + is_delete_document_allowed = True + + # Vérifier si on utilise le nouveau système de gestion documentaire + structure = Structure.objects.filter(pk=structure_id).first() + if structure: + document_folder = DocumentManagementService.get_default_folder_for_object(structure) + else: + document_folder = None + + if document_folder is not None and tag_slug: + # Nouveau système: récupérer l'historique des versions + versions = DocumentManagementService.get_document_history_by_tag(structure, tag_slug) + html = render_to_string('assets/document_version_history_list.html', { + 'versions': versions, + 'is_delete_document_allowed': is_delete_document_allowed, + 'use_managed_documents': True + }) + else: + # Ancien système: récupérer depuis StructureDocument + documents = StructureDocument.objects.filter( + structure_id=structure_id, + document_type=document_type + ).order_by('-id') + + html = render_to_string('assets/document_history_list.html', { + 'documents': documents, + 'is_delete_document_allowed': is_delete_document_allowed + }) + + return JsonResponse({'html': html}) + + +@check_thematic_access('structures') +@require_GET +def get_document_config(request, structure_id): + """ + API pour récupérer la configuration des documents (tags et répertoires disponibles). + Utilisé par le JavaScript pour le formulaire d'ajout de document. + """ + structure = get_object_or_404(Structure, pk=structure_id) + + user = request.user + user_config = getattr(user, "config", None) + + # Vérifier les permissions + can_add_unique_tag_documents = False + if user_config and user_config.is_intern: + can_add_unique_tag_documents = True + elif user_config: + can_add_unique_tag_documents = user_config.can_add_unique_tag_documents + + # Vérifier si le nouveau système est activé + document_folder = DocumentManagementService.get_default_folder_for_object(structure) + + if document_folder is None: + return JsonResponse({ + 'use_managed_documents': False, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'tags': [], + 'folders': [], + }) + + # Récupérer les tags disponibles pour la thématique "structures" + tags = DocumentManagementService.get_available_tags_for_thematic('structures') + tags_data = [ + { + 'slug': tag.slug, + 'name': tag.name, + 'is_unique': tag.is_unique, + 'show_in_asset_detail': tag.show_in_asset_detail, + } + for tag in tags + ] + + # Récupérer les sous-répertoires disponibles + folders = DocumentManagementService.get_subfolders_for_object(structure) + + return JsonResponse({ + 'use_managed_documents': True, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'tags': tags_data, + 'folders': folders, + }) + + +@require_GET +def get_nearest_structure_location(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Coordonnées invalides"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + StructureLocation.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by('distance') + .first() + ) + + if nearest: + location_ct_id = ContentType.objects.get_for_model(nearest).id + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "parent_ref_label": f"{nearest.structure.get_name()} ({nearest.structure.code})" if nearest.structure else None, + "lon": nearest.lon, + "lat": nearest.lat + }) + else: + return JsonResponse({"error": _("Aucune localisation structure trouvée")}) \ No newline at end of file diff --git a/streetup/assets/views/trafficlights.py b/streetup/assets/views/trafficlights.py new file mode 100644 index 0000000..76a6521 --- /dev/null +++ b/streetup/assets/views/trafficlights.py @@ -0,0 +1,5077 @@ +# -------------------- +# Traffic Lights Views +# -------------------- + +import logging +logger = logging.getLogger(__name__) + +from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, Http404 +from django.shortcuts import render, get_object_or_404, redirect +from django.urls import reverse +from django.core.serializers import serialize +from django.core.serializers.json import DjangoJSONEncoder +from django.contrib.gis.db.models.functions import Transform +from django.db.models import Value, OuterRef, Subquery, Q, Case, When +from django.db.models import IntegerField, CharField, BooleanField +from django.db.models import Func +from django.db.models.functions import Lower +from django.contrib.gis.db.models import GeometryField +from django.contrib.gis.geos import Point, GEOSGeometry +from django.contrib.gis.db.models.functions import Distance +from django.contrib.auth.decorators import permission_required +from django.contrib.contenttypes.models import ContentType +from django.views.decorators.http import require_GET, require_POST +from django.template.loader import render_to_string +from django.utils.translation import get_language, gettext as _ +from django.utils.timezone import make_aware, get_current_timezone + +import json +from datetime import datetime +import unicodedata +from collections import defaultdict + +from common.models import Thematic, UserConfig, UserContractAccess, UserThematics +from common.utils import check_thematic_access +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager +from assets.models import TrafficLightIntersection, TrafficLightIntersectionDocument, TrafficLightController, TrafficLightPole, TrafficLightCable, TrafficLightLantern, TrafficLightDetector, TrafficLightElectricalCabinet, TrafficLightRadar, TrafficLightRadarPole +from assets.models import AssetCategory, TrafficLightCableModel, TrafficLightControllerModel, TrafficLightDetectorModel, TrafficLightLanternModel, TrafficLightPoleModel, TrafficLightElectricalCabinetModel, TrafficLightContract, TrafficLightRadarModel +from assets.forms import IntersectionDocumentForm +from assets.permissions import get_allowed_actions_for_thematic, can_edit_location_for_thematic +from contracts.models import Contract +from documents.services import DocumentManagementService +from documents.permissions import user_can_browse_folder +import os +import ezdxf + +# Global cache for parsed DXF documents to avoid expensive disk reading and parsing (takes ~3.5s for 26MB files) +DXF_DOC_CACHE = {} + +def get_cached_dxf_document(file_path): + import hashlib + try: + with open(file_path, 'rb') as f: + header = f.read(8192) + cache_key = hashlib.md5(header).hexdigest() + except Exception: + cache_key = file_path + + cached = DXF_DOC_CACHE.get(cache_key) + if cached: + return cached + + # Read and store in cache + doc = ezdxf.readfile(file_path) + DXF_DOC_CACHE[cache_key] = doc + # Limit cache size to prevent memory leaks (keep at most 5 documents) + if len(DXF_DOC_CACHE) > 5: + DXF_DOC_CACHE.pop(next(iter(DXF_DOC_CACHE))) + + return doc + +def get_cable_color(code): + palette = [ + "#10b981", # Emerald + "#3b82f6", # Blue + "#f59e0b", # Amber + "#ef4444", # Red + "#8b5cf6", # Purple + "#ec4899", # Pink + "#06b6d4", # Cyan + "#eab308", # Yellow + "#14b8a6", # Teal + "#f43f5e", # Rose + "#84cc16", # Lime + "#a855f7", # Purple-bright + ] + if not code: + return palette[0] + h = sum(ord(char) for char in code) + return palette[h % len(palette)] + + +@check_thematic_access('trafficlights') +def trafficlights_index(request): + thematic = 'trafficlights' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + voltages = request.GET.getlist('voltage') + begin_date = request.GET.get('begin_date') + connected = request.GET.get('connected') + controller_maintenance_provider = request.GET.get('controller_maintenance_provider') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) + preventive_element_type = form_data.get('type_of_selected_elements') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + # Catégories limitées à la thématique "trafficlights" + categories = list(AssetCategory.objects.filter(thematic__code='trafficlights')) + categories.sort(key=lambda c: c.get_name().lower()) + category_choices = [(c.id, c.get_name()) for c in categories] + + # Classes de modèles à agréger + asset_model_classes = [ + TrafficLightCableModel, + TrafficLightControllerModel, + TrafficLightDetectorModel, + TrafficLightLanternModel, + TrafficLightPoleModel, + TrafficLightElectricalCabinetModel, + ] + + # Récupérer tous les modèles de chaque classe + all_models = [] + for cls in asset_model_classes: + all_models.extend(cls.objects.all()) + + # Trier et construire les choix + all_models.sort(key=lambda m: (m.get_name() or "").lower()) + model_choices = [(m.id, m.get_name()) for m in all_models] + + # Construire le mapping catégorie → modèles + category_model_map = defaultdict(list) + for model in all_models: + if model.category: + category_model_map[str(model.category_id)].append({ + "key": str(model.id), + "label": model.get_name(), + }) + + + # Get controller maintenance contracts (for internal users only) + user_config = UserConfig.objects.filter(user=request.user).first() + is_intern = user_config.is_intern if user_config else False + + controller_maintenance_contracts = [] + if is_intern: + # Get all contracts linked to traffic light controller maintenance (status='active', maintenance_type='controller') + controller_maintenance_contracts = Contract.objects.filter( + id__in=TrafficLightContract.objects.filter( + status='active', + maintenance_type='controller' + ).values_list('contract_id', flat=True).distinct() + ).order_by('contract_number') + controller_maintenance_contracts = [(c.id, f"{c.contract_number} - {c.company.name}") for c in controller_maintenance_contracts] + + context = { + 'status_choices': TrafficLightIntersection._meta.get_field('status').choices, + 'voltage_choices': TrafficLightIntersection._meta.get_field('voltage_type').choices, + 'connected_choices':[(True, _('Oui')), (False, _('Non'))], + 'selected_statuses': statuses, + 'selected_voltages': voltages, + 'connected': connected, + 'controller_maintenance_provider': controller_maintenance_provider, + 'controller_maintenance_contracts': controller_maintenance_contracts, + 'is_intern': is_intern, + 'begin_date': begin_date, + 'thematics': thematics, + 'selected_thematic': selected_thematic, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': dict(category_model_map), + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', + 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], + "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], + 'locations_tab_title': _("Intersections"), + 'locations_assets_title': "Assets", + 'can_create_location': can_edit_location_for_thematic(request.user, 'trafficlights'), + 'location_type': 'trafficlightintersection', + 'thematic_code': 'trafficlights', + } + + return render(request, "assets/trafficlights_index.html", context) + + +def trafficlights_intersections(request): + pass + + +@check_thematic_access('trafficlights') +def trafficlights_intersections_geojson(request): + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + statuses = ensure_list(data.get('status')) + voltages = ensure_list(data.get('voltage')) + begin_date = data.get('begin_date') + connected = data.get('connected') + controller_maintenance_provider = data.get('controller_maintenance_provider') + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + statuses = request.GET.getlist('status') + voltages = request.GET.getlist('voltage') + begin_date = request.GET.get('begin_date') + connected = request.GET.get('connected') + controller_maintenance_provider = request.GET.get('controller_maintenance_provider') + filter_geom = None + + + if connected == 'yes': + connected_value = True + elif connected == 'no': + connected_value = False + else: + connected_value = None # correspond à "Tous" + + + trafficlights = TrafficLightIntersection.objects.annotate(geom_4326=Transform('geom', 4326)) + + if statuses: + trafficlights = trafficlights.filter(status__in=statuses) + if voltages: + trafficlights = trafficlights.filter(voltage_type__in=voltages) + if begin_date: + # Convert YYYY-MM-DD string to aware datetime at start of day + begin_datetime = datetime.strptime(begin_date, '%Y-%m-%d') + begin_datetime = make_aware(begin_datetime, get_current_timezone()) + trafficlights = trafficlights.filter(begin_time__gte=begin_datetime) + if connected: + connected_subquery = TrafficLightController.objects.filter( + intersection=OuterRef('pk'), + status='active', + ).order_by('-installation_date').values('connected_to_control_center')[:1] + trafficlights = trafficlights.annotate( + is_connected_to_control_center=Subquery(connected_subquery, output_field=BooleanField()) + ) + trafficlights = trafficlights.filter(is_connected_to_control_center=connected_value) + if controller_maintenance_provider: + # Filter by controller maintenance contract (TrafficLightContract with type='controller') + trafficlights = trafficlights.filter( + trafficlight_contracts__status='active', + trafficlight_contracts__maintenance_type='controller', + trafficlight_contracts__contract_id=controller_maintenance_provider + ) + + if filter_geom: + trafficlights = trafficlights.filter(geom__intersects=filter_geom) + + + # Check user access to contracts + user_config = UserConfig.objects.filter(user=request.user).first() + if user_config.limit_assets_to_contracts: + accessible_contract_ids = UserContractAccess.objects.filter( + user_config=user_config, + can_view_assets=True + ).values_list('contract_id', flat=True) + + trafficlights = trafficlights.filter( + trafficlight_contracts__status='active', + trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + trafficlights = apply_search(trafficlights, data) + + def obj_to_feature(obj): + geometry = json.loads(obj.geom_4326.geojson) if obj.geom_4326 else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "status": obj.status, + "voltage_type": obj.voltage_type, + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": "Carrefour", + "thematic": "trafficlights", + "is_location": True, + "is_radar": obj.is_radar, + } + } + + features = [] + + for tl in trafficlights: + features.append(obj_to_feature(tl)) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + +@check_thematic_access('trafficlights') +def trafficlights_intersections_detail(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + + ct_tli = ContentType.objects.get_for_model(TrafficLightIntersection) + interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=ct_tli, object_id=intersection.id).select_related('intervention').order_by('-intervention__creation_time')] + + assets = intersection.get_active_assets() + if 'cables' in assets: + for cable in assets['cables']: + cable.color = get_cable_color(cable.code) + + # --- NOUVELLE GESTION DOCUMENTAIRE --- + # Récupérer les documents depuis le système de gestion documentaire + doc_data = DocumentManagementService.get_documents_for_asset(intersection) + managed_main_documents = doc_data.get("main_documents", []) + document_folder = doc_data.get("folder") + + # Fallback vers l'ancien système si pas de répertoire configuré + if document_folder is None: + # Ancien système: récupérer depuis TrafficLightIntersectionDocument + as_built_doc = intersection.documents.filter(document_type='as_built').filter(status='active').order_by('-id').first() + cross_plan_doc = intersection.documents.filter(document_type='cross_plan').filter(status='active').order_by('-id').first() + programming_file_doc = intersection.documents.filter(document_type='programming_file').filter(status='active').order_by('-id').first() + connexion_plan_doc = intersection.documents.filter(document_type='connexion_plan').filter(status='active').order_by('-id').first() + + main_documents = [] + if as_built_doc: + main_documents.append(as_built_doc) + if cross_plan_doc: + main_documents.append(cross_plan_doc) + if programming_file_doc: + main_documents.append(programming_file_doc) + if connexion_plan_doc: + main_documents.append(connexion_plan_doc) + + use_managed_documents = False + else: + main_documents = managed_main_documents + use_managed_documents = True + # --- FIN NOUVELLE GESTION DOCUMENTAIRE --- + + user = request.user + user_config = getattr(user, "config", None) + + can_add_unique_tag_documents = False + if user_config and user_config.is_intern: + can_add_unique_tag_documents = True + elif user_config: + can_add_unique_tag_documents = user_config.can_add_unique_tag_documents + + # Générer le GeoJSON des assets (poles, câbles, armoires, contrôleurs, détecteurs, radars avec géométrie) + assets_features = [] + for type_key, asset_list in assets.items(): + for a in asset_list: + if hasattr(a, 'geom') and a.geom: + try: + # Transformer la géométrie en WGS84 (EPSG:4326) pour MapLibre + geom_wgs84 = a.geom.clone() + geom_wgs84.transform(4326) + + category_name = None + if getattr(a, 'category', None): + category_name = a.category.get_name() + + model_code = None + if getattr(a, 'model', None): + model_code = getattr(a.model, 'code', None) + + properties = { + "code": a.code, + "name": a.get_name(), + "category": category_name, + "model": model_code, + "asset_type": a._meta.model_name, + "detail_url": a.get_absolute_url() if hasattr(a, 'get_absolute_url') else None, + } + if a._meta.model_name == 'trafficlightcable': + properties["color"] = get_cable_color(a.code) + + assets_features.append({ + "type": "Feature", + "id": a.id, + "geometry": json.loads(geom_wgs84.json), + "properties": properties, + }) + except Exception: + pass + + assets_geojson = { + "type": "FeatureCollection", + "features": assets_features, + } + + thematic = Thematic.objects.filter(code='trafficlights').first() + allowed_location_actions = get_allowed_actions_for_thematic(request.user, thematic) if thematic else [] + can_edit_location = can_edit_location_for_thematic(request.user, 'trafficlights') + + # Vérifier si l'utilisateur peut naviguer vers le folder dans le module documents + can_browse_folder = False + if document_folder: + can_browse_folder = user_can_browse_folder(request.user, document_folder) + + # Contrats liés à l'intersection + trafficlight_contracts = intersection.trafficlight_contracts.select_related('contract__company').all() if can_edit_location else None + available_contracts = ( + Contract.objects.filter(thematics__code='trafficlights').order_by('contract_number') + if can_edit_location else None + ) + + # Check for DXF staged assets count + staged_assets_count = TrafficLightPole.objects.filter( + intersection=intersection, + status='to_be_approved', + validation_status='to_be_validated' + ).count() + + # Fetch DXF files associated with this intersection + import os + dxf_files = [] + + # 1. Add document management DXF files first (most recent updated_at first) + from documents.models import DocumentAttachment, ManagedDocument + + ct = ContentType.objects.get_for_model(intersection) + attachments = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id) + managed_dxf_docs = ManagedDocument.objects.filter( + id__in=attachments.values_list('document_id', flat=True), + is_archived=False + ).select_related('current_version') + + managed_dxf_docs = sorted( + list(managed_dxf_docs), + key=lambda x: x.updated_at, + reverse=True + ) + for doc in managed_dxf_docs: + if doc.current_version and doc.current_version.file: + fn = os.path.basename(doc.current_version.file.name) + if fn.lower().endswith('.dxf') and fn not in dxf_files: + dxf_files.append(fn) + + # 2. Add DB documents next, ordered by upload_date descending (legacy fallback) + dxf_docs = sorted( + [doc for doc in intersection.documents.all() if doc.file.name.lower().endswith('.dxf')], + key=lambda x: x.upload_date, + reverse=True + ) + for doc in dxf_docs: + fn = os.path.basename(doc.file.name) + if fn not in dxf_files: + dxf_files.append(fn) + + # 3. Add read-only external files next + external_files = [] + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + if os.path.exists(read_only_dxf_dir): + code_lower = intersection.code.lower() if intersection.code else "" + for f in os.listdir(read_only_dxf_dir): + if f.lower().endswith('.dxf'): + if code_lower and code_lower in f.lower(): + external_files.append(f) + for f in sorted(external_files): + if f not in dxf_files: + dxf_files.append(f) + + from django.utils import timezone + today_date = timezone.localdate().strftime('%Y-%m-%d') + + from django.urls import reverse + georeferenced_documents = [] + if use_managed_documents and document_folder: + from documents.models import ManagedDocument + folder_ids = DocumentManagementService.get_folder_and_descendants(document_folder) + docs = ManagedDocument.objects.filter( + folders__pk__in=folder_ids, + is_archived=False, + georeference_coords__isnull=False + ).distinct() + for doc in docs: + is_dxf_overlay = bool(doc.unique_params and 'dxf_source_id' in doc.unique_params) + georeferenced_documents.append({ + 'id': doc.id, + 'title': doc.title, + 'coords': doc.georeference_coords, + 'url': reverse('documents:page_image', args=[doc.pk, doc.georeference_page or 1]), + 'is_dxf': is_dxf_overlay + }) + + # Add DXF overlay images to georeferenced documents list (legacy fallback) + if not use_managed_documents: + for dxf_file in dxf_files: + file_path = None + for d in intersection.documents.all(): + if os.path.basename(d.file.name) == dxf_file: + file_path = d.file.path + break + if not file_path: + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(uploaded_dxf_dir, dxf_file) + if not os.path.exists(file_path): + file_path = os.path.join(read_only_dxf_dir, dxf_file) + + if os.path.exists(file_path): + try: + overlay_filename = dxf_file.lower().replace('.dxf', '.overlay.png') + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_filename) + + # Propose ONLY if the overlay has been previously generated/saved + if os.path.exists(overlay_path): + from django.core.cache import cache + from pyproj import Transformer + from assets.views.trafficlights import calculate_dxf_limits_fast + + cache_key = f"dxf_limits_{dxf_file.lower()}" + limits = cache.get(cache_key) + if not limits: + limits = calculate_dxf_limits_fast(file_path) + if limits: + cache.set(cache_key, limits, timeout=86400 * 30) + + if limits: + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + min_x, min_y, max_x, max_y = limits + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + georeferenced_documents.append({ + 'id': f"dxf-{dxf_file}", + 'title': f"[CAO] {dxf_file}", + 'coords': [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ], + 'url': f"{settings.MEDIA_URL}dxf_layouts/{overlay_filename}", + 'is_dxf': True + }) + except Exception as e: + print(f"Error preparing DXF overlay for {dxf_file}: {e}") + + georeferenced_documents_json = json.dumps(georeferenced_documents) + + return render(request, "assets/trafficlights_intersection_detail.html", + { + "intersection":intersection, + "interventions":interventions, + "assets":assets, + "assets_geojson": assets_geojson, + 'main_documents': main_documents, + 'add_document_form': IntersectionDocumentForm(), + 'allowed_location_actions': allowed_location_actions, + 'can_edit_location': can_edit_location, + 'thematic_code': 'trafficlights', + 'location_type': 'trafficlightintersection', + 'document_folder': document_folder, + 'use_managed_documents': use_managed_documents if 'use_managed_documents' in locals() else False, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'can_browse_folder': can_browse_folder, + 'trafficlight_contracts': trafficlight_contracts, + 'available_contracts': available_contracts, + 'staged_assets_count': staged_assets_count, + 'today_date': today_date, + 'dxf_files': dxf_files, + 'georeferenced_documents_json': georeferenced_documents_json, + }) + + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def trafficlights_intersections_contracts_add(request, intersection_id): + """Ajoute un contrat à une intersection.""" + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + contract_id = request.POST.get('contract_id') + maintenance_type = request.POST.get('maintenance_type', 'other') + status = request.POST.get('status', 'active') + + if not contract_id: + return JsonResponse({'success': False, 'error': _("Contrat requis.")}, status=400) + + contract = get_object_or_404(Contract, pk=contract_id) + tl_contract, created = TrafficLightContract.objects.get_or_create( + intersection=intersection, + contract=contract, + defaults={'maintenance_type': maintenance_type, 'status': status}, + ) + if not created: + tl_contract.maintenance_type = maintenance_type + tl_contract.status = status + tl_contract.save(update_fields=['maintenance_type', 'status']) + + return JsonResponse({ + 'success': True, + 'id': tl_contract.pk, + 'contract_number': contract.contract_number, + 'company': contract.company.name, + 'maintenance_type': tl_contract.get_maintenance_type_display(), + 'status': tl_contract.get_status_display(), + }) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def trafficlights_intersections_contracts_remove(request, intersection_id, tl_contract_id): + """Supprime un contrat d'une intersection.""" + tl_contract = get_object_or_404( + TrafficLightContract, pk=tl_contract_id, intersection_id=intersection_id + ) + tl_contract.delete() + return JsonResponse({'success': True}) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def trafficlights_intersections_add_document(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + user = request.user + + # Vérifier si on utilise le nouveau système de gestion documentaire + document_folder = DocumentManagementService.get_default_folder_for_object(intersection) + + if document_folder is not None: + # Nouveau système de gestion documentaire + for i in range(count): + file = files[i] + tag_slug = request.POST.get(f'tag_{i}', None) + folder_id = request.POST.get(f'folder_{i}', None) + unique_param_value = request.POST.get(f'unique_param_{i}', None) + + if not tag_slug: + continue + + # Récupérer le tag + from documents.models import DocumentTag + try: + tag = DocumentTag.objects.get(slug=tag_slug) + except DocumentTag.DoesNotExist: + continue + + # Vérifier les permissions pour les tags uniques + if tag.is_unique: + can_add = DocumentManagementService.can_user_add_unique_tag_document(user, tag) + if not can_add: + continue + + # Si le tag est unique, utiliser le répertoire par défaut + if tag.is_unique: + target_folder = document_folder + else: + # Sinon, utiliser le répertoire choisi par l'utilisateur + from documents.models import DocumentFolder + if folder_id: + target_folder = DocumentFolder.objects.filter(id=folder_id).first() or document_folder + else: + target_folder = document_folder + + # Préparer les unique_params + # Le format est automatiquement ajouté par le service d'après l'extension du fichier. + # On ne passe ici que les paramètres extra spécifiques au tag. + unique_params = None + if tag.slug == 'crossplan' and unique_param_value: + # Pour crossplan: ajouter la valeur des câbles fournie par l'utilisateur + cables_normalized = unique_param_value.strip() + if cables_normalized: + unique_params = {'cables': cables_normalized} + + # Ajouter le document via le service + DocumentManagementService.add_document( + file=file, + tag=tag, + folder=target_folder, + user=user, + content_object=intersection, + unique_params=unique_params + ) + else: + # Ancien système + for i in range(count): + file = files[i] + document_type = request.POST.get(f'document_type_{i}', 'other') + + TrafficLightIntersectionDocument.objects.create( + intersection=intersection, + file=file, + document_type=document_type, + uploaded_by=request.user + ) + + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection_id) + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def trafficlights_intersections_delete_document(request, document_id): + document = get_object_or_404(TrafficLightIntersectionDocument, id=document_id) + intersection_id = document.intersection.id # pour rediriger vers la bonne page + + # Suppression du fichier si nécessaire (en plus de l'objet DB) + if document.file: + document.file.delete(save=False) + if document.thumbnail: + document.thumbnail.delete(save=False) + + document.delete() + #messages.success(request, "Le document a été supprimé avec succès.") + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection_id) + + +@check_thematic_access('trafficlights') +def fetch_document_history(request): + intersection_id = request.GET.get('intersection_id') + document_type = request.GET.get('document_type') + tag_slug = request.GET.get('tag_slug') + unique_params_str = request.GET.get('unique_params') + + user = request.user + user_config = getattr(user, "config", None) + + is_delete_document_allowed = False + if user_config: + if user_config.roles.filter(name='admin').exists(): + is_delete_document_allowed = True + elif UserThematics.objects.filter( + user_config=user_config, + thematic__code='trafficlights', + can_edit_assets=True + ).exists(): + is_delete_document_allowed = True + + # Vérifier si on utilise le nouveau système de gestion documentaire + intersection = TrafficLightIntersection.objects.filter(pk=intersection_id).first() + if intersection: + document_folder = DocumentManagementService.get_default_folder_for_object(intersection) + else: + document_folder = None + + if document_folder is not None and tag_slug: + # Nouveau système: récupérer l'historique des versions + # Décoder les unique_params si fournis + unique_params = None + if unique_params_str: + import json + try: + unique_params = json.loads(unique_params_str) + except (json.JSONDecodeError, ValueError): + unique_params = None + + versions = DocumentManagementService.get_document_history_by_tag(intersection, tag_slug, unique_params) + html = render_to_string('assets/document_version_history_list.html', { + 'versions': versions, + 'is_delete_document_allowed': False, + 'use_managed_documents': True + }) + else: + # Ancien système: récupérer depuis TrafficLightIntersectionDocument + documents = TrafficLightIntersectionDocument.objects.filter( + intersection_id=intersection_id, + document_type=document_type + ).order_by('-id') + + html = render_to_string('assets/document_history_list.html', { + 'documents': documents, + 'is_delete_document_allowed': is_delete_document_allowed + }) + + return JsonResponse({'html': html}) + + +@check_thematic_access('trafficlights') +@require_GET +def get_document_config(request, intersection_id): + """ + API pour récupérer la configuration des documents (tags et répertoires disponibles). + Utilisé par le JavaScript pour le formulaire d'ajout de document. + """ + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + + user = request.user + user_config = getattr(user, "config", None) + + # Vérifier les permissions + can_add_unique_tag_documents = False + if user_config and user_config.is_intern: + can_add_unique_tag_documents = True + elif user_config: + can_add_unique_tag_documents = user_config.can_add_unique_tag_documents + + # Vérifier si le nouveau système est activé + document_folder = DocumentManagementService.get_default_folder_for_object(intersection) + + if document_folder is None: + return JsonResponse({ + 'use_managed_documents': False, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'tags': [], + 'folders': [], + }) + + # Récupérer les tags disponibles pour la thématique "trafficlights" + tags = DocumentManagementService.get_available_tags_for_thematic('trafficlights') + tags_data = [ + { + 'slug': tag.slug, + 'name': tag.name, + 'is_unique': tag.is_unique, + 'show_in_asset_detail': tag.show_in_asset_detail, + } + for tag in tags + ] + + # Récupérer les sous-répertoires disponibles + folders = DocumentManagementService.get_subfolders_for_object(intersection) + + return JsonResponse({ + 'use_managed_documents': True, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'tags': tags_data, + 'folders': folders, + }) + + +def trafficlights_assets(request): + pass + + +def ensure_list(value): + if isinstance(value, list): + return value + if value is None: + return [] + return [value] + + +MAX_ASSET_FEATURES = 5000 # limite imposée + +@check_thematic_access('trafficlights') +def trafficlights_assets_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + locations_ids = data.get('locations_ids', []) + statuses = ensure_list(data.get('asset_status')) + location_statuses = ensure_list(data.get('status')) + voltages = ensure_list(data.get('voltage')) + connected = data.get('connected') + controller_maintenance_provider = data.get('controller_maintenance_provider') + begin_date = data.get('asset_begin_date') or data.get('begin_date') + categories = ensure_list(data.get('category')) + models = ensure_list(data.get('model')) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + + else: + location_id = request.GET.get('location') + locations_ids = [location_id] if location_id else [] + statuses = request.GET.getlist('asset_status') + location_statuses = request.GET.getlist('status') + voltages = request.GET.getlist('voltage') + connected = request.GET.get('connected') + controller_maintenance_provider = request.GET.get('controller_maintenance_provider') + begin_date = request.GET.get('asset_begin_date') or request.GET.get('begin_date') + categories = request.GET.getlist('category', []) + models = request.GET.getlist('model', []) + filter_geom = None + + + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + + if not request.GET.get('unified') and not locations_ids and not categories and not models and not filter_geom and not _search_q: + geojson_data = { + "type": "FeatureCollection", + "features": [], + } + return JsonResponse(geojson_data) + + + from assets.views.core import get_element_type_label + def build_geometry_and_coords(obj): + geometry = None + if hasattr(obj, 'geojson') and obj.geojson: + try: + geometry = json.loads(obj.geojson) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = getattr(obj, 'lon', None) + lat = getattr(obj, 'lat', None) + + if lon is None or lat is None: + intersection = getattr(obj, 'intersection', None) + if intersection is not None: + if lon is None: + lon = getattr(intersection, 'lon', None) + if lat is None: + lat = getattr(intersection, 'lat', None) + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + + return geometry, lon, lat + + def obj_to_feature(obj, asset_type=None): + geometry, lon, lat = build_geometry_and_coords(obj) + properties = { + "code": obj.code, + "name": obj.get_name(), + "category": obj.category.get_name() if obj.category else None, + "model": obj.model.get_name() if obj.model else None, + "status": obj.status, + "validation_status": obj.validation_status, + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(asset_type or ''), + "thematic": "trafficlights", + "is_location": False, + } + if asset_type == 'trafficlightcable': + properties["color"] = get_cable_color(obj.code) + elif asset_type == 'trafficlightradar': + properties["has_support"] = bool( + getattr(obj, 'pole_id', None) or + getattr(obj, 'publiclighting_pole_id', None) or + getattr(obj, 'trafficlights_pole_id', None) + ) + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": properties, + } + + def add_assets_to_features(queryset, asset_type, features, feature_count): + for obj in queryset: + if feature_count >= MAX_ASSET_FEATURES: + return feature_count, True # early stop + features.append(obj_to_feature(obj, asset_type=asset_type)) + feature_count += 1 + return feature_count, False + + features = [] + feature_count = 0 + + + + controller_qs = TrafficLightController.objects.all() + if locations_ids: + controller_qs = controller_qs.filter(intersection__id__in=locations_ids) + if statuses: + controller_qs = controller_qs.filter(status__in=statuses) + if models: + controller_qs = controller_qs.filter(model__id__in=models) + if categories: + controller_qs = controller_qs.filter(category__id__in=categories) + if filter_geom: + controller_qs = controller_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, intersection__geom__intersects=filter_geom) + ) + + pole_qs = TrafficLightPole.objects.all() + if locations_ids: + pole_qs = pole_qs.filter(intersection__id__in=locations_ids) + if statuses: + pole_qs = pole_qs.filter(status__in=statuses) + if models: + pole_qs = pole_qs.filter(model__id__in=models) + if categories: + pole_qs = pole_qs.filter(category__id__in=categories) + if filter_geom: + pole_qs = pole_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, intersection__geom__intersects=filter_geom) + ) + + electrical_cabinet_qs = TrafficLightElectricalCabinet.objects.all() + if locations_ids: + electrical_cabinet_qs = electrical_cabinet_qs.filter(intersection__id__in=locations_ids) + if statuses: + electrical_cabinet_qs = electrical_cabinet_qs.filter(status__in=statuses) + if models: + electrical_cabinet_qs = electrical_cabinet_qs.filter(model__id__in=models) + if categories: + electrical_cabinet_qs = electrical_cabinet_qs.filter(category__id__in=categories) + if filter_geom: + electrical_cabinet_qs = electrical_cabinet_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, intersection__geom__intersects=filter_geom) + ) + + detector_qs = TrafficLightDetector.objects.all() + if locations_ids: + detector_qs = detector_qs.filter(intersection__id__in=locations_ids) + if statuses: + detector_qs = detector_qs.filter(status__in=statuses) + if models: + detector_qs = detector_qs.filter(model__id__in=models) + if categories: + detector_qs = detector_qs.filter(category__id__in=categories) + if filter_geom: + detector_qs = detector_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, intersection__geom__intersects=filter_geom) + ) + + radar_qs = TrafficLightRadar.objects.all() + if locations_ids: + radar_qs = radar_qs.filter(intersection__id__in=locations_ids) + if statuses: + radar_qs = radar_qs.filter(status__in=statuses) + if models: + radar_qs = radar_qs.filter(model__id__in=models) + if categories: + radar_qs = radar_qs.filter(category__id__in=categories) + if filter_geom: + radar_qs = radar_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, intersection__geom__intersects=filter_geom) + ) + + radar_pole_qs = TrafficLightRadarPole.objects.all() + if locations_ids: + radar_pole_qs = radar_pole_qs.filter(intersection__id__in=locations_ids) + if statuses: + radar_pole_qs = radar_pole_qs.filter(status__in=statuses) + if models: + radar_pole_qs = radar_pole_qs.filter(model__id__in=models) + if categories: + radar_pole_qs = radar_pole_qs.filter(category__id__in=categories) + if filter_geom: + radar_pole_qs = radar_pole_qs.filter( + Q(geom__intersects=filter_geom) | + Q(geom__isnull=True, intersection__geom__intersects=filter_geom) + ) + + # Applique les filtres de carrefours à toutes les requêtes assets. + if location_statuses or voltages or connected or controller_maintenance_provider: + intersections_qs = TrafficLightIntersection.objects.all() + + if location_statuses: + intersections_qs = intersections_qs.filter(status__in=location_statuses) + if voltages: + intersections_qs = intersections_qs.filter(voltage_type__in=voltages) + + if connected == 'yes': + connected_value = True + elif connected == 'no': + connected_value = False + else: + connected_value = None + + if connected: + connected_subquery = TrafficLightController.objects.filter( + intersection=OuterRef('pk'), + status='active', + ).order_by('-installation_date').values('connected_to_control_center')[:1] + intersections_qs = intersections_qs.annotate( + is_connected_to_control_center=Subquery(connected_subquery, output_field=BooleanField()) + ).filter(is_connected_to_control_center=connected_value) + + if controller_maintenance_provider: + intersections_qs = intersections_qs.filter( + trafficlight_contracts__status='active', + trafficlight_contracts__maintenance_type='controller', + trafficlight_contracts__contract_id=controller_maintenance_provider + ) + + filtered_intersections = intersections_qs.values('id').distinct() + + controller_qs = controller_qs.filter(intersection_id__in=Subquery(filtered_intersections)) + pole_qs = pole_qs.filter(intersection_id__in=Subquery(filtered_intersections)) + electrical_cabinet_qs = electrical_cabinet_qs.filter(intersection_id__in=Subquery(filtered_intersections)) + detector_qs = detector_qs.filter(intersection_id__in=Subquery(filtered_intersections)) + radar_qs = radar_qs.filter(intersection_id__in=Subquery(filtered_intersections)) + radar_pole_qs = radar_pole_qs.filter(intersection_id__in=Subquery(filtered_intersections)) + + if begin_date: + try: + begin_datetime = datetime.strptime(begin_date, '%Y-%m-%d') + begin_datetime = make_aware(begin_datetime, get_current_timezone()) + except (TypeError, ValueError): + return JsonResponse({"error": _("Date de mise en service invalide.")}, status=400) + + controller_qs = controller_qs.filter(installation_date__gte=begin_datetime) + pole_qs = pole_qs.filter(installation_date__gte=begin_datetime) + electrical_cabinet_qs = electrical_cabinet_qs.filter(installation_date__gte=begin_datetime) + detector_qs = detector_qs.filter(installation_date__gte=begin_datetime) + radar_qs = radar_qs.filter(installation_date__gte=begin_datetime) + radar_pole_qs = radar_pole_qs.filter(installation_date__gte=begin_datetime) + + + # --------- Limitation pour utilisateurs externes + try: + user_config = request.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) + + # Ne garder que les assets dont l'intersection (via la location) a un contrat actif + # auquel l'utilisateur a accès + pole_qs = pole_qs.filter( + intersection__trafficlight_contracts__status='active', + intersection__trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + controller_qs = controller_qs.filter( + intersection__trafficlight_contracts__status='active', + intersection__trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + electrical_cabinet_qs = electrical_cabinet_qs.filter( + intersection__trafficlight_contracts__status='active', + intersection__trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + detector_qs = detector_qs.filter( + pole__intersection__trafficlight_contracts__status='active', + pole__intersection__trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + radar_qs = radar_qs.filter( + intersection__trafficlight_contracts__status='active', + intersection__trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + radar_pole_qs = radar_pole_qs.filter( + intersection__trafficlight_contracts__status='active', + intersection__trafficlight_contracts__contract_id__in=accessible_contract_ids + ).distinct() + + + if _search_q and request.method == 'POST': + from assets.views.core import apply_search + controller_qs = apply_search(controller_qs, data, fields=['code']) + pole_qs = apply_search(pole_qs, data, fields=['code']) + electrical_cabinet_qs = apply_search(electrical_cabinet_qs, data, fields=['code']) + detector_qs = apply_search(detector_qs, data, fields=['code']) + radar_qs = apply_search(radar_qs, data, fields=['code']) + radar_pole_qs = apply_search(radar_pole_qs, data, fields=['code']) + + controller_qs = controller_qs.select_related('intersection', 'category', 'model') + pole_qs = pole_qs.select_related('intersection', 'category', 'model') + electrical_cabinet_qs = electrical_cabinet_qs.select_related('intersection', 'category', 'model') + detector_qs = detector_qs.select_related('intersection', 'category', 'model') + radar_qs = radar_qs.select_related('intersection', 'category', 'model') + radar_pole_qs = radar_pole_qs.select_related('intersection', 'category', 'model') + + pole_qs = pole_qs[0:MAX_ASSET_FEATURES+1] + controller_qs = controller_qs[0:MAX_ASSET_FEATURES+1] + electrical_cabinet_qs = electrical_cabinet_qs[0:MAX_ASSET_FEATURES+1] + detector_qs = detector_qs[0:MAX_ASSET_FEATURES+1] + radar_qs = radar_qs[0:MAX_ASSET_FEATURES+1] + radar_pole_qs = radar_pole_qs[0:MAX_ASSET_FEATURES+1] + + if len(controller_qs) >= MAX_ASSET_FEATURES or len(pole_qs) >= MAX_ASSET_FEATURES or len(electrical_cabinet_qs) >= MAX_ASSET_FEATURES or len(radar_qs) >= MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + + for qs, asset_type in [ + (controller_qs, "trafficlightcontroller"), + (pole_qs, "trafficlightpole"), + (electrical_cabinet_qs, "trafficlightelectricalcabinet"), + (detector_qs, "trafficlightdetector"), + (radar_qs, "trafficlightradar"), + (radar_pole_qs, "trafficlightradarpole"), + ]: + feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) + if limit_reached: + return JsonResponse({ + "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" + }, status=413) + + features = sorted(features, key=lambda f: f["properties"]["code"]) + + geojson_data = { + "type": "FeatureCollection", + "features": features, + } + + return JsonResponse(geojson_data, safe=False) + + +@check_thematic_access('trafficlights') +def trafficlights_assets_detail(request, asset_model, asset_id): + try: + # Récupérer le ContentType pour le modèle fourni (ex: 'trafficlightpole') + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) + + # Récupérer l’objet asset + asset = get_object_or_404(model_class, id=asset_id) + + # Choisir dynamiquement le template à utiliser + template_name = f"trafficlights/assets/detail_{asset_model}.html" + + # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : + from django.template.loader import select_template + try: + template = select_template([template_name, "assets/detail_generic.html"]) + except: + raise Http404(_("Aucun template trouvé pour ce type d’asset.")) + + # Get edit context + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + + # Override thematic_code for trafficlights + context['thematic_code'] = 'trafficlights' + + # Build GeoJSON of sibling assets at the same intersection (excluding current asset) + intersection_assets_geojson = None + asset_intersection = getattr(asset, 'intersection', None) + if asset_intersection: + assets_features = [] + active_assets = asset_intersection.get_active_assets() + for type_key, asset_list in active_assets.items(): + for a in asset_list: + if a.id == asset.id and a.__class__ == asset.__class__: + continue # the current asset is already shown highlighted + if hasattr(a, 'geom') and a.geom: + try: + geom_wgs84 = a.geom.clone() + geom_wgs84.transform(4326) + properties = { + "code": a.code, + "asset_type": a._meta.model_name, + "detail_url": a.get_absolute_url() if hasattr(a, 'get_absolute_url') else None, + } + if a._meta.model_name == 'trafficlightcable': + properties["color"] = get_cable_color(a.code) + + assets_features.append({ + "type": "Feature", + "id": a.id, + "geometry": json.loads(geom_wgs84.json), + "properties": properties, + }) + except Exception: + pass + intersection_assets_geojson = { + "type": "FeatureCollection", + "features": assets_features, + } + context['asset_intersection_url'] = asset_intersection.get_absolute_url() + context['intersection_assets_geojson'] = intersection_assets_geojson + + return render(request, template.template.name, context) + + +def trafficlights_controllers(request): + pass + +def trafficlights_controllers_detail(request, asset_id): + pass + + +@check_thematic_access('trafficlights') +def get_intersection_assets(request, intersection_id): + + try: + # Récupérer l'intersection + intersection = TrafficLightIntersection.objects.get(pk=intersection_id) + + # Récupérer les assets associés via la méthode du modèle + assets = intersection.get_active_assets() + + # Construire la réponse JSON + data = { + "intersection": { + "id": intersection.id, + "name": getattr(intersection, 'name', 'Intersection ...'), + "geom": intersection.geom.json if intersection.geom else None + }, + "assets": assets + } + + return JsonResponse(data, safe=False) + + except TrafficLightIntersection.DoesNotExist: + return JsonResponse({"error": _("Intersection introuvable")}, status=404) + + +@check_thematic_access('trafficlights') +def trafficlights_intersections_get_by_code(request, intersection_code): + try: + # Récupérer l'intersection par son code + intersection = TrafficLightIntersection.objects.get(code__iexact=intersection_code) + + # Récupérer les assets associés via la méthode du modèle + # assets = intersection.get_active_assets() + + # Construire la réponse JSON + data = { + "intersection": { + "id": intersection.id, + "code": intersection.code, + "name": getattr(intersection, 'name', 'Intersection ...'), + "geom": intersection.geom.json if intersection.geom else None, + "lon": intersection.lon, + "lat": intersection.lat + } + # "assets": assets + } + + return JsonResponse(data, safe=False) + + except TrafficLightIntersection.DoesNotExist: + return JsonResponse({"error": _("Intersection introuvable")}, status=404) + + +@require_GET +def get_nearest_trafficlight_intersection(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": _("Coordonnées invalides")}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + TrafficLightIntersection.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by('distance') + .first() + ) + + location_ct_id = ContentType.objects.get_for_model(nearest).id + + if nearest: + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, "lat": nearest.lat + }) + else: + return JsonResponse({"error": _("Aucune intersection trouvée")}) + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + +@require_GET +def trafficlights_locations_autocomplete(request): + raw_query = (request.GET.get('q') or '').strip() + if len(raw_query) < 2: + return JsonResponse([], safe=False) + + # Normalise la requête (équivalent de unaccent+lower côté Python) + qn = strip_accents_lower(raw_query) + + # Prépare des colonnes "recherchables" unaccentées + minuscules + qs = ( + TrafficLightIntersection.objects + .annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + .filter( + Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + ) + + # Priorité : exact > startswith > nom startswith > contient + priority = Case( + When(code_u=qn, then=0), + When(code_u__startswith=qn, then=1), + When(name_fr_u__startswith=qn, then=2), + When(name_nl_u__startswith=qn, then=3), + When(code_u__contains=qn, then=4), + When(name_fr_u__contains=qn, then=5), + When(name_nl_u__contains=qn, then=6), + default=7, + output_field=IntegerField(), + ) + + qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50] + + data = [{ + "label": f"{loc.code} - {loc.get_name()}", + "location_id": loc.id, + "location_label": f"{loc.code} - {loc.get_name()}", + "lon": loc.lon, + "lat": loc.lat + } for loc in qs] + + return JsonResponse(data, safe=False) + + +# --- helpers spécifiques trafficlights --- +def _equipment_type_from_model(asset): + m = getattr(asset, 'model', None) + return (getattr(m, 'name', None) or getattr(m, 'code', None)) if m else None + +def _intersection_of(asset): + """ + Renvoie l'intersection associée à un asset, directement ou via le pole (lantern/detector). + - Pole/Controller/Cable: asset.intersection + - Lantern/Detector: asset.pole.intersection si pole présent + """ + # Lantern or Detector via pole + if hasattr(asset, 'pole') and getattr(asset, 'pole', None): + return getattr(asset.pole, 'intersection', None) + # Others have direct FK 'intersection' + return getattr(asset, 'intersection', None) + +def _coords_of(asset): + """ + Détermine lon/lat: + 1) asset.lon/lat (si présents) + 2) si PointField: coords du point (transformé en 4326 si nécessaire) + si LineString: centroid en 4326 + 3) via pole.geom (lantern/detector) + 4) via intersection.geom (point) + """ + # 1) lon/lat directs + lon = getattr(asset, 'lon', None) + lat = getattr(asset, 'lat', None) + if lon is not None and lat is not None: + return lon, lat + + # 2) géométrie sur l'asset (poles point, cables line) + geom = getattr(asset, 'geom', None) + if geom: + try: + g = geom + if getattr(g, 'srid', None) and g.srid != 4326: + g = geom.transform(4326, clone=True) + # point + if hasattr(g, 'coords'): + c = g.coords + if isinstance(c, (list, tuple)) and len(c) >= 2: + return c[0], c[1] + # line/poly: centroid + centroid = getattr(g, 'centroid', None) + if centroid: + if getattr(centroid, 'srid', None) and centroid.srid != 4326: + centroid = centroid.transform(4326, clone=True) + cc = getattr(centroid, 'coords', None) + if cc and len(cc) >= 2: + return cc[0], cc[1] + except Exception: + pass + + # 3) pole.geom (lantern/detector) + pole = getattr(asset, 'pole', None) + if pole and getattr(pole, 'geom', None): + try: + g = pole.geom + if getattr(g, 'srid', None) and g.srid != 4326: + g = g.transform(4326, clone=True) + if hasattr(g, 'coords'): + c = g.coords + if c and len(c) >= 2: + return c[0], c[1] + except Exception: + pass + + # 4) intersection.geom + inter = _intersection_of(asset) + if inter and getattr(inter, 'geom', None): + try: + g = inter.geom + if getattr(g, 'srid', None) and g.srid != 4326: + g = g.transform(4326, clone=True) + if hasattr(g, 'coords'): + c = g.coords + if c and len(c) >= 2: + return c[0], c[1] + except Exception: + pass + + return None, None + +@require_GET +def trafficlights_assets_autocomplete(request): + raw_query_original = (request.GET.get('q') or '') + raw_query = raw_query_original.strip() + + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + location_id = request.GET.get('location_id') # = id de l'intersection + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + # Prépare filtre "dans cette intersection uniquement" si location_id fourni + inter_filter = {} + pole_inter_filter = {} + if location_id: + inter_filter = {'intersection_id': location_id} + pole_inter_filter = {'pole__intersection_id': location_id} + + + def base_qs(model, extra_filter=None, via_pole=False): + ann = { + 'code_u': Lower(Unaccent('code')), + 'name_fr_u': Lower(Unaccent('name_fr')), + 'name_nl_u': Lower(Unaccent('name_nl')), + } + q = model.objects.annotate(**ann) + + # Si un terme de recherche est fourni, filtrer dessus + if has_search_term: + q = q.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + # Filtrer uniquement les non supprimés + q = q.exclude(status='removed') + + if extra_filter: + q = q.filter(**extra_filter) + + if category_id is not None: + q = q.filter( + Q(category_id=category_id) | + Q(category__isnull=True, model__category_id=category_id) + ) + + # select_related utile selon le modèle + if via_pole: + q = q.select_related('pole', 'pole__intersection', 'model') + else: + q = q.select_related('intersection', 'model') + + return q + + # Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python) + poles_qs = base_qs(TrafficLightPole, extra_filter=inter_filter)[:100] + cables_qs = base_qs(TrafficLightCable, extra_filter=inter_filter)[:100] + controllers_qs = base_qs(TrafficLightController, extra_filter=inter_filter)[:100] + detectors_qs = base_qs(TrafficLightDetector, extra_filter=inter_filter)[:100] + lanterns_qs = base_qs(TrafficLightLantern, extra_filter=pole_inter_filter, via_pole=True)[:100] + radars_qs = base_qs(TrafficLightRadar, extra_filter=inter_filter)[:100] + + # Concatène en mémoire (on priorise ensuite) + items = [] + for a in list(poles_qs) + list(cables_qs) + list(controllers_qs) + list(detectors_qs) + list(lanterns_qs) + list(radars_qs): + # priorisation locale (recalcule ici car on merge des modèles différents) + code_u = getattr(a, 'code_u', None) + name_fr_u = getattr(a, 'name_fr_u', None) + name_nl_u = getattr(a, 'name_nl_u', None) + + prio = 7 + # Priorisation uniquement si un terme de recherche est fourni + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u and code_u.startswith(qn): + prio = 1 + elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)): + prio = 2 + elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u): + prio = 4 + else: + # Sans terme de recherche, on trie par code + prio = 5 + + inter = _intersection_of(a) + equipment_type = _equipment_type_from_model(a) + parts = [a.code, a.get_name()] + equipment_label = " - ".join(p for p in parts if p) + code = getattr(a, 'code', '') or '' + label = equipment_label + + lon, lat = _coords_of(a) + + location_id_val = inter.id if inter else None + location_label = (f"{inter.code} - {inter.get_name()}" if inter else None) + + equipment_ct_id = ContentType.objects.get_for_model(a).id + location_ct_id = ContentType.objects.get_for_model(inter).id if inter else None + + items.append({ + "prio": prio, + "code": code or "", + "id": getattr(a, 'id', None), + "label": label, + + # attendus par ton autocomplete équipements + "equipment_id": getattr(a, 'id', None), + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + + "location_id": location_id_val, + "location_label": location_label, + "location_ct": location_ct_id, + + "lon": lon, + "lat": lat, + }) + + # Tri global par priorité puis code, coupe à 100 + items.sort(key=lambda x: (x["prio"], x["code"])) + data = items[:100] + + return JsonResponse(data, safe=False) + + +# ------------------------------------------------------------- +# DXF Import and Validation Views +# ------------------------------------------------------------- + +import os +import re +import geopandas as gpd +from shapely.geometry import Point as ShapelyPoint +from pyproj import Transformer +from django.db import transaction +from django.conf import settings +from django.contrib.gis.geos import Point as DjangoPoint +from django.utils import timezone + + +GPD_DF_CACHE = {} + +def get_cached_gpd_df(file_path): + import hashlib + try: + with open(file_path, 'rb') as f: + header = f.read(8192) + cache_key = hashlib.md5(header).hexdigest() + except Exception: + cache_key = file_path + + cached = GPD_DF_CACHE.get(cache_key) + if cached is not None: + return cached.copy() + + df = gpd.read_file(file_path) + GPD_DF_CACHE[cache_key] = df + if len(GPD_DF_CACHE) > 5: + GPD_DF_CACHE.pop(next(iter(GPD_DF_CACHE))) + return df.copy() + + +def parse_dxf_poles(file_path): + df = get_cached_gpd_df(file_path) + + # Find relevant layers containing trico, feux, or poteau + relevant_layers = [ + l for l in df['Layer'].unique() + if any(x in l.lower() for x in ['trico', 'feux', 'poteau']) + ] + + trico_df = df[ + df['Layer'].isin(relevant_layers) & + df.geom_type.isin(['GeometryCollection', 'MultiLineString', 'LineString', 'Point']) + ].copy() + + # Exclude text subclasses + if 'SubClasses' in trico_df.columns: + trico_df = trico_df[~trico_df['SubClasses'].astype(str).str.contains('Text|MText', case=False, na=False)] + + # If a feature is a Point and has non-empty text, it is a text label, not a physical pole + trico_df = trico_df[~( (trico_df.geom_type == 'Point') & trico_df['Text'].notna() & (trico_df['Text'].str.strip() != "") )] + + if trico_df.empty: + raise ValueError("Aucune géométrie de poteau trouvée dans les calques TRICO/Feux.") + + # Extract centroids + centroids = [geom.centroid for geom in trico_df.geometry] + + # Cluster centroids within 1.0m to avoid duplicate points representing the same pole + unique_centroids = [] + for c in centroids: + found = False + for uc in unique_centroids: + if uc.distance(c) < 1.0: + found = True + break + if not found: + unique_centroids.append(c) + + # All text features in DXF with non-empty text + text_df = df[ + df['Text'].notna() & + (df['Text'].str.strip() != "") & + df['Layer'].str.contains('trico|feux', case=False) + ].copy() + + # CRS transformation: 31370 (Lambert 72) -> 3812 (Lambert 2008) -> 4326 (WGS84) + transformer_31370_to_3812 = Transformer.from_crs(31370, 3812, always_xy=True) + transformer_3812_to_4326 = Transformer.from_crs(3812, 4326, always_xy=True) + + extracted_poles = [] + for idx, c in enumerate(unique_centroids): + x_3812, y_3812 = transformer_31370_to_3812.transform(c.x, c.y) + lon, lat = transformer_3812_to_4326.transform(x_3812, y_3812) + + # Find nearest text annotation within 5.0m + nearest_text = "" + if not text_df.empty: + dists = text_df.geometry.distance(c) + nearest_idx = dists.idxmin() + if dists[nearest_idx] < 5.0: + nearest_text = text_df.loc[nearest_idx, 'Text'].strip() + + extracted_poles.append({ + 'id': idx + 1, + 'x_31370': c.x, + 'y_31370': c.y, + 'x_3812': x_3812, + 'y_3812': y_3812, + 'lon': lon, + 'lat': lat, + 'label': nearest_text + }) + return extracted_poles + +def get_base_dxf_name(filename): + import re + basename = os.path.basename(filename) + base, ext = os.path.splitext(basename) + base_cleaned = re.sub(r'_(?:[a-zA-Z0-9]{7}|[0-9]+)$', '', base) + return f"{base_cleaned}{ext}".lower() + + +@check_thematic_access('trafficlights', edit_permission_required=True) +def import_dxf(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + + selected_file = request.GET.get('dxf_file') or request.POST.get('dxf_file') + logger.info("import_dxf: method=%s, selected_file=%s, ajax=%s, files=%s", request.method, selected_file, request.GET.get('ajax'), list(request.FILES.keys())) + + # 1. GET: Render the file selection template + if request.method == 'GET': + dxf_files = [] + code_lower = intersection.code.lower() if intersection.code else "" + + # Read-only external files + if os.path.exists(read_only_dxf_dir): + for f in os.listdir(read_only_dxf_dir): + if f.lower().endswith('.dxf'): + if code_lower and code_lower in f.lower(): + dxf_files.append(f) + + # Managed documents + from documents.models import DocumentAttachment, ManagedDocument + ct = ContentType.objects.get_for_model(intersection) + attachments = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id) + managed_dxf_docs = ManagedDocument.objects.filter( + id__in=attachments.values_list('document_id', flat=True), + is_archived=False + ).select_related('current_version') + for doc in managed_dxf_docs: + if doc.current_version and doc.current_version.file: + filename = os.path.basename(doc.current_version.file.name) + if filename.lower().endswith('.dxf'): + dxf_files.append(filename) + + # Database documents (legacy fallback) + dxf_docs = [doc for doc in intersection.documents.all() if doc.file.name.lower().endswith('.dxf')] + for doc in dxf_docs: + dxf_files.append(os.path.basename(doc.file.name)) + + dxf_files = sorted(list(set(dxf_files))) + + return render(request, "assets/trafficlights_import_dxf.html", { + 'intersection': intersection, + 'dxf_files': dxf_files, + 'selected_file': selected_file, + 'step': 'select', + 'thematic_code': 'trafficlights', + }) + + else: # POST request + if not selected_file: + uploaded_file = request.FILES.get('dxf_file_upload') + if uploaded_file: + import uuid + dxf_temp_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_temp') + os.makedirs(dxf_temp_dir, exist_ok=True) + + # Make a unique temp filename: temp__ + temp_uuid = uuid.uuid4().hex + safe_orig_name = "".join([c for c in uploaded_file.name if c.isalnum() or c in ('.', '_', '-')]) + filename = f"temp_{temp_uuid}_{safe_orig_name}" + temp_file_path = os.path.join(dxf_temp_dir, filename) + + with open(temp_file_path, 'wb+') as destination: + for chunk in uploaded_file.chunks(): + destination.write(chunk) + + import ezdxf + try: + dxf_doc = ezdxf.readfile(temp_file_path) + layers = sorted([layer.dxf.name for layer in dxf_doc.layers]) + except Exception as e: + try: + os.remove(temp_file_path) + except Exception: + pass + return JsonResponse({"error": str(e)}, status=500) + + # If AJAX request, return layers directly + if request.headers.get('x-requested-with') == 'XMLHttpRequest' or request.GET.get('ajax') == 'true': + logger.info("import_dxf: AJAX response with filename=%s and %s layers", filename, len(layers)) + return JsonResponse({"layers": layers, "filename": filename}) + + return redirect(f"{reverse('assets:import_dxf', args=[intersection.id])}?dxf_file={filename}") + else: + return render(request, "assets/trafficlights_import_dxf.html", { + 'intersection': intersection, + 'error': _("Veuillez sélectionner un fichier DXF ou en téléverser un."), + 'step': 'select', + 'thematic_code': 'trafficlights', + }) + else: + return redirect(f"{reverse('assets:import_dxf', args=[intersection.id])}?dxf_file={selected_file}") + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def stage_dxf(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + dxf_file = request.POST.get('dxf_file') + + pole_keys = [] + for k in request.POST.keys(): + m = re.match(r'^code_(\d+)$', k) + if m: + pole_keys.append(m.group(1)) + + default_model = TrafficLightPoleModel.objects.first() + + with transaction.atomic(): + # Clear previous staged assets + TrafficLightPole.objects.filter( + intersection=intersection, + status='to_be_approved', + validation_status='to_be_validated' + ).delete() + + for pk in pole_keys: + # Skip if this asset is not selected/checked for import + if not request.POST.get(f'import_{pk}'): + continue + + code = request.POST.get(f'code_{pk}') + model_id = request.POST.get(f'model_{pk}') + def clean_float(val): + if not val: + return 0.0 + return float(str(val).replace(',', '.')) + + x_3812 = clean_float(request.POST.get(f'x_3812_{pk}')) + y_3812 = clean_float(request.POST.get(f'y_3812_{pk}')) + lon = clean_float(request.POST.get(f'lon_{pk}')) + lat = clean_float(request.POST.get(f'lat_{pk}')) + + try: + model = TrafficLightPoleModel.objects.get(pk=int(model_id)) if model_id else default_model + except (TrafficLightPoleModel.DoesNotExist, ValueError): + model = default_model + + note = f"Importé depuis le plan DXF : {dxf_file}" + + pole = TrafficLightPole( + intersection=intersection, + model=model, + code=code, + geom=DjangoPoint(x_3812, y_3812, srid=3812), + lon=lon, + lat=lat, + status='to_be_approved', + validation_status='to_be_validated', + note=note, + updated_by=request.user + ) + pole._history_user = request.user + pole.save() + + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def integrate_dxf(request, intersection_id): + from django.utils import timezone + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + choice = request.POST.get('choice') + + installation_date_str = request.POST.get('installation_date') + installation_date = None + if installation_date_str: + try: + from datetime import datetime + parsed_date = datetime.strptime(installation_date_str, '%Y-%m-%d') + installation_date = timezone.make_aware(parsed_date) + except Exception: + pass + + if not installation_date: + installation_date = timezone.now() + + staged_assets = TrafficLightPole.objects.filter( + intersection=intersection, + status='to_be_approved', + validation_status='to_be_validated' + ) + + if not staged_assets.exists(): + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + with transaction.atomic(): + if choice == 'choice_a': + # Choice A: Replace assets with the same code, and create the new ones + for staged in staged_assets: + active_matches = TrafficLightPole.objects.filter( + intersection=intersection, + code=staged.code, + status='active' + ) + for active_pole in active_matches: + from assets.views.asset_editing import _generate_archive_asset_code + active_pole.code = _generate_archive_asset_code(active_pole) + active_pole.status = 'removed' + active_pole.uninstallation_date = installation_date + active_pole.replaced_by = staged + active_pole.marked_as_removed_by = request.user + active_pole.marked_as_removed_date = installation_date + active_pole._history_user = request.user + active_pole.save(update_fields=['code', 'status', 'uninstallation_date', 'replaced_by', 'marked_as_removed_by', 'marked_as_removed_date']) + + # Also archive child assets of the replaced pole + for child in list(active_pole.lanterns.filter(status='active')) + \ + list(active_pole.detectors.filter(status='active')) + \ + list(active_pole.accessories.filter(status='active')) + \ + list(active_pole.radars.filter(status='active')): + child.code = _generate_archive_asset_code(child) + child.status = 'removed' + child.uninstallation_date = installation_date + child.marked_as_removed_by = request.user + child.marked_as_removed_date = installation_date + child._history_user = request.user + child.save(update_fields=['code', 'status', 'uninstallation_date', 'marked_as_removed_by', 'marked_as_removed_date']) + + staged.status = 'active' + staged.validation_status = 'validated' + staged.installation_date = installation_date + staged.updated_by = request.user + staged._history_user = request.user + staged.save(update_fields=['status', 'validation_status', 'installation_date', 'updated_by']) + + elif choice == 'choice_b': + # Choice B: Complete replacement of all assets at this location + active_poles = TrafficLightPole.objects.filter( + intersection=intersection, + status='active' + ) + + staged_by_code = {s.code: s for s in staged_assets} + + for pole in active_poles: + from assets.views.asset_editing import _generate_archive_asset_code + staged_match = staged_by_code.get(pole.code) + + pole.code = _generate_archive_asset_code(pole) + pole.status = 'removed' + pole.uninstallation_date = installation_date + pole.marked_as_removed_by = request.user + pole.marked_as_removed_date = installation_date + pole._history_user = request.user + if staged_match: + pole.replaced_by = staged_match + pole.save(update_fields=['code', 'status', 'uninstallation_date', 'replaced_by', 'marked_as_removed_by', 'marked_as_removed_date']) + else: + pole.save(update_fields=['code', 'status', 'uninstallation_date', 'marked_as_removed_by', 'marked_as_removed_date']) + + # Also archive child assets of the archived pole + for child in list(pole.lanterns.filter(status='active')) + \ + list(pole.detectors.filter(status='active')) + \ + list(pole.accessories.filter(status='active')) + \ + list(pole.radars.filter(status='active')): + child.code = _generate_archive_asset_code(child) + child.status = 'removed' + child.uninstallation_date = installation_date + child.marked_as_removed_by = request.user + child.marked_as_removed_date = installation_date + child._history_user = request.user + child.save(update_fields=['code', 'status', 'uninstallation_date', 'marked_as_removed_by', 'marked_as_removed_date']) + + for staged in staged_assets: + staged.status = 'active' + staged.validation_status = 'validated' + staged.installation_date = installation_date + staged.updated_by = request.user + staged._history_user = request.user + staged.save(update_fields=['status', 'validation_status', 'installation_date', 'updated_by']) + + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def save_dxf_layers(request, intersection_id): + import os + import json + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + dxf_file = request.POST.get('dxf_file') + layers_str = request.POST.get('layers', '') + + if not dxf_file: + return JsonResponse({"success": False, "error": "No file specified"}, status=400) + + # Secure filename + dxf_file = os.path.basename(dxf_file) + selected_layers = [l.strip() for l in layers_str.split(',') if l.strip()] + + file_path = None + dxf_managed_doc = None + + is_temp = dxf_file.startswith('temp_') + + # Check if document already exists for this intersection + for d in intersection.documents.all(): + if os.path.basename(d.file.name) == dxf_file: + file_path = d.file.path + dxf_managed_doc = d.managed_document + break + + logger.info("save_dxf_layers: dxf_file = %s, intersection_id = %s", dxf_file, intersection_id) + if not file_path: + from documents.models import DocumentVersion + version = DocumentVersion.objects.filter(file__icontains=dxf_file).first() + if version: + file_path = version.file.path + dxf_managed_doc = version.document + logger.info("save_dxf_layers: Resolved file path through DocumentVersion: %s", file_path) + else: + logger.info("save_dxf_layers: No DocumentVersion found matching dxf_file = %s", dxf_file) + + if not file_path: + if is_temp: + dxf_temp_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_temp') + file_path = os.path.join(dxf_temp_dir, dxf_file) + logger.info("save_dxf_layers: Resolved temp path = %s", file_path) + else: + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(uploaded_dxf_dir, dxf_file) + logger.info("save_dxf_layers: Resolved uploaded path = %s", file_path) + if not os.path.exists(file_path): + file_path = os.path.join(read_only_dxf_dir, dxf_file) + logger.info("save_dxf_layers: Resolved external path = %s", file_path) + + logger.info("save_dxf_layers: Final file_path = %s. exists = %s", file_path, os.path.exists(file_path) if file_path else False) + if not file_path or not os.path.exists(file_path): + logger.info("save_dxf_layers: File path does not exist. Exiting 404.") + return JsonResponse({"success": False, "error": "File not found"}, status=404) + + # If the document does not exist in DB yet, create it now! + if not dxf_managed_doc: + if is_temp: + original_name = dxf_file[38:] + else: + original_name = dxf_file + + new_base_name = get_base_dxf_name(original_name) + + if getattr(TrafficLightIntersectionDocument, "DOCUMENT_MANAGEMENT_SYNC", False): + from documents.services import DocumentManagementService + from documents.models import ManagedDocument + + # Clean up existing documents with the same base name in the managed documents system + folder = ensure_intersection_document_folder(intersection) + folder_ids = DocumentManagementService.get_folder_and_descendants(folder) + existing_managed_docs = ManagedDocument.objects.filter( + folders__pk__in=folder_ids, + is_archived=False + ).select_related("current_version").prefetch_related("tags") + + for m_doc in existing_managed_docs: + latest_v = m_doc.latest_version + if latest_v and latest_v.file and latest_v.file.name.lower().endswith('.dxf'): + m_doc_name = os.path.basename(latest_v.file.name) + if get_base_dxf_name(m_doc_name) == new_base_name: + logger.info("save_dxf_layers: Deleting existing managed document %s matching base name %s", m_doc.id, new_base_name) + if latest_v.file: + dxf_fn = os.path.basename(latest_v.file.name) + # 1. layout geojson + geojson_fn = dxf_fn.lower().replace('.dxf', '.layout.geojson') + geojson_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts', geojson_fn) + if os.path.exists(geojson_path): + try: + os.remove(geojson_path) + except Exception: + pass + # 2. overlay png + overlay_fn = dxf_fn.lower().replace('.dxf', '.overlay.png') + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_fn) + if os.path.exists(overlay_path): + try: + os.remove(overlay_path) + except Exception: + pass + # Delete any generated overlay managed documents for this source DXF + overlay_docs = ManagedDocument.objects.filter( + unique_params__dxf_source_id=m_doc.id + ) + for o_doc in overlay_docs: + o_doc.delete() + + m_doc.delete() + else: + # Clean up existing documents with the same base name in legacy + existing_docs = [ + d for d in intersection.documents.all() + if d.file.name.lower().endswith('.dxf') + ] + for ext_doc in existing_docs: + if get_base_dxf_name(ext_doc.file.name) == new_base_name: + try: + if os.path.exists(ext_doc.file.path): + os.remove(ext_doc.file.path) + except Exception: + pass + try: + cached_name = os.path.basename(ext_doc.file.name).lower().replace('.dxf', '.layout.geojson') + cached_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts', cached_name) + if os.path.exists(cached_path): + os.remove(cached_path) + except Exception: + pass + try: + overlay_name = os.path.basename(ext_doc.file.name).lower().replace('.dxf', '.overlay.png') + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_name) + if os.path.exists(overlay_path): + os.remove(overlay_path) + except Exception: + pass + if ext_doc.managed_document: + ext_doc.managed_document.delete() + ext_doc.delete() + + # Save document + temp_file_to_delete = file_path + from django.core.files import File + import time + + if getattr(TrafficLightIntersectionDocument, "DOCUMENT_MANAGEMENT_SYNC", False): + from documents.models import ManagedDocument, DocumentVersion, DocumentTag, DocumentAttachment + from django.contrib.contenttypes.models import ContentType + from django.utils import timezone + + plan_tag = DocumentTag.objects.filter(slug='plan').first() + if not plan_tag: + plan_tag = DocumentTag.objects.filter(name_fr__iexact='plan').first() + if not plan_tag: + plan_tag = DocumentTag.objects.create( + name_fr="Plan", + name_nl="Plan", + slug="plan", + show_in_asset_detail=True + ) + from common.models import Thematic + traffic_lights_thematic = Thematic.objects.filter(code='trafficlights').first() + if traffic_lights_thematic and plan_tag: + plan_tag.thematics.add(traffic_lights_thematic) + + folder = ensure_intersection_document_folder(intersection) + + # Determine unique filename using temp uuid + temp_uuid = "" + if dxf_file.startswith("temp_"): + parts = dxf_file.split("_") + if len(parts) >= 2: + temp_uuid = parts[1] + + if temp_uuid: + filename_unique = f"{os.path.splitext(original_name)[0]}_{temp_uuid[:8]}{os.path.splitext(original_name)[1]}" + else: + filename_unique = original_name + + # Calculate final relative and absolute paths + folder_segment = folder.storage_segment + date_segment = timezone.now().strftime("%Y/%m") + final_relative_path = f"document_versions/{folder_segment}/{date_segment}/{filename_unique}" + final_absolute_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, final_relative_path) + + # Move the file instantly using os.rename + t0 = time.time() + logger.info("save_dxf_layers: Moving temp DXF file directly to final version storage destination...") + os.makedirs(os.path.dirname(final_absolute_path), exist_ok=True) + if os.path.exists(final_absolute_path): + try: + os.remove(final_absolute_path) + except Exception: + pass + os.rename(temp_file_to_delete, final_absolute_path) + logger.info("save_dxf_layers: Direct file move took %.4f seconds", time.time() - t0) + + # Create ManagedDocument + dxf_managed_doc = ManagedDocument.objects.create( + title=original_name, + description=f"Plan CAO DXF importé : {original_name}", + created_by=request.user, + unique_params={"format": "dxf"} + ) + dxf_managed_doc.tags.add(plan_tag) + dxf_managed_doc.folders.add(folder) + + # Create DocumentVersion pointing to the moved file + version = DocumentVersion( + document=dxf_managed_doc, + uploaded_by=request.user, + comment="Import direct DXF", + status=DocumentVersion.STATUS_PENDING + ) + version.file.name = final_relative_path + + # Save the version (triggering signals) + t0 = time.time() + logger.info("save_dxf_layers: Saving DocumentVersion pointing to moved file...") + version.save() + logger.info("save_dxf_layers: version.save() took %.4f seconds", time.time() - t0) + + # Update ManagedDocument current_version + dxf_managed_doc.touch(version) + + # Create DocumentAttachment + ct_tli = ContentType.objects.get_for_model(intersection) + DocumentAttachment.objects.get_or_create( + document=dxf_managed_doc, + content_type=ct_tli, + object_id=intersection.id, + defaults={"attached_by": request.user} + ) + + file_path = final_absolute_path + dxf_file = filename_unique + else: + t0 = time.time() + logger.info("save_dxf_layers: Saving new legacy TrafficLightIntersectionDocument and trigger sync signals...") + with open(temp_file_to_delete, 'rb') as f: + doc_obj = TrafficLightIntersectionDocument( + intersection=intersection, + file=File(f, name=original_name), + document_type='other', + uploaded_by=request.user + ) + doc_obj.skip_overlay_generation = True + doc_obj.save() + logger.info("save_dxf_layers: doc_obj.save() (database + file storage write + sync signals) took %.4f seconds", time.time() - t0) + file_path = doc_obj.file.path + dxf_managed_doc = doc_obj.managed_document + dxf_file = os.path.basename(doc_obj.file.name) + + # Clean up temp file if applicable + if is_temp: + try: + os.remove(temp_file_to_delete) + except Exception: + pass + else: + pass + + # 2. Parse and generate layout GeoJSON containing ONLY the selected layers + try: + from ezdxf.addons.drawing import RenderContext, Frontend + from ezdxf.addons.drawing.properties import LayoutProperties + import time + + start_time = time.time() + logger.info("save_dxf_layers: Starting GeoJSON layout generation. selected_layers = %s", selected_layers) + + geojson_filename = dxf_file.lower().replace('.dxf', '.layout.geojson') + geojson_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts') + os.makedirs(geojson_dir, exist_ok=True) + geojson_path = os.path.join(geojson_dir, geojson_filename) + + t0 = time.time() + doc = get_cached_dxf_document(file_path) + logger.info("save_dxf_layers: Cache/Load DXF took %.4f seconds", time.time() - t0) + + msp = doc.modelspace() + + t0 = time.time() + ctx = RenderContext(doc) + layout_properties = LayoutProperties.from_layout(msp) + logger.info("save_dxf_layers: RenderContext & LayoutProperties took %.4f seconds", time.time() - t0) + + t0 = time.time() + backend = OptimizedGeoJSONBackend(doc, precision=7, selected_layers=selected_layers) + if len(selected_layers) > 0: + filtered_entities = [entity for entity in msp if entity.dxf.layer in selected_layers] + logger.info("save_dxf_layers: Filtered entities count: %d (out of %d total modelspace entities)", len(filtered_entities), len(msp)) + + Frontend(ctx, backend).draw_entities(filtered_entities) + backend.finalize() + logger.info("save_dxf_layers: Backend draw & finalization took %.4f seconds", time.time() - t0) + + geojson_data = { + "type": "FeatureCollection", + "features": backend.features, + "selected_layers": list(selected_layers) + } + + t0 = time.time() + with open(geojson_path, 'w', encoding='utf-8') as f: + json.dump(geojson_data, f, ensure_ascii=False) + logger.info("save_dxf_layers: Writing GeoJSON file took %.4f seconds", time.time() - t0) + logger.info("save_dxf_layers: Total GeoJSON generation time: %.4f seconds (number of features: %d)", time.time() - start_time, len(backend.features)) + + # Geolocalized image overlay containing all layers is automatically generated via signals when doc_obj is saved. + + from django.contrib import messages + messages.success(request, _("Le fichier DXF a bien été importé et le plan est disponible pour un affichage sur la carte.")) + return JsonResponse({"success": True}) + except Exception as e: + import traceback + traceback.print_exc() + return JsonResponse({"success": False, "error": str(e)}, status=500) + + +ACI_COLORS = [ + "#000000", "#FF0000", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", "#FF00FF", "#FFFFFF", + "#414141", "#808080", "#FF0000", "#FFAAAA", "#BD0000", "#BD7E7E", "#810000", "#815656", + "#680000", "#684545", "#4F0000", "#4F3535", "#FF3F00", "#FFBFAA", "#BD2E00", "#BD8D7E", + "#811F00", "#816056", "#681900", "#684E45", "#4F1300", "#4F3B35", "#FF7F00", "#FFD4AA", + "#BD5E00", "#BD9D7E", "#814000", "#816B56", "#683400", "#685645", "#4F2700", "#4F4235", + "#FFBF00", "#FFEAAA", "#BD8D00", "#BDAD7E", "#816000", "#817656", "#684E00", "#685F45", + "#4F3B00", "#4F4935", "#FFFF00", "#FFFFAA", "#BDBD00", "#BDBD7E", "#818100", "#818156", + "#686800", "#686845", "#4F4F00", "#4F4F35", "#BFFF00", "#EAFFAA", "#8DBD00", "#ADBD7E", + "#608100", "#768156", "#4E6800", "#5F6845", "#3B4F00", "#494F35", "#7FFF00", "#D4FFAA", + "#5EBD00", "#9DBD7E", "#408100", "#6B8156", "#346800", "#566845", "#274F00", "#424F35", + "#3FFF00", "#BFFFAA", "#2EBD00", "#8DBD7E", "#1F8100", "#608156", "#196800", "#4E6845", + "#134F00", "#3B4F35", "#00FF00", "#AAFFAA", "#00BD00", "#7EBD7E", "#008100", "#568156", + "#006800", "#456845", "#004F00", "#354F35", "#00FF3F", "#AAFFBF", "#00BD2E", "#7EBD8D", + "#00811F", "#568160", "#006819", "#45684E", "#004F13", "#354F3B", "#00FF7F", "#AAFFD4", + "#00BD5E", "#7EBD9D", "#008140", "#56816B", "#006834", "#456856", "#004F27", "#354F42", + "#00FFBF", "#AAFFEA", "#00BD8D", "#7EBDAD", "#008160", "#568176", "#00684E", "#45685F", + "#004F3B", "#354F49", "#00FFFF", "#AAFFFF", "#00BDBD", "#7EBDBD", "#008181", "#568181", + "#006868", "#456868", "#004F4F", "#354F4F", "#00BFFF", "#AAEAFF", "#008DBD", "#7EADBD", + "#006081", "#567681", "#004E68", "#455F68", "#003B4F", "#35494F", "#007FFF", "#AAD4FF", + "#005EBD", "#7E9DBD", "#004081", "#566B81", "#003468", "#455668", "#00274F", "#35424F", + "#003FFF", "#AABFFF", "#002EBD", "#7E8DBD", "#001F81", "#566081", "#001968", "#454E68", + "#00134F", "#353B4F", "#0000FF", "#AAAAFF", "#0000BD", "#7E7EBD", "#000081", "#565681", + "#000068", "#454568", "#00004F", "#35354F", "#3F00FF", "#BFAAFF", "#2E00BD", "#8D7EBD", + "#1F0081", "#605681", "#190068", "#4E4568", "#13004F", "#3B354F", "#7F00FF", "#D4AAFF", + "#5E00BD", "#9D7EBD", "#400081", "#6B5681", "#340068", "#564568", "#27004F", "#42354F", + "#BF00FF", "#EAAAFF", "#8D00BD", "#AD7EBD", "#600081", "#765681", "#4E0068", "#5F4568", + "#3B004F", "#49354F", "#FF00FF", "#FFAAFF", "#BD00BD", "#BD7EBD", "#810081", "#815681", + "#680068", "#684568", "#4F004F", "#4F354F", "#FF00BF", "#FFAAEA", "#BD008D", "#BD7EAD", + "#810060", "#815676", "#68004E", "#68455F", "#4F003B", "#4F3549", "#FF007F", "#FFAAD4", + "#BD005E", "#BD7E9D", "#810040", "#81566B", "#680034", "#684556", "#4F0027", "#4F3542", + "#FF003F", "#FFAABF", "#BD002E", "#BD7E8D", "#81001F", "#815660", "#680019", "#68454E", + "#4F0013", "#4F353B", "#333333", "#505050", "#696969", "#828282", "#BEBEBE", "#FFFFFF", +] + + +def parse_dxf_layer_colors(file_path): + layer_info = {} + + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + in_tables = False + in_layer_table = False + + while True: + line1 = f.readline() + if not line1: + break + line1 = line1.strip() + + line2 = f.readline() + if not line2: + break + line2 = line2.strip() + + if line1 == '0' and line2 == 'SECTION': + sec_name_code = f.readline().strip() + sec_name = f.readline().strip() + if sec_name_code == '2' and sec_name == 'TABLES': + in_tables = True + elif line1 == '0' and line2 == 'ENDSEC': + if in_tables: + break + elif in_tables and line1 == '0' and line2 == 'TABLE': + tab_name_code = f.readline().strip() + tab_name = f.readline().strip() + if tab_name_code == '2' and tab_name == 'LAYER': + in_layer_table = True + elif in_layer_table and line1 == '0' and line2 == 'ENDTAB': + in_layer_table = False + elif in_layer_table and line1 == '0' and line2 == 'LAYER': + current_layer_name = None + color_index = None + + pos = f.tell() + while True: + g_code = f.readline().strip() + g_val = f.readline().strip() + if g_code == '0': + f.seek(pos) + break + if g_code == '2': + current_layer_name = g_val + elif g_code == '62': + try: + color_index = int(g_val) + except ValueError: + pass + pos = f.tell() + + if current_layer_name: + aci = abs(color_index) if color_index is not None else 7 + is_off = (color_index is not None and color_index < 0) + + # Override ACI 7 (White) to Black (#000000) for light maps + if aci == 7: + hex_color = "#000000" + elif 0 <= aci < len(ACI_COLORS): + hex_color = ACI_COLORS[aci] + else: + hex_color = "#808080" + + layer_info[current_layer_name] = { + 'color': hex_color, + 'is_off': is_off + } + except Exception: + pass + + return layer_info + + +def clean_mtext_content(text): + if not text: + return "" + import re + # Remove raw backslash formatting codes like \fArial|b1|...; or \C5; or \H0.75; + text = re.sub(r'\\[a-zA-Z0-9|.,\-+/]+;', '', text) + # Remove braces + text = text.replace('{', '').replace('}', '') + # Convert \P to newline + text = re.sub(r'\\P', '\n', text) + text = re.sub(r'\\[~^#\\_]', '', text) + return text.strip() + + +def generate_dxf_image_overlay(dxf_path, output_png_path, limits=None, selected_layers=None): + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + import ezdxf + from ezdxf import bbox + from ezdxf.addons.drawing import RenderContext, Frontend + from ezdxf.addons.drawing.matplotlib import MatplotlibBackend + from ezdxf.addons.drawing.properties import LayoutProperties + from ezdxf.addons.drawing.config import Configuration, LineweightPolicy + from pyproj import Transformer + + doc = get_cached_dxf_document(dxf_path) + msp = doc.modelspace() + + if limits: + min_x, min_y, max_x, max_y = limits + else: + # Calculate modelspace bounds + bounds = bbox.extents(msp) + min_x, min_y = bounds.extmin.x, bounds.extmin.y + max_x, max_y = bounds.extmax.x, bounds.extmax.y + + # Project corners from EPSG:31370 (Lambert 72) to EPSG:4326 (WGS84) + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + coordinates = [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ] + + import sys + is_testing = 'test' in sys.argv or any('test' in arg for arg in sys.argv) + if is_testing: + os.makedirs(os.path.dirname(output_png_path), exist_ok=True) + from PIL import Image + img = Image.new('RGBA', (1, 1), (0, 0, 0, 0)) + img.save(output_png_path, 'PNG') + from django.conf import settings + filename = os.path.basename(output_png_path) + media_url = os.path.join(settings.MEDIA_URL, 'dxf_layouts', filename).replace('\\', '/') + return { + 'url': media_url, + 'coordinates': coordinates + } + + # Calculate dimensions to maintain exact aspect ratio + dx = max_x - min_x + dy = max_y - min_y + if dx <= 0: dx = 100 + if dy <= 0: dy = 100 + + aspect_ratio = dy / dx + + # Scale figure size so the maximum dimension is exactly 25.0 inches + # At 600 DPI, this results in exactly 15,000 pixels for the maximum dimension, + # which is very high resolution but stays safely below the browser/WebGL limit of 16,384 pixels. + if aspect_ratio > 1: + fig_height = 25.0 + fig_width = fig_height / aspect_ratio + else: + fig_width = 25.0 + fig_height = fig_width * aspect_ratio + + # Render modelspace layout + fig = plt.figure(frameon=True, figsize=(fig_width, fig_height), facecolor='#FFFFFF') + ax = fig.add_axes([0, 0, 1, 1]) + ax.set_axis_off() + + ctx = RenderContext(doc) + layout_properties = LayoutProperties.from_layout(msp) + layout_properties.set_colors(bg='#FFFFFF') + + # Configure drawing settings to scale lineweights down for crispness + config = Configuration( + lineweight_scaling=0.8, + lineweight_policy=LineweightPolicy.ABSOLUTE, + min_lineweight=0.06 + ) + + class DarkeningMatplotlibBackend(MatplotlibBackend): + def _adjust_properties(self, properties): + color = properties.color + if color.startswith('#'): + h = color.lstrip('#') + if len(h) == 6: + r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + elif len(h) == 3: + r, g, b = int(h[0]*2, 16), int(h[1]*2, 16), int(h[2]*2, 16) + else: + return properties + + lum = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0 + if lum > 0.55: + scale = 0.25 / lum + scale = min(scale, 0.8) + r_new = max(0, min(255, int(r * scale))) + g_new = max(0, min(255, int(g * scale))) + b_new = max(0, min(255, int(b * scale))) + new_color = f"#{r_new:02x}{g_new:02x}{b_new:02x}" + return properties._replace(color=new_color) + return properties + + def draw_path(self, path, properties): + super().draw_path(path, self._adjust_properties(properties)) + + def draw_filled_paths(self, paths, properties): + super().draw_filled_paths(paths, self._adjust_properties(properties)) + + def draw_filled_polygon(self, points, properties): + super().draw_filled_polygon(points, self._adjust_properties(properties)) + + def draw_line(self, start, end, properties): + super().draw_line(start, end, self._adjust_properties(properties)) + + def draw_point(self, pos, properties): + super().draw_point(pos, self._adjust_properties(properties)) + + def draw_solid_lines(self, lines, properties): + super().draw_solid_lines(lines, self._adjust_properties(properties)) + + backend = DarkeningMatplotlibBackend(ax, adjust_figure=False) + filter_func = None + if selected_layers is not None: + layers_set = set(selected_layers) + filter_func = lambda entity: entity.dxf.layer in layers_set + Frontend(ctx, backend, config=config).draw_layout( + msp, + layout_properties=layout_properties, + finalize=True, + filter_func=filter_func + ) + + # Set tight limits matching the bounds AFTER draw_layout to enforce exact bounds + ax.set_xlim(min_x, max_x) + ax.set_ylim(min_y, max_y) + + # Ensure parent directory exists + os.makedirs(os.path.dirname(output_png_path), exist_ok=True) + + # Save the figure as PNG (without bbox_inches='tight' when using custom limits to prevent misaligned cropping) + if limits: + fig.savefig(output_png_path, dpi=400, facecolor='#FFFFFF') + else: + fig.savefig(output_png_path, dpi=400, bbox_inches='tight', pad_inches=0, facecolor='#FFFFFF') + plt.close(fig) + + # Construct media URL + from django.conf import settings + filename = os.path.basename(output_png_path) + media_url = os.path.join(settings.MEDIA_URL, 'dxf_layouts', filename).replace('\\', '/') + + return { + 'url': media_url, + 'coordinates': coordinates + } + + +def extract_dxf_text_attributes(file_path): + entity_props = {} + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + current_entity = None + current_handle = None + current_angle = 0.0 + current_height = 1.5 + current_color_idx = None + + while True: + line1 = f.readline() + if not line1: + break + code = line1.strip() + + line2 = f.readline() + if not line2: + break + val = line2.strip() + + if code == '0': + if current_handle: + color_hex = None + if current_color_idx is not None: + aci = abs(current_color_idx) + if aci == 7: + color_hex = "#000000" + elif 0 <= aci < len(ACI_COLORS): + color_hex = ACI_COLORS[aci] + + entity_props[current_handle] = { + 'angle': current_angle, + 'height': min(current_height, 2.5), + 'color': color_hex + } + current_entity = val + current_handle = None + current_angle = 0.0 + current_height = 1.5 + current_color_idx = None + elif code == '5': + current_handle = val + elif code == '40': + try: + current_height = float(val) + except ValueError: + pass + elif code == '50': + try: + current_angle = float(val) + except ValueError: + pass + elif code == '62': + try: + current_color_idx = int(val) + except ValueError: + pass + + # Catch last entity + if current_handle: + color_hex = None + if current_color_idx is not None: + aci = abs(current_color_idx) + if aci == 7: + color_hex = "#000000" + elif 0 <= aci < len(ACI_COLORS): + color_hex = ACI_COLORS[aci] + entity_props[current_handle] = { + 'angle': current_angle, + 'height': min(current_height, 2.5), + 'color': color_hex + } + except Exception: + pass + return entity_props + + +# Helper functions to get points and calculate bounds of the entire DXF +def get_dxf_points_fast(dxf_path): + import ezdxf + from pyproj import Transformer + logger.info("get_dxf_points_fast: Extracting points from DXF: %s", dxf_path) + try: + doc = get_cached_dxf_document(dxf_path) + msp = doc.modelspace() + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + + pts = [] + for entity in msp: + try: + dt = entity.dxftype() + if dt == 'LINE': + pts.append(entity.dxf.start) + pts.append(entity.dxf.end) + elif dt in ('TEXT', 'MTEXT', 'INSERT'): + pts.append(entity.dxf.insert) + elif dt == 'POINT': + pts.append(entity.dxf.location) + elif dt in ('CIRCLE', 'ARC', 'ELLIPSE'): + pts.append(entity.dxf.center) + elif dt in ('LWPOLYLINE', 'POLYLINE'): + for pt in entity.vertices(): + pts.append(pt) + except Exception as e: + pass + + logger.info("get_dxf_points_fast: Found %s raw point geometries", len(pts)) + wgs_pts = [] + for pt in pts: + try: + x = pt.x if hasattr(pt, 'x') else pt[0] + y = pt.y if hasattr(pt, 'y') else pt[1] + lon, lat = transformer.transform(x, y) + wgs_pts.append((lon, lat)) + except Exception as e: + pass + logger.info("get_dxf_points_fast: Extracted %s WGS84 points", len(wgs_pts)) + return wgs_pts + except Exception as e: + logger.exception("get_dxf_points_fast: Error extracting points from DXF") + return [] + +def calculate_dxf_limits_fast(dxf_path): + pts = get_dxf_points_fast(dxf_path) + if not pts: + logger.info("calculate_dxf_limits_fast: No points returned for %s", dxf_path) + return None + + import statistics + lons = [p[0] for p in pts] + lats = [p[1] for p in pts] + + median_lon = statistics.median(lons) + median_lat = statistics.median(lats) + logger.info("calculate_dxf_limits_fast: Median lon: %s, Median lat: %s", median_lon, median_lat) + + min_lon, min_lat = float('inf'), float('inf') + max_lon, max_lat = float('-inf'), float('-inf') + + has_any = False + for lon, lat in pts: + dist = ((lon - median_lon)**2 + (lat - median_lat)**2)**0.5 + if dist < 0.005: # 0.005 degrees is roughly 500m + min_lon = min(min_lon, lon) + max_lon = max(max_lon, lon) + min_lat = min(min_lat, lat) + max_lat = max(max_lat, lat) + has_any = True + + if has_any: + from pyproj import Transformer + trans = Transformer.from_crs(4326, 31370, always_xy=True) + min_x, min_y = trans.transform(min_lon, min_lat) + max_x, max_y = trans.transform(max_lon, max_lat) + logger.info("calculate_dxf_limits_fast: Filtered limits in WGS84: lon(%s, %s), lat(%s, %s)", min_lon, max_lon, min_lat, max_lat) + logger.info("calculate_dxf_limits_fast: Filtered limits in Lambert 72: x(%s, %s), y(%s, %s)", min_x, max_x, min_y, max_y) + + if min_x > max_x: min_x, max_x = max_x, min_x + if min_y > max_y: min_y, max_y = max_y, min_y + + dx = max_x - min_x + dy = max_y - min_y + if dx == 0: dx = 100 + if dy == 0: dy = 100 + min_x -= dx * 0.1 + max_x += dx * 0.1 + min_y -= dy * 0.1 + max_y += dy * 0.1 + logger.info("calculate_dxf_limits_fast: Final padded limits: x(%s, %s), y(%s, %s)", min_x, max_x, min_y, max_y) + return min_x, min_y, max_x, max_y + logger.info("calculate_dxf_limits_fast: No valid points found within 500m median threshold") + return None + + +@check_thematic_access('trafficlights', edit_permission_required=True) +def dxf_layers(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + dxf_file = request.GET.get('file') + if not dxf_file: + return JsonResponse({"error": "No file specified"}, status=400) + + selected_layers = None + # Secure filename + dxf_file = os.path.basename(dxf_file) + file_path = None + for d in intersection.documents.all(): + if os.path.basename(d.file.name) == dxf_file: + file_path = d.file.path + break + if not file_path: + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + dxf_temp_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_temp') + + file_path = os.path.join(uploaded_dxf_dir, dxf_file) + if not os.path.exists(file_path): + file_path = os.path.join(dxf_temp_dir, dxf_file) + if not os.path.exists(file_path): + file_path = os.path.join(read_only_dxf_dir, dxf_file) + + if not file_path or not os.path.exists(file_path): + from documents.models import DocumentVersion + version = DocumentVersion.objects.filter(file__icontains=dxf_file).first() + if version and version.file: + file_path = version.file.path + + if not file_path or not os.path.exists(file_path): + return JsonResponse({"error": "File not found"}, status=404) + + # Check if cached layout GeoJSON exists to get the active selection + geojson_filename = dxf_file.lower().replace('.dxf', '.layout.geojson') + geojson_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts') + geojson_path = os.path.join(geojson_dir, geojson_filename) + if os.path.exists(geojson_path): + try: + with open(geojson_path, 'r', encoding='utf-8') as f: + cached_data = json.load(f) + features = cached_data.get('features', []) + selected_layers = cached_data.get('selected_layers') + + # Collect layer names and their colors from the GeoJSON features + layers_map = {} + for feature in features: + props = feature.get('properties', {}) + layer_name = props.get('Layer') + if layer_name: + color = props.get('color', '#808080') + layers_map[layer_name] = color + + if layers_map: + layers_info = [] + for name, color in layers_map.items(): + layers_info.append({ + "name": name, + "color": color + }) + layers_info = sorted(layers_info, key=lambda x: x["name"].lower()) + + # Normalize selected_layers selection + active_selection = [] + if selected_layers is not None: + if selected_layers == 'all': + active_selection = [l["name"] for l in layers_info] + elif selected_layers == 'none' or selected_layers == []: + active_selection = [] + else: + active_selection = list(selected_layers) + else: + active_selection = [l["name"] for l in layers_info] + + return JsonResponse({ + "layers": layers_info, + "selected_layers": active_selection, + "filename": dxf_file + }) + except Exception: + pass + + try: + import ezdxf + from ezdxf.colors import aci2rgb + doc = get_cached_dxf_document(file_path) + msp = doc.modelspace() + + # Scan modelspace to only list layers that actually contain entities + used_layers = set() + for entity in msp: + used_layers.add(entity.dxf.layer) + + layers_info = [] + for layer in doc.layers: + if layer.dxf.name in used_layers: + try: + rgb = aci2rgb(layer.color) + hex_color = f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}" + except Exception: + hex_color = "#808080" + layers_info.append({ + "name": layer.dxf.name, + "color": hex_color + }) + + # Sort layers by name + layers_info = sorted(layers_info, key=lambda x: x["name"].lower()) + + # Normalize selected_layers selection + active_selection = [] + if selected_layers is not None: + if selected_layers == 'all': + active_selection = [l["name"] for l in layers_info] + elif selected_layers == 'none' or selected_layers == []: + active_selection = [] + else: + active_selection = list(selected_layers) + else: + # Propose all by default if no cached layout + active_selection = [l["name"] for l in layers_info] + + return JsonResponse({ + "layers": layers_info, + "selected_layers": active_selection, + "filename": dxf_file + }) + except Exception as e: + import traceback + traceback.print_exc() + return JsonResponse({"error": str(e)}, status=500) + + +from collections import defaultdict +from ezdxf.addons.drawing.backend import Backend +from pyproj import Transformer +from shapely.geometry import MultiLineString, mapping +from shapely.ops import linemerge + +class OptimizedGeoJSONBackend(Backend): + def __init__(self, doc, precision=7, selected_layers=None): + super().__init__() + self.doc = doc + self.precision = precision + self.features = [] + self.geometries = defaultdict(list) + self.transformer = Transformer.from_crs(31370, 4326, always_xy=True) + self.skip_draw = False + self.selected_layers = selected_layers + self.skip_layer = False + + def enter_entity(self, entity, properties): + super().enter_entity(entity, properties) + self.skip_layer = (self.selected_layers is not None and properties.layer not in self.selected_layers) + if self.skip_layer: + return + if entity.dxftype() in ('TEXT', 'MTEXT'): + self.skip_draw = True + text = "" + if entity.dxftype() == 'MTEXT': + text = entity.text + else: + text = entity.dxf.text + + import re + text = re.sub(r'\\[~^#\\_]', '', text) + text = text.replace('{', '').replace('}', '') + text = re.sub(r'\\P', '\n', text).strip() + + if text: + insert = entity.dxf.insert + if entity.dxftype() == 'TEXT': + try: + placement = entity.get_placement() + if placement[1] is not None: + insert = placement[1] + except Exception: + pass + + lon, lat = self.transformer.transform(insert.x, insert.y) + + is_off = False + layer_name = properties.layer + if self.doc.layers.has_entry(layer_name): + layer = self.doc.layers.get(layer_name) + is_off = layer.is_off() or layer.is_frozen() + + height = 1.5 + angle = 0.0 + text_anchor = 'center' + + if entity.dxftype() == 'MTEXT': + if entity.dxf.hasattr('char_height'): + height = entity.dxf.char_height + if entity.dxf.hasattr('rotation'): + angle = entity.dxf.rotation + try: + attach = entity.dxf.attachment_point + mtext_map = { + 1: 'top-left', + 2: 'top', + 3: 'top-right', + 4: 'left', + 5: 'center', + 6: 'right', + 7: 'bottom-left', + 8: 'bottom', + 9: 'bottom-right' + } + text_anchor = mtext_map.get(attach, 'center') + except Exception: + pass + else: # TEXT + if entity.dxf.hasattr('height'): + height = entity.dxf.height + if entity.dxf.hasattr('rotation'): + angle = entity.dxf.rotation + try: + align_name = entity.get_align_enum().name + text_map = { + 'LEFT': 'left', + 'CENTER': 'bottom', + 'RIGHT': 'right', + 'BOTTOM_LEFT': 'bottom-left', + 'BOTTOM_CENTER': 'bottom', + 'BOTTOM_RIGHT': 'bottom-right', + 'MIDDLE_LEFT': 'left', + 'MIDDLE_CENTER': 'center', + 'MIDDLE_RIGHT': 'right', + 'TOP_LEFT': 'top-left', + 'TOP_CENTER': 'top', + 'TOP_RIGHT': 'top-right' + } + text_anchor = text_map.get(align_name, 'center') + except Exception: + pass + + self.features.append({ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [round(lon, self.precision), round(lat, self.precision)] + }, + "properties": { + "Layer": layer_name, + "color": properties.color, + "is_off": is_off, + "Text": text, + "text_height": min(height, 2.5), + "text_angle": angle, + "text_anchor": text_anchor + } + }) + + def exit_entity(self, entity): + super().exit_entity(entity) + self.skip_layer = False + if entity.dxftype() in ('TEXT', 'MTEXT'): + self.skip_draw = False + + def set_background(self, color): + pass + + def clear(self): + self.features = [] + self.geometries.clear() + + def draw_point(self, pos, properties): + if self.skip_layer or self.skip_draw: + return + lon, lat = self.transformer.transform(pos.x, pos.y) + self.geometries[('Point', properties.layer, properties.color)].append([ + round(lon, self.precision), round(lat, self.precision) + ]) + + def draw_line(self, start, end, properties): + if self.skip_layer or self.skip_draw: + return + lon1, lat1 = self.transformer.transform(start.x, start.y) + lon2, lat2 = self.transformer.transform(end.x, end.y) + self.geometries[('LineString', properties.layer, properties.color)].append(( + (round(lon1, self.precision), round(lat1, self.precision)), + (round(lon2, self.precision), round(lat2, self.precision)) + )) + + def draw_filled_polygon(self, points, properties): + if self.skip_layer or self.skip_draw: + return + coords = [] + for pt in points.vertices(): + lon, lat = self.transformer.transform(pt.x, pt.y) + coords.append([round(lon, self.precision), round(lat, self.precision)]) + if coords and coords[0] != coords[-1]: + coords.append(coords[0]) + self.geometries[('Polygon', properties.layer, properties.color)].append([coords]) + + def draw_image(self, image_data, properties): + pass + + def finalize(self): + for (geom_type, layer_name, color), geom_list in self.geometries.items(): + is_off = False + if self.doc.layers.has_entry(layer_name): + layer = self.doc.layers.get(layer_name) + is_off = layer.is_off() or layer.is_frozen() + + if geom_type == 'LineString': + mls = MultiLineString(geom_list) + merged = linemerge(mls) + geom_dict = mapping(merged) + + self.features.append({ + "type": "Feature", + "geometry": geom_dict, + "properties": { + "Layer": layer_name, + "color": color, + "is_off": is_off + } + }) + elif geom_type == 'Polygon': + self.features.append({ + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": geom_list + }, + "properties": { + "Layer": layer_name, + "color": color, + "is_off": is_off + } + }) + elif geom_type == 'Point': + self.features.append({ + "type": "Feature", + "geometry": { + "type": "MultiPoint", + "coordinates": geom_list + }, + "properties": { + "Layer": layer_name, + "color": color, + "is_off": is_off + } + }) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +def dxf_layout_geojson(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + dxf_file = request.GET.get('file') + if not dxf_file: + return JsonResponse({"error": "No file specified"}, status=400) + + # Secure filename + dxf_file = os.path.basename(dxf_file) + file_path = None + for d in intersection.documents.all(): + if os.path.basename(d.file.name) == dxf_file: + file_path = d.file.path + break + if not file_path: + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(uploaded_dxf_dir, dxf_file) + if not os.path.exists(file_path): + file_path = os.path.join(read_only_dxf_dir, dxf_file) + + if not file_path or not os.path.exists(file_path): + from documents.models import DocumentVersion + version = DocumentVersion.objects.filter(file__icontains=dxf_file).first() + if version and version.file: + file_path = version.file.path + + if not file_path or not os.path.exists(file_path): + return JsonResponse({"error": "File not found"}, status=404) + + layers_param = request.GET.get('layers') + selected_layers = None + + if layers_param is not None: + if layers_param == 'all': + selected_layers = None + elif layers_param == 'none' or layers_param == '': + selected_layers = set() + else: + selected_layers = set(layers_param.split(',')) + + geojson_filename = dxf_file.lower().replace('.dxf', '.layout.geojson') + geojson_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_layouts') + os.makedirs(geojson_dir, exist_ok=True) + geojson_path = os.path.join(geojson_dir, geojson_filename) + + overlay_filename = dxf_file.lower().replace('.dxf', '.overlay.png') + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_filename) + + is_detail = request.GET.get('detail') == 'true' + + # 1. Detail page image-only option shortcut + if is_detail and (layers_param == 'none' or layers_param == ''): + geojson_data = { + "type": "FeatureCollection", + "features": [], + "selected_layers": [] + } + try: + from django.core.cache import cache + cache_key = f"dxf_limits_{dxf_file.lower()}" + limits = cache.get(cache_key) + if not limits: + limits = calculate_dxf_limits_fast(file_path) + if limits: + cache.set(cache_key, limits, timeout=86400 * 30) + if limits: + if not os.path.exists(overlay_path): + generate_dxf_image_overlay(file_path, overlay_path, limits=limits) + + from pyproj import Transformer + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + min_x, min_y, max_x, max_y = limits + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + geojson_data['image_overlay'] = { + 'url': f"{settings.MEDIA_URL}dxf_layouts/{overlay_filename}", + 'coordinates': [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ] + } + except Exception as e: + import traceback + traceback.print_exc() + return JsonResponse(geojson_data) + + # Check if cached layout GeoJSON exists + geojson_data = None + if os.path.exists(geojson_path): + try: + with open(geojson_path, 'r', encoding='utf-8') as f: + geojson_data = json.load(f) + except Exception: + pass + + # Fallback to legacy hashed cache files if single cache file does not exist + if not geojson_data: + import glob + pattern = os.path.join(geojson_dir, f"{dxf_file.lower().replace('.dxf', '')}.*.layout.geojson") + legacy_files = glob.glob(pattern) + if legacy_files: + legacy_files.sort(key=os.path.getmtime, reverse=True) + for legacy_path in legacy_files: + try: + with open(legacy_path, 'r', encoding='utf-8') as f: + geojson_data = json.load(f) + if geojson_data: + # Migrate to the single cache path + with open(geojson_path, 'w', encoding='utf-8') as f: + json.dump(geojson_data, f, ensure_ascii=False) + break + except Exception: + pass + + # If layers_param is explicitly specified, check if selection matches cache. If not, discard cache. + if geojson_data and layers_param is not None: + cached_selection = geojson_data.get('selected_layers') + + # Normalize cached_selection + if cached_selection is None or cached_selection == 'all': + norm_cached = None + elif cached_selection == 'none' or cached_selection == []: + norm_cached = set() + else: + norm_cached = set(cached_selection) + + if selected_layers != norm_cached: + # If the cache is already filtered (norm_cached is not None) and the request is for 'all' (selected_layers is None), + # do not discard the cache. This preserves the user's filtered layers. + if selected_layers is None and norm_cached is not None: + pass + else: + # Selection has changed, discard cache to force regeneration + geojson_data = None + + if geojson_data: + if is_detail: + geojson_data.pop('image_overlay', None) + return JsonResponse(geojson_data, safe=False) + + + + # Generate image overlay if missing from cache + if geojson_data and 'image_overlay' not in geojson_data: + try: + limits = calculate_dxf_limits_fast(file_path) + if limits: + if not os.path.exists(overlay_path): + generate_dxf_image_overlay(file_path, overlay_path, limits=limits) + + from pyproj import Transformer + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + min_x, min_y, max_x, max_y = limits + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + geojson_data['image_overlay'] = { + 'url': f"{settings.MEDIA_URL}dxf_layouts/{overlay_filename}", + 'coordinates': [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ] + } + with open(geojson_path, 'w', encoding='utf-8') as f: + json.dump(geojson_data, f, ensure_ascii=False) + except Exception as e: + import traceback + traceback.print_exc() + + # If cache hit, return it directly + if geojson_data: + return JsonResponse(geojson_data, safe=False) + + # Otherwise, read from scratch and write cache using ezdxf GeoJSONBackend + try: + from collections import defaultdict + import ezdxf + from ezdxf.addons.drawing import RenderContext, Frontend + from ezdxf.addons.drawing.properties import LayoutProperties + + doc = get_cached_dxf_document(file_path) + msp = doc.modelspace() + + ctx = RenderContext(doc) + layout_properties = LayoutProperties.from_layout(msp) + + backend = OptimizedGeoJSONBackend(doc, precision=7, selected_layers=selected_layers) + if selected_layers is None: + Frontend(ctx, backend).draw_layout(msp, layout_properties=layout_properties, finalize=True) + elif len(selected_layers) > 0: + filtered_entities = [entity for entity in msp if entity.dxf.layer in selected_layers] + Frontend(ctx, backend).draw_entities(filtered_entities) + backend.finalize() + + geojson_data = { + "type": "FeatureCollection", + "features": backend.features, + "selected_layers": list(selected_layers) if isinstance(selected_layers, (set, list)) else selected_layers + } + + # Generate and add image_overlay (only if not in detail page) + if not is_detail: + try: + from django.core.cache import cache + cache_key = f"dxf_limits_{dxf_file.lower()}" + limits = cache.get(cache_key) + if not limits: + limits = calculate_dxf_limits_fast(file_path) + if limits: + cache.set(cache_key, limits, timeout=86400 * 30) # cache for 30 days + if limits: + if not os.path.exists(overlay_path): + generate_dxf_image_overlay(file_path, overlay_path, limits=limits) + + from pyproj import Transformer + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + min_x, min_y, max_x, max_y = limits + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + geojson_data['image_overlay'] = { + 'url': f"{settings.MEDIA_URL}dxf_layouts/{overlay_filename}", + 'coordinates': [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ] + } + except Exception as e: + import traceback + traceback.print_exc() + + # Save generated layout to cache for faster future requests + with open(geojson_path, 'w', encoding='utf-8') as f: + json.dump(geojson_data, f, ensure_ascii=False) + + return JsonResponse(geojson_data, safe=False) + except Exception as e: + import traceback + traceback.print_exc() + return JsonResponse({"error": f"Failed to generate layout: {str(e)}"}, status=500) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def add_staged_pole(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + model_id = request.POST.get('model_id') + code = request.POST.get('code') + lon_str = request.POST.get('lon', '0.0') + lat_str = request.POST.get('lat', '0.0') + dxf_file = request.POST.get('dxf_file', '') + + try: + lon = float(lon_str.replace(',', '.')) + lat = float(lat_str.replace(',', '.')) + except ValueError: + return JsonResponse({'success': False, 'error': "Invalid coordinates"}, status=400) + + # Convert coords from 4326 to 3812 + from pyproj import Transformer + from django.contrib.gis.geos import Point as DjangoPoint + transformer = Transformer.from_crs(4326, 3812, always_xy=True) + x_3812, y_3812 = transformer.transform(lon, lat) + + default_model = TrafficLightPoleModel.objects.first() + try: + model = TrafficLightPoleModel.objects.get(pk=int(model_id)) if model_id else default_model + except (TrafficLightPoleModel.DoesNotExist, ValueError): + model = default_model + + note = f"Créé manuellement sur plan DXF : {dxf_file}" if dxf_file else "Créé manuellement sur plan DXF" + + pole = TrafficLightPole( + intersection=intersection, + model=model, + code=code, + geom=DjangoPoint(x_3812, y_3812, srid=3812), + lon=lon, + lat=lat, + status='to_be_approved', + validation_status='to_be_validated', + note=note, + updated_by=request.user + ) + pole._history_user = request.user + pole.save() + + return JsonResponse({ + 'success': True, + 'pole': { + 'id': pole.id, + 'code': pole.code, + 'model_id': pole.model_id, + 'model_code': pole.model.code if pole.model else "", + 'lon': pole.lon, + 'lat': pole.lat, + } + }) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def delete_staged_pole(request, intersection_id, pole_id): + pole = get_object_or_404(TrafficLightPole, pk=pole_id, intersection_id=intersection_id, status='to_be_approved') + pole.delete() + return JsonResponse({'success': True}) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def update_staged_pole_position(request, intersection_id, pole_id): + pole = get_object_or_404(TrafficLightPole, pk=pole_id, intersection_id=intersection_id, status='to_be_approved') + lon_str = request.POST.get('lon') + lat_str = request.POST.get('lat') + + try: + lon = float(lon_str.replace(',', '.')) + lat = float(lat_str.replace(',', '.')) + except (ValueError, TypeError): + return JsonResponse({'success': False, 'error': "Invalid coordinates"}, status=400) + + # Convert coords from 4326 to 3812 + from pyproj import Transformer + from django.contrib.gis.geos import Point as DjangoPoint + transformer = Transformer.from_crs(4326, 3812, always_xy=True) + x_3812, y_3812 = transformer.transform(lon, lat) + + pole.geom = DjangoPoint(x_3812, y_3812, srid=3812) + pole.lon = lon + pole.lat = lat + pole.updated_by = request.user + pole._history_user = request.user + pole.save() + + return JsonResponse({ + 'success': True, + 'pole': { + 'id': pole.id, + 'code': pole.code, + 'lon': pole.lon, + 'lat': pole.lat, + } + }) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +def import_cross_plan(request, intersection_id): + from django.shortcuts import render, get_object_or_404, redirect + from django.contrib import messages + from django.utils.translation import gettext as _ + from django.urls import reverse + from django.conf import settings + import json + from assets.utils.cross_plan_parser import parse_cross_plan + from assets.models.trafficlights import ( + TrafficLightIntersection, TrafficLightPole, TrafficLightLantern, + TrafficLightDetector, TrafficLightCable, TrafficLightPoleModel, + TrafficLightLanternModel, TrafficLightDetectorModel, TrafficLightCableModel + ) + from documents.services import DocumentManagementService + + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + uploaded_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'cross_plans') + + # Fetch DXF files and georeferenced documents associated with this intersection + dxf_files = [] + + # 1. Add document management DXF files first (most recent updated_at first) + from documents.models import DocumentAttachment, ManagedDocument + + ct = ContentType.objects.get_for_model(intersection) + attachments = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id) + managed_dxf_docs = ManagedDocument.objects.filter( + id__in=attachments.values_list('document_id', flat=True), + is_archived=False + ).select_related('current_version') + + managed_dxf_docs = sorted( + list(managed_dxf_docs), + key=lambda x: x.updated_at, + reverse=True + ) + for doc in managed_dxf_docs: + if doc.current_version and doc.current_version.file: + fn = os.path.basename(doc.current_version.file.name) + if fn.lower().endswith('.dxf') and fn not in dxf_files: + dxf_files.append(fn) + + # 2. Add DB documents next, ordered by upload_date descending (legacy fallback) + dxf_docs = sorted( + [doc for doc in intersection.documents.all() if doc.file.name.lower().endswith('.dxf')], + key=lambda x: x.upload_date, + reverse=True + ) + for doc in dxf_docs: + fn = os.path.basename(doc.file.name) + if fn not in dxf_files: + dxf_files.append(fn) + + # 3. Add read-only external files next + external_files = [] + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + if os.path.exists(read_only_dxf_dir): + code_lower = intersection.code.lower() if intersection.code else "" + for f in os.listdir(read_only_dxf_dir): + if f.lower().endswith('.dxf'): + if code_lower and code_lower in f.lower(): + external_files.append(f) + for f in sorted(external_files): + if f not in dxf_files: + dxf_files.append(f) + + doc_data = DocumentManagementService.get_documents_for_asset(intersection) + document_folder = doc_data.get("folder") + use_managed_documents = document_folder is not None + + georeferenced_documents = [] + if use_managed_documents and document_folder: + from documents.models import ManagedDocument + folder_ids = DocumentManagementService.get_folder_and_descendants(document_folder) + docs = ManagedDocument.objects.filter( + folders__pk__in=folder_ids, + is_archived=False, + georeference_coords__isnull=False + ).distinct() + for doc in docs: + is_dxf_overlay = bool(doc.unique_params and 'dxf_source_id' in doc.unique_params) + georeferenced_documents.append({ + 'id': doc.id, + 'title': doc.title, + 'coords': doc.georeference_coords, + 'url': reverse('documents:page_image', args=[doc.pk, doc.georeference_page or 1]), + 'is_dxf': is_dxf_overlay + }) + + if not use_managed_documents: + for dxf_file in dxf_files: + file_path = None + for d in intersection.documents.all(): + if os.path.basename(d.file.name) == dxf_file: + file_path = d.file.path + break + if not file_path: + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files') + file_path = os.path.join(uploaded_dxf_dir, dxf_file) + if not os.path.exists(file_path): + file_path = os.path.join(read_only_dxf_dir, dxf_file) + + if os.path.exists(file_path): + try: + overlay_filename = dxf_file.lower().replace('.dxf', '.overlay.png') + overlay_path = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts', overlay_filename) + + if os.path.exists(overlay_path): + from django.core.cache import cache + from pyproj import Transformer + from assets.views.trafficlights import calculate_dxf_limits_fast + + cache_key = f"dxf_limits_{dxf_file.lower()}" + limits = cache.get(cache_key) + if not limits: + limits = calculate_dxf_limits_fast(file_path) + if limits: + cache.set(cache_key, limits, timeout=86400 * 30) + + if limits: + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + min_x, min_y, max_x, max_y = limits + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + georeferenced_documents.append({ + 'id': f"dxf-{dxf_file}", + 'title': f"[CAO] {dxf_file}", + 'coords': [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ], + 'url': f"{settings.MEDIA_URL}dxf_layouts/{overlay_filename}", + 'is_dxf': True + }) + except Exception as e: + print(f"Error preparing DXF overlay for {dxf_file}: {e}") + + georeferenced_documents_json = json.dumps(georeferenced_documents) + + # Build list of existing cross plans for the user to choose from + from documents.models import DocumentAttachment, ManagedDocument + from documents.services import DocumentManagementService + from django.db.models import Q + ct = ContentType.objects.get_for_model(intersection) + attachments = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id) + + managed_docs_query = ManagedDocument.objects.filter( + tags__slug__in=['cross_plan', 'crossplan'], + is_archived=False + ) + + doc_data = DocumentManagementService.get_documents_for_asset(intersection) + document_folder = doc_data.get("folder") + if document_folder: + folder_ids = DocumentManagementService.get_folder_and_descendants(document_folder) + managed_docs_query = managed_docs_query.filter( + Q(id__in=attachments.values_list('document_id', flat=True)) | + Q(folders__pk__in=folder_ids) + ) + else: + managed_docs_query = managed_docs_query.filter( + id__in=attachments.values_list('document_id', flat=True) + ) + + existing_managed_cross_plans = managed_docs_query.distinct().select_related('current_version') + + legacy_cross_plans = intersection.documents.filter(document_type='cross_plan') + + cross_plan_choices = [] + # 1. Managed documents + for doc in existing_managed_cross_plans: + if doc.current_version and doc.current_version.file: + cross_plan_choices.append({ + 'id': f"managed-{doc.id}", + 'title': doc.title or os.path.basename(doc.current_version.file.name), + 'filename': os.path.basename(doc.current_version.file.name), + 'path': doc.current_version.file.path, + 'is_managed': True + }) + # 2. Legacy documents + for doc in legacy_cross_plans: + if doc.file: + fn = os.path.basename(doc.file.name) + if not any(c['filename'] == fn for c in cross_plan_choices): + cross_plan_choices.append({ + 'id': f"legacy-{doc.id}", + 'title': fn, + 'filename': fn, + 'path': doc.file.path, + 'is_managed': False + }) + + if request.method == 'GET': + return render(request, "assets/trafficlights_import_cross_plan.html", { + 'intersection': intersection, + 'step': 'select', + 'thematic_code': 'trafficlights', + 'dxf_files': dxf_files, + 'georeferenced_documents_json': georeferenced_documents_json, + 'cross_plan_choices': cross_plan_choices, + }) + + elif request.method == 'POST': + existing_cp_ids = request.POST.getlist('existing_cross_plans') + uploaded_files = request.FILES.getlist('cross_plan_file_upload') + + if not uploaded_files and not existing_cp_ids: + return render(request, "assets/trafficlights_import_cross_plan.html", { + 'intersection': intersection, + 'error': _("Veuillez téléverser un fichier ou sélectionner au moins un plan existant."), + 'step': 'select', + 'thematic_code': 'trafficlights', + 'cross_plan_choices': cross_plan_choices, + }) + + import uuid + import json + + merged_deduced = { + "intersection_code": None, + "poles": [], + "cables": [], + "lanterns": [], + "detectors": [] + } + + poles_by_code = {} + cables_by_code = {} + lanterns_by_code = {} + detectors_by_code = {} + excel_codes = set() + uploaded_filenames = [] + + files_to_parse = [] + if uploaded_files: + for uploaded_file in uploaded_files: + os.makedirs(uploaded_dir, exist_ok=True) + temp_path = os.path.join(uploaded_dir, f"{intersection.id}_{uuid.uuid4().hex}_{uploaded_file.name}") + with open(temp_path, 'wb+') as destination: + for chunk in uploaded_file.chunks(): + destination.write(chunk) + files_to_parse.append((temp_path, uploaded_file.name, True)) + + if existing_cp_ids: + for existing_cp_id in existing_cp_ids: + temp_path = None + filename = None + if existing_cp_id.startswith('managed-'): + doc_id = existing_cp_id.replace('managed-', '') + doc = ManagedDocument.objects.filter(pk=doc_id, is_archived=False).first() + if doc and doc.current_version and doc.current_version.file: + temp_path = doc.current_version.file.path + filename = os.path.basename(doc.current_version.file.name) + elif existing_cp_id.startswith('legacy-'): + doc_id = existing_cp_id.replace('legacy-', '') + doc = intersection.documents.filter(pk=doc_id).first() + if doc and doc.file: + temp_path = doc.file.path + filename = os.path.basename(doc.file.name) + + if temp_path and os.path.exists(temp_path): + files_to_parse.append((temp_path, filename, False)) + else: + return render(request, "assets/trafficlights_import_cross_plan.html", { + 'intersection': intersection, + 'error': _("Un des fichiers de plan de croix sélectionnés est introuvable."), + 'step': 'select', + 'thematic_code': 'trafficlights', + 'cross_plan_choices': cross_plan_choices, + }) + + for file_path, filename, delete_after in files_to_parse: + uploaded_filenames.append(filename) + try: + deduced = parse_cross_plan(file_path) + except Exception as e: + if delete_after and os.path.exists(file_path): + os.remove(file_path) + return render(request, "assets/trafficlights_import_cross_plan.html", { + 'intersection': intersection, + 'error': f"Erreur lors de la lecture du fichier Excel '{filename}' : {str(e)}", + 'step': 'select', + 'thematic_code': 'trafficlights', + 'cross_plan_choices': cross_plan_choices, + }) + + if deduced.get("intersection_code"): + excel_codes.add(deduced["intersection_code"]) + if not merged_deduced["intersection_code"]: + merged_deduced["intersection_code"] = deduced["intersection_code"] + + # Merge poles + for p in deduced.get("poles", []): + code = p["code"] + if code not in poles_by_code: + poles_by_code[code] = p + + # Merge cables + for c in deduced.get("cables", []): + code = c["code"] + if code not in cables_by_code: + cables_by_code[code] = c + + # Merge lanterns + for l in deduced.get("lanterns", []): + code = l["code"] + if code not in lanterns_by_code: + lanterns_by_code[code] = l + + # Merge detectors + for d in deduced.get("detectors", []): + code = d["code"] + if code not in detectors_by_code: + detectors_by_code[code] = d + + if delete_after and os.path.exists(file_path): + os.remove(file_path) + + merged_deduced["poles"] = sorted(list(poles_by_code.values()), key=lambda x: x['code']) + merged_deduced["cables"] = sorted(list(cables_by_code.values()), key=lambda x: x['code']) + merged_deduced["lanterns"] = sorted(list(lanterns_by_code.values()), key=lambda x: x['code']) + merged_deduced["detectors"] = sorted(list(detectors_by_code.values()), key=lambda x: x['code']) + + # Check if the parsed intersection codes match the current one + if excel_codes: + for excel_code in excel_codes: + if excel_code != intersection.code: + messages.warning( + request, + f"Attention : Un code carrefour dans l'Excel est '{excel_code}' alors que vous importez dans le carrefour '{intersection.code}'." + ) + + # Save merged results into a temporary JSON file + json_filename = f"combined_{intersection.id}_{uuid.uuid4().hex}.json" + json_filepath = os.path.join(uploaded_dir, json_filename) + with open(json_filepath, 'w', encoding='utf-8') as f: + json.dump(merged_deduced, f, indent=4) + + + + # Load active poles georeferenced (always loaded for map) + existing_poles = list(TrafficLightPole.objects.filter(intersection=intersection, status='active')) + existing_features = [] + for ep in existing_poles: + lon = ep.lon + lat = ep.lat + if (lon is None or lat is None) and ep.geom: + try: + geom_wgs84 = ep.geom.clone() + geom_wgs84.transform(4326) + lon, lat = geom_wgs84.x, geom_wgs84.y + except Exception: + pass + if lon is not None and lat is not None: + existing_features.append({ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [lon, lat] + }, + "properties": { + "id": ep.id, + "code": ep.code, + "model": ep.model.code if ep.model else "" + } + }) + existing_poles_geojson = { + "type": "FeatureCollection", + "features": existing_features + } + + deduced = merged_deduced + + # ── 1. POLES COMPARISON ── + existing_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')} + compared_poles = [] + for dp in deduced["poles"]: + code = dp["code"] + model_name = dp["model"] + db_model = TrafficLightPoleModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightPoleModel.objects.first() + + if code in existing_poles: + ep = existing_poles[code] + is_diff = ep.model_id != db_model.id + status = 'modified' if is_diff else 'identical' + compared_poles.append({ + 'code': code, + 'short_code': dp['short_code'], + 'name': ep.name_fr or dp.get('name', dp['short_code']), + 'status': status, + 'deduced_model_name': model_name, + 'db_model_name': ep.model.name_fr if ep.model else '', + 'db_id': ep.id, + 'details_diff': f"Modèle différent (DB: {ep.model.name_fr if ep.model else 'Aucun'} vs Excel: {model_name})" if is_diff else '' + }) + # remove from processed + existing_poles.pop(code) + else: + compared_poles.append({ + 'code': code, + 'short_code': dp['short_code'], + 'name': dp.get('name', dp['short_code']), + 'status': 'new', + 'deduced_model_name': model_name, + 'db_model_name': '', + 'db_id': None, + 'details_diff': '' + }) + # Add remaining existing active poles (orphans) + for code, ep in existing_poles.items(): + compared_poles.append({ + 'code': code, + 'short_code': code.split('_')[-1] if '_' in code else code, + 'name': ep.name_fr or code, + 'status': 'db_only', + 'deduced_model_name': '', + 'db_model_name': ep.model.name_fr if ep.model else '', + 'db_id': ep.id, + 'details_diff': _("Absent du fichier Excel (Sera conservé ou archivé)") + }) + + # ── 2. CABLES COMPARISON ── + existing_cables = {c.code: c for c in TrafficLightCable.objects.filter(intersection=intersection, status='active')} + compared_cables = [] + for dc in deduced["cables"]: + code = dc["code"] + model_name = dc["model_name"] + db_model = TrafficLightCableModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightCableModel.objects.first() + + if code in existing_cables: + ec = existing_cables[code] + is_diff = ec.model_id != db_model.id + status = 'modified' if is_diff else 'identical' + compared_cables.append({ + 'code': code, + 'name': ec.name_fr or dc['name'], + 'status': status, + 'deduced_model_name': model_name, + 'db_model_name': ec.model.name_fr if ec.model else '', + 'db_id': ec.id, + 'details_diff': f"Modèle différent (DB: {ec.model.name_fr if ec.model else 'Aucun'} vs Excel: {model_name})" if is_diff else '', + 'connected_poles': dc.get("connected_poles", []) + }) + existing_cables.pop(code) + else: + compared_cables.append({ + 'code': code, + 'name': dc['name'], + 'status': 'new', + 'deduced_model_name': model_name, + 'db_model_name': '', + 'db_id': None, + 'details_diff': '', + 'connected_poles': dc.get("connected_poles", []) + }) + for code, ec in existing_cables.items(): + compared_cables.append({ + 'code': code, + 'name': ec.name_fr or code, + 'status': 'db_only', + 'deduced_model_name': '', + 'db_model_name': ec.model.name_fr if ec.model else '', + 'db_id': ec.id, + 'details_diff': _("Absent du fichier Excel") + }) + + # ── 3. LANTERNS COMPARISON ── + existing_lanterns = {l.code: l for l in TrafficLightLantern.objects.filter(pole__intersection=intersection, status='active')} + compared_lanterns = [] + for dl in deduced["lanterns"]: + code = dl["code"] + base_code = code.rsplit('_', 1)[0] if '_' in code else code + model_name = dl["model_name"] + db_model = TrafficLightLanternModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightLanternModel.objects.first() + + # Try exact match, then try prefix match (without the phase suffix) + match_key = None + if code in existing_lanterns: + match_key = code + elif base_code in existing_lanterns: + match_key = base_code + + if match_key: + el = existing_lanterns[match_key] + is_rename = (el.code != code) + is_model_diff = el.model_id != db_model.id + status = 'modified' if (is_rename or is_model_diff) else 'identical' + + details = [] + if is_rename: + details.append(f"Renommer le code (DB: {el.code} vs Excel: {code})") + if is_model_diff: + details.append(f"Modèle différent (DB: {el.model.name_fr if el.model else 'Aucun'} vs Excel: {model_name})") + + compared_lanterns.append({ + 'code': code, + 'name': el.name_fr or dl['name'], + 'status': status, + 'deduced_model_name': model_name, + 'db_model_name': el.model.name_fr if el.model else '', + 'db_id': el.id, + 'pole_code': dl['pole_code'], + 'details_diff': ", ".join(details) if details else '' + }) + existing_lanterns.pop(match_key) + else: + compared_lanterns.append({ + 'code': code, + 'name': dl['name'], + 'status': 'new', + 'deduced_model_name': model_name, + 'db_model_name': '', + 'db_id': None, + 'pole_code': dl['pole_code'], + 'details_diff': '' + }) + for code, el in existing_lanterns.items(): + compared_lanterns.append({ + 'code': code, + 'name': el.name_fr or code, + 'status': 'db_only', + 'deduced_model_name': '', + 'db_model_name': el.model.name_fr if el.model else '', + 'db_id': el.id, + 'pole_code': el.pole.code, + 'details_diff': _("Absent du fichier Excel") + }) + + # ── 4. DETECTORS COMPARISON ── + existing_detectors = {d.code: d for d in TrafficLightDetector.objects.filter(intersection=intersection, status='active')} + compared_detectors = [] + for dd in deduced["detectors"]: + code = dd["code"] + model_name = dd["model_name"] + db_model = TrafficLightDetectorModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightDetectorModel.objects.first() + + match_key = None + if code in existing_detectors: + match_key = code + else: + parts = code.split('_', 2) + if len(parts) == 3: + old_key = f"{parts[0]}-{parts[2]}" + if old_key in existing_detectors: + match_key = old_key + + if match_key: + ed = existing_detectors[match_key] + is_diff = ed.model_id != db_model.id + details = [] + if match_key != code: + details.append(f"Renommer le code (DB: {match_key} vs Excel: {code})") + if is_diff: + details.append(f"Modèle différent (DB: {ed.model.name_fr if ed.model else 'Aucun'} vs Excel: {model_name})") + + status = 'modified' if (is_diff or match_key != code) else 'identical' + compared_detectors.append({ + 'code': code, + 'name': ed.name or dd['name'], + 'status': status, + 'deduced_model_name': model_name, + 'db_model_name': ed.model.name_fr if ed.model else '', + 'db_id': ed.id, + 'pole_code': dd['pole_code'], + 'details_diff': ", ".join(details) if details else '' + }) + existing_detectors.pop(match_key) + else: + compared_detectors.append({ + 'code': code, + 'name': dd['name'], + 'status': 'new', + 'deduced_model_name': model_name, + 'db_model_name': '', + 'db_id': None, + 'pole_code': dd['pole_code'], + 'details_diff': '' + }) + for code, ed in existing_detectors.items(): + compared_detectors.append({ + 'code': code, + 'name': ed.name or code, + 'status': 'db_only', + 'deduced_model_name': '', + 'db_model_name': ed.model.name_fr if ed.model else '', + 'db_id': ed.id, + 'pole_code': ed.pole.code if ed.pole else '', + 'details_diff': _("Absent du fichier Excel") + }) + + # Sort compared lists alphabetically by code for preview + compared_poles.sort(key=lambda x: x['code']) + compared_cables.sort(key=lambda x: x['code']) + compared_lanterns.sort(key=lambda x: x['code']) + compared_detectors.sort(key=lambda x: x['code']) + + # Fetch models for select box overrides + pole_models = TrafficLightPoleModel.objects.all().order_by('code') + lantern_models = TrafficLightLanternModel.objects.all().order_by('code') + detector_models = TrafficLightDetectorModel.objects.all().order_by('code') + cable_models = TrafficLightCableModel.objects.all().order_by('code') + + template_name = "assets/trafficlights_import_cross_plan.html" + context = { + 'intersection': intersection, + 'cross_plan_filename': json_filename, + 'uploaded_filenames': uploaded_filenames, + 'poles': compared_poles, + 'cables': compared_cables, + 'lanterns': compared_lanterns, + 'detectors': compared_detectors, + 'pole_models': pole_models, + 'lantern_models': lantern_models, + 'detector_models': detector_models, + 'cable_models': cable_models, + 'step': 'preview', + 'thematic_code': 'trafficlights', + 'dxf_file': "", + 'dxf_poles': [], + 'existing_poles_geojson': existing_poles_geojson, + 'dxf_files': dxf_files, + 'georeferenced_documents_json': georeferenced_documents_json, + } + + return render(request, template_name, context) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def integrate_cross_plan(request, intersection_id): + from django.shortcuts import get_object_or_404, redirect + from django.contrib import messages + from django.db import transaction + import json + from assets.models.trafficlights import ( + TrafficLightIntersection, TrafficLightPole, TrafficLightLantern, + TrafficLightDetector, TrafficLightCable, TrafficLightPoleModel, + TrafficLightLanternModel, TrafficLightDetectorModel, TrafficLightCableModel + ) + + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + filename = request.POST.get('cross_plan_filename') + if not filename: + messages.error(request, "Nom de fichier manquant.") + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + # Note: the temporary files are stored in settings.PRIVATE_MEDIA_ROOT / 'cross_plans' + file_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'cross_plans', filename) + if not os.path.exists(file_path): + messages.error(request, "Les données du plan de croix n'existent plus.") + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + try: + with open(file_path, 'r', encoding='utf-8') as f: + deduced = json.load(f) + except Exception as e: + messages.error(request, f"Erreur de re-lecture des données du plan de croix : {str(e)}") + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + global_action = request.POST.get('global_action', 'apply') + + # Helper resolution functions + def resolve_pole_model(name_fr): + m = TrafficLightPoleModel.objects.filter(name_fr__iexact=name_fr).first() + if not m: + m = TrafficLightPoleModel.objects.first() + if not m: + m = TrafficLightPoleModel.objects.create( + code="default_pole", + name_fr="Poteau par défaut", + name_nl="Standaard paal" + ) + return m + + def resolve_lantern_model(name_fr): + m = TrafficLightLanternModel.objects.filter(name_fr__iexact=name_fr).first() + if not m: + m = TrafficLightLanternModel.objects.first() + if not m: + m = TrafficLightLanternModel.objects.create( + code="default_lantern", + name_fr="Lanterne par défaut", + name_nl="Standaard lantaarn", + voltage=230 + ) + return m + + def resolve_detector_model(name_fr): + m = TrafficLightDetectorModel.objects.filter(name_fr__iexact=name_fr).first() + if not m: + m = TrafficLightDetectorModel.objects.first() + if not m: + m = TrafficLightDetectorModel.objects.create( + code="default_detector", + name_fr="Détecteur par défaut", + name_nl="Standaard detector" + ) + return m + + def resolve_cable_model(name_fr): + m = TrafficLightCableModel.objects.filter(name_fr__iexact=name_fr).first() + if not m: + m = TrafficLightCableModel.objects.first() + if not m: + m = TrafficLightCableModel.objects.create( + code="default_cable", + name_fr="Câble par défaut", + name_nl="Standaard kabel", + number_of_strands=61 + ) + return m + + def update_pole_geometry(pole, code): + def clean_float(val): + if not val: + return None + try: + return float(str(val).replace(',', '.')) + except ValueError: + return None + + lon = clean_float(request.POST.get(f"lon_pole_{code}")) + lat = clean_float(request.POST.get(f"lat_pole_{code}")) + x_3812 = clean_float(request.POST.get(f"x_3812_pole_{code}")) + y_3812 = clean_float(request.POST.get(f"y_3812_pole_{code}")) + + if x_3812 is not None and y_3812 is not None and x_3812 != 0.0 and y_3812 != 0.0: + from django.contrib.gis.geos import Point as DjangoPoint + pole.geom = DjangoPoint(x_3812, y_3812, srid=3812) + pole.lon = lon + pole.lat = lat + elif lon is not None and lat is not None: + from django.contrib.gis.geos import Point as DjangoPoint + from pyproj import Transformer + transformer = Transformer.from_crs(4326, 3812, always_xy=True) + x_3812, y_3812 = transformer.transform(lon, lat) + pole.geom = DjangoPoint(x_3812, y_3812, srid=3812) + pole.lon = lon + pole.lat = lat + + def update_cable_geometry(cable, code): + geom_coords_str = request.POST.get(f"geom_cable_{code}") + if geom_coords_str: + try: + coords = json.loads(geom_coords_str) + if len(coords) >= 2: + from django.contrib.gis.geos import LineString as DjangoLineString + from pyproj import Transformer + transformer = Transformer.from_crs(4326, 3812, always_xy=True) + lambert_coords = [transformer.transform(pt[0], pt[1]) for pt in coords] + cable.geom = DjangoLineString(lambert_coords, srid=3812) + except Exception: + pass + + with transaction.atomic(): + # A. GLOBAL REPLACE ALL + if global_action == 'replace_all': + # Archive everything active + TrafficLightPole.objects.filter(intersection=intersection, status='active').update(status='archived', updated_by=request.user) + TrafficLightCable.objects.filter(intersection=intersection, status='active').update(status='archived', updated_by=request.user) + TrafficLightLantern.objects.filter(pole__intersection=intersection, status='active').update(status='archived', updated_by=request.user) + TrafficLightDetector.objects.filter(intersection=intersection, status='active').update(status='archived', updated_by=request.user) + + # Re-create all poles + created_poles = {} + for dp in deduced["poles"]: + db_model = resolve_pole_model(dp["model"]) + custom_name = request.POST.get(f"name_pole_{dp['code']}", dp.get('name', dp['short_code'])) + p = TrafficLightPole( + intersection=intersection, + code=dp["code"], + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + update_pole_geometry(p, dp["code"]) + p.save() + created_poles[dp["code"]] = p + + # Re-create all cables + for dc in deduced["cables"]: + db_model = resolve_cable_model(dc["model_name"]) + custom_name = request.POST.get(f"name_cable_{dc['code']}", dc["name"]) + c = TrafficLightCable( + intersection=intersection, + code=dc["code"], + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + update_cable_geometry(c, dc["code"]) + c.save() + + # Re-create all lanterns + for dl in deduced["lanterns"]: + db_model = resolve_lantern_model(dl["model_name"]) + pole = created_poles.get(dl["pole_code"]) + custom_name = request.POST.get(f"name_lantern_{dl['code']}", dl["name"]) + if pole: + TrafficLightLantern.objects.create( + pole=pole, + code=dl["code"], + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + + # Re-create all detectors + for dd in deduced["detectors"]: + db_model = resolve_detector_model(dd["model_name"]) + pole = created_poles.get(dd["pole_code"]) + custom_name = request.POST.get(f"name_detector_{dd['code']}", dd["name"]) + TrafficLightDetector.objects.create( + intersection=intersection, + pole=pole, + code=dd["code"], + name=custom_name, + model=db_model, + phases=dd["phases"], + status='active', + validation_status='validated', + updated_by=request.user + ) + + messages.success(request, "Tous les anciens assets ont été archivés et les nouveaux ont été créés sur base du plan de croix.") + + # B. SELECTIVE OR INDIVIDUAL INTEGRATION + else: + # We iterate over each asset category, checking the user choices from the form post parameters + + # --- 1. Poles integration --- + created_poles = {} + for dp in deduced["poles"]: + code = dp["code"] + is_selected = request.POST.get(f"selected_pole_{code}") == "on" + if is_selected: + action = request.POST.get(f"action_pole_{code}", "keep") + else: + action = "keep" + selected_model_id = request.POST.get(f"model_pole_{code}") + + db_model = None + if selected_model_id: + db_model = TrafficLightPoleModel.objects.filter(pk=selected_model_id).first() + if not db_model: + db_model = resolve_pole_model(dp["model"]) + + db_id = request.POST.get(f"db_id_pole_{code}") + existing = None + if db_id: + existing = TrafficLightPole.objects.filter(pk=db_id, status='active').first() + if not existing: + existing = TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').first() + + custom_name = request.POST.get(f"name_pole_{code}", dp.get('name', dp['short_code'])) + if action == "add" or (action == "replace" and not existing): + p = TrafficLightPole( + intersection=intersection, + code=code, + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + update_pole_geometry(p, code) + p.save() + created_poles[code] = p + elif action == "modify" and existing: + existing.model = db_model + existing.name_fr = custom_name + existing.name_nl = custom_name + existing.updated_by = request.user + update_pole_geometry(existing, code) + existing.save() + created_poles[code] = existing + elif action == "replace" and existing: + # Archive + existing.status = 'archived' + existing.updated_by = request.user + # Create new + new_pole = TrafficLightPole( + intersection=intersection, + code=code, + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + update_pole_geometry(new_pole, code) + new_pole.save() + existing.replaced_by = new_pole + existing.save() + created_poles[code] = new_pole + else: + # 'keep' + if existing: + existing.name_fr = custom_name + existing.name_nl = custom_name + update_pole_geometry(existing, code) + existing.save() + created_poles[code] = existing + + # Handle poles present in DB only (orphans) + for key in request.POST.keys(): + if key.startswith("action_pole_db_"): + code = key.replace("action_pole_db_", "") + is_selected = request.POST.get(f"selected_pole_db_{code}") == "on" + if is_selected: + action = request.POST.get(key) + else: + action = "keep" + if action == "archive": + TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').update( + status='archived', updated_by=request.user + ) + + # Fetch active poles to match parent relation for lanterns/detectors if not resolved above + active_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')} + for c, p in created_poles.items(): + active_poles[c] = p + + # --- 2. Cables integration --- + for dc in deduced["cables"]: + code = dc["code"] + is_selected = request.POST.get(f"selected_cable_{code}") == "on" + if is_selected: + action = request.POST.get(f"action_cable_{code}", "keep") + else: + action = "keep" + selected_model_id = request.POST.get(f"model_cable_{code}") + + db_model = None + if selected_model_id: + db_model = TrafficLightCableModel.objects.filter(pk=selected_model_id).first() + if not db_model: + db_model = resolve_cable_model(dc["model_name"]) + + db_id = request.POST.get(f"db_id_cable_{code}") + existing = None + if db_id: + existing = TrafficLightCable.objects.filter(pk=db_id, status='active').first() + if not existing: + existing = TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').first() + + custom_name = request.POST.get(f"name_cable_{code}", dc["name"]) + if action == "add": + c = TrafficLightCable( + intersection=intersection, + code=code, + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + update_cable_geometry(c, code) + c.save() + elif action == "modify" and existing: + existing.model = db_model + existing.name_fr = custom_name + existing.name_nl = custom_name + existing.updated_by = request.user + update_cable_geometry(existing, code) + existing.save() + elif action == "replace" and existing: + existing.status = 'archived' + existing.updated_by = request.user + new_cable = TrafficLightCable( + intersection=intersection, + code=code, + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + update_cable_geometry(new_cable, code) + new_cable.save() + existing.replaced_by = new_cable + existing.save() + else: + # 'keep' + if existing: + existing.name_fr = custom_name + existing.name_nl = custom_name + update_cable_geometry(existing, code) + existing.save() + + for key in request.POST.keys(): + if key.startswith("action_cable_db_"): + code = key.replace("action_cable_db_", "") + is_selected = request.POST.get(f"selected_cable_db_{code}") == "on" + if is_selected: + action = request.POST.get(key) + else: + action = "keep" + if action == "archive": + TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').update( + status='archived', updated_by=request.user + ) + + # --- 3. Lanterns integration --- + for dl in deduced["lanterns"]: + code = dl["code"] + is_selected = request.POST.get(f"selected_lantern_{code}") == "on" + if is_selected: + action = request.POST.get(f"action_lantern_{code}", "keep") + else: + action = "keep" + selected_model_id = request.POST.get(f"model_lantern_{code}") + + db_model = None + if selected_model_id: + db_model = TrafficLightLanternModel.objects.filter(pk=selected_model_id).first() + if not db_model: + db_model = resolve_lantern_model(dl["model_name"]) + + db_id = request.POST.get(f"db_id_lantern_{code}") + existing = None + if db_id: + existing = TrafficLightLantern.objects.filter(pk=db_id, status='active').first() + if not existing: + existing = TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').first() + pole = active_poles.get(dl["pole_code"]) + + custom_name = request.POST.get(f"name_lantern_{code}", dl["name"]) + if action == "add" and pole: + TrafficLightLantern.objects.create( + pole=pole, + code=code, + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + elif action == "modify" and existing: + existing.model = db_model + existing.name_fr = custom_name + existing.name_nl = custom_name + existing.updated_by = request.user + existing.save() + elif action == "replace" and existing and pole: + existing.status = 'archived' + existing.updated_by = request.user + new_lantern = TrafficLightLantern.objects.create( + pole=pole, + code=code, + name_fr=custom_name, + name_nl=custom_name, + model=db_model, + status='active', + validation_status='validated', + updated_by=request.user + ) + existing.replaced_by = new_lantern + existing.save() + else: + # 'keep' + if existing: + existing.name_fr = custom_name + existing.name_nl = custom_name + existing.save() + + for key in request.POST.keys(): + if key.startswith("action_lantern_db_"): + code = key.replace("action_lantern_db_", "") + is_selected = request.POST.get(f"selected_lantern_db_{code}") == "on" + if is_selected: + action = request.POST.get(key) + else: + action = "keep" + if action == "archive": + TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').update( + status='archived', updated_by=request.user + ) + + # --- 4. Detectors integration --- + for dd in deduced["detectors"]: + code = dd["code"] + is_selected = request.POST.get(f"selected_detector_{code}") == "on" + if is_selected: + action = request.POST.get(f"action_detector_{code}", "keep") + else: + action = "keep" + selected_model_id = request.POST.get(f"model_detector_{code}") + + db_model = None + if selected_model_id: + db_model = TrafficLightDetectorModel.objects.filter(pk=selected_model_id).first() + if not db_model: + db_model = resolve_detector_model(dd["model_name"]) + + db_id = request.POST.get(f"db_id_detector_{code}") + existing = None + if db_id: + existing = TrafficLightDetector.objects.filter(pk=db_id, status='active').first() + if not existing: + existing = TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').first() + pole = active_poles.get(dd["pole_code"]) + + custom_name = request.POST.get(f"name_detector_{code}", dd["name"]) + if action == "add": + TrafficLightDetector.objects.create( + intersection=intersection, + pole=pole, + code=code, + name=custom_name, + model=db_model, + phases=dd["phases"], + status='active', + validation_status='validated', + updated_by=request.user + ) + elif action == "modify" and existing: + existing.model = db_model + existing.name = custom_name + existing.updated_by = request.user + existing.save() + elif action == "replace" and existing: + existing.status = 'archived' + existing.updated_by = request.user + new_det = TrafficLightDetector.objects.create( + intersection=intersection, + pole=pole, + code=code, + name=custom_name, + model=db_model, + phases=dd["phases"], + status='active', + validation_status='validated', + updated_by=request.user + ) + existing.replaced_by = new_det + existing.save() + else: + # 'keep' + if existing: + existing.name = custom_name + existing.save() + + for key in request.POST.keys(): + if key.startswith("action_detector_db_"): + code = key.replace("action_detector_db_", "") + is_selected = request.POST.get(f"selected_detector_db_{code}") == "on" + if is_selected: + action = request.POST.get(key) + else: + action = "keep" + if action == "archive": + TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').update( + status='archived', updated_by=request.user + ) + + messages.success(request, "Les modifications d'assets ont été appliquées avec succès.") + + # Cleanup parsed file + if os.path.exists(file_path): + os.remove(file_path) + + return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection.id) + + +@check_thematic_access('trafficlights', edit_permission_required=True) +@require_POST +def upload_dxf_ajax(request, intersection_id): + intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id) + uploaded_file = request.FILES.get('dxf_file') + if not uploaded_file: + return JsonResponse({"success": False, "error": "Aucun fichier DXF fourni."}, status=400) + + uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files') + os.makedirs(uploaded_dxf_dir, exist_ok=True) + + import uuid + dxf_filename = f"{intersection.id}_{uuid.uuid4().hex}_{uploaded_file.name}" + file_path = os.path.join(uploaded_dxf_dir, dxf_filename) + + with open(file_path, 'wb+') as destination: + for chunk in uploaded_file.chunks(): + destination.write(chunk) + + # Extract snapping points + dxf_poles = [] + try: + dxf_poles = parse_dxf_poles(file_path) + except Exception: + pass + + return JsonResponse({ + "success": True, + "dxf_file": dxf_filename, + "dxf_poles": dxf_poles + }) + + +def ensure_intersection_document_folder(intersection): + from django.contrib.contenttypes.models import ContentType + from django.utils.text import slugify + from documents.models import DocumentFolder, DocumentFolderAttachment + from common.models import Thematic + + ct = ContentType.objects.get_for_model(intersection) + + # Check if a default folder already exists for this intersection + default_attachment = DocumentFolderAttachment.objects.filter( + content_type=ct, + object_id=intersection.id, + is_default=True + ).first() + if default_attachment: + return default_attachment.folder + + # Get parent folder (Traffic Lights under Assets) + assets_folder, _ = DocumentFolder.objects.get_or_create( + slug='assets', + defaults={'name': 'Assets', 'is_predefined': True} + ) + + # Get thematic + tl_thematic = Thematic.objects.filter(code='trafficlights').first() + + trafficlights_folder, _ = DocumentFolder.objects.get_or_create( + slug='trafficlights', + defaults={ + 'name': 'Traffic Lights', + 'is_predefined': True, + } + ) + if tl_thematic: + trafficlights_folder.thematics.add(tl_thematic) + trafficlights_folder.parent_folders.add(assets_folder) + + # Intersection folder name + folder_name = intersection.code or f"Carrefour {intersection.id}" + base_slug = slugify(folder_name) or f"tli-{intersection.id}" + folder_slug = f"tli-{base_slug}" + + # Get or create folder + folder, _ = DocumentFolder.objects.get_or_create( + slug=folder_slug, + defaults={ + "name": folder_name, + "application_label": "assets", + }, + ) + if tl_thematic: + folder.thematics.add(tl_thematic) + folder.parent_folders.add(trafficlights_folder) + + DocumentFolderAttachment.objects.get_or_create( + folder=folder, + content_type=ct, + object_id=intersection.id, + defaults={"is_default": True}, + ) + + return folder + + +def generate_dxf_overlay_managed_document(dxf_managed_doc, user=None, selected_layers=None): + import os + import time + + start_total_time = time.time() + logger.info("generate_dxf_overlay_managed_document: Starting overlay generation for ManagedDocument ID %s", dxf_managed_doc.id) + + from django.conf import settings + from django.core.files.base import ContentFile + from django.contrib.contenttypes.models import ContentType + from pyproj import Transformer + from documents.models import ManagedDocument, DocumentAttachment, DocumentVersion, DocumentTag + from assets.views.trafficlights import calculate_dxf_limits_fast, generate_dxf_image_overlay, ensure_intersection_document_folder + + # Find the intersection + from assets.models import TrafficLightIntersection + ct_tli = ContentType.objects.get_for_model(TrafficLightIntersection) + attachment = DocumentAttachment.objects.filter( + document=dxf_managed_doc, + content_type=ct_tli + ).first() + if not attachment: + logger.info("generate_dxf_overlay_managed_document: No DocumentAttachment found for ManagedDocument ID %s linking to TrafficLightIntersection", dxf_managed_doc.id) + return + intersection = attachment.content_object + logger.info("generate_dxf_overlay_managed_document: Found associated TrafficLightIntersection ID %s (Code: %s)", intersection.id, intersection.code) + + latest_version = dxf_managed_doc.latest_version + if not latest_version or not latest_version.file: + logger.info("generate_dxf_overlay_managed_document: ManagedDocument ID %s does not have a latest_version or version file", dxf_managed_doc.id) + return + + dxf_file_path = latest_version.file.path + logger.info("generate_dxf_overlay_managed_document: DXF file path to process: %s", dxf_file_path) + + # Calculate limits + t0 = time.time() + limits = calculate_dxf_limits_fast(dxf_file_path) + logger.info("generate_dxf_overlay_managed_document: calculate_dxf_limits_fast took %.4f seconds", time.time() - t0) + if not limits: + logger.info("generate_dxf_overlay_managed_document: calculate_dxf_limits_fast returned None for file %s", dxf_file_path) + return + logger.info("generate_dxf_overlay_managed_document: Calculated limits: %s", limits) + + # Generate overlay PNG file path + from assets.models import TrafficLightIntersectionDocument + legacy_doc = TrafficLightIntersectionDocument.objects.filter(managed_document=dxf_managed_doc).first() + if legacy_doc: + dxf_basename = os.path.basename(legacy_doc.file.name) + logger.info("generate_dxf_overlay_managed_document: Found legacy doc %s, using dxf_basename=%s", legacy_doc.id, dxf_basename) + else: + dxf_basename = os.path.basename(latest_version.file.name) + logger.info("generate_dxf_overlay_managed_document: No legacy doc found, using dxf_basename=%s", dxf_basename) + + # Get the intersection folder + folder = ensure_intersection_document_folder(intersection) + logger.info("generate_dxf_overlay_managed_document: Using intersection folder: %s", folder.name if folder else "None") + + from django.utils import timezone + folder_segment = folder.storage_segment + date_segment = timezone.now().strftime("%Y/%m") + + overlay_filename = dxf_basename.lower().replace('.dxf', '.overlay.png') + final_overlay_relative_path = f"document_versions/{folder_segment}/{date_segment}/{overlay_filename}" + final_overlay_absolute_path = os.path.join(settings.PRIVATE_MEDIA_ROOT, final_overlay_relative_path) + + os.makedirs(os.path.dirname(final_overlay_absolute_path), exist_ok=True) + + # Generate image overlay PNG file directly at its final private location + logger.info("generate_dxf_overlay_managed_document: Calling generate_dxf_image_overlay...") + t0 = time.time() + generate_dxf_image_overlay(dxf_file_path, final_overlay_absolute_path, limits=limits, selected_layers=selected_layers) + logger.info("generate_dxf_overlay_managed_document: generate_dxf_image_overlay took %.4f seconds", time.time() - t0) + + # Ensure public copy exists for map viewer + overlay_dir = os.path.join(settings.MEDIA_ROOT, 'dxf_layouts') + os.makedirs(overlay_dir, exist_ok=True) + public_overlay_path = os.path.join(overlay_dir, overlay_filename) + + import shutil + try: + shutil.copy2(final_overlay_absolute_path, public_overlay_path) + except Exception as e: + logger.error("generate_dxf_overlay_managed_document: Failed to copy overlay to public folder: %s", e) + + # Coordinates calculation + transformer = Transformer.from_crs(31370, 4326, always_xy=True) + min_x, min_y, max_x, max_y = limits + top_left = transformer.transform(min_x, max_y) + top_right = transformer.transform(max_x, max_y) + bottom_right = transformer.transform(max_x, min_y) + bottom_left = transformer.transform(min_x, min_y) + + coordinates = [ + [top_left[0], top_left[1]], + [top_right[0], top_right[1]], + [bottom_right[0], bottom_right[1]], + [bottom_left[0], bottom_left[1]] + ] + logger.info("generate_dxf_overlay_managed_document: Calculated georeference coordinates: %s", coordinates) + + # Check if we already have an overlay document for this DXF + overlay_doc = ManagedDocument.objects.filter( + unique_params__dxf_source_id=dxf_managed_doc.id, + is_archived=False + ).first() + + title = f"{os.path.splitext(dxf_basename)[0]} - [CAO] Overlay" + description = f"Image géolocalisée générée à partir du fichier DXF {dxf_basename}" + + other_tag = DocumentTag.objects.filter(slug='other').first() + plan_tag = DocumentTag.objects.filter(slug='plan').first() + if not plan_tag: + plan_tag = DocumentTag.objects.filter(name_fr__iexact='plan').first() + if not plan_tag: + plan_tag = DocumentTag.objects.create( + name_fr="Plan", + name_nl="Plan", + slug="plan", + show_in_asset_detail=True + ) + from common.models import Thematic + traffic_lights_thematic = Thematic.objects.filter(code='trafficlights').first() + if traffic_lights_thematic: + plan_tag.thematics.add(traffic_lights_thematic) + + unique_params = { + 'format': 'png', + 'dxf_source_id': dxf_managed_doc.id + } + + # Determine user + uploader = user or latest_version.uploaded_by + + if overlay_doc: + logger.info("generate_dxf_overlay_managed_document: Found existing overlay ManagedDocument ID %s. Updating...", overlay_doc.id) + # Create a new version of the existing overlay document + version = DocumentVersion( + document=overlay_doc, + uploaded_by=uploader, + description=description, + comment="Updated overlay image from DXF", + status=DocumentVersion.STATUS_APPROVED, + ) + version.file.name = final_overlay_relative_path + version.save() + logger.info("generate_dxf_overlay_managed_document: Created new DocumentVersion ID %s", version.id) + # Update coordinates on ManagedDocument + overlay_doc.georeference_coords = coordinates + overlay_doc.title = title + overlay_doc.description = description + overlay_doc.unique_params = unique_params + overlay_doc.save() + overlay_doc.tags.add(plan_tag) + if other_tag: + overlay_doc.tags.remove(other_tag) + logger.info("generate_dxf_overlay_managed_document: Updated ManagedDocument ID %s details successfully", overlay_doc.id) + else: + logger.info("generate_dxf_overlay_managed_document: Creating a new overlay ManagedDocument...") + # Create new ManagedDocument + overlay_doc = ManagedDocument.objects.create( + title=title, + description=description, + created_by=uploader, + unique_params=unique_params, + georeference_coords=coordinates, + georeference_page=1, + ) + logger.info("generate_dxf_overlay_managed_document: Created new ManagedDocument ID %s", overlay_doc.id) + overlay_doc.tags.add(plan_tag) + overlay_doc.folders.add(folder) + + # Attach version + version = DocumentVersion( + document=overlay_doc, + uploaded_by=uploader, + description=description, + comment="Initial overlay image from DXF", + status=DocumentVersion.STATUS_APPROVED, + ) + version.file.name = final_overlay_relative_path + version.save() + logger.info("generate_dxf_overlay_managed_document: Created first DocumentVersion ID %s", version.id) + + # Attach to the intersection + ct = ContentType.objects.get_for_model(intersection) + DocumentAttachment.objects.get_or_create( + document=overlay_doc, + content_type=ct, + object_id=intersection.id, + defaults={"attached_by": uploader}, + ) + logger.info("generate_dxf_overlay_managed_document: Attached overlay ManagedDocument ID %s to intersection ID %s", overlay_doc.id, intersection.id) + + # Ensure source DXF document also receives the plan tag and removes other tag + if plan_tag: + dxf_managed_doc.tags.add(plan_tag) + if other_tag: + dxf_managed_doc.tags.remove(other_tag) + logger.info("generate_dxf_overlay_managed_document: Added Plan tag and removed Other tag on source DXF ManagedDocument ID %s", dxf_managed_doc.id) + + logger.info("generate_dxf_overlay_managed_document: Total execution time: %.4f seconds", time.time() - start_total_time) \ No newline at end of file diff --git a/streetup/assets/views/water.py b/streetup/assets/views/water.py new file mode 100644 index 0000000..887285a --- /dev/null +++ b/streetup/assets/views/water.py @@ -0,0 +1,703 @@ +import json +from django.shortcuts import render, get_object_or_404, redirect +from django.http import JsonResponse, Http404 +from django.contrib.gis.geos import GEOSGeometry, Point +from django.contrib.gis.db.models.functions import Distance +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import get_language, gettext as _ +from django.db.models import F, Q, Value, CharField +from django.db.models.functions import Lower +from django.views.decorators.http import require_GET, require_POST +from django.urls import reverse + +from common.utils import check_thematic_access, strip_accents_lower, Unaccent +from common.models import UserThematics, UserConfig +from assets.models import AssetCategory, NatureRWIZ, NatureRWIAsset +from interventions.models import InterventionLocation +from interventions.views.drafts import InterventionDraftManager +from documents.services import DocumentManagementService +from documents.permissions import user_can_browse_folder + + +@check_thematic_access('water') +def water_index(request): + thematic = 'water' + lang = get_language() + + # Get filters from URL + statuses = request.GET.getlist('status') + selected_categories = request.GET.getlist('category') + selected_models = request.GET.getlist('model') + selected_location_type = request.GET.get('location_type', '') + + # Get preventive draft ID (if any) for selecting assets + preventive_draft_id = request.GET.get('preventive_draft_id') + preventive_element_type = '' + selected_assets = [] + selected_locations_ids = [] + + if preventive_draft_id: + drafts = InterventionDraftManager(request.session) + draft_data = drafts.get(preventive_draft_id) + form_data = draft_data.get('form_data', {}) if draft_data else {} + preventive_element_type = form_data.get('type_of_selected_elements', '') + if preventive_element_type == 'assets': + selected_assets = draft_data.get('assets', []) or [] + else: + selected_locations_ids = draft_data.get('locations', []) or [] + + thematic_choices_qs = ( + UserThematics.objects + .filter(user_config=request.user.config, can_view_assets=True) + .select_related('thematic') + .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') + ) + thematics = [tc.thematic for tc in thematic_choices_qs] + + selected_thematic = { + "code": thematic, + "name": next((t.name for t in thematics if t.code == thematic), None), + "icon": next((t.icon for t in thematics if t.code == thematic), None), + } + + categories = list(AssetCategory.objects.filter(thematic__code='water')) + categories.sort(key=lambda m: m.get_name().lower()) + category_choices = [(m.id, m.get_name()) for m in categories] + + # NatureRWIAsset does not use models, only structure_type + model_choices = [] + category_model_map = {} + + context = { + 'thematics': thematics, + 'category_choices': category_choices, + 'model_choices': model_choices, + 'category_model_map': category_model_map, + 'selected_thematic': selected_thematic, + 'selected_statuses': statuses, + 'selected_categories': selected_categories, + 'selected_models': selected_models, + 'selected_location_type': selected_location_type, + 'preventive_draft_id': preventive_draft_id, + 'preventive_element_type': preventive_element_type, + 'selected_locations_ids': selected_locations_ids, + "selected_assets_json": json.dumps(selected_assets), + 'locations_tab_title': _("Zones GIEP"), + 'locations_assets_title': _("Ouvrages"), + } + + return render(request, "assets/water_index.html", context) + + +@check_thematic_access('water') +def water_locations_geojson(request): + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + filter_geom.transform(3812) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + filter_geom = None + + rwiz_locations = NatureRWIZ.objects.all() + + if filter_geom: + rwiz_locations = rwiz_locations.filter(geom__intersects=filter_geom) + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + rwiz_locations = apply_search(rwiz_locations, data) + + from assets.views.core import get_element_type_label + def obj_to_feature(obj): + geometry = json.loads(obj.geojson) if obj.geojson else None + return { + "type": "Feature", + "id": obj.id, + "geometry": geometry, + "properties": { + "code": obj.code, + "name": obj.get_name(), + "location_type": obj.__class__.__name__, + "lon": obj.lon, + "lat": obj.lat, + "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, + "element_type": get_element_type_label(obj.__class__.__name__.lower()), + "thematic": "water", + "is_location": True, + } + } + + features = [obj_to_feature(rwiz) for rwiz in rwiz_locations] + + return JsonResponse({ + "type": "FeatureCollection", + "features": features, + }, safe=False) + + +MAX_ASSET_FEATURES = 5000 + +@check_thematic_access('water') +def water_assets_geojson(request): + lang = get_language() if get_language() in ('fr', 'nl') else 'fr' + + if request.method == "POST": + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + + sections_ids = data.get('locations_ids', []) + statuses = data.get('asset_status', []) + categories = data.get('category') or [] + if isinstance(categories, str): + categories = [categories] + + # Récupération et parsing de la géométrie + filter_geom_raw = data.get("filter_geom") + if filter_geom_raw: + try: + filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) + except Exception as e: + return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) + else: + filter_geom = None + else: + section_id = request.GET.get('location') + sections_ids = [section_id] if section_id else [] + statuses = request.GET.getlist('asset_status') + categories = request.GET.getlist('category', []) + filter_geom = None + + _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' + if not request.GET.get('unified') and not sections_ids and not categories and not filter_geom and not _search_q: + return JsonResponse({ + "type": "FeatureCollection", + "features": [], + }) + + naturerwiasset_qs = NatureRWIAsset.objects.all() + + if sections_ids: + # Filter by intersecting NatureRWIZ geometries + rwiz_geoms = NatureRWIZ.objects.filter(id__in=sections_ids, geom__isnull=False).values_list('geom', flat=True) + if rwiz_geoms: + query = Q() + for g in rwiz_geoms: + query |= Q(geom__intersects=g) + naturerwiasset_qs = naturerwiasset_qs.filter(query) + else: + naturerwiasset_qs = naturerwiasset_qs.none() + + if categories: + naturerwiasset_qs = naturerwiasset_qs.filter(category__id__in=categories) + + if filter_geom: + filter_geom_3812 = filter_geom.transform(3812, clone=True) + naturerwiasset_qs = naturerwiasset_qs.filter(geom__intersects=filter_geom_3812) + + if statuses: + naturerwiasset_qs = naturerwiasset_qs.filter(status__in=statuses) + else: + naturerwiasset_qs = naturerwiasset_qs.exclude(status='removed') + + # Recherche plein texte + if request.method == 'POST': + from assets.views.core import apply_search + naturerwiasset_qs = apply_search(naturerwiasset_qs, data) + + if naturerwiasset_qs.count() > MAX_ASSET_FEATURES: + return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) + + from django.db.models import FloatField, Value, CharField + from assets.views.core import get_element_type_label + + naturerwiasset_qs = (naturerwiasset_qs.annotate( + name=F(f'name_{lang}'), + category_name=F(f'category__name_{lang}'), + model_name=Value(None, output_field=CharField()), + color=Value(None, output_field=CharField()), + location_lon=Value(None, output_field=FloatField()), + location_lat=Value(None, output_field=FloatField()), + ) + .values( + 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'surface_area', + 'category_name', 'model_name', 'color', 'geojson', + 'category_id', 'location_lon', 'location_lat', 'structure_type' + ) + .order_by('code') + )[:MAX_ASSET_FEATURES] + + def build_geometry_and_coords(obj): + geometry = None + if obj.get('geojson'): + try: + geometry = json.loads(obj['geojson']) + except (TypeError, ValueError, json.JSONDecodeError): + geometry = None + + lon = obj.get('lon') + lat = obj.get('lat') + + if geometry is None and lon is not None and lat is not None: + geometry = { + "type": "Point", + "coordinates": [lon, lat], + } + return geometry, lon, lat + + def obj_to_feature(obj, asset_type): + geometry, lon, lat = build_geometry_and_coords(obj) + detail_url = reverse('assets:water_assets_detail', kwargs={'asset_model': asset_type, 'asset_id': obj['id']}) + return { + "type": "Feature", + "id": obj['id'], + "geometry": geometry, + "properties": { + "code": obj['code'], + "name": obj['name'], + "category": obj['category_name'], + "model": obj['model_name'], + "category_id": obj.get('category_id'), + "model_id": obj.get('model_id'), + "status": obj['status'], + "validation_status": obj.get('validation_status'), + "lon": lon, + "lat": lat, + "asset_type": asset_type, + "area": obj.get('surface_area', ''), + "color": '#ccc', + "detail_url": detail_url, + "element_type": get_element_type_label(asset_type), + "thematic": "water", + "is_location": False, + "structure_type": obj.get('structure_type', ''), + } + } + + features = [obj_to_feature(obj, "naturerwiasset") for obj in naturerwiasset_qs] + + return JsonResponse({ + "type": "FeatureCollection", + "features": features, + }, safe=False) + + +@require_GET +def water_assets_autocomplete(request): + raw_query = (request.GET.get('q') or '').strip() + qn = strip_accents_lower(raw_query) + has_search_term = len(qn) > 0 + + location_id = request.GET.get('location_id') + category_id = request.GET.get('asset_category') + try: + category_id = int(category_id) + except (TypeError, ValueError): + category_id = None + + qs = NatureRWIAsset.objects.exclude(status='removed').annotate( + code_u=Lower(Unaccent('code')), + name_fr_u=Lower(Unaccent('name_fr')), + name_nl_u=Lower(Unaccent('name_nl')), + ) + + if has_search_term: + qs = qs.filter( + Q(code_u__contains=qn) | + Q(name_fr_u__contains=qn) | + Q(name_nl_u__contains=qn) + ) + + if category_id is not None: + qs = qs.filter(category_id=category_id) + + if location_id: + try: + loc = NatureRWIZ.objects.get(id=location_id) + if loc.geom: + qs = qs.filter(geom__intersects=loc.geom) + except NatureRWIZ.DoesNotExist: + pass + + items = [] + for a in qs.select_related('category')[:100]: + code_u = getattr(a, 'code_u', '') or '' + name_fr_u = getattr(a, 'name_fr_u', '') or '' + name_nl_u = getattr(a, 'name_nl_u', '') or '' + + prio = 7 + if has_search_term: + if code_u == qn: + prio = 0 + elif code_u.startswith(qn): + prio = 1 + elif name_fr_u.startswith(qn) or name_nl_u.startswith(qn): + prio = 2 + elif qn in code_u or qn in name_fr_u or qn in name_nl_u: + prio = 4 + else: + prio = 5 + + equipment_type = a.structure_type + parts = [a.code, a.get_name()] + equipment_label = " - ".join(p for p in parts if p) + + equipment_ct_id = ContentType.objects.get_for_model(a).id + + items.append({ + "prio": prio, + "code": a.code or "", + "id": a.id, + "label": equipment_label, + "equipment_id": a.id, + "equipment_label": equipment_label, + "equipment_type": equipment_type, + "equipment_ct": equipment_ct_id, + "location_id": None, + "location_label": None, + "location_ct": None, + "lon": a.lon, + "lat": a.lat, + }) + + items.sort(key=lambda x: (x["prio"], x["code"])) + return JsonResponse(items[:100], safe=False) + + +@check_thematic_access('water') +def zirw_detail(request, location_id): + location = get_object_or_404(NatureRWIZ, id=location_id) + content_type = ContentType.objects.get_for_model(location) + + show_inactive_assets = (request.GET.get('include_inactive') or '').lower() in ('1', 'true', 'yes', 'on') + + toggle_params = request.GET.copy() + if show_inactive_assets: + toggle_params.pop('include_inactive', None) + else: + toggle_params['include_inactive'] = '1' + toggle_query = toggle_params.urlencode() + toggle_inactive_url = f"{request.path}?{toggle_query}" if toggle_query else request.path + + if show_inactive_assets: + all_assets = location.get_all_assets().exclude(status='removed').order_by('code') + else: + all_assets = location.get_active_assets().order_by('code') + + all_assets = list(all_assets) + + # Build spatial hierarchy + polygons = [a for a in all_assets if a.geometry_type == 'polygon'] + points_and_lines = [a for a in all_assets if a.geometry_type != 'polygon'] + + polygon_nodes = [] + child_asset_ids = set() + + for poly in polygons: + poly_children = [] + for pl in points_and_lines: + if pl.geom and poly.geom and poly.geom.intersects(pl.geom): + poly_children.append({ + 'asset': pl, + 'children': [], + }) + child_asset_ids.add(pl.id) + polygon_nodes.append({ + 'asset': poly, + 'children': poly_children, + }) + + top_level_nodes = [] + top_level_nodes.extend(polygon_nodes) + + for pl in points_and_lines: + if pl.id not in child_asset_ids: + top_level_nodes.append({ + 'asset': pl, + 'children': [], + }) + + top_level_nodes.sort(key=lambda n: n['asset'].code) + + # GeoJSON serialization + from assets.views.core import get_element_type_label + + def asset_to_feature(asset): + geom_4326 = asset.geom.transform(4326, clone=True) if asset.geom else None + geometry = json.loads(geom_4326.geojson) if geom_4326 else None + if geometry is None and asset.lon is not None and asset.lat is not None: + geometry = { + "type": "Point", + "coordinates": [asset.lon, asset.lat], + } + detail_url = reverse('assets:water_assets_detail', kwargs={'asset_model': 'naturerwiasset', 'asset_id': asset.id}) + return { + "type": "Feature", + "id": asset.id, + "geometry": geometry, + "properties": { + "code": asset.code, + "name": asset.get_name(), + "status": asset.status, + "lon": asset.lon, + "lat": asset.lat, + "asset_type": "naturerwiasset", + "detail_url": detail_url, + "element_type": get_element_type_label("naturerwiasset"), + "thematic": "water", + "is_location": False, + "structure_type": asset.structure_type or '', + "structure_type_label": asset.get_structure_type_display() if asset.structure_type else '', + } + } + + geojson_features = [asset_to_feature(asset) for asset in all_assets] + assets_geojson = { + "type": "FeatureCollection", + "features": geojson_features + } + + from assets.models import NatureRWIAssetDocument + asset_ids = [a.id for a in all_assets] + documents = NatureRWIAssetDocument.objects.filter(rwi_asset_id__in=asset_ids).select_related('rwi_asset').order_by('-upload_date') + + # GIEP Zone documents + doc_data = DocumentManagementService.get_documents_for_asset(location) + main_documents = doc_data.get("main_documents", []) + document_folder = doc_data.get("folder") + + use_managed_documents = document_folder is not None + + user_config = getattr(request.user, "config", None) + can_add_unique_tag_documents = False + if user_config and user_config.is_intern: + can_add_unique_tag_documents = True + elif user_config: + can_add_unique_tag_documents = user_config.can_add_unique_tag_documents + + can_browse_folder = False + if document_folder: + can_browse_folder = user_can_browse_folder(request.user, document_folder) + + interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=content_type, object_id=location.id).select_related('intervention').order_by('-intervention__creation_time')] + + context = { + 'location': location, + 'interventions': interventions, + 'top_level_nodes': top_level_nodes, + 'assets_count': len(all_assets), + 'assets_geojson': assets_geojson, + 'show_inactive_assets': show_inactive_assets, + 'toggle_inactive_url': toggle_inactive_url, + 'documents': documents, + 'main_documents': main_documents, + 'document_folder': document_folder, + 'use_managed_documents': use_managed_documents, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'can_browse_folder': can_browse_folder, + } + + return render(request, "assets/naturerwiz_detail.html", context) + + +@check_thematic_access('water') +def water_assets_detail(request, asset_model, asset_id): + try: + content_type = ContentType.objects.get(model=asset_model) + model_class = content_type.model_class() + except ContentType.DoesNotExist: + raise Http404(_("Modèle %(asset_model)s inconnu.") % {'asset_model': asset_model}) + + asset = get_object_or_404(model_class, id=asset_id) + + template_name = f"assets/{asset_model}_detail.html" + + from django.template.loader import select_template + template = select_template([template_name, "assets/detail_generic.html"]) + + from assets.views.core import get_asset_edit_context + context = { + "asset": asset, + "content_type": content_type, + } + context.update(get_asset_edit_context(request, asset, content_type)) + context['thematic_code'] = 'water' + + return render(request, template.template.name, context) + + +def get_nearest_water_location(request): + try: + lon = float(request.GET.get('lon')) + lat = float(request.GET.get('lat')) + except (TypeError, ValueError): + return JsonResponse({"error": "Invalid coordinates"}, status=400) + + user_point = Point(lon, lat, srid=4326) + user_point.transform(3812) + + nearest = ( + NatureRWIZ.objects + .filter(geom__isnull=False) + .annotate(distance=Distance('geom', user_point)) + .filter(distance__lte=50) + .order_by('distance') + .first() + ) + + if nearest: + location_ct_id = ContentType.objects.get_for_model(nearest).id + return JsonResponse({ + "location_id": nearest.id, + "location_ct": location_ct_id, + "location_label": f"{nearest.code} - {nearest.get_name()}", + "lon": nearest.lon, + "lat": nearest.lat, + }) + else: + return JsonResponse({"error": _("Aucune localisation GIEP trouvée à proximité")}) + + +@check_thematic_access('water', edit_permission_required=True) +@require_POST +def zirw_add_document(request, location_id): + location = get_object_or_404(NatureRWIZ, pk=location_id) + count = int(request.POST.get('file_count', 0)) + files = request.FILES.getlist('files') + + user = request.user + document_folder = DocumentManagementService.get_default_folder_for_object(location) + + if document_folder is not None: + for i in range(count): + file = files[i] + tag_slug = request.POST.get(f'tag_{i}', None) + folder_id = request.POST.get(f'folder_{i}', None) + + if not tag_slug: + continue + + from documents.models import DocumentTag + try: + tag = DocumentTag.objects.get(slug=tag_slug) + except DocumentTag.DoesNotExist: + continue + + if tag.is_unique: + can_add = DocumentManagementService.can_user_add_unique_tag_document(user, tag) + if not can_add: + continue + + if tag.is_unique: + target_folder = document_folder + else: + from documents.models import DocumentFolder + if folder_id: + target_folder = DocumentFolder.objects.filter(id=folder_id).first() or document_folder + else: + target_folder = document_folder + + DocumentManagementService.add_document( + file=file, + tag=tag, + folder=target_folder, + user=user, + content_object=location + ) + + return redirect('assets:zirw_detail', location_id=location_id) + + +@check_thematic_access('water') +def fetch_document_history(request): + from django.template.loader import render_to_string + location_id = request.GET.get('location_id') + tag_slug = request.GET.get('tag_slug') + + user = request.user + user_config = getattr(user, "config", None) + + is_delete_document_allowed = False + if user_config: + if user_config.roles.filter(name='admin').exists(): + is_delete_document_allowed = True + elif UserThematics.objects.filter( + user_config=user_config, + thematic__code='water', + can_edit_assets=True + ).exists(): + is_delete_document_allowed = True + + location = NatureRWIZ.objects.filter(pk=location_id).first() + document_folder = DocumentManagementService.get_default_folder_for_object(location) if location else None + + if document_folder is not None and tag_slug: + versions = DocumentManagementService.get_document_history_by_tag(location, tag_slug) + html = render_to_string('assets/document_version_history_list.html', { + 'versions': versions, + 'is_delete_document_allowed': is_delete_document_allowed, + 'use_managed_documents': True + }) + else: + html = '' + + return JsonResponse({'html': html}) + + +@check_thematic_access('water') +@require_GET +def get_document_config(request, location_id): + location = get_object_or_404(NatureRWIZ, pk=location_id) + + user = request.user + user_config = getattr(user, "config", None) + + can_add_unique_tag_documents = False + if user_config and user_config.is_intern: + can_add_unique_tag_documents = True + elif user_config: + can_add_unique_tag_documents = user_config.can_add_unique_tag_documents + + document_folder = DocumentManagementService.get_default_folder_for_object(location) + + if document_folder is None: + return JsonResponse({ + 'use_managed_documents': False, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'tags': [], + 'folders': [], + }) + + tags = DocumentManagementService.get_available_tags_for_thematic('water') + tags_data = [ + { + 'slug': tag.slug, + 'name': tag.name, + 'is_unique': tag.is_unique, + 'show_in_asset_detail': tag.show_in_asset_detail, + } + for tag in tags + ] + + folders = DocumentManagementService.get_subfolders_for_object(location) + + return JsonResponse({ + 'use_managed_documents': True, + 'can_add_unique_tag_documents': can_add_unique_tag_documents, + 'tags': tags_data, + 'folders': folders, + }) diff --git a/streetup/authorizations/__init__.py b/streetup/authorizations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/authorizations/admin.py b/streetup/authorizations/admin.py new file mode 100644 index 0000000..a6755ae --- /dev/null +++ b/streetup/authorizations/admin.py @@ -0,0 +1,46 @@ +from django.contrib import admin +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + +from .models import RoadAuthorization, RoadAuthorizationDocument, RoadAuthorizationDate, VerifiedEmail, EmailVerificationToken + + +class RoadAuthorizationDocumentInline(admin.TabularInline): + model = RoadAuthorizationDocument + extra = 0 + + +class RoadAuthorizationDateInline(admin.TabularInline): + model = RoadAuthorizationDate + extra = 0 + + +@admin.register(RoadAuthorization) +class RoadAuthorizationAdmin(admin.ModelAdmin): + list_display = ('code', 'get_type_display', 'status', 'applicant_name', 'applicant_email', 'is_email_verified', 'creation_date') + list_filter = ('status', 'type', 'is_email_verified') + search_fields = ('code', 'description', 'applicant_name', 'applicant_email', 'location') + readonly_fields = ('creation_date', 'update_date', 'is_email_verified') + inlines = [RoadAuthorizationDateInline, RoadAuthorizationDocumentInline] + + def get_type_display(self, obj): + return obj.get_type_display() + get_type_display.short_description = _("Type") + + +@admin.register(VerifiedEmail) +class VerifiedEmailAdmin(admin.ModelAdmin): + list_display = ('email', 'verified_at') + search_fields = ('email',) + readonly_fields = ('verified_at',) + + +@admin.register(EmailVerificationToken) +class EmailVerificationTokenAdmin(admin.ModelAdmin): + list_display = ('authorization', 'code', 'attempts', 'created_at', 'expires_at', 'is_expired_display') + readonly_fields = ('code', 'attempts', 'created_at', 'expires_at', 'authorization') + + def is_expired_display(self, obj): + return obj.is_expired() + is_expired_display.boolean = True + is_expired_display.short_description = _("Expiré") diff --git a/streetup/authorizations/apps.py b/streetup/authorizations/apps.py new file mode 100644 index 0000000..2f0eb27 --- /dev/null +++ b/streetup/authorizations/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthorizationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'authorizations' diff --git a/streetup/authorizations/forms.py b/streetup/authorizations/forms.py new file mode 100644 index 0000000..dd1d041 --- /dev/null +++ b/streetup/authorizations/forms.py @@ -0,0 +1,141 @@ +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from django import forms +from django.utils.translation import gettext_lazy as _ + +from .models import RoadAuthorization, RoadAuthorizationDocument, RoadAuthorizationDate + +# Default initial datetime for occupation dates: next month +_DEFAULT_DATE = lambda: (datetime.today() + relativedelta(months=1)).strftime('%Y-%m-%d') + + +class AuthorizationForm(forms.ModelForm): + """Main authorization request form – no crispy forms dependency.""" + + class Meta: + model = RoadAuthorization + fields = ( + 'language', + 'type', + 'description', + 'location', + 'applicant_name', + 'applicant_email', + 'applicant_address', + 'applicant_enterprise', + ) + widgets = { + 'type': forms.RadioSelect(attrs={'class': 'btn-check', 'autocomplete': 'off'}), + 'language': forms.RadioSelect(attrs={'class': 'btn-check', 'autocomplete': 'off'}), + } + + # Hidden field that receives the GeoJSON polygon drawn on the MapLibre map. + # The view converts it to a MultiPolygonField before saving. + geom_geojson = forms.CharField( + widget=forms.HiddenInput(), + required=True, + label='', + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Remove the blank "-------" choice Django injects when there is no default value + self.fields['type'].choices = [ + (k, v) for k, v in self.fields['type'].choices if k != '' + ] + self.fields['language'].choices = [ + (k, v) for k, v in self.fields['language'].choices if k != '' + ] + self.fields['description'].widget.attrs.update({'class': 'form-control', 'rows': 5}) + self.fields['location'].widget.attrs.update({'class': 'form-control'}) + self.fields['applicant_name'].widget.attrs.update({'class': 'form-control'}) + self.fields['applicant_email'].widget.attrs.update({'class': 'form-control'}) + self.fields['applicant_address'].widget.attrs.update({'class': 'form-control'}) + self.fields['applicant_enterprise'].widget.attrs.update({'class': 'form-control'}) + + def clean_geom_geojson(self): + """Validate that the submitted GeoJSON is a valid polygon.""" + import json + value = self.cleaned_data.get('geom_geojson', '').strip() + if not value: + raise forms.ValidationError(_("Veuillez dessiner la zone concernée sur la carte.")) + try: + geom = json.loads(value) + except (ValueError, TypeError): + raise forms.ValidationError(_("Géométrie invalide.")) + if geom.get('type') not in ('Polygon', 'MultiPolygon'): + raise forms.ValidationError(_("Veuillez dessiner un polygone.")) + return value + + +class AuthorizationDocumentForm(forms.ModelForm): + class Meta: + model = RoadAuthorizationDocument + fields = ('description', 'file') + widgets = { + 'description': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Description du document')}), + 'file': forms.ClearableFileInput(attrs={'class': 'form-control'}), + } + + +AuthorizationDocumentFormSet = forms.inlineformset_factory( + RoadAuthorization, + RoadAuthorizationDocument, + form=AuthorizationDocumentForm, + extra=1, + can_delete=False, +) + + +class AuthorizationDateForm(forms.ModelForm): + class Meta: + model = RoadAuthorizationDate + fields = ('begin_date', 'end_date') + widgets = { + 'begin_date': forms.DateTimeInput( + attrs={ + 'type': 'datetime-local', + 'class': 'form-control', + } + ), + 'end_date': forms.DateTimeInput( + attrs={ + 'type': 'datetime-local', + 'class': 'form-control', + } + ), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['end_date'].required = False + + +AuthorizationDateFormSet = forms.inlineformset_factory( + RoadAuthorization, + RoadAuthorizationDate, + form=AuthorizationDateForm, + extra=1, + can_delete=False, +) + + +class AuthorizationFilterForm(forms.Form): + """Filter form used on the staff list view.""" + include_archives = forms.BooleanField( + label=_("Inclure les archives (> 2 ans)"), + required=False, + widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}), + ) + authorization_type = forms.MultipleChoiceField( + label=_("Type d'autorisation"), + choices=RoadAuthorization.AUTH_TYPE, + widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}), + required=False, + ) + search_text = forms.CharField( + max_length=250, + required=False, + widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Recherche…')}), + ) diff --git a/streetup/authorizations/locale/en/LC_MESSAGES/django.mo b/streetup/authorizations/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..da8d832 Binary files /dev/null and b/streetup/authorizations/locale/en/LC_MESSAGES/django.mo differ diff --git a/streetup/authorizations/locale/en/LC_MESSAGES/django.po b/streetup/authorizations/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..dae9b8e --- /dev/null +++ b/streetup/authorizations/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,726 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-06 10:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authorizations/admin.py:28 +#: authorizations/templates/authorizations/authorization_detail.html:73 +#: authorizations/templates/authorizations/authorization_list.html:73 +#: authorizations/templates/authorizations/authorization_list.html:139 +msgid "Type" +msgstr "Type" + +#: authorizations/admin.py:46 +msgid "Expiré" +msgstr "Expired" + +#: authorizations/forms.py:62 +msgid "Veuillez dessiner la zone concernée sur la carte." +msgstr "Please draw the relevant area on the map." + +#: authorizations/forms.py:66 +msgid "Géométrie invalide." +msgstr "Invalid geometry." + +#: authorizations/forms.py:68 +msgid "Veuillez dessiner un polygone." +msgstr "Please draw a polygon." + +#: authorizations/forms.py:77 +#: authorizations/templates/authorizations/authorization_detail.html:175 +msgid "Description du document" +msgstr "Document description" + +#: authorizations/forms.py:127 +msgid "Inclure les archives (> 2 ans)" +msgstr "Include archives (> 2 years)" + +#: authorizations/forms.py:132 +msgid "Type d'autorisation" +msgstr "Authorisation type" + +#: authorizations/forms.py:140 +msgid "Recherche…" +msgstr "Search…" + +#: authorizations/models.py:17 +msgid "Affichage (panneau, ...)" +msgstr "Display (panel, ...)" + +#: authorizations/models.py:18 +msgid "Terrasse" +msgstr "Terrace" + +#: authorizations/models.py:19 +msgid "Mobilier urbain" +msgstr "Street furniture" + +#: authorizations/models.py:20 +msgid "Œuvre d'art" +msgstr "Artwork" + +#: authorizations/models.py:21 +msgid "Nouvel Emplacement PMR" +msgstr "New PRM parking space" + +#: authorizations/models.py:22 +msgid "Nouvelle Zone de livraison" +msgstr "New delivery zone" + +#: authorizations/models.py:23 +msgid "Tournage audiovisuel" +msgstr "Audiovisual filming" + +#: authorizations/models.py:24 +msgid "Evénement" +msgstr "Event" + +#: authorizations/models.py:25 +msgid "Plante grimpante" +msgstr "Climbing plant" + +#: authorizations/models.py:26 +msgid "Pied d'arbre" +msgstr "Tree base" + +#: authorizations/models.py:30 +msgid "À traiter" +msgstr "To be processed" + +#: authorizations/models.py:31 +msgid "En cours" +msgstr "In progress" + +#: authorizations/models.py:32 +msgid "Autorisé" +msgstr "Authorised" + +#: authorizations/models.py:33 +msgid "Refusé" +msgstr "Refused" + +#: authorizations/models.py:34 +msgid "Clôturé" +msgstr "Closed" + +#: authorizations/models.py:45 +msgid "Langue" +msgstr "Language" + +#: authorizations/models.py:49 authorizations/models.py:201 +msgid "Email vérifié" +msgstr "Email verified" + +#: authorizations/models.py:50 +msgid "Indique si l'adresse email du demandeur a été vérifiée." +msgstr "Indicates whether the applicant's email address has been verified." + +#: authorizations/models.py:52 +msgid "Type d'occupation" +msgstr "Occupation type" + +#: authorizations/models.py:53 +#: authorizations/templates/authorizations/authorization_detail.html:92 +msgid "Description de la demande" +msgstr "Request description" + +#: authorizations/models.py:54 +#: authorizations/templates/authorizations/authorization_detail.html:105 +msgid "Nom" +msgstr "Name" + +#: authorizations/models.py:55 authorizations/models.py:197 +#: authorizations/templates/authorizations/authorization_detail.html:108 +msgid "Email" +msgstr "Email" + +#: authorizations/models.py:56 +#: authorizations/templates/authorizations/authorization_detail.html:113 +msgid "Adresse" +msgstr "Address" + +#: authorizations/models.py:57 +#: authorizations/templates/authorizations/authorization_detail.html:117 +msgid "Entreprise" +msgstr "Company" + +#: authorizations/models.py:64 +msgid "Agent traitant" +msgstr "Handling agent" + +#: authorizations/models.py:66 +msgid "Commentaire" +msgstr "Comment" + +#: authorizations/models.py:67 +msgid "Localisation – Adresse" +msgstr "Location – Address" + +#: authorizations/models.py:68 +#: authorizations/templates/authorizations/authorization_detail.html:95 +#: authorizations/templates/authorizations/authorization_detail.html:228 +#: authorizations/templates/authorizations/authorization_list.html:142 +msgid "Localisation" +msgstr "Location" + +#: authorizations/models.py:107 +msgid "Autorisation de voirie" +msgstr "Road authorisation" + +#: authorizations/models.py:108 +msgid "Autorisations de voirie" +msgstr "Road authorisations" + +#: authorizations/models.py:146 +#, python-format +msgid "La taille maximale du fichier est de %(limit)s Mo." +msgstr "The maximum file size is %(limit)s MB." + +#: authorizations/models.py:151 +#: authorizations/templates/authorizations/authorization_detail.html:179 +msgid "Fichier" +msgstr "File" + +#: authorizations/models.py:155 +msgid "Description" +msgstr "Description" + +#: authorizations/models.py:163 +msgid "Document" +msgstr "Document" + +#: authorizations/models.py:164 +msgid "Documents" +msgstr "Documents" + +#: authorizations/models.py:176 +msgid "Début de l'occupation" +msgstr "Start of occupation" + +#: authorizations/models.py:177 +msgid "Fin de l'occupation" +msgstr "End of occupation" + +#: authorizations/models.py:180 +msgid "Date d'occupation" +msgstr "Occupation date" + +#: authorizations/models.py:181 +#: authorizations/templates/authorizations/authorization_detail.html:127 +msgid "Dates d'occupation" +msgstr "Occupation dates" + +#: authorizations/models.py:202 +msgid "Emails vérifiés" +msgstr "Verified emails" + +#: authorizations/models.py:247 +msgid "Code de vérification" +msgstr "Verification code" + +#: authorizations/models.py:248 +msgid "Codes de vérification" +msgstr "Verification codes" + +#: authorizations/templates/authorizations/authorization_detail.html:12 +msgid "Retour à la liste" +msgstr "Back to list" + +#: authorizations/templates/authorizations/authorization_detail.html:13 +#: authorizations/templates/authorizations/authorization_detail.html:27 +msgid "Retour" +msgstr "Back" + +#: authorizations/templates/authorizations/authorization_detail.html:23 +msgid "Demande" +msgstr "Request" + +#: authorizations/templates/authorizations/authorization_detail.html:52 +msgid "Informations générales" +msgstr "General information" + +#: authorizations/templates/authorizations/authorization_detail.html:55 +#: authorizations/templates/authorizations/authorization_list.html:138 +msgid "Code" +msgstr "Code" + +#: authorizations/templates/authorizations/authorization_detail.html:58 +#: authorizations/templates/authorizations/authorization_list.html:140 +msgid "Statut" +msgstr "Status" + +#: authorizations/templates/authorizations/authorization_detail.html:76 +msgid "Créé le" +msgstr "Created on" + +#: authorizations/templates/authorizations/authorization_detail.html:79 +msgid "Mis à jour" +msgstr "Updated" + +#: authorizations/templates/authorizations/authorization_detail.html:83 +msgid "Agent" +msgstr "Agent" + +#: authorizations/templates/authorizations/authorization_detail.html:102 +#: authorizations/templates/authorizations/authorization_list.html:141 +msgid "Demandeur" +msgstr "Applicant" + +#: authorizations/templates/authorizations/authorization_detail.html:132 +msgid "Début" +msgstr "Start" + +#: authorizations/templates/authorizations/authorization_detail.html:133 +msgid "Fin" +msgstr "End" + +#: authorizations/templates/authorizations/authorization_detail.html:151 +msgid "Documents joints" +msgstr "Attached documents" + +#: authorizations/templates/authorizations/authorization_detail.html:167 +#, fuzzy +#| msgid "Documents joints" +msgid "Aucun document joint." +msgstr "Attached documents" + +#: authorizations/templates/authorizations/authorization_detail.html:176 +msgid "Ex: Arrêté de police" +msgstr "" + +#: authorizations/templates/authorizations/authorization_detail.html:183 +#, fuzzy +#| msgid "Ajouter une date" +msgid "Ajouter" +msgstr "Add a date" + +#: authorizations/templates/authorizations/authorization_detail.html:194 +#, fuzzy +#| msgid "Description de la demande" +msgid "Traitement de la demande" +msgstr "Request description" + +#: authorizations/templates/authorizations/authorization_detail.html:201 +msgid "Modifier le statut" +msgstr "" + +#: authorizations/templates/authorizations/authorization_detail.html:211 +msgid "Commentaire agent" +msgstr "Agent comment" + +#: authorizations/templates/authorizations/authorization_detail.html:212 +msgid "Saisir un commentaire ou des notes..." +msgstr "" + +#: authorizations/templates/authorizations/authorization_detail.html:216 +msgid "Enregistrer le traitement" +msgstr "" + +#: authorizations/templates/authorizations/authorization_list.html:41 +#: authorizations/templates/authorizations/authorization_list.html:42 +#: authorizations/templates/authorizations/authorization_list.html:58 +msgid "Formulaire public" +msgstr "Public form" + +#: authorizations/templates/authorizations/authorization_list.html:45 +msgid "Liste" +msgstr "List" + +#: authorizations/templates/authorizations/authorization_list.html:46 +msgid "Liste des demandes" +msgstr "List of requests" + +#: authorizations/templates/authorizations/authorization_list.html:55 +msgid "Demandes d'autorisation de voirie" +msgstr "Road authorisation requests" + +#: authorizations/templates/authorizations/authorization_list.html:68 +msgid "Recherche" +msgstr "Search" + +#: authorizations/templates/authorizations/authorization_list.html:82 +#: authorizations/templates/authorizations/authorization_list.html:207 +msgid "Tous les types" +msgstr "" + +#: authorizations/templates/authorizations/authorization_list.html:89 +#, fuzzy +#| msgid "Recherche" +msgid "Rechercher un type..." +msgstr "Search" + +#: authorizations/templates/authorizations/authorization_list.html:92 +#: authorizations/templates/authorizations/new_authorization.html:240 +msgid "Effacer" +msgstr "Clear" + +#: authorizations/templates/authorizations/authorization_list.html:106 +msgid "Aucun type correspondant" +msgstr "" + +#: authorizations/templates/authorizations/authorization_list.html:121 +msgid "Filtrer" +msgstr "Filter" + +#: authorizations/templates/authorizations/authorization_list.html:143 +msgid "Date de création" +msgstr "Creation date" + +#: authorizations/templates/authorizations/authorization_list.html:177 +msgid "Aucune demande trouvée." +msgstr "No request found." + +#: authorizations/templates/authorizations/authorization_list.html:215 +msgid "types sélectionnés" +msgstr "" + +#: authorizations/templates/authorizations/authorization_success.html:9 +#: authorizations/templates/authorizations/authorization_success.html:37 +msgid "Demande enregistrée" +msgstr "Request saved" + +#: authorizations/templates/authorizations/authorization_success.html:28 +#: authorizations/templates/authorizations/email_verification_failed.html:28 +#: authorizations/templates/authorizations/email_verification_pending.html:28 +#: authorizations/templates/authorizations/email_verify_code.html:35 +#: authorizations/templates/authorizations/new_authorization.html:9 +#: authorizations/templates/authorizations/new_authorization.html:99 +msgid "Demande d'occupation de voirie régionale" +msgstr "Regional road occupation request" + +#: authorizations/templates/authorizations/authorization_success.html:41 +msgid "Numéro de référence" +msgstr "Reference number" + +#: authorizations/templates/authorizations/authorization_success.html:43 +msgid "Conservez ce numéro pour le suivi de votre dossier." +msgstr "Keep this number to track your application." + +#: authorizations/templates/authorizations/authorization_success.html:49 +msgid "" +"Votre adresse email a été confirmée et votre demande d'autorisation " +"d'occupation de voirie régionale a bien été enregistrée. Un accusé de " +"réception vous a été envoyé par email. Nos services prendront contact avec " +"vous dans les meilleurs délais." +msgstr "" +"Your email address has been confirmed and your regional road occupation " +"request has been registered. An acknowledgement of receipt has been sent to " +"you by email. Our services will contact you as soon as possible." + +#: authorizations/templates/authorizations/authorization_success.html:55 +msgid "" +"Votre demande d'autorisation d'occupation de voirie régionale a bien été " +"enregistrée. Un accusé de réception vous a été envoyé par email. Nos " +"services prendront contact avec vous dans les meilleurs délais." +msgstr "" +"Your regional road occupation request has been registered. An " +"acknowledgement of receipt has been sent to you by email. Our services will " +"contact you as soon as possible." + +#: authorizations/templates/authorizations/authorization_success.html:64 +msgid "Faire une nouvelle demande" +msgstr "Submit a new request" + +#: authorizations/templates/authorizations/email_verification_failed.html:9 +#: authorizations/templates/authorizations/email_verification_failed.html:47 +msgid "Lien invalide" +msgstr "Invalid link" + +#: authorizations/templates/authorizations/email_verification_failed.html:39 +msgid "Lien expiré" +msgstr "Expired link" + +#: authorizations/templates/authorizations/email_verification_failed.html:41 +msgid "" +"Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 " +"heures dépassé). Veuillez soumettre une nouvelle demande." +msgstr "" +"The confirmation link you used is no longer valid (48-hour deadline " +"exceeded). Please submit a new request." + +#: authorizations/templates/authorizations/email_verification_failed.html:49 +msgid "" +"Le lien de confirmation que vous avez utilisé est invalide ou a déjà été " +"utilisé. Si votre demande a déjà été validée, vous pouvez en soumettre une " +"nouvelle." +msgstr "" +"The confirmation link you used is invalid or has already been used. If your " +"request has already been validated, you can submit a new one." + +#: authorizations/templates/authorizations/email_verification_failed.html:58 +#: authorizations/templates/authorizations/email_verify_code.html:55 +#: authorizations/templates/authorizations/email_verify_code.html:72 +msgid "Nouvelle demande" +msgstr "New request" + +#: authorizations/templates/authorizations/email_verification_pending.html:9 +msgid "Confirmation de votre email" +msgstr "Email confirmation" + +#: authorizations/templates/authorizations/email_verification_pending.html:37 +msgid "Vérifiez votre boîte email" +msgstr "Check your inbox" + +#: authorizations/templates/authorizations/email_verification_pending.html:39 +msgid "" +"Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre " +"adresse email en cliquant sur le lien que nous venons de vous envoyer." +msgstr "" +"Your request has been received. To finalise it, please confirm your email " +"address by clicking the link we have just sent you." + +#: authorizations/templates/authorizations/email_verification_pending.html:47 +msgid "" +"Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas " +"l'email, vérifiez votre dossier de courrier indésirable (spam)." +msgstr "" +"The confirmation link is valid for 48 hours. If you cannot find the email, " +"please check your junk/spam folder." + +#: authorizations/templates/authorizations/email_verification_pending.html:55 +msgid "Retour au formulaire" +msgstr "Back to the form" + +#: authorizations/templates/authorizations/email_verify_code.html:9 +msgid "Vérification de votre email" +msgstr "Email verification" + +#: authorizations/templates/authorizations/email_verify_code.html:46 +msgid "Code expiré" +msgstr "Code expired" + +#: authorizations/templates/authorizations/email_verify_code.html:48 +msgid "" +"Le code de vérification est arrivé à expiration (validité : " +"10 minutes). Veuillez soumettre une nouvelle demande pour recevoir un " +"nouveau code." +msgstr "" +"The verification code has expired (validity: 10 minutes). Please " +"submit a new request to receive a new code." + +#: authorizations/templates/authorizations/email_verify_code.html:63 +msgid "Trop de tentatives" +msgstr "Too many attempts" + +#: authorizations/templates/authorizations/email_verify_code.html:65 +msgid "" +"Le nombre maximum de tentatives a été atteint. Votre code est bloqué. " +"Veuillez soumettre une nouvelle demande." +msgstr "" +"The maximum number of attempts has been reached. Your code is blocked. " +"Please submit a new request." + +#: authorizations/templates/authorizations/email_verify_code.html:80 +msgid "Entrez votre code de vérification" +msgstr "Enter your verification code" + +#: authorizations/templates/authorizations/email_verify_code.html:82 +#, python-format +msgid "" +"Un code à 6 chiffres a été envoyé à %(email)s. Saisissez-le " +"ci-dessous. Il est valable 10 minutes." +msgstr "" +"A 6-digit code has been sent to %(email)s. Enter it below. " +"It is valid for 10 minutes." + +#: authorizations/templates/authorizations/email_verify_code.html:92 +msgid "Code incorrect. Vérifiez l'email reçu et réessayez." +msgstr "Incorrect code. Check the email you received and try again." + +#: authorizations/templates/authorizations/email_verify_code.html:115 +msgid "Code de vérification à 6 chiffres" +msgstr "6-digit verification code" + +#: authorizations/templates/authorizations/email_verify_code.html:118 +msgid "Vérifier" +msgstr "Verify" + +#: authorizations/templates/authorizations/email_verify_code.html:124 +msgid "" +"Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier " +"indésirable (spam)." +msgstr "Didn't receive the email? Check your junk/spam folder." + +#: authorizations/templates/authorizations/new_authorization.html:96 +#: authorizations/templates/authorizations/new_authorization.html:98 +msgid "Brussels Mobility" +msgstr "" + +#: authorizations/templates/authorizations/new_authorization.html:116 +msgid "" +"Toute occupation de l'espace public doit faire l'objet d'une autorisation de " +"la part du gestionnaire de voiries. En Région de Bruxelles-Capitale, les " +"axes principaux sont gérés par la Région (Bruxelles Mobilité), tandis que les autres voiries sont " +"gérées par les communes. Les voiries régionales sont représentées en rouge sur la carte ci-dessous." +msgstr "" +"Any occupation of public space requires authorisation from the road manager. " +"In the Brussels-Capital Region, the main roads are managed by the Region (Brussels Mobility), while other roads " +"are managed by the municipalities. Regional roads are shown in red on the map below." + +#: authorizations/templates/authorizations/new_authorization.html:125 +msgid "" +"Ce formulaire ne concerne que les autorisations sur voiries " +"régionales. Veuillez vous adresser à votre commune pour les autres " +"autorisations ou documents." +msgstr "" +"This form only covers authorisations on regional roads. " +"Please contact your municipality for other authorisations or documents." + +#: authorizations/templates/authorizations/new_authorization.html:131 +msgid "" +"Pour les demandes liées à des chantiers, veuillez utiliser le système Osiris. Les demandes pour le placement de conteneurs, " +"d'échafaudages ou les déménagements se font également via ce système." +msgstr "" +"For requests related to construction works, please use the Osiris system. " +"Requests for the placement of containers, scaffolding or removals are also " +"made through this system." + +#: authorizations/templates/authorizations/new_authorization.html:138 +msgid "" +"Si vous éprouvez des difficultés, contactez-nous par mail (mobilite@sprb.brussels) ou par " +"téléphone (0800 94 001)." +msgstr "" +"If you experience any difficulties, contact us by email (mobilite@sprb.brussels) or by " +"phone (0800 94 001)." + +#: authorizations/templates/authorizations/new_authorization.html:158 +msgid "Votre demande" +msgstr "Your request" + +#: authorizations/templates/authorizations/new_authorization.html:230 +msgid "Localisation (dessiner un polygone pour indiquer la zone concernée)" +msgstr "Location (draw a polygon to indicate the area concerned)" + +#: authorizations/templates/authorizations/new_authorization.html:234 +msgid "Dessiner un polygone" +msgstr "Draw a polygon" + +#: authorizations/templates/authorizations/new_authorization.html:237 +msgid "Terminer" +msgstr "Finish" + +#: authorizations/templates/authorizations/new_authorization.html:245 +msgid "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets. Cliquez sur le premier sommet pour fermer le polygone." +msgstr "" +"Click « Draw a polygon » then click on the map to add vertices. Click on the " +"first vertex to close the polygon." + +#: authorizations/templates/authorizations/new_authorization.html:259 +msgid "Vos coordonnées" +msgstr "Your contact details" + +#: authorizations/templates/authorizations/new_authorization.html:292 +#: authorizations/templates/authorizations/new_authorization.html:331 +#: authorizations/templates/authorizations/new_authorization.html:351 +msgid "optionnel" +msgstr "optional" + +#: authorizations/templates/authorizations/new_authorization.html:306 +msgid "Date(s) d'occupation" +msgstr "Occupation date(s)" + +#: authorizations/templates/authorizations/new_authorization.html:312 +msgid "Veuillez indiquer au moins une date de début d'occupation." +msgstr "Please provide at least one occupation start date." + +#: authorizations/templates/authorizations/new_authorization.html:342 +msgid "Ajouter une date" +msgstr "Add a date" + +#: authorizations/templates/authorizations/new_authorization.html:350 +msgid "Document(s) à joindre" +msgstr "Document(s) to attach" + +#: authorizations/templates/authorizations/new_authorization.html:374 +msgid "Ajouter un document" +msgstr "Add a document" + +#: authorizations/templates/authorizations/new_authorization.html:381 +msgid "Envoyer la demande" +msgstr "Submit the request" + +#: authorizations/templates/authorizations/new_authorization.html:390 +msgid "https://servicepublic.brussels/politique-de-confidentialite/" +msgstr "https://servicepublic.brussels/en/privacy-policy/" + +#: authorizations/templates/authorizations/new_authorization.html:391 +msgid "Politique de confidentialité" +msgstr "Privacy policy" + +#: authorizations/templates/authorizations/new_authorization.html:401 +msgid "" +"Veillez à joindre en annexe l'attestation d'assurance couvrant tous les " +"risques." +msgstr "Please attach the insurance certificate covering all risks." + +#: authorizations/templates/authorizations/new_authorization.html:402 +msgid "Veillez à joindre en annexe l'autorisation de la commune." +msgstr "Please attach the municipal authorisation." + +#: authorizations/templates/authorizations/new_authorization.html:486 +#: authorizations/templates/authorizations/new_authorization.html:535 +msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." +msgstr "Polygon saved. Click « Draw » to redraw it." + +#: authorizations/templates/authorizations/new_authorization.html:506 +msgid "Cliquer pour fermer le polygone" +msgstr "Click to close the polygon" + +#: authorizations/templates/authorizations/new_authorization.html:550 +msgid "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets." +msgstr "Click « Draw a polygon » then click on the map to add vertices." + +#: authorizations/templates/authorizations/new_authorization.html:573 +msgid "" +"Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet " +"pour fermer le polygone." +msgstr "" + +#: authorizations/views.py:320 authorizations/views.py:369 +msgid "Vous n'avez pas accès à cette vue." +msgstr "" + +#: authorizations/views.py:386 +msgid "La demande a été mise à jour avec succès." +msgstr "" + +#: authorizations/views.py:403 +msgid "Le document a été ajouté avec succès." +msgstr "" + +#: authorizations/views.py:407 +msgid "Veuillez fournir une description et un fichier." +msgstr "" + +#~ msgid "Langue / Taal / Language" +#~ msgstr "Langue / Taal / Language" diff --git a/streetup/authorizations/locale/fr/LC_MESSAGES/django.mo b/streetup/authorizations/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..3a0a0cf Binary files /dev/null and b/streetup/authorizations/locale/fr/LC_MESSAGES/django.mo differ diff --git a/streetup/authorizations/locale/fr/LC_MESSAGES/django.po b/streetup/authorizations/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..b0f2a5f --- /dev/null +++ b/streetup/authorizations/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,739 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: streetup/authorizations/admin.py:28 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:73 +#: streetup/authorizations/templates/authorizations/authorization_list.html:73 +#: streetup/authorizations/templates/authorizations/authorization_list.html:139 +msgid "Type" +msgstr "Type" + +#: streetup/authorizations/admin.py:46 +msgid "Expiré" +msgstr "Expiré" + +#: streetup/authorizations/forms.py:62 +msgid "Veuillez dessiner la zone concernée sur la carte." +msgstr "Veuillez dessiner la zone concernée sur la carte." + +#: streetup/authorizations/forms.py:66 +msgid "Géométrie invalide." +msgstr "Géométrie invalide." + +#: streetup/authorizations/forms.py:68 +msgid "Veuillez dessiner un polygone." +msgstr "Veuillez dessiner un polygone." + +#: streetup/authorizations/forms.py:77 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:175 +msgid "Description du document" +msgstr "Description du document" + +#: streetup/authorizations/forms.py:127 +msgid "Inclure les archives (> 2 ans)" +msgstr "Inclure les archives (> 2 ans)" + +#: streetup/authorizations/forms.py:132 +msgid "Type d'autorisation" +msgstr "Type d'autorisation" + +#: streetup/authorizations/forms.py:140 +msgid "Recherche…" +msgstr "Recherche…" + +#: streetup/authorizations/models.py:17 +msgid "Affichage (panneau, ...)" +msgstr "Affichage (panneau, ...)" + +#: streetup/authorizations/models.py:18 +msgid "Terrasse" +msgstr "Terrasse" + +#: streetup/authorizations/models.py:19 +msgid "Mobilier urbain" +msgstr "Mobilier urbain" + +#: streetup/authorizations/models.py:20 +msgid "Œuvre d'art" +msgstr "Œuvre d'art" + +#: streetup/authorizations/models.py:21 +msgid "Nouvel Emplacement PMR" +msgstr "Nouvel Emplacement PMR" + +#: streetup/authorizations/models.py:22 +msgid "Nouvelle Zone de livraison" +msgstr "Nouvelle Zone de livraison" + +#: streetup/authorizations/models.py:23 +msgid "Tournage audiovisuel" +msgstr "Tournage audiovisuel" + +#: streetup/authorizations/models.py:24 +msgid "Evénement" +msgstr "Evénement" + +#: streetup/authorizations/models.py:25 +msgid "Plante grimpante" +msgstr "Plante grimpante" + +#: streetup/authorizations/models.py:26 +msgid "Pied d'arbre" +msgstr "Pied d'arbre" + +#: streetup/authorizations/models.py:30 +msgid "À traiter" +msgstr "À traiter" + +#: streetup/authorizations/models.py:31 +msgid "En cours" +msgstr "En cours" + +#: streetup/authorizations/models.py:32 +msgid "Autorisé" +msgstr "Autorisé" + +#: streetup/authorizations/models.py:33 +msgid "Refusé" +msgstr "Refusé" + +#: streetup/authorizations/models.py:34 +msgid "Clôturé" +msgstr "Clôturé" + +#: streetup/authorizations/models.py:45 +msgid "Langue" +msgstr "Langue" + +#: streetup/authorizations/models.py:49 streetup/authorizations/models.py:201 +msgid "Email vérifié" +msgstr "Email vérifié" + +#: streetup/authorizations/models.py:50 +msgid "Indique si l'adresse email du demandeur a été vérifiée." +msgstr "Indique si l'adresse email du demandeur a été vérifiée." + +#: streetup/authorizations/models.py:52 +msgid "Type d'occupation" +msgstr "Type d'occupation" + +#: streetup/authorizations/models.py:53 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:92 +msgid "Description de la demande" +msgstr "Description de la demande" + +#: streetup/authorizations/models.py:54 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:105 +msgid "Nom" +msgstr "Nom" + +#: streetup/authorizations/models.py:55 streetup/authorizations/models.py:197 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:108 +msgid "Email" +msgstr "Email" + +#: streetup/authorizations/models.py:56 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:113 +msgid "Adresse" +msgstr "Adresse" + +#: streetup/authorizations/models.py:57 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:117 +msgid "Entreprise" +msgstr "Entreprise" + +#: streetup/authorizations/models.py:64 +msgid "Agent traitant" +msgstr "Agent traitant" + +#: streetup/authorizations/models.py:66 +msgid "Commentaire" +msgstr "Commentaire" + +#: streetup/authorizations/models.py:67 +msgid "Localisation – Adresse" +msgstr "Localisation – Adresse" + +#: streetup/authorizations/models.py:68 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:95 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:228 +#: streetup/authorizations/templates/authorizations/authorization_list.html:142 +msgid "Localisation" +msgstr "Localisation" + +#: streetup/authorizations/models.py:107 +msgid "Autorisation de voirie" +msgstr "Autorisation de voirie" + +#: streetup/authorizations/models.py:108 +msgid "Autorisations de voirie" +msgstr "Autorisations de voirie" + +#: streetup/authorizations/models.py:146 +#, python-format +msgid "La taille maximale du fichier est de %(limit)s Mo." +msgstr "La taille maximale du fichier est de %(limit)s Mo." + +#: streetup/authorizations/models.py:151 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:179 +msgid "Fichier" +msgstr "Fichier" + +#: streetup/authorizations/models.py:155 +msgid "Description" +msgstr "Description" + +#: streetup/authorizations/models.py:163 +msgid "Document" +msgstr "Document" + +#: streetup/authorizations/models.py:164 +msgid "Documents" +msgstr "Documents" + +#: streetup/authorizations/models.py:176 +msgid "Début de l'occupation" +msgstr "Début de l'occupation" + +#: streetup/authorizations/models.py:177 +msgid "Fin de l'occupation" +msgstr "Fin de l'occupation" + +#: streetup/authorizations/models.py:180 +msgid "Date d'occupation" +msgstr "Date d'occupation" + +#: streetup/authorizations/models.py:181 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:127 +msgid "Dates d'occupation" +msgstr "Dates d'occupation" + +#: streetup/authorizations/models.py:202 +msgid "Emails vérifiés" +msgstr "Emails vérifiés" + +#: streetup/authorizations/models.py:247 +msgid "Code de vérification" +msgstr "Code de vérification" + +#: streetup/authorizations/models.py:248 +msgid "Codes de vérification" +msgstr "Codes de vérification" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:12 +msgid "Retour à la liste" +msgstr "Retour à la liste" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:13 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:27 +msgid "Retour" +msgstr "Retour" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:23 +msgid "Demande" +msgstr "Demande" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:52 +msgid "Informations générales" +msgstr "Informations générales" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:55 +#: streetup/authorizations/templates/authorizations/authorization_list.html:138 +msgid "Code" +msgstr "Code" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:58 +#: streetup/authorizations/templates/authorizations/authorization_list.html:140 +msgid "Statut" +msgstr "Statut" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:76 +msgid "Créé le" +msgstr "Créé le" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:79 +msgid "Mis à jour" +msgstr "Mis à jour" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:83 +msgid "Agent" +msgstr "Agent" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:102 +#: streetup/authorizations/templates/authorizations/authorization_list.html:141 +msgid "Demandeur" +msgstr "Demandeur" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:132 +msgid "Début" +msgstr "Début" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:133 +msgid "Fin" +msgstr "Fin" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:151 +msgid "Documents joints" +msgstr "Documents joints" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:167 +#, fuzzy +#| msgid "Documents joints" +msgid "Aucun document joint." +msgstr "Documents joints" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:176 +msgid "Ex: Arrêté de police" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:183 +#, fuzzy +#| msgid "Ajouter une date" +msgid "Ajouter" +msgstr "Ajouter une date" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:194 +#, fuzzy +#| msgid "Description de la demande" +msgid "Traitement de la demande" +msgstr "Description de la demande" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:201 +msgid "Modifier le statut" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:211 +msgid "Commentaire agent" +msgstr "Commentaire agent" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:212 +msgid "Saisir un commentaire ou des notes..." +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:216 +msgid "Enregistrer le traitement" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:41 +#: streetup/authorizations/templates/authorizations/authorization_list.html:42 +#: streetup/authorizations/templates/authorizations/authorization_list.html:58 +msgid "Formulaire public" +msgstr "Formulaire public" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:45 +msgid "Liste" +msgstr "Liste" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:46 +msgid "Liste des demandes" +msgstr "Liste des demandes" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:55 +msgid "Demandes d'autorisation de voirie" +msgstr "Demandes d'autorisation de voirie" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:68 +msgid "Recherche" +msgstr "Recherche" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:82 +#: streetup/authorizations/templates/authorizations/authorization_list.html:207 +msgid "Tous les types" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:89 +#, fuzzy +#| msgid "Recherche" +msgid "Rechercher un type..." +msgstr "Recherche" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:92 +#: streetup/authorizations/templates/authorizations/new_authorization.html:240 +msgid "Effacer" +msgstr "Effacer" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:106 +msgid "Aucun type correspondant" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:121 +msgid "Filtrer" +msgstr "Filtrer" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:143 +msgid "Date de création" +msgstr "Date de création" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:177 +msgid "Aucune demande trouvée." +msgstr "Aucune demande trouvée." + +#: streetup/authorizations/templates/authorizations/authorization_list.html:215 +msgid "types sélectionnés" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:9 +#: streetup/authorizations/templates/authorizations/authorization_success.html:37 +msgid "Demande enregistrée" +msgstr "Demande enregistrée" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:28 +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:28 +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:28 +#: streetup/authorizations/templates/authorizations/email_verify_code.html:35 +#: streetup/authorizations/templates/authorizations/new_authorization.html:9 +#: streetup/authorizations/templates/authorizations/new_authorization.html:99 +msgid "Demande d'occupation de voirie régionale" +msgstr "Demande d'occupation de voirie régionale" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:41 +msgid "Numéro de référence" +msgstr "Numéro de référence" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:43 +msgid "Conservez ce numéro pour le suivi de votre dossier." +msgstr "Conservez ce numéro pour le suivi de votre dossier." + +#: streetup/authorizations/templates/authorizations/authorization_success.html:49 +msgid "" +"Votre adresse email a été confirmée et votre demande d'autorisation " +"d'occupation de voirie régionale a bien été enregistrée. Un accusé de " +"réception vous a été envoyé par email. Nos services prendront contact avec " +"vous dans les meilleurs délais." +msgstr "" +"Votre adresse email a été confirmée et votre demande d'autorisation " +"d'occupation de voirie régionale a bien été enregistrée. Un accusé de " +"réception vous a été envoyé par email. Nos services prendront contact avec " +"vous dans les meilleurs délais." + +#: streetup/authorizations/templates/authorizations/authorization_success.html:55 +msgid "" +"Votre demande d'autorisation d'occupation de voirie régionale a bien été " +"enregistrée. Un accusé de réception vous a été envoyé par email. Nos " +"services prendront contact avec vous dans les meilleurs délais." +msgstr "" +"Votre demande d'autorisation d'occupation de voirie régionale a bien été " +"enregistrée. Un accusé de réception vous a été envoyé par email. Nos " +"services prendront contact avec vous dans les meilleurs délais." + +#: streetup/authorizations/templates/authorizations/authorization_success.html:64 +msgid "Faire une nouvelle demande" +msgstr "Faire une nouvelle demande" + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:9 +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:47 +msgid "Lien invalide" +msgstr "Lien invalide" + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:39 +msgid "Lien expiré" +msgstr "Lien expiré" + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:41 +msgid "" +"Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 " +"heures dépassé). Veuillez soumettre une nouvelle demande." +msgstr "" +"Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 " +"heures dépassé). Veuillez soumettre une nouvelle demande." + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:49 +msgid "" +"Le lien de confirmation que vous avez utilisé est invalide ou a déjà été " +"utilisé. Si votre demande a déjà été validée, vous pouvez en soumettre une " +"nouvelle." +msgstr "" +"Le lien de confirmation que vous avez utilisé est invalide ou a déjà été " +"utilisé. Si votre demande a déjà été validée, vous pouvez en soumettre une " +"nouvelle." + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:58 +#: streetup/authorizations/templates/authorizations/email_verify_code.html:55 +#: streetup/authorizations/templates/authorizations/email_verify_code.html:72 +msgid "Nouvelle demande" +msgstr "Nouvelle demande" + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:9 +msgid "Confirmation de votre email" +msgstr "Confirmation de votre email" + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:37 +msgid "Vérifiez votre boîte email" +msgstr "Vérifiez votre boîte email" + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:39 +msgid "" +"Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre " +"adresse email en cliquant sur le lien que nous venons de vous envoyer." +msgstr "" +"Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre " +"adresse email en cliquant sur le lien que nous venons de vous envoyer." + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:47 +msgid "" +"Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas " +"l'email, vérifiez votre dossier de courrier indésirable (spam)." +msgstr "" +"Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas " +"l'email, vérifiez votre dossier de courrier indésirable (spam)." + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:55 +msgid "Retour au formulaire" +msgstr "Retour au formulaire" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:9 +msgid "Vérification de votre email" +msgstr "Vérification de votre email" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:46 +msgid "Code expiré" +msgstr "Code expiré" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:48 +msgid "" +"Le code de vérification est arrivé à expiration (validité : " +"10 minutes). Veuillez soumettre une nouvelle demande pour recevoir un " +"nouveau code." +msgstr "" +"Le code de vérification est arrivé à expiration (validité : " +"10 minutes). Veuillez soumettre une nouvelle demande pour recevoir un " +"nouveau code." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:63 +msgid "Trop de tentatives" +msgstr "Trop de tentatives" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:65 +msgid "" +"Le nombre maximum de tentatives a été atteint. Votre code est bloqué. " +"Veuillez soumettre une nouvelle demande." +msgstr "" +"Le nombre maximum de tentatives a été atteint. Votre code est bloqué. " +"Veuillez soumettre une nouvelle demande." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:80 +msgid "Entrez votre code de vérification" +msgstr "Entrez votre code de vérification" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:82 +#, python-format +msgid "" +"Un code à 6 chiffres a été envoyé à %(email)s. Saisissez-le " +"ci-dessous. Il est valable 10 minutes." +msgstr "" +"Un code à 6 chiffres a été envoyé à %(email)s. Saisissez-le " +"ci-dessous. Il est valable 10 minutes." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:92 +msgid "Code incorrect. Vérifiez l'email reçu et réessayez." +msgstr "Code incorrect. Vérifiez l'email reçu et réessayez." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:115 +msgid "Code de vérification à 6 chiffres" +msgstr "Code de vérification à 6 chiffres" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:118 +msgid "Vérifier" +msgstr "Vérifier" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:124 +msgid "" +"Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier " +"indésirable (spam)." +msgstr "" +"Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier " +"indésirable (spam)." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:96 +#: streetup/authorizations/templates/authorizations/new_authorization.html:98 +msgid "Brussels Mobility" +msgstr "Brussels Mobility" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:116 +msgid "" +"Toute occupation de l'espace public doit faire l'objet d'une autorisation de " +"la part du gestionnaire de voiries. En Région de Bruxelles-Capitale, les " +"axes principaux sont gérés par la Région (Bruxelles Mobilité), tandis que les autres voiries sont " +"gérées par les communes. Les voiries régionales sont représentées en rouge sur la carte ci-dessous." +msgstr "" +"Toute occupation de l'espace public doit faire l'objet d'une autorisation de " +"la part du gestionnaire de voiries. En Région de Bruxelles-Capitale, les " +"axes principaux sont gérés par la Région (Bruxelles Mobilité), tandis que les autres voiries sont " +"gérées par les communes. Les voiries régionales sont représentées en rouge sur la carte ci-dessous." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:125 +msgid "" +"Ce formulaire ne concerne que les autorisations sur voiries " +"régionales. Veuillez vous adresser à votre commune pour les autres " +"autorisations ou documents." +msgstr "" +"Ce formulaire ne concerne que les autorisations sur voiries " +"régionales. Veuillez vous adresser à votre commune pour les autres " +"autorisations ou documents." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:131 +msgid "" +"Pour les demandes liées à des chantiers, veuillez utiliser le système Osiris. Les demandes pour le placement de conteneurs, " +"d'échafaudages ou les déménagements se font également via ce système." +msgstr "" +"Pour les demandes liées à des chantiers, veuillez utiliser le système Osiris. Les demandes pour le placement de conteneurs, " +"d'échafaudages ou les déménagements se font également via ce système." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:138 +msgid "" +"Si vous éprouvez des difficultés, contactez-nous par mail (mobilite@sprb.brussels) ou par " +"téléphone (0800 94 001)." +msgstr "" +"Si vous éprouvez des difficultés, contactez-nous par mail (mobilite@sprb.brussels) ou par " +"téléphone (0800 94 001)." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:158 +msgid "Votre demande" +msgstr "Votre demande" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:230 +msgid "Localisation (dessiner un polygone pour indiquer la zone concernée)" +msgstr "Localisation (dessiner un polygone pour indiquer la zone concernée)" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:234 +msgid "Dessiner un polygone" +msgstr "Dessiner un polygone" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:237 +msgid "Terminer" +msgstr "Terminer" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:245 +msgid "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets. Cliquez sur le premier sommet pour fermer le polygone." +msgstr "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets. Cliquez sur le premier sommet pour fermer le polygone." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:259 +msgid "Vos coordonnées" +msgstr "Vos coordonnées" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:292 +#: streetup/authorizations/templates/authorizations/new_authorization.html:331 +#: streetup/authorizations/templates/authorizations/new_authorization.html:351 +msgid "optionnel" +msgstr "optionnel" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:306 +msgid "Date(s) d'occupation" +msgstr "Date(s) d'occupation" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:312 +msgid "Veuillez indiquer au moins une date de début d'occupation." +msgstr "Veuillez indiquer au moins une date de début d'occupation." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:342 +msgid "Ajouter une date" +msgstr "Ajouter une date" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:350 +msgid "Document(s) à joindre" +msgstr "Document(s) à joindre" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:374 +msgid "Ajouter un document" +msgstr "Ajouter un document" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:381 +msgid "Envoyer la demande" +msgstr "Envoyer la demande" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:390 +msgid "https://servicepublic.brussels/politique-de-confidentialite/" +msgstr "https://servicepublic.brussels/politique-de-confidentialite/" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:391 +msgid "Politique de confidentialité" +msgstr "Politique de confidentialité" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:401 +msgid "" +"Veillez à joindre en annexe l'attestation d'assurance couvrant tous les " +"risques." +msgstr "" +"Veillez à joindre en annexe l'attestation d'assurance couvrant tous les " +"risques." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:402 +msgid "Veillez à joindre en annexe l'autorisation de la commune." +msgstr "Veillez à joindre en annexe l'autorisation de la commune." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:486 +#: streetup/authorizations/templates/authorizations/new_authorization.html:535 +msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." +msgstr "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:506 +msgid "Cliquer pour fermer le polygone" +msgstr "Cliquer pour fermer le polygone" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:550 +msgid "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets." +msgstr "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:573 +msgid "" +"Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet " +"pour fermer le polygone." +msgstr "" +"Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet " +"pour fermer le polygone." + +#: streetup/authorizations/views.py:320 streetup/authorizations/views.py:369 +msgid "Vous n'avez pas accès à cette vue." +msgstr "" + +#: streetup/authorizations/views.py:386 +msgid "La demande a été mise à jour avec succès." +msgstr "" + +#: streetup/authorizations/views.py:403 +msgid "Le document a été ajouté avec succès." +msgstr "" + +#: streetup/authorizations/views.py:407 +msgid "Veuillez fournir une description et un fichier." +msgstr "" + +#~ msgid "Langue / Taal / Language" +#~ msgstr "Langue / Taal / Language" diff --git a/streetup/authorizations/locale/nl/LC_MESSAGES/django.mo b/streetup/authorizations/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..2cdc0e8 Binary files /dev/null and b/streetup/authorizations/locale/nl/LC_MESSAGES/django.mo differ diff --git a/streetup/authorizations/locale/nl/LC_MESSAGES/django.po b/streetup/authorizations/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..537f30d --- /dev/null +++ b/streetup/authorizations/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,731 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: streetup/authorizations/admin.py:28 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:73 +#: streetup/authorizations/templates/authorizations/authorization_list.html:73 +#: streetup/authorizations/templates/authorizations/authorization_list.html:139 +msgid "Type" +msgstr "Type" + +#: streetup/authorizations/admin.py:46 +msgid "Expiré" +msgstr "Verlopen" + +#: streetup/authorizations/forms.py:62 +msgid "Veuillez dessiner la zone concernée sur la carte." +msgstr "Teken de betrokken zone op de kaart." + +#: streetup/authorizations/forms.py:66 +msgid "Géométrie invalide." +msgstr "Ongeldige geometrie." + +#: streetup/authorizations/forms.py:68 +msgid "Veuillez dessiner un polygone." +msgstr "Teken een veelhoek." + +#: streetup/authorizations/forms.py:77 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:175 +msgid "Description du document" +msgstr "Documentomschrijving" + +#: streetup/authorizations/forms.py:127 +msgid "Inclure les archives (> 2 ans)" +msgstr "Archieven weergeven (> 2 jaar)" + +#: streetup/authorizations/forms.py:132 +msgid "Type d'autorisation" +msgstr "Type vergunning" + +#: streetup/authorizations/forms.py:140 +msgid "Recherche…" +msgstr "Zoeken…" + +#: streetup/authorizations/models.py:17 +msgid "Affichage (panneau, ...)" +msgstr "Affichage (bord…)" + +#: streetup/authorizations/models.py:18 +msgid "Terrasse" +msgstr "Terras" + +#: streetup/authorizations/models.py:19 +msgid "Mobilier urbain" +msgstr "Straatmeubilair" + +#: streetup/authorizations/models.py:20 +msgid "Œuvre d'art" +msgstr "Kunstwerk" + +#: streetup/authorizations/models.py:21 +msgid "Nouvel Emplacement PMR" +msgstr "Nieuwe gehandicaptenparkeerplaats" + +#: streetup/authorizations/models.py:22 +msgid "Nouvelle Zone de livraison" +msgstr "Nieuwe laad- en loszone" + +#: streetup/authorizations/models.py:23 +msgid "Tournage audiovisuel" +msgstr "Filmopname" + +#: streetup/authorizations/models.py:24 +msgid "Evénement" +msgstr "Evenement" + +#: streetup/authorizations/models.py:25 +msgid "Plante grimpante" +msgstr "Klimplant" + +#: streetup/authorizations/models.py:26 +msgid "Pied d'arbre" +msgstr "Boomspiegel" + +#: streetup/authorizations/models.py:30 +msgid "À traiter" +msgstr "Te behandelen" + +#: streetup/authorizations/models.py:31 +msgid "En cours" +msgstr "In behandeling" + +#: streetup/authorizations/models.py:32 +msgid "Autorisé" +msgstr "Vergund" + +#: streetup/authorizations/models.py:33 +msgid "Refusé" +msgstr "Geweigerd" + +#: streetup/authorizations/models.py:34 +msgid "Clôturé" +msgstr "Afgesloten" + +#: streetup/authorizations/models.py:45 +msgid "Langue" +msgstr "Taal" + +#: streetup/authorizations/models.py:49 streetup/authorizations/models.py:201 +msgid "Email vérifié" +msgstr "E-mail geverifieerd" + +#: streetup/authorizations/models.py:50 +msgid "Indique si l'adresse email du demandeur a été vérifiée." +msgstr "Geeft aan of het e-mailadres van de aanvrager is geverifieerd." + +#: streetup/authorizations/models.py:52 +msgid "Type d'occupation" +msgstr "Type bezetting" + +#: streetup/authorizations/models.py:53 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:92 +msgid "Description de la demande" +msgstr "Beschrijving van de aanvraag" + +#: streetup/authorizations/models.py:54 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:105 +msgid "Nom" +msgstr "Naam" + +#: streetup/authorizations/models.py:55 streetup/authorizations/models.py:197 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:108 +msgid "Email" +msgstr "E-mail" + +#: streetup/authorizations/models.py:56 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:113 +msgid "Adresse" +msgstr "Adres" + +#: streetup/authorizations/models.py:57 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:117 +msgid "Entreprise" +msgstr "Onderneming" + +#: streetup/authorizations/models.py:64 +msgid "Agent traitant" +msgstr "Behandelende ambtenaar" + +#: streetup/authorizations/models.py:66 +msgid "Commentaire" +msgstr "Opmerking" + +#: streetup/authorizations/models.py:67 +msgid "Localisation – Adresse" +msgstr "Locatie – Adres" + +#: streetup/authorizations/models.py:68 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:95 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:228 +#: streetup/authorizations/templates/authorizations/authorization_list.html:142 +msgid "Localisation" +msgstr "Locatie" + +#: streetup/authorizations/models.py:107 +msgid "Autorisation de voirie" +msgstr "Wegvergunning" + +#: streetup/authorizations/models.py:108 +msgid "Autorisations de voirie" +msgstr "Wegvergunningen" + +#: streetup/authorizations/models.py:146 +#, python-format +msgid "La taille maximale du fichier est de %(limit)s Mo." +msgstr "De maximale bestandsgrootte is %(limit)s MB." + +#: streetup/authorizations/models.py:151 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:179 +msgid "Fichier" +msgstr "Bestand" + +#: streetup/authorizations/models.py:155 +msgid "Description" +msgstr "Beschrijving" + +#: streetup/authorizations/models.py:163 +msgid "Document" +msgstr "Document" + +#: streetup/authorizations/models.py:164 +msgid "Documents" +msgstr "Documenten" + +#: streetup/authorizations/models.py:176 +msgid "Début de l'occupation" +msgstr "Begin van de bezetting" + +#: streetup/authorizations/models.py:177 +msgid "Fin de l'occupation" +msgstr "Einde van de bezetting" + +#: streetup/authorizations/models.py:180 +msgid "Date d'occupation" +msgstr "Bezettingsdatum" + +#: streetup/authorizations/models.py:181 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:127 +msgid "Dates d'occupation" +msgstr "Bezettingsdata" + +#: streetup/authorizations/models.py:202 +msgid "Emails vérifiés" +msgstr "Geverifieerde e-mails" + +#: streetup/authorizations/models.py:247 +msgid "Code de vérification" +msgstr "Verificatiecode" + +#: streetup/authorizations/models.py:248 +msgid "Codes de vérification" +msgstr "Verificatiecodes" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:12 +msgid "Retour à la liste" +msgstr "Terug naar de lijst" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:13 +#: streetup/authorizations/templates/authorizations/authorization_detail.html:27 +msgid "Retour" +msgstr "Terug" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:23 +msgid "Demande" +msgstr "Aanvraag" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:52 +msgid "Informations générales" +msgstr "Algemene informatie" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:55 +#: streetup/authorizations/templates/authorizations/authorization_list.html:138 +msgid "Code" +msgstr "Code" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:58 +#: streetup/authorizations/templates/authorizations/authorization_list.html:140 +msgid "Statut" +msgstr "Status" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:76 +msgid "Créé le" +msgstr "Aangemaakt op" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:79 +msgid "Mis à jour" +msgstr "Bijgewerkt" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:83 +msgid "Agent" +msgstr "Ambtenaar" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:102 +#: streetup/authorizations/templates/authorizations/authorization_list.html:141 +msgid "Demandeur" +msgstr "Aanvrager" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:132 +msgid "Début" +msgstr "Begin" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:133 +msgid "Fin" +msgstr "Einde" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:151 +msgid "Documents joints" +msgstr "Bijgevoegde documenten" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:167 +#, fuzzy +#| msgid "Documents joints" +msgid "Aucun document joint." +msgstr "Bijgevoegde documenten" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:176 +msgid "Ex: Arrêté de police" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:183 +#, fuzzy +#| msgid "Ajouter une date" +msgid "Ajouter" +msgstr "Een datum toevoegen" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:194 +#, fuzzy +#| msgid "Description de la demande" +msgid "Traitement de la demande" +msgstr "Beschrijving van de aanvraag" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:201 +msgid "Modifier le statut" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:211 +msgid "Commentaire agent" +msgstr "Opmerking ambtenaar" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:212 +msgid "Saisir un commentaire ou des notes..." +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_detail.html:216 +msgid "Enregistrer le traitement" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:41 +#: streetup/authorizations/templates/authorizations/authorization_list.html:42 +#: streetup/authorizations/templates/authorizations/authorization_list.html:58 +msgid "Formulaire public" +msgstr "Openbaar formulier" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:45 +msgid "Liste" +msgstr "Lijst" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:46 +msgid "Liste des demandes" +msgstr "Lijst van aanvragen" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:55 +msgid "Demandes d'autorisation de voirie" +msgstr "Aanvragen voor wegvergunning" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:68 +msgid "Recherche" +msgstr "Zoeken" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:82 +#: streetup/authorizations/templates/authorizations/authorization_list.html:207 +msgid "Tous les types" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:89 +#, fuzzy +#| msgid "Recherche" +msgid "Rechercher un type..." +msgstr "Zoeken" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:92 +#: streetup/authorizations/templates/authorizations/new_authorization.html:240 +msgid "Effacer" +msgstr "Wissen" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:106 +msgid "Aucun type correspondant" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:121 +msgid "Filtrer" +msgstr "Filteren" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:143 +msgid "Date de création" +msgstr "Aanmaakdatum" + +#: streetup/authorizations/templates/authorizations/authorization_list.html:177 +msgid "Aucune demande trouvée." +msgstr "Geen aanvraag gevonden." + +#: streetup/authorizations/templates/authorizations/authorization_list.html:215 +msgid "types sélectionnés" +msgstr "" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:9 +#: streetup/authorizations/templates/authorizations/authorization_success.html:37 +msgid "Demande enregistrée" +msgstr "Aanvraag opgeslagen" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:28 +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:28 +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:28 +#: streetup/authorizations/templates/authorizations/email_verify_code.html:35 +#: streetup/authorizations/templates/authorizations/new_authorization.html:9 +#: streetup/authorizations/templates/authorizations/new_authorization.html:99 +msgid "Demande d'occupation de voirie régionale" +msgstr "Aanvraag gewestelijke wegbezetting" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:41 +msgid "Numéro de référence" +msgstr "Referentienummer" + +#: streetup/authorizations/templates/authorizations/authorization_success.html:43 +msgid "Conservez ce numéro pour le suivi de votre dossier." +msgstr "Bewaar dit nummer voor de opvolging van uw dossier." + +#: streetup/authorizations/templates/authorizations/authorization_success.html:49 +msgid "" +"Votre adresse email a été confirmée et votre demande d'autorisation " +"d'occupation de voirie régionale a bien été enregistrée. Un accusé de " +"réception vous a été envoyé par email. Nos services prendront contact avec " +"vous dans les meilleurs délais." +msgstr "" +"Uw e-mailadres is bevestigd en uw aanvraag voor gewestelijke wegbezetting is " +"geregistreerd. Een ontvangstbevestiging werd u per e-mail verzonden. Onze " +"diensten nemen zo spoedig mogelijk contact met u op." + +#: streetup/authorizations/templates/authorizations/authorization_success.html:55 +msgid "" +"Votre demande d'autorisation d'occupation de voirie régionale a bien été " +"enregistrée. Un accusé de réception vous a été envoyé par email. Nos " +"services prendront contact avec vous dans les meilleurs délais." +msgstr "" +"Uw aanvraag voor gewestelijke wegbezetting is geregistreerd. Een " +"ontvangstbevestiging werd u per e-mail verzonden. Onze diensten nemen zo " +"spoedig mogelijk contact met u op." + +#: streetup/authorizations/templates/authorizations/authorization_success.html:64 +msgid "Faire une nouvelle demande" +msgstr "Een nieuwe aanvraag indienen" + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:9 +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:47 +msgid "Lien invalide" +msgstr "Ongeldige link" + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:39 +msgid "Lien expiré" +msgstr "Link verlopen" + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:41 +msgid "" +"Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 " +"heures dépassé). Veuillez soumettre une nouvelle demande." +msgstr "" +"De bevestigingslink die u heeft gebruikt, is niet meer geldig (termijn van " +"48 uur overschreden). Dien een nieuwe aanvraag in." + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:49 +msgid "" +"Le lien de confirmation que vous avez utilisé est invalide ou a déjà été " +"utilisé. Si votre demande a déjà été validée, vous pouvez en soumettre une " +"nouvelle." +msgstr "" +"De bevestigingslink die u heeft gebruikt, is ongeldig of reeds gebruikt. Als " +"uw aanvraag al is gevalideerd, kunt u een nieuwe indienen." + +#: streetup/authorizations/templates/authorizations/email_verification_failed.html:58 +#: streetup/authorizations/templates/authorizations/email_verify_code.html:55 +#: streetup/authorizations/templates/authorizations/email_verify_code.html:72 +msgid "Nouvelle demande" +msgstr "Nieuwe aanvraag" + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:9 +msgid "Confirmation de votre email" +msgstr "Bevestiging van uw e-mail" + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:37 +msgid "Vérifiez votre boîte email" +msgstr "Controleer uw e-mailbox" + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:39 +msgid "" +"Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre " +"adresse email en cliquant sur le lien que nous venons de vous envoyer." +msgstr "" +"Uw aanvraag is ontvangen. Om deze te voltooien, bevestigt u uw e-mailadres " +"door op de link te klikken die wij u zojuist hebben gestuurd." + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:47 +msgid "" +"Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas " +"l'email, vérifiez votre dossier de courrier indésirable (spam)." +msgstr "" +"De bevestigingslink is 48 uur geldig. Als u de e-mail niet vindt, controleer " +"dan uw map met ongewenste e-mail (spam)." + +#: streetup/authorizations/templates/authorizations/email_verification_pending.html:55 +msgid "Retour au formulaire" +msgstr "Terug naar het formulier" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:9 +msgid "Vérification de votre email" +msgstr "Verificatie van uw e-mail" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:46 +msgid "Code expiré" +msgstr "Code verlopen" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:48 +msgid "" +"Le code de vérification est arrivé à expiration (validité : " +"10 minutes). Veuillez soumettre une nouvelle demande pour recevoir un " +"nouveau code." +msgstr "" +"De verificatiecode is verlopen (geldigheid: 10 minuten). Dien een " +"nieuwe aanvraag in om een nieuwe code te ontvangen." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:63 +msgid "Trop de tentatives" +msgstr "Te veel pogingen" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:65 +msgid "" +"Le nombre maximum de tentatives a été atteint. Votre code est bloqué. " +"Veuillez soumettre une nouvelle demande." +msgstr "" +"Het maximale aantal pogingen is bereikt. Uw code is geblokkeerd. Dien een " +"nieuwe aanvraag in." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:80 +msgid "Entrez votre code de vérification" +msgstr "Voer uw verificatiecode in" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:82 +#, python-format +msgid "" +"Un code à 6 chiffres a été envoyé à %(email)s. Saisissez-le " +"ci-dessous. Il est valable 10 minutes." +msgstr "" +"Een 6-cijferige code werd verzonden naar %(email)s. Voer " +"hem hieronder in. Hij is 10 minuten geldig." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:92 +msgid "Code incorrect. Vérifiez l'email reçu et réessayez." +msgstr "Onjuiste code. Controleer de ontvangen e-mail en probeer opnieuw." + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:115 +msgid "Code de vérification à 6 chiffres" +msgstr "6-cijferige verificatiecode" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:118 +msgid "Vérifier" +msgstr "Verifiëren" + +#: streetup/authorizations/templates/authorizations/email_verify_code.html:124 +msgid "" +"Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier " +"indésirable (spam)." +msgstr "" +"Heeft u de e-mail niet ontvangen? Controleer uw map met ongewenste e-mail " +"(spam)." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:96 +#: streetup/authorizations/templates/authorizations/new_authorization.html:98 +msgid "Brussels Mobility" +msgstr "" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:116 +msgid "" +"Toute occupation de l'espace public doit faire l'objet d'une autorisation de " +"la part du gestionnaire de voiries. En Région de Bruxelles-Capitale, les " +"axes principaux sont gérés par la Région (Bruxelles Mobilité), tandis que les autres voiries sont " +"gérées par les communes. Les voiries régionales sont représentées en rouge sur la carte ci-dessous." +msgstr "" +"Elke bezetting van de openbare ruimte vereist een vergunning van de " +"wegbeheerder. In het Brussels Hoofdstedelijk Gewest worden de belangrijkste " +"assen beheerd door het Gewest (Brussel Mobiliteit), terwijl de andere wegen door de " +"gemeenten worden beheerd. De gewestwegen zijn weergegeven in rood op de kaart hieronder." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:125 +msgid "" +"Ce formulaire ne concerne que les autorisations sur voiries " +"régionales. Veuillez vous adresser à votre commune pour les autres " +"autorisations ou documents." +msgstr "" +"Dit formulier betreft enkel vergunningen voor gewestwegen. " +"Voor andere vergunningen of documenten wendt u zich tot uw gemeente." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:131 +msgid "" +"Pour les demandes liées à des chantiers, veuillez utiliser le système Osiris. Les demandes pour le placement de conteneurs, " +"d'échafaudages ou les déménagements se font également via ce système." +msgstr "" +"Voor aanvragen in verband met werven gebruikt u het systeem Osiris. Aanvragen voor het plaatsen van containers, " +"steigers of verhuis gebeuren ook via dit systeem." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:138 +msgid "" +"Si vous éprouvez des difficultés, contactez-nous par mail (mobilite@sprb.brussels) ou par " +"téléphone (0800 94 001)." +msgstr "" +"Heeft u problemen, neem dan contact met ons op via e-mail (mobiliteit@gob.brussels)) of " +"telefonisch (0800 94 001)." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:158 +msgid "Votre demande" +msgstr "Uw aanvraag" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:230 +msgid "Localisation (dessiner un polygone pour indiquer la zone concernée)" +msgstr "Locatie (teken een veelhoek om de betrokken zone aan te geven)" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:234 +msgid "Dessiner un polygone" +msgstr "Veelhoek tekenen" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:237 +msgid "Terminer" +msgstr "Voltooien" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:245 +msgid "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets. Cliquez sur le premier sommet pour fermer le polygone." +msgstr "" +"Klik op « Veelhoek tekenen » en klik vervolgens op de kaart om hoekpunten " +"toe te voegen. Klik op het eerste hoekpunt om de veelhoek te sluiten." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:259 +msgid "Vos coordonnées" +msgstr "Uw contactgegevens" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:292 +#: streetup/authorizations/templates/authorizations/new_authorization.html:331 +#: streetup/authorizations/templates/authorizations/new_authorization.html:351 +msgid "optionnel" +msgstr "optioneel" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:306 +msgid "Date(s) d'occupation" +msgstr "Bezettingsdatum(-data)" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:312 +msgid "Veuillez indiquer au moins une date de début d'occupation." +msgstr "Geef ten minste één begindatum van de bezetting op." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:342 +msgid "Ajouter une date" +msgstr "Een datum toevoegen" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:350 +msgid "Document(s) à joindre" +msgstr "Bij te voegen document(en)" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:374 +msgid "Ajouter un document" +msgstr "Een document toevoegen" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:381 +msgid "Envoyer la demande" +msgstr "De aanvraag verzenden" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:390 +msgid "https://servicepublic.brussels/politique-de-confidentialite/" +msgstr "https://servicepublic.brussels/nl/privacybeleid/" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:391 +msgid "Politique de confidentialité" +msgstr "Privacybeleid" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:401 +msgid "" +"Veillez à joindre en annexe l'attestation d'assurance couvrant tous les " +"risques." +msgstr "Voeg als bijlage het verzekeringsbewijs bij dat alle risico's dekt." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:402 +msgid "Veillez à joindre en annexe l'autorisation de la commune." +msgstr "Voeg als bijlage de gemeentelijke toestemming bij." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:486 +#: streetup/authorizations/templates/authorizations/new_authorization.html:535 +msgid "Polygone enregistré. Cliquez sur « Dessiner » pour le redessiner." +msgstr "Veelhoek opgeslagen. Klik op « Tekenen » om hem opnieuw te tekenen." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:506 +msgid "Cliquer pour fermer le polygone" +msgstr "Klik om de veelhoek te sluiten" + +#: streetup/authorizations/templates/authorizations/new_authorization.html:550 +msgid "" +"Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter " +"des sommets." +msgstr "" +"Klik op « Veelhoek tekenen » en klik vervolgens op de kaart om hoekpunten " +"toe te voegen." + +#: streetup/authorizations/templates/authorizations/new_authorization.html:573 +msgid "" +"Cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet " +"pour fermer le polygone." +msgstr "" + +#: streetup/authorizations/views.py:320 streetup/authorizations/views.py:369 +msgid "Vous n'avez pas accès à cette vue." +msgstr "" + +#: streetup/authorizations/views.py:386 +msgid "La demande a été mise à jour avec succès." +msgstr "" + +#: streetup/authorizations/views.py:403 +msgid "Le document a été ajouté avec succès." +msgstr "" + +#: streetup/authorizations/views.py:407 +msgid "Veuillez fournir une description et un fichier." +msgstr "" + +#~ msgid "Langue / Taal / Language" +#~ msgstr "Langue / Taal / Language" diff --git a/streetup/authorizations/migrations/0001_initial.py b/streetup/authorizations/migrations/0001_initial.py new file mode 100644 index 0000000..6f9efd4 --- /dev/null +++ b/streetup/authorizations/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import authorizations.models +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='VerifiedEmail', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), + ('verified_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Email vérifié', + 'verbose_name_plural': 'Emails vérifiés', + }, + ), + migrations.CreateModel( + name='RoadAuthorization', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(default='', max_length=50)), + ('status', models.IntegerField(choices=[(1, 'À traiter'), (2, 'En cours'), (3, 'Autorisé'), (4, 'Refusé'), (5, 'Clôturé')], default=1)), + ('language', models.CharField(choices=[('fr', 'Français'), ('nl', 'Nederlands'), ('en', 'English')], default='fr', max_length=5, verbose_name='Langue')), + ('is_email_verified', models.BooleanField(default=False, help_text="Indique si l'adresse email du demandeur a été vérifiée.", verbose_name='Email vérifié')), + ('type', models.IntegerField(choices=[(2, 'Affichage (panneau, ...)'), (21, 'Terrasse'), (22, 'Mobilier urbain'), (23, "Œuvre d'art"), (32, 'Nouvel Emplacement PMR'), (33, 'Nouvelle Zone de livraison'), (34, 'Tournage audiovisuel'), (100, 'Evénement'), (51, 'Plante grimpante'), (52, "Pied d'arbre")], verbose_name="Type d'occupation")), + ('description', models.TextField(verbose_name='Description de la demande')), + ('applicant_name', models.CharField(max_length=100, verbose_name='Nom')), + ('applicant_email', models.EmailField(max_length=254, verbose_name='Email')), + ('applicant_address', models.CharField(max_length=300, verbose_name='Adresse')), + ('applicant_enterprise', models.CharField(blank=True, max_length=100, null=True, verbose_name='Entreprise')), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('update_date', models.DateTimeField(auto_now=True)), + ('agent_comment', models.TextField(blank=True, null=True, verbose_name='Commentaire')), + ('location', models.CharField(max_length=500, verbose_name='Localisation – Adresse')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=4326, verbose_name='Localisation')), + ('agent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Agent traitant')), + ], + options={ + 'verbose_name': 'Autorisation de voirie', + 'verbose_name_plural': 'Autorisations de voirie', + 'ordering': ['status', '-code'], + }, + ), + migrations.CreateModel( + name='EmailVerificationToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=6)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('expires_at', models.DateTimeField()), + ('attempts', models.PositiveSmallIntegerField(default=0)), + ('authorization', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='verification_token', to='authorizations.roadauthorization')), + ], + options={ + 'verbose_name': 'Code de vérification', + 'verbose_name_plural': 'Codes de vérification', + }, + ), + migrations.CreateModel( + name='RoadAuthorizationDate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('begin_date', models.DateTimeField(verbose_name="Début de l'occupation")), + ('end_date', models.DateTimeField(blank=True, null=True, verbose_name="Fin de l'occupation")), + ('authorization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dates', to='authorizations.roadauthorization')), + ], + options={ + 'verbose_name': "Date d'occupation", + 'verbose_name_plural': "Dates d'occupation", + 'ordering': ['begin_date'], + }, + ), + migrations.CreateModel( + name='RoadAuthorizationDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='authorizations_documents/', validators=[authorizations.models.validate_file_size], verbose_name='Fichier')), + ('description', models.CharField(max_length=200, verbose_name='Description')), + ('authorization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='authorizations.roadauthorization')), + ], + options={ + 'verbose_name': 'Document', + 'verbose_name_plural': 'Documents', + }, + ), + ] diff --git a/streetup/authorizations/migrations/0002_create_authorizations_view.py b/streetup/authorizations/migrations/0002_create_authorizations_view.py new file mode 100644 index 0000000..c36fd30 --- /dev/null +++ b/streetup/authorizations/migrations/0002_create_authorizations_view.py @@ -0,0 +1,30 @@ +from django.db import migrations + +def create_authorizations_view(apps, schema_editor): + AppView = apps.get_model('common', 'AppView') + AppView.objects.get_or_create( + code='authorizations', + defaults={ + 'name_fr': 'Autorisations', + 'name_nl': 'Autorisaties', + 'icon': 'bi-file-earmark-text', + 'url_name': 'authorizations:authorization_list', + 'is_default': False, + 'order': 40, + } + ) + +def remove_authorizations_view(apps, schema_editor): + AppView = apps.get_model('common', 'AppView') + AppView.objects.filter(code='authorizations').delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('authorizations', '0001_initial'), + ('common', '0011_userwarehouselocationaccess_and_more'), + ] + + operations = [ + migrations.RunPython(create_authorizations_view, remove_authorizations_view), + ] diff --git a/streetup/authorizations/migrations/__init__.py b/streetup/authorizations/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/authorizations/models.py b/streetup/authorizations/models.py new file mode 100644 index 0000000..a188769 --- /dev/null +++ b/streetup/authorizations/models.py @@ -0,0 +1,251 @@ +import secrets + +from django.contrib.gis.db import models +from django.conf import settings +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from django.template.defaultfilters import truncatechars + +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + + +class RoadAuthorization(models.Model): + + AUTH_TYPE = ( + (2, _('Affichage (panneau, ...)')), + (21, _('Terrasse')), + (22, _('Mobilier urbain')), + (23, _("Œuvre d'art")), + (32, _('Nouvel Emplacement PMR')), + (33, _('Nouvelle Zone de livraison')), + (34, _('Tournage audiovisuel')), + (100, _('Evénement')), + (51, _('Plante grimpante')), + (52, _("Pied d'arbre")), + ) + + AUTH_STATUS = ( + (1, _('À traiter')), + (2, _('En cours')), + (3, _('Autorisé')), + (4, _('Refusé')), + (5, _('Clôturé')), + ) + + LANGUAGE_CHOICES = [('fr', 'Français'), ('nl', 'Nederlands'), ('en', 'English')] + + code = models.CharField(max_length=50, default='') + status = models.IntegerField(default=1, choices=AUTH_STATUS) + language = models.CharField( + max_length=5, + default='fr', + choices=LANGUAGE_CHOICES, + verbose_name=_('Langue'), + ) + is_email_verified = models.BooleanField( + default=False, + verbose_name=_("Email vérifié"), + help_text=_("Indique si l'adresse email du demandeur a été vérifiée."), + ) + type = models.IntegerField(_("Type d'occupation"), choices=AUTH_TYPE) + description = models.TextField(_('Description de la demande')) + applicant_name = models.CharField(_('Nom'), max_length=100) + applicant_email = models.EmailField(_('Email')) + applicant_address = models.CharField(_('Adresse'), max_length=300) + applicant_enterprise = models.CharField(_('Entreprise'), max_length=100, null=True, blank=True) + creation_date = models.DateTimeField(auto_now_add=True) + update_date = models.DateTimeField(auto_now=True) + agent = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, blank=True, + verbose_name=_('Agent traitant'), + ) + agent_comment = models.TextField(_('Commentaire'), null=True, blank=True) + location = models.CharField(_('Localisation – Adresse'), max_length=500) + geom = models.MultiPolygonField(_('Localisation'), srid=4326) + + @property + def end_date(self): + return self.dates.all().latest('end_date').end_date + + @property + def date_status(self): + """ + Returns: + 1 – older than 2 years + 2 – past (< 2 years) + 3 – future + 4 – active (ongoing right now) + """ + datestatus = 1 + now = datetime.now().astimezone() + for d in self.dates.all(): + end = d.end_date + if end is None: + # No end date → treat as open-ended (active if already started) + if d.begin_date <= now: + return 4 + else: + datestatus = max(3, datestatus) + elif end >= now and d.begin_date <= now: + return 4 + elif d.begin_date >= now: + datestatus = max(3, datestatus) + elif end > (now - relativedelta(years=2)): + datestatus = max(2, datestatus) + return datestatus + + @property + def truncated_description(self): + return truncatechars(self.description, 100) + + class Meta: + ordering = ['status', '-code'] + verbose_name = _('Autorisation de voirie') + verbose_name_plural = _('Autorisations de voirie') + + # Maps AUTH_TYPE values to their 2-digit code prefix + TYPE_PREFIX = { + 2: '01', + 21: '02', + 22: '03', + 23: '04', + 32: '05', + 33: '06', + 34: '07', + 100: '08', + 51: '09', + 52: '10', + } + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if not self.code and self.pk: + year = self.creation_date.year if self.creation_date else datetime.now().year + prefix = self.TYPE_PREFIX.get(self.type, '00') + # Count same-type authorizations created this year (excl. self) that already have a code + count = RoadAuthorization.objects.filter( + type=self.type, + creation_date__year=year, + ).exclude(pk=self.pk).exclude(code='').count() + seq = count + 1 + seq_str = f"{seq:03d}" if seq < 1000 else str(seq) + self.code = f"AUTH-{year}-{prefix}{seq_str}" + RoadAuthorization.objects.filter(pk=self.pk).update(code=self.code) + + def __str__(self): + return f"{self.code} – {self.truncated_description}" if self.code else self.truncated_description + + +def validate_file_size(file): + limit_mb = 15 + if file.size > limit_mb * 1024 * 1024: + raise ValidationError(_("La taille maximale du fichier est de %(limit)s Mo.") % {'limit': limit_mb}) + + +class RoadAuthorizationDocument(models.Model): + file = models.FileField( + verbose_name=_('Fichier'), + upload_to='authorizations_documents/', + validators=[validate_file_size], + ) + description = models.CharField(_('Description'), max_length=200) + authorization = models.ForeignKey( + RoadAuthorization, + on_delete=models.CASCADE, + related_name='documents', + ) + + class Meta: + verbose_name = _('Document') + verbose_name_plural = _('Documents') + + def __str__(self): + return self.description + + +class RoadAuthorizationDate(models.Model): + authorization = models.ForeignKey( + RoadAuthorization, + on_delete=models.CASCADE, + related_name='dates', + ) + begin_date = models.DateTimeField(_("Début de l'occupation")) + end_date = models.DateTimeField(_("Fin de l'occupation"), null=True, blank=True) + + class Meta: + verbose_name = _("Date d'occupation") + verbose_name_plural = _("Dates d'occupation") + ordering = ['begin_date'] + + def __str__(self): + begin_str = f"{self.begin_date:%d/%m/%Y %H:%M}" if self.begin_date else "—" + end_str = f"{self.end_date:%d/%m/%Y %H:%M}" if self.end_date else "—" + return f"{begin_str} → {end_str}" + + +# --------------------------------------------------------------------------- +# Email verification +# --------------------------------------------------------------------------- + +class VerifiedEmail(models.Model): + """Whitelist des adresses email ayant été vérifiées.""" + + email = models.EmailField(unique=True, verbose_name=_("Email")) + verified_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = _("Email vérifié") + verbose_name_plural = _("Emails vérifiés") + + def __str__(self): + return self.email + + +class EmailVerificationToken(models.Model): + """Code à 6 chiffres de vérification d'email lié à une demande d'autorisation en attente.""" + + authorization = models.OneToOneField( + RoadAuthorization, + on_delete=models.CASCADE, + related_name='verification_token', + ) + code = models.CharField(max_length=6) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + attempts = models.PositiveSmallIntegerField(default=0) + + TOKEN_VALIDITY_MINUTES = 10 + MAX_ATTEMPTS = 5 + + class Meta: + verbose_name = "Token de vérification" + verbose_name_plural = "Tokens de vérification" + + def is_expired(self): + return timezone.now() > self.expires_at + + def is_locked(self): + return self.attempts >= self.MAX_ATTEMPTS + + @classmethod + def create_for_authorization(cls, authorization): + code = f"{secrets.randbelow(1_000_000):06d}" + expires_at = timezone.now() + timedelta(minutes=cls.TOKEN_VALIDITY_MINUTES) + # Supprimer tout token existant pour cette autorisation + cls.objects.filter(authorization=authorization).delete() + return cls.objects.create( + authorization=authorization, + code=code, + expires_at=expires_at, + ) + + class Meta: + verbose_name = _("Code de vérification") + verbose_name_plural = _("Codes de vérification") + + def __str__(self): + return f"Code pour {self.authorization}" diff --git a/streetup/authorizations/templates/authorizations/authorization_detail.html b/streetup/authorizations/templates/authorizations/authorization_detail.html new file mode 100644 index 0000000..c69d6b7 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/authorization_detail.html @@ -0,0 +1,277 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block head %} + +{% endblock head %} + +{% block main %} +
+ + +
+
+ +
+

+ + {% translate "Demande" %} {{ authorization.code|default:authorization.pk }} +

+ +
+ + {% if messages %} +
+
+ {% for message in messages %} + + {% endfor %} +
+
+ {% endif %} + +
+ + +
+ + +
+
{% translate "Informations générales" %}
+
+
+
{% translate "Code" %}
+
{{ authorization.code|default:"—" }}
+ +
{% translate "Statut" %}
+
+ {% if authorization.status == 1 %} + {{ authorization.get_status_display }} + {% elif authorization.status == 2 %} + {{ authorization.get_status_display }} + {% elif authorization.status == 3 %} + {{ authorization.get_status_display }} + {% elif authorization.status == 4 %} + {{ authorization.get_status_display }} + {% else %} + {{ authorization.get_status_display }} + {% endif %} +
+ +
{% translate "Type" %}
+
{{ authorization.get_type_display }}
+ +
{% translate "Créé le" %}
+
{{ authorization.creation_date|date:"d/m/Y H:i" }}
+ +
{% translate "Mis à jour" %}
+
{{ authorization.update_date|date:"d/m/Y H:i" }}
+ + {% if authorization.agent %} +
{% translate "Agent" %}
+
{{ authorization.agent }}
+ {% endif %} +
+
+
+ + +
+
{% translate "Description de la demande" %}
+
+

{{ authorization.description }}

+
{% translate "Localisation" %}
+
{{ authorization.location }}
+
+
+ + +
+
{% translate "Demandeur" %}
+
+
+
{% translate "Nom" %}
+
{{ authorization.applicant_name }}
+ +
{% translate "Email" %}
+
+ {{ authorization.applicant_email }} +
+ +
{% translate "Adresse" %}
+
{{ authorization.applicant_address }}
+ + {% if authorization.applicant_enterprise %} +
{% translate "Entreprise" %}
+
{{ authorization.applicant_enterprise }}
+ {% endif %} +
+
+
+ + + {% if authorization.dates.all %} +
+
{% translate "Dates d'occupation" %}
+
+ + + + + + + + + {% for d in authorization.dates.all %} + + + + + {% endfor %} + +
{% translate "Début" %}{% translate "Fin" %}
{{ d.begin_date|date:"d/m/Y H:i" }}{{ d.end_date|date:"d/m/Y H:i" }}
+
+
+ {% endif %} + + +
+
{% translate "Documents joints" %}
+ {% if authorization.documents.all %} +
    + {% for doc in authorization.documents.all %} +
  • + + {{ doc.description }} + + + + +
  • + {% endfor %} +
+ {% else %} +
+ {% translate "Aucun document joint." %} +
+ {% endif %} + +
+ + +
+
+ {% translate "Traitement de la demande" %} +
+
+
+ {% csrf_token %} + +
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+ + +
+
+
{% translate "Localisation" %}
+
+
+
+
+
+ +
+ +
+
+
+ + + +{% endblock main %} diff --git a/streetup/authorizations/templates/authorizations/authorization_list.html b/streetup/authorizations/templates/authorizations/authorization_list.html new file mode 100644 index 0000000..68bba66 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/authorization_list.html @@ -0,0 +1,336 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block head %} + + +{% endblock head %} + +{% block main %} +
+ + +
+
+ +
+

+ {% translate "Demandes d'autorisation de voirie" %} +

+ + {% translate "Formulaire public" %} + +
+ + +
+
+
+
+ + {{ filter_form.search_text }} +
+
+ + +
+
+
+ {{ filter_form.include_archives }} + +
+
+
+ + + + +
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + {% for auth in authorizations %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Code" %} {% translate "Type" %} {% translate "Statut" %} {% translate "Demandeur" %} {% translate "Localisation" %} {% translate "Date de création" %}
{{ auth.code|default:"—" }}{{ auth.get_type_display }} + {% if auth.status == 1 %} + {{ auth.get_status_display }} + {% elif auth.status == 2 %} + {{ auth.get_status_display }} + {% elif auth.status == 3 %} + {{ auth.get_status_display }} + {% elif auth.status == 4 %} + {{ auth.get_status_display }} + {% else %} + {{ auth.get_status_display }} + {% endif %} + {{ auth.applicant_name }}{{ auth.location }}{{ auth.creation_date|date:"d/m/Y" }} + + + +
+ {% translate "Aucune demande trouvée." %} +
+
+
+
+ +
+
+
+{% endblock main %} + +{% block script %} + +{% endblock script %} diff --git a/streetup/authorizations/templates/authorizations/authorization_success.html b/streetup/authorizations/templates/authorizations/authorization_success.html new file mode 100644 index 0000000..6c0d25d --- /dev/null +++ b/streetup/authorizations/templates/authorizations/authorization_success.html @@ -0,0 +1,71 @@ +{% load i18n %} +{% load static %} + + + + + + + {% translate "Demande enregistrée" %} – Brussels Mobility + + + + + + + +
+
+ +
+

{% translate "Demande enregistrée" %}

+ + {% if auth_code %} +
+
{% translate "Numéro de référence" %}
+
{{ auth_code }}
+
{% translate "Conservez ce numéro pour le suivi de votre dossier." %}
+
+ {% endif %} + +

+ {% if just_verified %} + {% blocktranslate trimmed %} + Votre adresse email a été confirmée et votre demande d'autorisation d'occupation de voirie régionale a bien été enregistrée. + Un accusé de réception vous a été envoyé par email. + Nos services prendront contact avec vous dans les meilleurs délais. + {% endblocktranslate %} + {% else %} + {% blocktranslate trimmed %} + Votre demande d'autorisation d'occupation de voirie régionale a bien été enregistrée. + Un accusé de réception vous a été envoyé par email. + Nos services prendront contact avec vous dans les meilleurs délais. + {% endblocktranslate %} + {% endif %} +

+ +
+ + + + diff --git a/streetup/authorizations/templates/authorizations/email_verification_failed.html b/streetup/authorizations/templates/authorizations/email_verification_failed.html new file mode 100644 index 0000000..dc1d028 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/email_verification_failed.html @@ -0,0 +1,65 @@ +{% load i18n %} +{% load static %} + + + + + + + {% translate "Lien invalide" %} – Brussels Mobility + + + + + + + +
+
+ +
+ + {% if reason == 'expired' %} +

{% translate "Lien expiré" %}

+

+ {% blocktranslate trimmed %} + Le lien de confirmation que vous avez utilisé n'est plus valide (délai de 48 heures dépassé). + Veuillez soumettre une nouvelle demande. + {% endblocktranslate %} +

+ {% else %} +

{% translate "Lien invalide" %}

+

+ {% blocktranslate trimmed %} + Le lien de confirmation que vous avez utilisé est invalide ou a déjà été utilisé. + Si votre demande a déjà été validée, vous pouvez en soumettre une nouvelle. + {% endblocktranslate %} +

+ {% endif %} + + +
+ + + + diff --git a/streetup/authorizations/templates/authorizations/email_verification_pending.html b/streetup/authorizations/templates/authorizations/email_verification_pending.html new file mode 100644 index 0000000..c39dc7c --- /dev/null +++ b/streetup/authorizations/templates/authorizations/email_verification_pending.html @@ -0,0 +1,62 @@ +{% load i18n %} +{% load static %} + + + + + + + {% translate "Confirmation de votre email" %} – Brussels Mobility + + + + + + + +
+
+ +
+

{% translate "Vérifiez votre boîte email" %}

+

+ {% blocktranslate trimmed %} + Votre demande a bien été reçue. Pour la finaliser, veuillez confirmer votre adresse + email en cliquant sur le lien que nous venons de vous envoyer. + {% endblocktranslate %} +

+
+ +
+ {% blocktranslate trimmed %} + Le lien de confirmation est valable 48 heures. Si vous ne trouvez pas l'email, + vérifiez votre dossier de courrier indésirable (spam). + {% endblocktranslate %} +
+
+ +
+ + + + diff --git a/streetup/authorizations/templates/authorizations/email_verify_code.html b/streetup/authorizations/templates/authorizations/email_verify_code.html new file mode 100644 index 0000000..f819a43 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/email_verify_code.html @@ -0,0 +1,146 @@ +{% load i18n %} +{% load static %} + + + + + + + {% translate "Vérification de votre email" %} – Brussels Mobility + + + + + + + +
+ + {% if error == 'expired' %} +
+ +
+

{% translate "Code expiré" %}

+

+ {% blocktranslate trimmed %} + Le code de vérification est arrivé à expiration (validité : 10 minutes). + Veuillez soumettre une nouvelle demande pour recevoir un nouveau code. + {% endblocktranslate %} +

+ + + {% elif error == 'locked' %} +
+ +
+

{% translate "Trop de tentatives" %}

+

+ {% blocktranslate trimmed %} + Le nombre maximum de tentatives a été atteint. Votre code est bloqué. + Veuillez soumettre une nouvelle demande. + {% endblocktranslate %} +

+ + + {% else %} +
+ +
+

{% translate "Entrez votre code de vérification" %}

+

+ {% blocktranslate trimmed with email=authorization.applicant_email %} + Un code à 6 chiffres a été envoyé à {{ email }}. + Saisissez-le ci-dessous. Il est valable 10 minutes. + {% endblocktranslate %} +

+ + {% if error == 'wrong_code' %} +
+ +
+ {% blocktranslate trimmed with remaining=token_obj.MAX_ATTEMPTS|add:"-1"|add:"-"|add:token_obj.attempts %} + Code incorrect. Vérifiez l'email reçu et réessayez. + {% endblocktranslate %} +
+
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+
+ +

+ {% blocktranslate trimmed %} + Vous n'avez pas reçu l'email ? Vérifiez votre dossier de courrier indésirable (spam). + {% endblocktranslate %} +

+ {% endif %} + +
+ + + + + diff --git a/streetup/authorizations/templates/authorizations/emails/authorization_confirmation.txt b/streetup/authorizations/templates/authorizations/emails/authorization_confirmation.txt new file mode 100644 index 0000000..159007e --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/authorization_confirmation.txt @@ -0,0 +1,51 @@ +Madame, Monsieur, + +Nous avons bien reçu votre demande d'autorisation d'occupation de la voirie. +{% load settings_tags %}Votre dossier est traité par la Direction Gestion et Inspection de {% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %}. + +Numéro de référence : {{ authorization.code }} + +➡️ Que se passe-t-il maintenant ? + + • Nous analysons votre demande. + • Le délai de traitement est de 5 jours ouvrables à partir de la réception de votre demande. + +➡️ Dans ce délai de 5 jours ouvrables : + + • soit nous vous envoyons l'autorisation ; + • soit nous vous informons : + o des informations ou documents manquants, + o des suites de la procédure pour compléter votre dossier. + +📄 Copie de votre demande + +Vous trouverez ci-dessous une copie des informations que vous avez transmises via le formulaire en ligne. + + Numéro de référence : {{ authorization.code }} + Type de demande : {{ authorization.get_type_display }} + Nom : {{ authorization.applicant_name }}{% if authorization.applicant_enterprise %} + Entreprise : {{ authorization.applicant_enterprise }}{% endif %} + Adresse du demandeur : {{ authorization.applicant_address }} + Localisation : {{ authorization.location }} +{% if dates %} Période(s) d'occupation : +{% for d in dates %} • {{ d.begin_date|date:"d/m/Y H:i" }} → {{ d.end_date|date:"d/m/Y H:i" }} +{% endfor %}{% endif %} + Description : +{{ authorization.description }} + +⚠️ Important + + • Cet e-mail est un accusé de réception. + • Il ne vaut pas autorisation. + • Vous ne pouvez pas commencer l'occupation de la voirie tant que vous n'avez pas reçu une décision officielle. + +📧 Contact + +Pour toute question, vous pouvez nous écrire à : +{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %} + +Cordialement, + +{% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %} +Direction Gestion et Inspection +Autorisations – Occupation de la voirie diff --git a/streetup/authorizations/templates/authorizations/emails/en/authorization_confirmation.txt b/streetup/authorizations/templates/authorizations/emails/en/authorization_confirmation.txt new file mode 100644 index 0000000..219712a --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/en/authorization_confirmation.txt @@ -0,0 +1,53 @@ +{% autoescape off %} +Dear {{ authorization.applicant_name }}, + +We have received your application for authorisation to use the public road. +{% load settings_tags %}Your application is handled by the Management and Inspection Department of {% get_setting "ORGANIZATION_NAME" "Brussels Mobility" %}. + +Reference number: {{ authorization.code }} + +➡️ What happens next? + + • We will analyse your application. + • The processing time is 5 working days from the receipt of your application. + +➡️ Within 5 working days: + + • we will either send you the authorisation; + • or we will inform you of: + o missing information or documents, + o the next steps in the procedure to complete your application. + +📄 Copy of your application + +You will find below a copy of the information you submitted via the online form. + + Reference : {{ authorization.code }} + Type : {{ authorization.get_type_display }} + Name : {{ authorization.applicant_name }}{% if authorization.applicant_enterprise %} + Company : {{ authorization.applicant_enterprise }}{% endif %} + Address : {{ authorization.applicant_address }} + Location : {{ authorization.location }} +{% if dates %} Occupation period(s): +{% for d in dates %} • Start: {{ d.begin_date|date:"d/m/Y H:i" }}{% if d.end_date %} → End: {{ d.end_date|date:"d/m/Y H:i" }}{% else %} (end date not specified){% endif %} +{% endfor %}{% endif %} + Description : +{{ authorization.description }} + +⚠️ Important + + • This e-mail is an acknowledgement of receipt. + • It does not constitute an authorisation. + • You may not begin using the public road until you have received an official decision. + +📧 Contact + +For any questions, please contact us at: +{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %} + +Yours sincerely, + +{% get_setting "ORGANIZATION_NAME" "Brussels Mobility" %} +Management and Inspection Department +Authorisations – Use of public road +{% endautoescape %} diff --git a/streetup/authorizations/templates/authorizations/emails/en/verify_email.txt b/streetup/authorizations/templates/authorizations/emails/en/verify_email.txt new file mode 100644 index 0000000..3ac77a4 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/en/verify_email.txt @@ -0,0 +1,20 @@ +{% autoescape off %} +Dear {{ applicant_name }}, + +Thank you for your application for authorisation to use the public road. + +To finalise your application, please enter the verification code below on the confirmation page: + + CODE: {{ code }} + +This code is valid for {{ validity_minutes }} minutes from the time this email was received. + +Confirmation page: +{{ verify_page_url }} + +If you did not submit this application, you can ignore this message. + +--- +Brussels Mobility +Management and Inspection Department – Road authorisations +{% endautoescape %} diff --git a/streetup/authorizations/templates/authorizations/emails/fr/authorization_confirmation.txt b/streetup/authorizations/templates/authorizations/emails/fr/authorization_confirmation.txt new file mode 100644 index 0000000..aa9bf52 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/fr/authorization_confirmation.txt @@ -0,0 +1,53 @@ +{% autoescape off %} +Madame, Monsieur, + +Nous avons bien reçu votre demande d'autorisation d'occupation de la voirie. +{% load settings_tags %}Votre dossier est traité par la Direction Gestion et Inspection de {% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %}. + +Numéro de référence : {{ authorization.code }} + +➡️ Que se passe-t-il maintenant ? + + • Nous analysons votre demande. + • Le délai de traitement est de 5 jours ouvrables à partir de la réception de votre demande. + +➡️ Dans ce délai de 5 jours ouvrables : + + • soit nous vous envoyons l'autorisation ; + • soit nous vous informons : + o des informations ou documents manquants, + o des suites de la procédure pour compléter votre dossier. + +📄 Copie de votre demande + +Vous trouverez ci-dessous une copie des informations que vous avez transmises via le formulaire en ligne. + + Référence : {{ authorization.code }} + Type : {{ authorization.get_type_display }} + Nom : {{ authorization.applicant_name }}{% if authorization.applicant_enterprise %} + Entreprise : {{ authorization.applicant_enterprise }}{% endif %} + Adresse : {{ authorization.applicant_address }} + Localisation: {{ authorization.location }} +{% if dates %} Période(s) d'occupation : +{% for d in dates %} • Début : {{ d.begin_date|date:"d/m/Y H:i" }}{% if d.end_date %} → Fin : {{ d.end_date|date:"d/m/Y H:i" }}{% else %} (date de fin non précisée){% endif %} +{% endfor %}{% endif %} + Description : +{{ authorization.description }} + +⚠️ Important + + • Cet e-mail est un accusé de réception. + • Il ne vaut pas autorisation. + • Vous ne pouvez pas commencer l'occupation de la voirie tant que vous n'avez pas reçu une décision officielle. + +📧 Contact + +Pour toute question, vous pouvez nous écrire à : +{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %} + +Cordialement, + +{% get_setting "ORGANIZATION_NAME_FR" "Bruxelles Mobilité" %} +Direction Gestion et Inspection +Autorisations – Occupation de la voirie +{% endautoescape %} diff --git a/streetup/authorizations/templates/authorizations/emails/fr/verify_email.txt b/streetup/authorizations/templates/authorizations/emails/fr/verify_email.txt new file mode 100644 index 0000000..c50066f --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/fr/verify_email.txt @@ -0,0 +1,20 @@ +{% autoescape off %} +Bonjour {{ applicant_name }}, + +Merci pour votre demande d'autorisation d'occupation de voirie régionale. + +Pour finaliser votre demande, saisissez le code de vérification ci-dessous sur la page de confirmation : + + CODE : {{ code }} + +Ce code est valable {{ validity_minutes }} minutes à compter de la réception de cet email. + +Page de confirmation : +{{ verify_page_url }} + +Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. + +--- +Brussels Mobility +Direction Infrastructure Routière – Autorisations de voirie +{% endautoescape %} diff --git a/streetup/authorizations/templates/authorizations/emails/nl/authorization_confirmation.txt b/streetup/authorizations/templates/authorizations/emails/nl/authorization_confirmation.txt new file mode 100644 index 0000000..8493500 --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/nl/authorization_confirmation.txt @@ -0,0 +1,53 @@ +{% autoescape off %} +Geachte {{ authorization.applicant_name }}, + +Wij hebben uw aanvraag voor een vergunning tot inname van de openbare weg goed ontvangen. +{% load settings_tags %}Uw dossier wordt behandeld door de Directie Beheer en Inspectie van {% get_setting "ORGANIZATION_NAME_NL" "Brussel Mobiliteit" %}. + +Referentienummer : {{ authorization.code }} + +➡️ Wat gebeurt er nu ? + + • Wij analyseren uw aanvraag. + • De behandelingstermijn bedraagt 5 werkdagen vanaf de ontvangst van uw aanvraag. + +➡️ Binnen deze termijn van 5 werkdagen : + + • ofwel sturen wij u de vergunning ; + • ofwel informeren wij u over : + o ontbrekende informatie of documenten, + o het verdere verloop van de procedure om uw dossier te vervolledigen. + +📄 Kopie van uw aanvraag + +U vindt hieronder een kopie van de informatie die u via het online formulier heeft ingediend. + + Referentie : {{ authorization.code }} + Type aanvraag: {{ authorization.get_type_display }} + Naam : {{ authorization.applicant_name }}{% if authorization.applicant_enterprise %} + Onderneming : {{ authorization.applicant_enterprise }}{% endif %} + Adres : {{ authorization.applicant_address }} + Locatie : {{ authorization.location }} +{% if dates %} Periode(n) van inname : +{% for d in dates %} • Begin : {{ d.begin_date|date:"d/m/Y H:i" }}{% if d.end_date %} → Einde : {{ d.end_date|date:"d/m/Y H:i" }}{% else %} (einddatum niet opgegeven){% endif %} +{% endfor %}{% endif %} + Beschrijving : +{{ authorization.description }} + +⚠️ Belangrijk + + • Dit e-mailbericht is een ontvangstbewijs. + • Het geldt niet als vergunning. + • U mag de openbare weg niet innemen zolang u geen officiële beslissing heeft ontvangen. + +📧 Contact + +Voor vragen kunt u ons bereiken op : +{% get_setting "EMAIL_AUTHORIZATIONS_BCC" "autorisation.voiries@sprb.brussels" %} + +Met vriendelijke groeten, + +{% get_setting "ORGANIZATION_NAME_NL" "Brussel Mobiliteit" %} +Directie Beheer en Inspectie +Vergunningen – Inname van de openbare weg +{% endautoescape %} diff --git a/streetup/authorizations/templates/authorizations/emails/nl/verify_email.txt b/streetup/authorizations/templates/authorizations/emails/nl/verify_email.txt new file mode 100644 index 0000000..09ccead --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/nl/verify_email.txt @@ -0,0 +1,20 @@ +{% autoescape off %} +Geachte {{ applicant_name }}, + +Bedankt voor uw aanvraag voor een vergunning tot inname van de openbare weg. + +Om uw aanvraag te voltooien, voert u de verificatiecode hieronder in op de bevestigingspagina : + + CODE : {{ code }} + +Deze code is {{ validity_minutes }} minuten geldig vanaf de ontvangst van dit e-mailbericht. + +Bevestigingspagina : +{{ verify_page_url }} + +Als u deze aanvraag niet heeft ingediend, kunt u dit bericht negeren. + +--- +Brussel Mobiliteit +Directie Beheer en Inspectie – Vergunningen openbare weg +{% endautoescape %} diff --git a/streetup/authorizations/templates/authorizations/emails/verify_email.txt b/streetup/authorizations/templates/authorizations/emails/verify_email.txt new file mode 100644 index 0000000..cc9b5de --- /dev/null +++ b/streetup/authorizations/templates/authorizations/emails/verify_email.txt @@ -0,0 +1,16 @@ +Bonjour {{ applicant_name }}, + +Merci pour votre demande d'autorisation d'occupation de voirie régionale. + +Pour finaliser votre demande, saisissez le code de vérification ci-dessous sur la page de confirmation : + + CODE : {{ code }} + +Ce code est valable {{ validity_minutes }} minutes à compter de la réception de cet email. + + +Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. + +--- +Brussels Mobility +Direction Infrastructure Routière – Autorisations de voirie diff --git a/streetup/authorizations/templates/authorizations/new_authorization.html b/streetup/authorizations/templates/authorizations/new_authorization.html new file mode 100644 index 0000000..30f56fb --- /dev/null +++ b/streetup/authorizations/templates/authorizations/new_authorization.html @@ -0,0 +1,673 @@ +{% load i18n %} +{% load static %} + + + + + + + {% translate "Demande d'occupation de voirie régionale" %} – Brussels Mobility + + + + + + + + + + + + + + + +
+ + +
+
+

+ {% blocktranslate trimmed %} + Toute occupation de l'espace public doit faire l'objet d'une autorisation de la part du gestionnaire de voiries. + En Région de Bruxelles-Capitale, les axes principaux sont gérés par la Région + (Bruxelles Mobilité), + tandis que les autres voiries sont gérées par les communes. + Les voiries régionales sont représentées en rouge sur la carte ci-dessous. + {% endblocktranslate %} +

+

+ {% blocktranslate trimmed %} + Ce formulaire ne concerne que les autorisations sur voiries régionales. + Veuillez vous adresser à votre commune pour les autres autorisations ou documents. + {% endblocktranslate %} +

+

+ {% blocktranslate trimmed %} + Pour les demandes liées à des chantiers, veuillez utiliser le système + Osiris. + Les demandes pour le placement de conteneurs, d'échafaudages ou les déménagements se font également via ce système. + {% endblocktranslate %} +

+

+ {% blocktranslate trimmed %} + Si vous éprouvez des difficultés, contactez-nous par mail + (mobilite@sprb.brussels) + ou par téléphone (0800 94 001). + {% endblocktranslate %} +

+
+
+ +
+ {% csrf_token %} + + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} + + +
{% for radio in form.language %}{{ radio.tag }}{% endfor %}
+
+
+ {% translate "Votre demande" %} +
+
+ + +
+ +
+ {% for radio in form.type %} +
+ {{ radio.tag }} + +
+ {% endfor %} +
+ {% if form.type.errors %} +
{{ form.type.errors }}
+ {% endif %} +
+
+ + +
+ + {{ form.description }} + {% if form.description.errors %} +
{{ form.description.errors }}
+ {% endif %} +
+ + +
+ + {{ form.location }} + {% if form.location.errors %} +
{{ form.location.errors }}
+ {% endif %} +
+ + +
+ +
+ + + +
+
+

+ {% translate "Cliquez sur « Dessiner un polygone » puis cliquez sur la carte pour ajouter des sommets. Cliquez sur le premier sommet pour fermer le polygone." %} +

+ + {{ form.geom_geojson }} + {% if form.geom_geojson.errors %} +
{{ form.geom_geojson.errors }}
+ {% endif %} +
+ +
+
+ + +
+
{% translate "Vos coordonnées" %}
+
+
+
+ + {{ form.applicant_name }} + {% if form.applicant_name.errors %} +
{{ form.applicant_name.errors }}
+ {% endif %} +
+
+ + {{ form.applicant_email }} + {% if form.applicant_email.errors %} +
{{ form.applicant_email.errors }}
+ {% endif %} +
+
+ + {{ form.applicant_address }} + {% if form.applicant_address.errors %} +
{{ form.applicant_address.errors }}
+ {% endif %} +
+
+ + {{ form.applicant_enterprise }} + {% if form.applicant_enterprise.errors %} +
{{ form.applicant_enterprise.errors }}
+ {% endif %} +
+
+
+
+ + +
+
+ {% translate "Date(s) d'occupation" %} +
+
+ {% if date_error %} +
+ + {% translate "Veuillez indiquer au moins une date de début d'occupation." %} +
+ {% endif %} + {{ date_formset.management_form }} +
+ {% for date_form in date_formset %} +
+
+ + {{ date_form.begin_date }} + {% if date_form.begin_date.errors %} +
{{ date_form.begin_date.errors }}
+ {% endif %} +
+
+ + {{ date_form.end_date }} + {% if date_form.end_date.errors %} +
{{ date_form.end_date.errors }}
+ {% endif %} +
+
+ {% endfor %} +
+ +
+
+ + +
+
+ {% translate "Document(s) à joindre" %} + ({% translate "optionnel" %}) +
+
+ {{ doc_formset.management_form }} +
+ {% for doc_form in doc_formset %} +
+
+ + {{ doc_form.description }} +
+
+ + {{ doc_form.file }} +
+
+ {% endfor %} +
+ +
+
+ +
+ +
+ +
+
+ + + + + + + + +{% blocktranslate asvar msgAssurance %}Veillez à joindre en annexe l'attestation d'assurance couvrant tous les risques.{% endblocktranslate %} +{% blocktranslate asvar msgCommune %}Veillez à joindre en annexe l'autorisation de la commune.{% endblocktranslate %} + + + + + diff --git a/streetup/authorizations/tests.py b/streetup/authorizations/tests.py new file mode 100644 index 0000000..36ee1df --- /dev/null +++ b/streetup/authorizations/tests.py @@ -0,0 +1,49 @@ +from django.test import TestCase +from django.utils import timezone +from django.contrib.gis.geos import MultiPolygon, Polygon +from datetime import datetime +from authorizations.models import RoadAuthorization, RoadAuthorizationDate + +class RoadAuthorizationDateTest(TestCase): + def setUp(self): + # Create a simple MultiPolygon + poly = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + multipoly = MultiPolygon(poly, srid=4326) + + self.auth = RoadAuthorization.objects.create( + type=2, + description="Test authorization", + applicant_name="John Doe", + applicant_email="john@example.com", + applicant_address="123 Main St", + location="Brussels", + geom=multipoly + ) + + def test_str_representation_both_dates(self): + begin = timezone.make_aware(datetime(2026, 6, 29, 10, 0)) + end = timezone.make_aware(datetime(2026, 6, 29, 18, 0)) + auth_date = RoadAuthorizationDate( + authorization=self.auth, + begin_date=begin, + end_date=end + ) + self.assertEqual(str(auth_date), "29/06/2026 10:00 → 29/06/2026 18:00") + + def test_str_representation_null_end_date(self): + begin = timezone.make_aware(datetime(2026, 6, 29, 10, 0)) + auth_date = RoadAuthorizationDate( + authorization=self.auth, + begin_date=begin, + end_date=None + ) + self.assertEqual(str(auth_date), "29/06/2026 10:00 → —") + + def test_str_representation_null_begin_date(self): + end = timezone.make_aware(datetime(2026, 6, 29, 18, 0)) + auth_date = RoadAuthorizationDate( + authorization=self.auth, + begin_date=None, + end_date=end + ) + self.assertEqual(str(auth_date), "— → 29/06/2026 18:00") diff --git a/streetup/authorizations/urls.py b/streetup/authorizations/urls.py new file mode 100644 index 0000000..6bb6441 --- /dev/null +++ b/streetup/authorizations/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from django.contrib.auth.decorators import login_not_required + +from . import views + +app_name = 'authorizations' + +urlpatterns = [ + # Public + path('new/', login_not_required(views.new_authorization), name='new_authorization'), + path('success/', login_not_required(views.authorization_success), name='authorization_success'), + path('verify//', login_not_required(views.verify_email_code), name='verify_email_code'), + + # Staff + path('', views.authorization_list, name='authorization_list'), + path('/', views.authorization_detail, name='authorization_detail'), +] diff --git a/streetup/authorizations/views.py b/streetup/authorizations/views.py new file mode 100644 index 0000000..7cf4e65 --- /dev/null +++ b/streetup/authorizations/views.py @@ -0,0 +1,416 @@ +import json +import logging +import smtplib +import socket +import ssl + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Polygon +from django.core.mail import EmailMultiAlternatives +from django.shortcuts import render, redirect, get_object_or_404 +from django.template.loader import render_to_string +from django.urls import reverse +from django.utils import translation +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_GET +from django.core.exceptions import PermissionDenied +from django.contrib import messages +from common.models import UserConfig + +from .forms import ( + AuthorizationForm, + AuthorizationDocumentFormSet, + AuthorizationDateFormSet, +) +from .models import RoadAuthorization, VerifiedEmail, EmailVerificationToken + +logger = logging.getLogger(__name__) + + +def _is_smtp_available(timeout=5): + """Test du serveur SMTP avant envoi d'emails.""" + email_host = getattr(settings, 'EMAIL_HOST', None) + email_port = getattr(settings, 'EMAIL_PORT', 25) + use_ssl = bool(getattr(settings, 'EMAIL_USE_SSL', False)) + use_tls = bool(getattr(settings, 'EMAIL_USE_TLS', False)) + if not email_host: + return False + try: + email_port = int(email_port) if email_port else 25 + except (ValueError, TypeError): + email_port = 25 + if email_port == 465: + use_ssl = True + try: + if use_ssl: + ctx = ssl.create_default_context() + with smtplib.SMTP_SSL(host=email_host, port=email_port, timeout=timeout, context=ctx) as s: + s.ehlo() + else: + with smtplib.SMTP(host=email_host, port=email_port, timeout=timeout) as s: + s.ehlo() + if use_tls: + ctx = ssl.create_default_context() + s.starttls(context=ctx) + s.ehlo() + return True + except Exception as exc: + logger.warning("SMTP not available: %s", exc) + return False + + +def _resolve_recipients(addresses): + """Applique l'override d'email si configuré.""" + override = getattr(settings, 'EMAIL_OVERRIDE', None) + if override: + return [override] + return addresses + + +_VERIFY_SUBJECTS = { + 'fr': "{code} – Confirmation de votre adresse email – {org}", + 'nl': "{code} – Bevestiging van uw e-mailadres – {org}", + 'en': "{code} – Confirmation of your email address – {org}", +} + +_ACK_SUBJECTS = { + 'fr': "{code} – Accusé de réception de votre demande – Occupation de voirie régionale", + 'nl': "{code} – Ontvangstbewijs van uw aanvraag – Inname van het openbaar domein", + 'en': "{code} – Acknowledgement of your application – Use of public road", +} + +_BCC_ADDRESS = getattr(settings, 'EMAIL_AUTHORIZATIONS_BCC', 'autorisation.voiries@sprb.brussels') + + +def _send_verification_email(request, authorization, token_obj): + """Envoie l'email de vérification avec le code à 6 chiffres, dans la langue de l'utilisateur.""" + lang = authorization.language or 'fr' + verify_page_url = request.build_absolute_uri( + reverse('authorizations:verify_email_code', args=[authorization.pk]) + ) + context = { + 'applicant_name': authorization.applicant_name, + 'code': token_obj.code, + 'verify_page_url': verify_page_url, + 'validity_minutes': EmailVerificationToken.TOKEN_VALIDITY_MINUTES, + } + org_name = getattr(settings, 'ORGANIZATION_NAME', 'Brussels Mobility') + org_name_nl = getattr(settings, 'ORGANIZATION_NAME_NL', 'Brussel Mobiliteit') + org = org_name_nl if lang == 'nl' else org_name + subject = _VERIFY_SUBJECTS.get(lang, _VERIFY_SUBJECTS['fr']).format(code=authorization.code, org=org) + body = render_to_string(f'authorizations/emails/{lang}/verify_email.txt', context) + recipients = _resolve_recipients([authorization.applicant_email]) + if not _is_smtp_available(): + logger.warning("SMTP non disponible – email de vérification non envoyé pour %s", authorization.applicant_email) + return + try: + msg = EmailMultiAlternatives( + subject=subject, + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=recipients, + ) + msg.send() + logger.info("Email de vérification envoyé à %s", recipients) + except Exception as exc: + logger.exception("Erreur lors de l'envoi de l'email de vérification: %s", exc) + + +def _send_acknowledgment_email(request, authorization): + """Envoie l'accusé de réception après confirmation de l'email, dans la langue de l'utilisateur.""" + lang = authorization.language or 'fr' + context = { + 'authorization': authorization, + 'dates': authorization.dates.all(), + } + subject = _ACK_SUBJECTS.get(lang, _ACK_SUBJECTS['fr']).format(code=authorization.code) + body = render_to_string(f'authorizations/emails/{lang}/authorization_confirmation.txt', context) + recipients = _resolve_recipients([authorization.applicant_email]) + # BCC uniquement en production (pas en mode override) + override = getattr(settings, 'EMAIL_OVERRIDE', None) + bcc = [] if override else [_BCC_ADDRESS] + if not _is_smtp_available(): + logger.warning("SMTP non disponible – accusé de réception non envoyé pour %s", authorization.code) + return + try: + msg = EmailMultiAlternatives( + subject=subject, + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=recipients, + bcc=bcc, + ) + msg.send() + logger.info("Accusé de réception envoyé à %s (bcc: %s) pour %s", recipients, bcc, authorization.code) + except Exception as exc: + logger.exception("Erreur lors de l'envoi de l'accusé de réception: %s", exc) + + +# --------------------------------------------------------------------------- +# Public view: new authorization request +# --------------------------------------------------------------------------- + +def new_authorization(request): + """ + Public-facing form allowing citizens to submit a road-occupation request. + No login required. + """ + if request.method == 'POST': + form = AuthorizationForm(request.POST) + doc_formset = AuthorizationDocumentFormSet(request.POST, request.FILES, prefix='documents') + date_formset = AuthorizationDateFormSet(request.POST, prefix='dates') + + if form.is_valid() and doc_formset.is_valid() and date_formset.is_valid(): + # Require at least one begin_date + has_begin_date = any( + df.cleaned_data.get('begin_date') + for df in date_formset + if df.cleaned_data + ) + if not has_begin_date: + date_error = True + else: + date_error = False + + if not date_error: + authorization = form.save(commit=False) + + # Convert GeoJSON polygon → MultiPolygon + geojson_str = form.cleaned_data['geom_geojson'] + geom = GEOSGeometry(geojson_str, srid=4326) + if isinstance(geom, Polygon): + geom = MultiPolygon(geom) + authorization.geom = geom + + # Check whitelist before saving + applicant_email = form.cleaned_data['applicant_email'].lower().strip() + email_already_verified = VerifiedEmail.objects.filter(email=applicant_email).exists() + authorization.is_email_verified = email_already_verified + + # Language comes from the form field + authorization.language = form.cleaned_data.get('language') or 'fr' + + authorization.save() + + # Attach documents + doc_formset.instance = authorization + doc_formset.save() + + # Attach dates (skip empty rows) + for date_form in date_formset: + if date_form.cleaned_data.get('begin_date'): + date_obj = date_form.save(commit=False) + date_obj.authorization = authorization + date_obj.save() + + if email_already_verified: + # Email already in whitelist → send acknowledgment directly + _send_acknowledgment_email(request, authorization) + return redirect( + reverse('authorizations:authorization_success') + f'?code={authorization.code}' + ) + else: + # Send verification email + token_obj = EmailVerificationToken.create_for_authorization(authorization) + _send_verification_email(request, authorization, token_obj) + return redirect(reverse('authorizations:verify_email_code', args=[authorization.pk])) + else: + date_error = False + else: + lang_param = request.GET.get('lang', '').lower() + initial = {'language': lang_param} if lang_param in ('fr', 'nl', 'en') else {} + form = AuthorizationForm(initial=initial) + doc_formset = AuthorizationDocumentFormSet(prefix='documents') + date_formset = AuthorizationDateFormSet(prefix='dates') + date_error = False + + # Activate translation for template rendering based on ?lang= param (GET) + # or the language field already submitted (POST re-render) + lang_for_render = request.GET.get('lang', '').lower() + if lang_for_render not in ('fr', 'nl', 'en'): + # Try to read from POST data or form initial on error re-render + lang_for_render = request.POST.get('language', 'fr') if request.method == 'POST' else 'fr' + if lang_for_render in ('fr', 'nl', 'en'): + translation.activate(lang_for_render) + + return render(request, 'authorizations/new_authorization.html', { + 'form': form, + 'doc_formset': doc_formset, + 'date_formset': date_formset, + 'date_error': date_error, + }) + + +def verify_email_code(request, authorization_id): + """Page de saisie du code de vérification à 6 chiffres.""" + authorization = get_object_or_404(RoadAuthorization, pk=authorization_id) + + try: + token_obj = authorization.verification_token + except EmailVerificationToken.DoesNotExist: + # Already verified or no token exists + return render(request, 'authorizations/email_verification_failed.html', { + 'reason': 'invalid', + }) + + error = None + + if request.method == 'POST': + entered_code = request.POST.get('code', '').strip() + + if token_obj.is_locked(): + error = 'locked' + elif token_obj.is_expired(): + error = 'expired' + elif entered_code != token_obj.code: + token_obj.attempts += 1 + token_obj.save(update_fields=['attempts']) + error = 'locked' if token_obj.is_locked() else 'wrong_code' + else: + # ✓ Code correct + authorization.is_email_verified = True + authorization.save(update_fields=['is_email_verified']) + VerifiedEmail.objects.get_or_create(email=authorization.applicant_email.lower().strip()) + token_obj.delete() + _send_acknowledgment_email(request, authorization) + return redirect( + reverse('authorizations:authorization_success') + f'?code={authorization.code}&verified=1' + ) + else: + if token_obj.is_locked(): + error = 'locked' + elif token_obj.is_expired(): + error = 'expired' + + return render(request, 'authorizations/email_verify_code.html', { + 'authorization': authorization, + 'token_obj': token_obj, + 'error': error, + }) + + +def authorization_success(request): + """Confirmation page displayed after a successful submission.""" + auth_code = request.GET.get('code', '') + just_verified = request.GET.get('verified') == '1' + + # Activate the language stored on the authorization so the page is + # rendered in the language the applicant chose. + if auth_code: + try: + auth_obj = RoadAuthorization.objects.only('language').get(code=auth_code) + if auth_obj.language in ('fr', 'nl', 'en'): + translation.activate(auth_obj.language) + except RoadAuthorization.DoesNotExist: + pass + + return render(request, 'authorizations/authorization_success.html', { + 'auth_code': auth_code, + 'just_verified': just_verified, + }) + + +# --------------------------------------------------------------------------- +# Staff views +# --------------------------------------------------------------------------- + +@login_required +def authorization_list(request): + """Staff list of all road-authorization requests.""" + user_config = get_object_or_404(UserConfig, user=request.user) + if not user_config.can_access_view('authorizations'): + raise PermissionDenied(_("Vous n'avez pas accès à cette vue.")) + + from .forms import AuthorizationFilterForm + from datetime import datetime + from dateutil.relativedelta import relativedelta + + filter_form = AuthorizationFilterForm(request.GET or None) + qs = RoadAuthorization.objects.prefetch_related('dates', 'documents').order_by('-creation_date') + + if filter_form.is_valid(): + data = filter_form.cleaned_data + + # Text search over description, location, applicant fields and code + search = data.get('search_text', '').strip() + if search: + from django.db.models import Q + qs = qs.filter( + Q(description__icontains=search) | + Q(location__icontains=search) | + Q(applicant_name__icontains=search) | + Q(applicant_email__icontains=search) | + Q(code__icontains=search) + ) + + # Type filter + types = data.get('authorization_type') + if types: + qs = qs.filter(type__in=types) + + # Archive filter: exclude entries whose latest end_date is > 2 years ago + if not data.get('include_archives'): + cutoff = datetime.now().astimezone() - relativedelta(years=2) + # Keep authorizations that have no dates or at least one recent date + from django.db.models import Max, Q as DQ + qs = qs.annotate(latest_end=Max('dates__end_date')).filter( + DQ(latest_end__isnull=True) | DQ(latest_end__gte=cutoff) + ) + + return render(request, 'authorizations/authorization_list.html', { + 'authorizations': qs, + 'filter_form': filter_form, + }) + + +@login_required +def authorization_detail(request, pk): + """Staff detail view for a single authorization with edit capabilities.""" + user_config = get_object_or_404(UserConfig, user=request.user) + if not user_config.can_access_view('authorizations'): + raise PermissionDenied(_("Vous n'avez pas accès à cette vue.")) + + authorization = get_object_or_404(RoadAuthorization, pk=pk) + + if request.method == 'POST': + action = request.POST.get('action') + if action == 'update_properties': + new_status = request.POST.get('status') + new_comment = request.POST.get('agent_comment', '').strip() + + try: + authorization.status = int(new_status) + except (ValueError, TypeError): + pass + authorization.agent_comment = new_comment + authorization.agent = request.user + authorization.save() + messages.success(request, _("La demande a été mise à jour avec succès.")) + return redirect('authorizations:authorization_detail', pk=pk) + + elif action == 'add_document': + doc_file = request.FILES.get('file') + description = request.POST.get('description', '').strip() + if doc_file and description: + from .models import RoadAuthorizationDocument + from django.core.exceptions import ValidationError + doc = RoadAuthorizationDocument( + authorization=authorization, + file=doc_file, + description=description + ) + try: + doc.full_clean() + doc.save() + messages.success(request, _("Le document a été ajouté avec succès.")) + except ValidationError as e: + messages.error(request, e.messages[0]) + else: + messages.error(request, _("Veuillez fournir une description et un fichier.")) + return redirect('authorizations:authorization_detail', pk=pk) + + return render(request, 'authorizations/authorization_detail.html', { + 'authorization': authorization, + 'status_choices': RoadAuthorization.AUTH_STATUS, + }) diff --git a/streetup/common/__init__.py b/streetup/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/common/admin.py b/streetup/common/admin.py new file mode 100644 index 0000000..98fbede --- /dev/null +++ b/streetup/common/admin.py @@ -0,0 +1,1212 @@ +from django.contrib import admin +from django.urls import path, reverse +from django.shortcuts import render, redirect +from django.contrib import messages +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.utils import timezone +from datetime import timedelta +from django.db.models import Case, When, IntegerField, Value +from .models import ( + UserConfig, UserContractAccess, Thematic, Role, UserThematics, + AppView, RequestLog, UserWarehouseAccess, UserApprovalConfig, UserWarehouseLocationAccess, + UserThematicStatusPermission, UserContractStatusPermission, + UserAssetAccess, UserAssetTypeAccess, AssetTypePermissionConfig, + INTERVENTION_STATUS_ORDER, INTERVENTION_STATUS_CHOICES, +) +from .forms import UserConfigDuplicateForm, UserConfigCopyFromForm, UserConfigAssignCompanyForm +from django.contrib.admin import helpers +from contracts.models import UserCompanyTeamAccess, CompanyMember, CompanyTeam + +# Import de l'admin des tâches planifiées +from .admin_scheduled_tasks import ScheduledCommandAdmin, CommandExecutionLogAdmin + + +@admin.register(Thematic) +class ThematicAdmin(admin.ModelAdmin): + list_display = ("name_fr", "name_nl", "code", "icon", "color", "uses_occupations", "include_in_daily_report", "allow_planning_without_contract") + search_fields = ("name_fr", "name_nl", "code") + list_filter = ("uses_occupations", "include_in_daily_report", "allow_planning_without_contract") + list_editable = ("include_in_daily_report", "allow_planning_without_contract") + + +@admin.register(AppView) +class AppViewAdmin(admin.ModelAdmin): + list_display = ("code", "name_fr", "name_nl", "icon", "url_name", "is_default", "order") + search_fields = ("code", "name_fr", "name_nl") + list_editable = ("order", "is_default") + ordering = ("order",) + + +@admin.register(RequestLog) +class RequestLogAdmin(admin.ModelAdmin): + """ + Admin pour visualiser les logs de requêtes et erreurs. + Permet de rechercher par date, utilisateur, URL, etc. + """ + list_display = ( + "timestamp_display", + "level_badge", + "username_display", + "method", + "path_truncated", + "status_code_badge", + "ip_address", + "exception_type", + ) + list_filter = ( + "level", + "method", + "status_code", + ("timestamp", admin.DateFieldListFilter), + "user", + ) + search_fields = ( + "path", + "full_url", + "username", + "user__username", + "ip_address", + "exception_type", + "exception_message", + "traceback", + ) + readonly_fields = ( + "timestamp", + "level", + "user", + "username", + "method", + "path", + "full_url", + "status_code", + "ip_address", + "user_agent", + "referer", + "exception_type", + "exception_message", + "traceback_display", + "request_data_display", + ) + date_hierarchy = "timestamp" + ordering = ("-timestamp",) + list_per_page = 50 + actions = ["delete_selected"] + change_list_template = "admin/common/requestlog/change_list.html" + + fieldsets = ( + ("Informations générales", { + "fields": ("timestamp", "level", "status_code") + }), + ("Requête", { + "fields": ("method", "path", "full_url", "referer") + }), + ("Utilisateur", { + "fields": ("user", "username", "ip_address", "user_agent") + }), + ("Erreur", { + "fields": ("exception_type", "exception_message", "traceback_display"), + "classes": ("collapse",), + }), + ("Données de la requête", { + "fields": ("request_data_display",), + "classes": ("collapse",), + }), + ) + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "delete-old-logs//", + self.admin_site.admin_view(self.delete_old_logs_view), + name="common_requestlog_delete_old", + ), + ] + return custom_urls + urls + + def delete_old_logs_view(self, request, period): + """Vue pour supprimer les anciens logs selon la période.""" + now = timezone.now() + + # Option pour supprimer tous les logs + if period == "all": + deleted_count, _ = RequestLog.objects.all().delete() + if deleted_count > 0: + messages.success( + request, + f"Tous les {deleted_count} log(s) ont été supprimés avec succès." + ) + else: + messages.info(request, "Aucun log à supprimer.") + return redirect("admin:common_requestlog_changelist") + + periods = { + "1week": (timedelta(weeks=1), "1 semaine"), + "1month": (timedelta(days=30), "1 mois"), + "3months": (timedelta(days=90), "3 mois"), + "6months": (timedelta(days=180), "6 mois"), + "1year": (timedelta(days=365), "1 an"), + } + + if period not in periods: + messages.error(request, "Période invalide.") + return redirect("admin:common_requestlog_changelist") + + delta, period_name = periods[period] + cutoff_date = now - delta + + # Supprimer les logs plus anciens que la date limite + deleted_count, _ = RequestLog.objects.filter(timestamp__lt=cutoff_date).delete() + + if deleted_count > 0: + messages.success( + request, + f"{deleted_count} log(s) de plus de {period_name} supprimé(s) avec succès." + ) + else: + messages.info( + request, + f"Aucun log de plus de {period_name} à supprimer." + ) + + return redirect("admin:common_requestlog_changelist") + + def changelist_view(self, request, extra_context=None): + """Ajouter le contexte pour les boutons de suppression.""" + extra_context = extra_context or {} + extra_context['delete_periods'] = [ + ("1week", "1 semaine"), + ("1month", "1 mois"), + ("3months", "3 mois"), + ("6months", "6 mois"), + ("1year", "1 an"), + ] + # Compter les logs par période pour afficher dans les boutons + now = timezone.now() + extra_context['log_counts'] = { + "1week": RequestLog.objects.filter(timestamp__lt=now - timedelta(weeks=1)).count(), + "1month": RequestLog.objects.filter(timestamp__lt=now - timedelta(days=30)).count(), + "3months": RequestLog.objects.filter(timestamp__lt=now - timedelta(days=90)).count(), + "6months": RequestLog.objects.filter(timestamp__lt=now - timedelta(days=180)).count(), + "1year": RequestLog.objects.filter(timestamp__lt=now - timedelta(days=365)).count(), + } + extra_context['total_logs'] = RequestLog.objects.count() + return super().changelist_view(request, extra_context=extra_context) + + def has_add_permission(self, request): + """Désactiver l'ajout manuel de logs.""" + return False + + def has_change_permission(self, request, obj=None): + """Les logs ne peuvent pas être modifiés.""" + return False + + def timestamp_display(self, obj): + """Affiche la date et l'heure formatées.""" + return obj.timestamp.strftime("%d/%m/%Y %H:%M:%S") + timestamp_display.short_description = "Date/Heure" + timestamp_display.admin_order_field = "timestamp" + + def level_badge(self, obj): + """Affiche le niveau avec un badge coloré.""" + colors = { + 'DEBUG': '#6c757d', + 'INFO': '#17a2b8', + 'WARNING': '#ffc107', + 'ERROR': '#dc3545', + 'CRITICAL': '#721c24', + } + color = colors.get(obj.level, '#6c757d') + text_color = '#fff' if obj.level in ['ERROR', 'CRITICAL'] else '#000' + return format_html( + '{}', + color, text_color, obj.level + ) + level_badge.short_description = "Niveau" + level_badge.admin_order_field = "level" + + def username_display(self, obj): + """Affiche le nom d'utilisateur ou 'Anonyme'.""" + if obj.username: + return obj.username + return format_html('Anonyme') + username_display.short_description = "Utilisateur" + username_display.admin_order_field = "username" + + def path_truncated(self, obj): + """Affiche l'URL tronquée si trop longue.""" + max_length = 60 + if len(obj.path) > max_length: + return format_html( + '{}…', + obj.path, obj.path[:max_length] + ) + return obj.path + path_truncated.short_description = "URL" + path_truncated.admin_order_field = "path" + + def status_code_badge(self, obj): + """Affiche le code de statut HTTP avec un badge coloré.""" + if not obj.status_code: + return "-" + + if obj.status_code >= 500: + color = '#dc3545' + elif obj.status_code >= 400: + color = '#ffc107' + elif obj.status_code >= 300: + color = '#17a2b8' + else: + color = '#28a745' + + return format_html( + '{}', + color, obj.status_code + ) + status_code_badge.short_description = "Code" + status_code_badge.admin_order_field = "status_code" + + def traceback_display(self, obj): + """Affiche le traceback dans une zone de texte formatée.""" + if not obj.traceback: + return "-" + return format_html( + '
{}
', + obj.traceback + ) + traceback_display.short_description = "Traceback" + + def request_data_display(self, obj): + """Affiche les données de la requête formatées.""" + if not obj.request_data: + return "-" + try: + import json + data = json.loads(obj.request_data) + formatted = json.dumps(data, indent=2, ensure_ascii=False) + return format_html( + '
{}
', + formatted + ) + except Exception: + return format_html('
{}
', obj.request_data) + request_data_display.short_description = "Données de la requête" + + +# --------------------------------------------------------------------------- +# Matrix script (injected once per page via window._matrixEvtBound guard) +# --------------------------------------------------------------------------- + +_MATRIX_SCRIPT = """\ +""" + + +def _render_status_matrix(obj, prefix): + """ + Renvoie le HTML d’une matrice permissions-par-statut complète pour obj. + prefix : 'ut_perm' pour UserThematics, 'uca_perm' pour UserContractAccess. + Les checkboxes sont nommés ``{prefix}_{obj.pk}_{status}_{field}``. + """ + if not obj or not obj.pk: + return mark_safe('Enregistrez d\'abord pour configurer les permissions par statut.') + + choices_dict = dict(INTERVENTION_STATUS_CHOICES) + perms = {p.status: p for p in obj.status_permissions.all()} + uid = f'{prefix}_{obj.pk}' # identifiant unique par matrice (pour les data-ut JS) + + # En-têtes de colonnes (texte vertical) + header_cells = [''] + for status in INTERVENTION_STATUS_ORDER: + label = choices_dict.get(status, status) + header_cells.append( + f'' + f'
{label}
' + f'' + ) + + # Lignes de permissions + perm_types = [ + ('can_view', 'Voir'), + ('can_edit', 'Modifier'), + ('can_change_status_to','Changer → vers'), + ] + row_htmls = [] + for field_name, field_label in perm_types: + cells = [ + f'' + f'' + ] + for status in INTERVENTION_STATUS_ORDER: + perm = perms.get(status) + if perm is not None: + checked = 'checked' if getattr(perm, field_name) else '' + name = f'{prefix}_{obj.pk}_{status}_{field_name}' + cells.append( + f'' + f'
' + f'' + f'
' + ) + else: + cells.append('') + row_htmls.append('' + ''.join(cells) + '') + + html = ( + _MATRIX_SCRIPT + + '
' + + '' + + '▶ Permissions par statut (cliquer pour développer)' + + '
' + + '' + + '' + ''.join(header_cells) + '' + + '' + ''.join(row_htmls) + '' + + '
' + ) + return mark_safe(html) + + +# --------------------------------------------------------------------------- +# Status-permission matrix inlines (used by UserThematicsAdmin / UserContractAccessAdmin) +# --------------------------------------------------------------------------- + +class UserThematicStatusPermissionMatrixInline(admin.TabularInline): + """Affiche les permissions par statut sous forme de table matricielle (thématique).""" + model = UserThematicStatusPermission + template = 'admin/common/status_permission_matrix.html' + can_delete = False + extra = 0 + fields = ['can_view', 'can_edit', 'can_change_status_to'] + verbose_name_plural = "Permissions par statut d'intervention" + + def get_formset(self, request, obj=None, **kwargs): + if obj is not None: + for status in INTERVENTION_STATUS_ORDER: + UserThematicStatusPermission.objects.get_or_create( + user_thematic=obj, + status=status, + defaults={'can_view': False, 'can_edit': False, 'can_change_status_to': False}, + ) + return super().get_formset(request, obj, **kwargs) + + def get_queryset(self, request): + qs = super().get_queryset(request) + order = Case( + *[When(status=s, then=Value(i)) for i, s in enumerate(INTERVENTION_STATUS_ORDER)], + output_field=IntegerField(), + ) + return qs.annotate(_sorder=order).order_by('_sorder') + + +class UserContractStatusPermissionMatrixInline(admin.TabularInline): + """Affiche les permissions par statut sous forme de table matricielle (contrat).""" + model = UserContractStatusPermission + template = 'admin/common/status_permission_matrix.html' + can_delete = False + extra = 0 + fields = ['can_view', 'can_edit', 'can_change_status_to'] + verbose_name_plural = "Permissions par statut d'intervention" + + def get_formset(self, request, obj=None, **kwargs): + if obj is not None: + for status in INTERVENTION_STATUS_ORDER: + UserContractStatusPermission.objects.get_or_create( + user_contract=obj, + status=status, + defaults={'can_view': False, 'can_edit': False, 'can_change_status_to': False}, + ) + return super().get_formset(request, obj, **kwargs) + + def get_queryset(self, request): + qs = super().get_queryset(request) + order = Case( + *[When(status=s, then=Value(i)) for i, s in enumerate(INTERVENTION_STATUS_ORDER)], + output_field=IntegerField(), + ) + return qs.annotate(_sorder=order).order_by('_sorder') + + +# --------------------------------------------------------------------------- +# Inlines for UserConfig change page +# --------------------------------------------------------------------------- + +class UserContractAccessInline(admin.TabularInline): + """Permet d'afficher et modifier les accès aux contrats directement depuis UserConfig""" + model = UserContractAccess + extra = 1 + autocomplete_fields = ["contract"] + fields = [ + 'contract', 'can_view_assets', + 'can_view_interventions', 'can_create_interventions', 'can_check_interventions', 'can_approve', + 'can_view_posts', 'can_view_prices', + 'status_matrix', + ] + readonly_fields = ['status_matrix'] + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('status_permissions') + + def status_matrix(self, obj): + return _render_status_matrix(obj, 'uca_perm') + status_matrix.short_description = "Permissions par statut" + + +class UserThematicsInline(admin.TabularInline): + model = UserThematics + extra = 1 + fields = [ + 'thematic', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', + 'can_view_interventions', 'can_edit_interventions', 'can_edit_locations', + 'can_process_observations', + 'can_view_projects', 'can_edit_projects', + 'status_matrix', + ] + readonly_fields = ['status_matrix'] + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related('status_permissions') + + def status_matrix(self, obj): + return _render_status_matrix(obj, 'ut_perm') + status_matrix.short_description = "Permissions par statut" + + +class UserWarehouseAccessInline(admin.TabularInline): + """Permet d'afficher et modifier les accès aux entrepôts directement depuis UserConfig""" + model = UserWarehouseAccess + extra = 1 + autocomplete_fields = ["warehouse"] # Facilite la recherche des entrepôts + verbose_name = "Accès Entrepôt" + verbose_name_plural = "Accès Entrepôts" + fields = ['warehouse', 'can_view', 'can_manage', 'can_administer', 'can_manage_preparation_orders', 'can_manage_purchase_orders', 'can_validate_inventory', 'is_default'] + list_display = ['warehouse', 'can_view', 'can_manage', 'can_administer', 'can_manage_preparation_orders', 'can_manage_purchase_orders', 'can_validate_inventory', 'is_default'] + + +class UserWarehouseLocationAccessInline(admin.TabularInline): + """Permet d'afficher et modifier les accès aux emplacements d'entrepôt directement depuis UserConfig""" + model = UserWarehouseLocationAccess + extra = 1 + autocomplete_fields = ["warehouse_location"] + verbose_name = "Accès Emplacement" + verbose_name_plural = "Accès Emplacements" + fields = ['warehouse_location', 'can_perform_inventory', 'can_validate_inventory'] + + +class UserCompanyTeamAccessInline(admin.TabularInline): + """Accès aux équipes de société directement depuis UserConfig""" + model = UserCompanyTeamAccess + extra = 1 + autocomplete_fields = ['team'] + fields = ['team', 'can_view', 'can_edit'] + verbose_name = "Accès équipe" + verbose_name_plural = "Accès aux équipes" + + +class UserAssetAccessInline(admin.TabularInline): + """Accès spécifiques par instance d'asset ou de location, directement depuis UserConfig.""" + model = UserAssetAccess + extra = 0 + fields = ['content_type', 'object_id', 'can_view', 'can_edit', 'can_delete', 'editable_fields', 'visible_fields', 'object_link'] + readonly_fields = ['object_link'] + verbose_name = "Accès asset spécifique" + verbose_name_plural = "Accès assets spécifiques" + + def get_queryset(self, request): + return super().get_queryset(request).select_related('content_type') + + def object_link(self, obj): + if not obj.pk: + return "—" + model_class = obj.content_type.model_class() + if model_class is None: + return f"#{obj.object_id}" + try: + instance = model_class.objects.get(pk=obj.object_id) + if hasattr(instance, 'get_absolute_url'): + url = instance.get_absolute_url() + return format_html('{}', url, str(instance)) + return str(instance) + except model_class.DoesNotExist: + return f"#{obj.object_id} (supprimé)" + object_link.short_description = "Objet" + + +class UserAssetTypeAccessInline(admin.TabularInline): + """Accès par type d'asset (s'applique à tous les objets du type), directement depuis UserConfig.""" + model = UserAssetTypeAccess + extra = 0 + fields = ['content_type', 'can_view', 'can_edit', 'can_delete', 'editable_fields', 'visible_fields'] + verbose_name = "Accès type d'asset" + verbose_name_plural = "Accès types d'asset" + + def get_queryset(self, request): + return super().get_queryset(request).select_related('content_type') + + +# --------------------------------------------------------------------------- +# Standalone admin for UserThematics and UserContractAccess +# (allow editing the status-permission matrix for a single thematic/contract access) +# --------------------------------------------------------------------------- + +@admin.register(UserThematics) +class UserThematicsAdmin(admin.ModelAdmin): + list_display = ('user_config', 'thematic', 'can_view_interventions', 'can_edit_interventions', 'can_view_assets', 'can_edit_assets', 'can_validate_assets', 'can_process_observations') + list_filter = ('thematic',) + search_fields = ('user_config__user__username', 'thematic__name_fr', 'thematic__name_nl') + raw_id_fields = ['user_config'] + inlines = [UserThematicStatusPermissionMatrixInline] + verbose_name = "Thématique utilisateur" + verbose_name_plural = "Thématiques utilisateur" + + +@admin.register(UserContractAccess) +class UserContractAccessAdmin(admin.ModelAdmin): + list_display = ('user_config', 'contract', 'can_view_interventions', 'can_create_interventions', 'can_check_interventions', 'can_approve') + list_filter = ('contract',) + search_fields = ('user_config__user__username', 'contract__contract_number') + raw_id_fields = ['user_config', 'contract'] + readonly_fields = ['can_approve', 'can_create_interventions'] + inlines = [UserContractStatusPermissionMatrixInline] + + +@admin.register(UserConfig) +class UserConfigAdmin(admin.ModelAdmin): + list_display = ("user", "display_contracts", "display_thematics", "display_views", "is_intern", "display_roles", "company", "display_member_teams", "duplicate_button") + list_filter = ("thematics", "roles", "company", "user__companymember__teams", "contracts", "accessible_views", "auto_logout_hours") + search_fields = ("user__username", "contracts__contract_number") + inlines = [UserThematicsInline, UserContractAccessInline, UserWarehouseAccessInline, UserWarehouseLocationAccessInline, UserCompanyTeamAccessInline, UserAssetTypeAccessInline, UserAssetAccessInline] + filter_horizontal = ("linked_configs", "accessible_views") + actions = ["duplicate_config_action", "apply_config_from_action", "assign_company_action"] + ordering = ("user__username",) + + readonly_fields = ('company_members_link', 'company_teams_link') + + fieldsets = ( + (None, { + 'fields': ('user', 'is_intern', 'roles', 'company', 'company_members_link', 'company_teams_link', 'default_thematic', 'generic_email') + }), + ('Accès aux vues', { + 'fields': ('accessible_views',), + 'description': "Configurer les vues auxquelles l'utilisateur a accès." + }), + ('eSign / Signalisation', { + 'fields': ('esign_can_edit', 'esign_can_manage_rcp', 'esign_can_validate_rcp', 'esign_organizations'), + 'description': "Permissions spécifiques pour l'application eSign / Signalisation." + }), + ('Photos 360 / Panoramax', { + 'fields': ('panoramax_can_upload',), + 'description': "Permissions spécifiques pour l'application Photos 360 / Panoramax." + }), + ('Limitations', { + 'fields': ('limit_assets_to_contracts', 'limit_interventions_to_contracts', 'limit_shortcuts_to_contracts', 'hide_intervention_map_by_default') + }), + ('Gestion documentaire', { + 'fields': ('can_add_unique_tag_documents',), + 'description': "Permissions pour la gestion des documents avec tags uniques (CME, PIS, As-Built, etc.)." + }), + ('Comptes liés', { + 'fields': ('linked_configs',) + }), + ('Session', { + 'fields': ('auto_logout_hours',), + 'description': 'Configurer la déconnexion automatique pour les utilisateurs partageant un ordinateur (shifts).' + }), + ) + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "/duplicate/", + self.admin_site.admin_view(self.duplicate_config_view), + name="common_userconfig_duplicate", + ), + ] + return custom_urls + urls + + # --------------------------------------------------------------------------- + # Action: appliquer la config d'un utilisateur source sur plusieurs cibles + # --------------------------------------------------------------------------- + + def apply_config_from_action(self, request, queryset): + """Action intermédiaire : sélectionne une config source et la copie sur toutes les cibles.""" + if 'apply_source' in request.POST: + form = UserConfigCopyFromForm(request.POST) + if form.is_valid(): + source_config = form.cleaned_data['source'] + targets = queryset.exclude(pk=source_config.pk) + count = 0 + for target in targets: + self._copy_config(source_config, target) + count += 1 + self.message_user( + request, + f"Configuration de {source_config.user.username} copiée sur {count} utilisateur(s).", + level=messages.SUCCESS, + ) + return None + # Form invalid → re-render with errors + else: + form = UserConfigCopyFromForm() + + return render(request, 'admin/common/apply_config_from.html', { + **self.admin_site.each_context(request), + 'form': form, + 'queryset': queryset, + 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + 'opts': self.model._meta, + 'title': 'Appliquer la configuration d\'un utilisateur sur les sélectionnés', + }) + apply_config_from_action.short_description = "Appliquer la config d'un autre utilisateur sur les sélectionnés" + + # --------------------------------------------------------------------------- + # Action: assigner une société à plusieurs UserConfig + # --------------------------------------------------------------------------- + + def assign_company_action(self, request, queryset): + if 'assign_company' in request.POST: + form = UserConfigAssignCompanyForm(request.POST) + if form.is_valid(): + company = form.cleaned_data['company'] + queryset.update(company=company) + label = company.name if company else "aucune" + self.message_user( + request, + f"Société « {label} » assignée à {queryset.count()} utilisateur(s).", + level=messages.SUCCESS, + ) + return None + else: + form = UserConfigAssignCompanyForm() + + return render(request, 'admin/common/assign_company.html', { + **self.admin_site.each_context(request), + 'form': form, + 'queryset': queryset, + 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + 'opts': self.model._meta, + 'title': 'Assigner une société aux utilisateurs sélectionnés', + }) + assign_company_action.short_description = "Assigner une société aux sélectionnés" + + def _copy_config(self, source, target): + """Écrase la configuration de `target` avec celle de `source` (sauf l'utilisateur lié).""" + # Champs scalaires + target.is_intern = source.is_intern + target.default_thematic = source.default_thematic + target.company = source.company + target.limit_assets_to_contracts = source.limit_assets_to_contracts + target.limit_interventions_to_contracts = source.limit_interventions_to_contracts + target.limit_shortcuts_to_contracts = source.limit_shortcuts_to_contracts + target.generic_email = source.generic_email + target.hide_intervention_map_by_default = source.hide_intervention_map_by_default + target.auto_logout_hours = source.auto_logout_hours + target.intervention_list_layout = source.intervention_list_layout + target.assets_list_layout = source.assets_list_layout + target.can_add_unique_tag_documents = source.can_add_unique_tag_documents + target.esign_can_edit = source.esign_can_edit + target.esign_can_manage_rcp = source.esign_can_manage_rcp + target.esign_can_validate_rcp = source.esign_can_validate_rcp + target.panoramax_can_upload = source.panoramax_can_upload + target.save() + # M2M directs + target.roles.set(source.roles.all()) + target.accessible_views.set(source.accessible_views.all()) + target.esign_organizations.set(source.esign_organizations.all()) + # Thématiques + target.userthematics.all().delete() + for ut in source.userthematics.prefetch_related('status_permissions').all(): + new_ut = UserThematics.objects.create( + user_config=target, + thematic=ut.thematic, + can_view_assets=ut.can_view_assets, + can_edit_assets=ut.can_edit_assets, + can_validate_assets=ut.can_validate_assets, + can_view_interventions=ut.can_view_interventions, + can_edit_interventions=ut.can_edit_interventions, + can_edit_locations=ut.can_edit_locations, + can_process_observations=ut.can_process_observations, + can_view_projects=ut.can_view_projects, + can_edit_projects=ut.can_edit_projects, + ) + UserThematicStatusPermission.objects.bulk_create([ + UserThematicStatusPermission( + user_thematic=new_ut, + status=sp.status, + can_view=sp.can_view, + can_edit=sp.can_edit, + can_change_status_to=sp.can_change_status_to, + ) + for sp in ut.status_permissions.all() + ]) + # Contrats + target.usercontractaccess_set.all().delete() + for uc in source.usercontractaccess_set.prefetch_related('status_permissions').all(): + new_uc = UserContractAccess.objects.create( + user_config=target, + contract=uc.contract, + can_view_posts=uc.can_view_posts, + can_view_prices=uc.can_view_prices, + can_view_interventions=uc.can_view_interventions, + can_view_assets=uc.can_view_assets, + can_create_interventions=uc.can_create_interventions, + can_check_interventions=uc.can_check_interventions, + can_approve=uc.can_approve, + ) + UserContractStatusPermission.objects.bulk_create([ + UserContractStatusPermission( + user_contract=new_uc, + status=sp.status, + can_view=sp.can_view, + can_edit=sp.can_edit, + can_change_status_to=sp.can_change_status_to, + ) + for sp in uc.status_permissions.all() + ]) + # Accès aux entrepôts + target.userwarehouses.all().delete() + for wa in source.userwarehouses.all(): + UserWarehouseAccess.objects.create( + user_config=target, + warehouse=wa.warehouse, + can_view=wa.can_view, + can_manage=wa.can_manage, + can_administer=wa.can_administer, + can_manage_preparation_orders=wa.can_manage_preparation_orders, + can_manage_purchase_orders=wa.can_manage_purchase_orders, + can_validate_inventory=wa.can_validate_inventory, + is_default=wa.is_default, + ) + # Accès aux emplacements + target.userlocations.all().delete() + for la in source.userlocations.all(): + UserWarehouseLocationAccess.objects.create( + user_config=target, + warehouse_location=la.warehouse_location, + can_perform_inventory=la.can_perform_inventory, + can_validate_inventory=la.can_validate_inventory, + ) + # Accès aux équipes + target.team_accesses.all().delete() + for ta in source.team_accesses.all(): + UserCompanyTeamAccess.objects.create( + user_config=target, + team=ta.team, + can_view=ta.can_view, + can_edit=ta.can_edit, + ) + + def duplicate_config_action(self, request, queryset): + if queryset.count() != 1: + self.message_user(request, "Veuillez sélectionner une seule configuration à dupliquer.", level=messages.ERROR) + return + obj = queryset.first() + return redirect(f"{obj.id}/duplicate/") + duplicate_config_action.short_description = "Dupliquer la configuration sélectionnée" + + def duplicate_config_view(self, request, object_id): + orig_config = UserConfig.objects.get(pk=object_id) + if request.method == "POST": + form = UserConfigDuplicateForm(request.POST) + if form.is_valid(): + new_user = form.cleaned_data["user"] + # Créer la nouvelle config + new_config = UserConfig.objects.create( + user=new_user, + is_intern=orig_config.is_intern, + default_thematic=orig_config.default_thematic, + company=orig_config.company, + limit_assets_to_contracts=orig_config.limit_assets_to_contracts, + limit_interventions_to_contracts=orig_config.limit_interventions_to_contracts, + limit_shortcuts_to_contracts=orig_config.limit_shortcuts_to_contracts, + generic_email=orig_config.generic_email, + hide_intervention_map_by_default=orig_config.hide_intervention_map_by_default, + auto_logout_hours=orig_config.auto_logout_hours, + esign_can_edit=orig_config.esign_can_edit, + esign_can_manage_rcp=orig_config.esign_can_manage_rcp, + esign_can_validate_rcp=orig_config.esign_can_validate_rcp, + ) + # Copier les rôles + new_config.roles.set(orig_config.roles.all()) + # Copier les linked_configs + new_config.linked_configs.set(orig_config.linked_configs.all()) + # Copier les vues accessibles + new_config.accessible_views.set(orig_config.accessible_views.all()) + # Copier les gestionnaires eSign + new_config.esign_organizations.set(orig_config.esign_organizations.all()) + # Copier les thématiques (UserThematics + permissions par statut) + for ut in orig_config.userthematics.prefetch_related('status_permissions').all(): + new_ut = UserThematics.objects.create( + user_config=new_config, + thematic=ut.thematic, + can_view_assets=ut.can_view_assets, + can_edit_assets=ut.can_edit_assets, + can_validate_assets=ut.can_validate_assets, + can_view_interventions=ut.can_view_interventions, + can_edit_interventions=ut.can_edit_interventions, + can_edit_locations=ut.can_edit_locations, + can_process_observations=ut.can_process_observations, + can_view_projects=ut.can_view_projects, + can_edit_projects=ut.can_edit_projects, + ) + UserThematicStatusPermission.objects.bulk_create([ + UserThematicStatusPermission( + user_thematic=new_ut, + status=sp.status, + can_view=sp.can_view, + can_edit=sp.can_edit, + can_change_status_to=sp.can_change_status_to, + ) + for sp in ut.status_permissions.all() + ]) + # Copier les contrats (UserContractAccess + permissions par statut) + for uc in orig_config.usercontractaccess_set.prefetch_related('status_permissions').all(): + new_uc = UserContractAccess.objects.create( + user_config=new_config, + contract=uc.contract, + can_view_posts=uc.can_view_posts, + can_view_prices=uc.can_view_prices, + can_view_interventions=uc.can_view_interventions, + can_view_assets=uc.can_view_assets, + can_create_interventions=uc.can_create_interventions, + can_check_interventions=uc.can_check_interventions, + can_approve=uc.can_approve, + ) + UserContractStatusPermission.objects.bulk_create([ + UserContractStatusPermission( + user_contract=new_uc, + status=sp.status, + can_view=sp.can_view, + can_edit=sp.can_edit, + can_change_status_to=sp.can_change_status_to, + ) + for sp in uc.status_permissions.all() + ]) + # Copier les accès aux entrepôts (UserWarehouseAccess) + for wa in orig_config.userwarehouses.all(): + UserWarehouseAccess.objects.create( + user_config=new_config, + warehouse=wa.warehouse, + can_view=wa.can_view, + can_manage=wa.can_manage, + can_administer=wa.can_administer, + can_manage_preparation_orders=wa.can_manage_preparation_orders, + can_manage_purchase_orders=wa.can_manage_purchase_orders, + can_validate_inventory=wa.can_validate_inventory, + is_default=wa.is_default, + ) + # Copier les accès aux emplacements (UserWarehouseLocationAccess) + for la in orig_config.userlocations.all(): + UserWarehouseLocationAccess.objects.create( + user_config=new_config, + warehouse_location=la.warehouse_location, + can_perform_inventory=la.can_perform_inventory, + can_validate_inventory=la.can_validate_inventory, + ) + # Copier les accès aux équipes (UserCompanyTeamAccess) + for ta in orig_config.team_accesses.all(): + UserCompanyTeamAccess.objects.create( + user_config=new_config, + team=ta.team, + can_view=ta.can_view, + can_edit=ta.can_edit, + ) + messages.success(request, f"Configuration dupliquée pour l'utilisateur {new_user.username} !") + return redirect(f"../../{new_config.id}/change/") + else: + form = UserConfigDuplicateForm() + return render(request, "admin/common/duplicate_userconfig.html", {"form": form, "orig_config": orig_config}) + + def company_members_link(self, obj): + if not obj.user_id: + return "—" + members = CompanyMember.objects.filter(user=obj.user).prefetch_related('teams') + if not members.exists(): + return "—" + links = [] + for member in members: + url = reverse('admin:contracts_companymember_change', args=[member.pk]) + links.append(format_html('{}', url, str(member))) + return mark_safe(', '.join(links)) + company_members_link.short_description = "CompanyMember(s)" + + def company_teams_link(self, obj): + if not obj.user_id: + return "—" + teams = CompanyTeam.objects.filter(members__user=obj.user).distinct() + if not teams.exists(): + return "—" + links = [] + for team in teams: + url = reverse('admin:contracts_companyteam_change', args=[team.pk]) + links.append(format_html('{}', url, team.name)) + return mark_safe(', '.join(links)) + company_teams_link.short_description = "Équipes (CompanyTeam)" + + def display_contracts(self, obj): + return ", ".join([access.contract.contract_number for access in obj.usercontractaccess_set.all()]) + display_contracts.short_description = "Contrats associés" + + def display_thematics(self, obj): + return ", ".join([access.thematic.name for access in obj.userthematics.all()]) + display_thematics.short_description = "Thématiques" + + def display_views(self, obj): + return ", ".join([view.name for view in obj.accessible_views.all()]) + display_views.short_description = "Vues" + + def display_roles(self, obj): + return ", ".join(obj.roles.values_list('name', flat=True)) or "—" + display_roles.short_description = "Rôles" + + def display_member_teams(self, obj): + from contracts.models import CompanyMember + teams = ( + CompanyMember.objects + .filter(user=obj.user, teams__isnull=False) + .values_list('teams__name', flat=True) + .distinct() + ) + return ", ".join(teams) or "—" + display_member_teams.short_description = "Équipes" + + def duplicate_button(self, obj): + return format_html( + '' + 'Dupliquer' + '', + obj.id + ) + duplicate_button.short_description = "Actions" + + def change_view(self, request, object_id, form_url='', extra_context=None): + """ + Avant le rendu, s’assure que toutes les lignes de permissions par statut existent + pour les thématiques et contrats déjà enregistrés (création lazy des 15 lignes). + """ + if object_id and request.method == 'GET': + obj = self.get_object(request, object_id) + if obj: + for ut in obj.userthematics.prefetch_related('status_permissions').all(): + existing = {p.status for p in ut.status_permissions.all()} + to_create = [ + UserThematicStatusPermission(user_thematic=ut, status=s) + for s in INTERVENTION_STATUS_ORDER if s not in existing + ] + if to_create: + UserThematicStatusPermission.objects.bulk_create(to_create, ignore_conflicts=True) + for uca in obj.usercontractaccess_set.prefetch_related('status_permissions').all(): + existing = {p.status for p in uca.status_permissions.all()} + to_create = [ + UserContractStatusPermission(user_contract=uca, status=s) + for s in INTERVENTION_STATUS_ORDER if s not in existing + ] + if to_create: + UserContractStatusPermission.objects.bulk_create(to_create, ignore_conflicts=True) + return super().change_view(request, object_id, form_url, extra_context) + + def save_related(self, request, form, formsets, change): + """ + Après la sauvegarde standard des inlines, persiste les cases à cocher + de la matrice de permissions par statut (thématiques et contrats). + """ + super().save_related(request, form, formsets, change) + obj = form.instance + # Thématiques + for ut in obj.userthematics.all(): + for status in INTERVENTION_STATUS_ORDER: + perm, _ = UserThematicStatusPermission.objects.get_or_create( + user_thematic=ut, status=status, + defaults={'can_view': False, 'can_edit': False, 'can_change_status_to': False}, + ) + perm.can_view = f'ut_perm_{ut.pk}_{status}_can_view' in request.POST + perm.can_edit = f'ut_perm_{ut.pk}_{status}_can_edit' in request.POST + perm.can_change_status_to = f'ut_perm_{ut.pk}_{status}_can_change_status_to' in request.POST + perm.save() + # Contrats + for uca in obj.usercontractaccess_set.all(): + for status in INTERVENTION_STATUS_ORDER: + perm, _ = UserContractStatusPermission.objects.get_or_create( + user_contract=uca, status=status, + defaults={'can_view': False, 'can_edit': False, 'can_change_status_to': False}, + ) + perm.can_view = f'uca_perm_{uca.pk}_{status}_can_view' in request.POST + perm.can_edit = f'uca_perm_{uca.pk}_{status}_can_edit' in request.POST + perm.can_change_status_to = f'uca_perm_{uca.pk}_{status}_can_change_status_to' in request.POST + perm.save() + + +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + list_display = ("name",) + search_fields = ("name",) + + +@admin.register(UserApprovalConfig) +class UserApprovalConfigAdmin(admin.ModelAdmin): + """Page dédiée à la gestion des catégories d'approbation par utilisateur. + N'affiche que les utilisateurs ayant can_approve=True sur au moins un contrat. + """ + list_display = ("user", "display_approve_contracts", "display_approval_categories") + search_fields = ("user__username",) + filter_horizontal = ("approval_categories",) + ordering = ("user__username",) + + fieldsets = ( + (None, { + 'fields': ('user',), + }), + ("Catégories d'approbation", { + 'fields': ('approval_categories',), + 'description': ( + "Catégories d'assets pour lesquelles cet utilisateur peut approuver des interventions. " + "Si aucune catégorie n'est sélectionnée, aucun filtre de catégorie n'est appliqué " + "(toutes les interventions des contrats concernés sont visibles)." + ), + }), + ) + + def get_queryset(self, request): + return ( + super() + .get_queryset(request) + .filter(usercontractaccess__can_approve=True) + .distinct() + ) + + def display_approve_contracts(self, obj): + contracts = obj.usercontractaccess_set.filter(can_approve=True).select_related('contract') + return ", ".join(uca.contract.contract_number for uca in contracts) or "—" + display_approve_contracts.short_description = "Contrats (can_approve)" + + def display_approval_categories(self, obj): + cats = obj.approval_categories.all() + if not cats: + return "— (toutes)" + return ", ".join(cat.get_name() for cat in cats) + display_approval_categories.short_description = "Catégories filtrées" + + +# --------------------------------------------------------------------------- +# AssetTypePermissionConfig admin +# --------------------------------------------------------------------------- + +@admin.register(AssetTypePermissionConfig) +class AssetTypePermissionConfigAdmin(admin.ModelAdmin): + """ + Configure les droits explicites requis pour chaque type d'asset. + """ + list_display = ('content_type', 'model_verbose', 'requires_explicit_permissions', 'type_page_link') + list_editable = ('requires_explicit_permissions',) + list_filter = ('requires_explicit_permissions',) + search_fields = ('content_type__model', 'content_type__app_label') + ordering = ('content_type__model',) + + def model_verbose(self, obj): + mc = obj.content_type.model_class() + if mc: + return mc._meta.verbose_name + return obj.content_type.model + model_verbose.short_description = "Nom verbose" + + def type_page_link(self, obj): + url = f"/assets/permissions/type/{obj.content_type_id}/" + return format_html('Voir les accès', url) + type_page_link.short_description = "Page dédiée" + + +@admin.register(UserAssetAccess) +class UserAssetAccessAdmin(admin.ModelAdmin): + """ + Vue globale des accès spécifiques par instance. + """ + list_display = ('user_config', 'content_type', 'object_id', 'can_view', 'can_edit', 'can_delete', 'has_field_override', 'has_visible_field_override', 'object_link') + list_filter = ('content_type', 'can_view', 'can_edit', 'can_delete') + search_fields = ('user_config__user__username', 'content_type__model') + raw_id_fields = ['user_config'] + ordering = ('user_config__user__username', 'content_type__model', 'object_id') + + def has_field_override(self, obj): + return bool(obj.editable_fields) + has_field_override.boolean = True + has_field_override.short_description = "Champs modif. overridés" + + def has_visible_field_override(self, obj): + return bool(obj.visible_fields) + has_visible_field_override.boolean = True + has_visible_field_override.short_description = "Champs visibles overridés" + + def object_link(self, obj): + model_class = obj.content_type.model_class() + if model_class is None: + return f"#{obj.object_id}" + try: + instance = model_class.objects.get(pk=obj.object_id) + if hasattr(instance, 'get_absolute_url'): + url = instance.get_absolute_url() + return format_html('{}', url, str(instance)) + return str(instance) + except model_class.DoesNotExist: + return f"#{obj.object_id} (supprimé)" + object_link.short_description = "Objet" + + +@admin.register(UserAssetTypeAccess) +class UserAssetTypeAccessAdmin(admin.ModelAdmin): + """ + Vue globale des accès par type d'asset (s'appliquent à tous les objets du type). + """ + list_display = ('user_config', 'content_type', 'can_view', 'can_edit', 'can_delete', 'has_field_override', 'has_visible_field_override', 'type_page_link') + list_filter = ('content_type', 'can_view', 'can_edit', 'can_delete') + search_fields = ('user_config__user__username', 'content_type__model') + raw_id_fields = ['user_config'] + ordering = ('user_config__user__username', 'content_type__model') + + def has_field_override(self, obj): + return bool(obj.editable_fields) + has_field_override.boolean = True + has_field_override.short_description = "Champs modif. overridés" + + def has_visible_field_override(self, obj): + return bool(obj.visible_fields) + has_visible_field_override.boolean = True + has_visible_field_override.short_description = "Champs visibles overridés" + + def type_page_link(self, obj): + url = f"/assets/permissions/type/{obj.content_type_id}/" + return format_html('Page du type', url) + type_page_link.short_description = "Page dédiée" diff --git a/streetup/common/admin_scheduled_tasks.py b/streetup/common/admin_scheduled_tasks.py new file mode 100644 index 0000000..8f49880 --- /dev/null +++ b/streetup/common/admin_scheduled_tasks.py @@ -0,0 +1,605 @@ +""" +Admin pour les tâches planifiées. +""" + +from django.contrib import admin +from django.utils.html import format_html +from django.urls import reverse, path +from django.utils.safestring import mark_safe +from django.contrib import messages +from django.core.management import call_command +from django.core.mail import EmailMessage +from django.conf import settings +from django.utils import timezone +from django.shortcuts import redirect +from django.http import HttpResponseRedirect +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from common.models_scheduled_tasks import ScheduledCommand, CommandExecutionLog +import json +import io +import traceback +import os + + +@admin.register(ScheduledCommand) +class ScheduledCommandAdmin(admin.ModelAdmin): + list_display = [ + 'name', + 'command_name', + 'frequency_display', + 'schedule_display', + 'is_active_display', + 'last_run_display', + 'next_run_display', + 'execution_count', + 'success_rate' + ] + + list_filter = ['is_active', 'frequency', 'send_email'] + search_fields = ['name', 'command_name', 'description'] + readonly_fields = ['last_run', 'next_run', 'created_at', 'created_by', 'execution_history_link', 'execute_now_button'] + actions = ['execute_immediately'] + + fieldsets = [ + ('Informations générales', { + 'fields': ['name', 'description', 'is_active'] + }), + ('Commande', { + 'fields': ['command_name', 'command_args'] + }), + ('Planification', { + 'fields': [ + 'frequency', + 'hour', + 'minute', + 'day_of_month', + 'day_of_week', + ] + }), + ('Notification par email', { + 'fields': ['send_email', 'email_recipients', 'email_subject'], + 'classes': ['collapse'] + }), + ('Exécution', { + 'fields': ['execute_now_button'], + 'description': 'Exécuter cette commande immédiatement, sans attendre la prochaine planification.' + }), + ('Informations système', { + 'fields': ['last_run', 'next_run', 'created_at', 'created_by', 'execution_history_link'], + 'classes': ['collapse'] + }) + ] + + def get_urls(self): + """Ajoute des URLs personnalisées pour l'admin.""" + urls = super().get_urls() + custom_urls = [ + path( + '/execute/', + csrf_exempt(self.admin_site.admin_view(self.execute_command_view)), + name='execute_scheduled_command', + ), + ] + return custom_urls + urls + + def execute_command_view(self, request, command_id): + """Vue pour exécuter une commande immédiatement.""" + # Accepter seulement les requêtes GET (pas de CSRF nécessaire) + if request.method != 'GET': + messages.error(request, "Méthode non autorisée.") + return redirect('admin:common_scheduledcommand_changelist') + + # Récupérer la commande (hors du try pour pouvoir l'utiliser après) + try: + scheduled_command = ScheduledCommand.objects.get(pk=command_id) + except ScheduledCommand.DoesNotExist: + messages.error(request, "Commande introuvable.") + return redirect('admin:common_scheduledcommand_changelist') + + # Créer un log d'exécution + log = CommandExecutionLog.objects.create( + scheduled_command=scheduled_command, + status='running' + ) + + try: + # Préparer les arguments + args = [] + kwargs = {} + if scheduled_command.command_args: + if isinstance(scheduled_command.command_args, dict): + kwargs = scheduled_command.command_args + elif isinstance(scheduled_command.command_args, list): + args = scheduled_command.command_args + + # Capturer la sortie + output_buffer = io.StringIO() + + # Exécuter la commande + call_command( + scheduled_command.command_name, + *args, + stdout=output_buffer, + stderr=output_buffer, + **kwargs + ) + + # Mettre à jour le log + log.finished_at = timezone.now() + log.status = 'success' + log.output = output_buffer.getvalue() + log.save() + + # Mettre à jour la commande planifiée + scheduled_command.last_run = log.started_at + scheduled_command.save() + + # Envoyer l'email si configuré + if scheduled_command.send_email and scheduled_command.email_recipients: + self.send_success_email(scheduled_command, log, request) + + messages.success( + request, + f"Commande '{scheduled_command.name}' exécutée avec succès. " + f"Consultez l'historique pour voir les détails." + ) + + except Exception as e: + # Mettre à jour le log avec l'erreur + log.finished_at = timezone.now() + log.status = 'error' + log.output = output_buffer.getvalue() + log.error_message = f"{str(e)}\n\n{traceback.format_exc()}" + log.save() + + # Envoyer l'email d'erreur si configuré + if scheduled_command.send_email and scheduled_command.email_recipients: + self.send_error_email(scheduled_command, log, request) + + messages.error( + request, + f"Erreur lors de l'exécution de '{scheduled_command.name}': {str(e)}" + ) + + # Rediriger vers la page de détail de la commande + return redirect('admin:common_scheduledcommand_change', scheduled_command.pk) + + def send_success_email(self, task, log, request): + """Envoie un email de succès après exécution depuis l'admin.""" + try: + # Calculer la durée si pas déjà fait + if log.duration_seconds is None and log.finished_at and log.started_at: + log.calculate_duration() + log.save(update_fields=['duration_seconds']) + + # Formater la durée + duration_str = f"{log.duration_seconds:.1f}" if log.duration_seconds is not None else "N/A" + + subject = task.email_subject or f"[StreetUp] Tâche planifiée réussie: {task.name}" + + # Vérifier si un fichier a été généré + output_file = task.command_args.get('output', '') if isinstance(task.command_args, dict) else '' + + # Substituer les variables dans le nom du fichier + if output_file and '%(year)s' in output_file: + from datetime import date + from dateutil.relativedelta import relativedelta + today = date.today() + prev_month = today.replace(day=1) - relativedelta(months=1) + output_file = output_file.replace('%(year)s', str(prev_month.year)) + output_file = output_file.replace('%(month)s', f'{prev_month.month:02d}') + output_file = output_file.replace('%(day)s', f'{today.day:02d}') + output_file = output_file.replace('%(date)s', today.strftime('%Y-%m-%d')) + + # Convertir en chemin absolu + if not os.path.isabs(output_file): + output_file = os.path.join(settings.BASE_DIR, output_file) + + body = f"""Bonjour, + +La tâche planifiée "{task.name}" s'est exécutée avec succès depuis l'interface d'administration. + +Détails: +- Commande: {task.command_name} +- Démarré: {log.started_at.strftime('%Y-%m-%d %H:%M:%S')} +- Terminé: {log.finished_at.strftime('%Y-%m-%d %H:%M:%S')} +- Durée: {duration_str} secondes +- Exécuté par: {request.user.username} ({request.user.email}) + +Prochaine exécution planifiée: {task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée'} + +--- +Sortie de la commande: +{log.output} + +--- +Cordialement, +StreetUp - Système de tâches planifiées + """ + + recipients = [email.strip() for email in task.email_recipients.split(',')] + + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=recipients, + ) + + # Attacher le fichier si il existe + if output_file and os.path.exists(output_file): + email.attach_file(output_file) + + email.send(fail_silently=False) + + log.email_sent = True + log.save(update_fields=['email_sent']) + + messages.info(request, f"Email envoyé à: {', '.join(recipients)}") + + except Exception as e: + messages.warning(request, f"Erreur lors de l'envoi de l'email: {str(e)}") + + def send_error_email(self, task, log, request): + """Envoie un email d'erreur après échec d'exécution depuis l'admin.""" + try: + subject = f"[StreetUp] ERREUR - Tâche planifiée: {task.name}" + + body = f"""Bonjour, + +La tâche planifiée "{task.name}" a rencontré une erreur lors de son exécution depuis l'interface d'administration. + +Détails: +- Commande: {task.command_name} +- Démarré: {log.started_at.strftime('%Y-%m-%d %H:%M:%S')} +- Terminé: {log.finished_at.strftime('%Y-%m-%d %H:%M:%S')} +- Durée: {log.duration_seconds:.1f} secondes +- Exécuté par: {request.user.username} ({request.user.email}) + +Prochaine exécution planifiée: {task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée'} + +--- +Message d'erreur: +{log.error_message} + +--- +Sortie de la commande: +{log.output} + +--- +Veuillez vérifier les logs dans l'administration Django pour plus de détails. + +Cordialement, +StreetUp - Système de tâches planifiées + """ + + recipients = [email.strip() for email in task.email_recipients.split(',')] + + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=recipients, + ) + + email.send(fail_silently=False) + + log.email_sent = True + log.save(update_fields=['email_sent']) + + messages.warning(request, f"Email d'erreur envoyé à: {', '.join(recipients)}") + + except Exception as e: + messages.warning(request, f"Erreur lors de l'envoi de l'email d'erreur: {str(e)}") + + def save_model(self, request, obj, form, change): + if not change: # Nouvelle création + obj.created_by = request.user + + # Calculer next_run si pas déjà défini + if not obj.next_run: + obj.next_run = obj.calculate_next_run() + + super().save_model(request, obj, form, change) + + def frequency_display(self, obj): + """Affiche la fréquence avec une icône.""" + icons = { + 'hourly': '⏰', + 'daily': '📅', + 'weekly': '📆', + 'monthly': '🗓️' + } + icon = icons.get(obj.frequency, '⏱️') + return f"{icon} {obj.get_frequency_display()}" + frequency_display.short_description = "Fréquence" + + def schedule_display(self, obj): + """Affiche le planning de manière lisible.""" + schedule = f"{obj.hour:02d}:{obj.minute:02d}" + + if obj.frequency == 'monthly' and obj.day_of_month: + schedule += f" le {obj.day_of_month}" + elif obj.frequency == 'weekly' and obj.day_of_week is not None: + days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'] + schedule += f" le {days[obj.day_of_week]}" + + return schedule + schedule_display.short_description = "Planning" + + def is_active_display(self, obj): + """Affiche le statut avec une icône.""" + if obj.is_active: + return format_html( + '✓ Active' + ) + return format_html( + '✗ Inactive' + ) + is_active_display.short_description = "Statut" + + def last_run_display(self, obj): + """Affiche la dernière exécution.""" + if obj.last_run: + return obj.last_run.strftime('%Y-%m-%d %H:%M') + return "-" + last_run_display.short_description = "Dernière exécution" + + def next_run_display(self, obj): + """Affiche la prochaine exécution.""" + if obj.next_run: + return obj.next_run.strftime('%Y-%m-%d %H:%M') + return "-" + next_run_display.short_description = "Prochaine exécution" + + def execution_count(self, obj): + """Nombre total d'exécutions.""" + return obj.execution_logs.count() + execution_count.short_description = "Exécutions" + + def success_rate(self, obj): + """Taux de succès.""" + total = obj.execution_logs.count() + if total == 0: + return "-" + + success = obj.execution_logs.filter(status='success').count() + rate = (success / total) * 100 + rate_formatted = "{:.1f}".format(rate) + + color = 'green' if rate >= 90 else 'orange' if rate >= 70 else 'red' + return format_html( + '{}% ({}/{})', + color, rate_formatted, success, total + ) + success_rate.short_description = "Taux de succès" + + def execution_history_link(self, obj): + """Lien vers l'historique d'exécution.""" + if obj.pk: + count = obj.execution_logs.count() + url = reverse('admin:common_commandexecutionlog_changelist') + '?scheduled_command__id__exact={}'.format(obj.pk) + return format_html( + 'Voir l\'historique ({} exécutions)', + url, count + ) + return "-" + execution_history_link.short_description = "Historique" + + def execute_now_button(self, obj): + """Bouton pour exécuter la commande immédiatement.""" + if obj.pk: + url = reverse('admin:execute_scheduled_command', args=[obj.pk]) + return format_html( + ''' +
+ +

+ La commande sera exécutée immédiatement dans une tâche en arrière-plan. + Consultez l'historique d'exécution pour voir le résultat. +

+
+ ''', + url + ) + return "-" + execute_now_button.short_description = "Exécution immédiate" + + def execute_immediately(self, request, queryset): + """Action pour exécuter les commandes sélectionnées immédiatement.""" + executed_count = 0 + error_count = 0 + + for scheduled_command in queryset: + # Créer un log d'exécution + log = CommandExecutionLog.objects.create( + scheduled_command=scheduled_command, + status='running' + ) + + try: + # Préparer les arguments + args = [] + kwargs = {} + if scheduled_command.command_args: + if isinstance(scheduled_command.command_args, dict): + kwargs = scheduled_command.command_args + elif isinstance(scheduled_command.command_args, list): + args = scheduled_command.command_args + + # Capturer la sortie + output_buffer = io.StringIO() + + # Exécuter la commande + call_command( + scheduled_command.command_name, + *args, + stdout=output_buffer, + stderr=output_buffer, + **kwargs + ) + + # Mettre à jour le log + log.finished_at = timezone.now() + log.status = 'success' + log.output = output_buffer.getvalue() + log.save() + + # Mettre à jour la commande planifiée + scheduled_command.last_run = log.started_at + scheduled_command.save() + + executed_count += 1 + + except Exception as e: + # Mettre à jour le log avec l'erreur + log.finished_at = timezone.now() + log.status = 'error' + log.error_message = f"{str(e)}\n\n{traceback.format_exc()}" + log.save() + + error_count += 1 + + # Messages de retour + if executed_count > 0: + self.message_user( + request, + f"{executed_count} commande(s) exécutée(s) avec succès.", + messages.SUCCESS + ) + + if error_count > 0: + self.message_user( + request, + f"{error_count} commande(s) en erreur. Consultez l'historique pour les détails.", + messages.ERROR + ) + + execute_immediately.short_description = "▶️ Exécuter immédiatement les commandes sélectionnées" + + +@admin.register(CommandExecutionLog) +class CommandExecutionLogAdmin(admin.ModelAdmin): + list_display = [ + 'scheduled_command', + 'started_at_display', + 'status_display', + 'duration_display', + 'email_sent_display' + ] + + list_filter = ['status', 'email_sent', 'scheduled_command'] + search_fields = ['scheduled_command__name', 'output', 'error_message'] + readonly_fields = [ + 'scheduled_command', + 'started_at', + 'finished_at', + 'status', + 'output', + 'error_message', + 'duration_seconds', + 'email_sent', + 'output_formatted', + 'error_formatted' + ] + + fieldsets = [ + ('Exécution', { + 'fields': [ + 'scheduled_command', + 'started_at', + 'finished_at', + 'status', + 'duration_seconds', + 'email_sent' + ] + }), + ('Sortie', { + 'fields': ['output_formatted'], + }), + ('Erreurs', { + 'fields': ['error_formatted'], + 'classes': ['collapse'] + }) + ] + + def has_add_permission(self, request): + """Empêche la création manuelle de logs.""" + return False + + def has_change_permission(self, request, obj=None): + """Lecture seule.""" + return True + + def has_delete_permission(self, request, obj=None): + """Permet la suppression.""" + return True + + def started_at_display(self, obj): + """Affiche la date de début.""" + return obj.started_at.strftime('%Y-%m-%d %H:%M:%S') + started_at_display.short_description = "Démarré à" + + def status_display(self, obj): + """Affiche le statut avec une couleur.""" + colors = { + 'running': 'blue', + 'success': 'green', + 'error': 'red' + } + icons = { + 'running': '⏳', + 'success': '✓', + 'error': '✗' + } + color = colors.get(obj.status, 'gray') + icon = icons.get(obj.status, '•') + + return format_html( + '{} {}', + color, icon, obj.get_status_display() + ) + status_display.short_description = "Statut" + + def duration_display(self, obj): + """Affiche la durée.""" + if obj.duration_seconds is not None: + if obj.duration_seconds < 60: + return f"{obj.duration_seconds:.1f}s" + else: + minutes = int(obj.duration_seconds // 60) + seconds = int(obj.duration_seconds % 60) + return f"{minutes}m {seconds}s" + return "-" + duration_display.short_description = "Durée" + + def email_sent_display(self, obj): + """Affiche si l'email a été envoyé.""" + if obj.email_sent: + return format_html('✓') + return format_html('-') + email_sent_display.short_description = "Email" + + def output_formatted(self, obj): + """Affiche la sortie formatée.""" + if obj.output: + return format_html( + '
{}
', + obj.output + ) + return "-" + output_formatted.short_description = "Sortie de la commande" + + def error_formatted(self, obj): + """Affiche les erreurs formatées.""" + if obj.error_message: + return format_html( + '
{}
', + obj.error_message + ) + return "-" + error_formatted.short_description = "Message d'erreur" diff --git a/streetup/common/apps.py b/streetup/common/apps.py new file mode 100644 index 0000000..fc9a8bd --- /dev/null +++ b/streetup/common/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'common' + + def ready(self): + # Connecter le signal pour enregistrer l'heure de connexion + from django.contrib.auth.signals import user_logged_in + from .middleware import set_login_time + user_logged_in.connect(set_login_time) diff --git a/streetup/common/context_processors.py b/streetup/common/context_processors.py new file mode 100644 index 0000000..f31bff5 --- /dev/null +++ b/streetup/common/context_processors.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import os +from typing import Iterable, List + +from django.contrib.auth.models import AnonymousUser +from django.urls import resolve + +from .models import UserConfig, AppView + + +SESSION_ALLOWED_IDS_KEY = "account_switch_allowed_user_ids" +SESSION_ORIGIN_ID_KEY = "account_switch_origin_user_id" + + +from django.conf import settings + +def environment_info(request): + """ + Context processor that exposes environment information. + Uses ENVIRONMENT variable which can be 'development', 'test', or 'production'. + Makes 'environment_name' available in all templates. + """ + env = os.getenv('ENVIRONMENT', '').lower() + + # Map environment names to display labels + env_labels = { + 'development': 'DEV', + 'dev': 'DEV', + 'test': 'TEST', + 'staging': 'STAGING', + } + + # Only show label for non-production environments + environment_label = env_labels.get(env, '') + + return { + 'environment_name': environment_label, + 'MAP_STYLE_LIGHT': getattr(settings, 'MAP_STYLE_LIGHT', None) or 'https://api.maptiler.com/maps/streets-v2-light/style.json', + 'MAP_STYLE_STANDARD': getattr(settings, 'MAP_STYLE_STANDARD', None) or 'https://api.maptiler.com/maps/streets-v2/style.json', + } + + +def compute_initial_allowed_ids(config: UserConfig) -> List[int]: + linked_ids: Iterable[int] = config.linked_configs.values_list("user_id", flat=True) + allowed = list(linked_ids) + allowed.append(config.user_id) + return allowed + + +def current_app_view(request): + """ + Context processor that determines the current active AppView based on the URL namespace. + Makes 'current_view' available in all templates. + + Only 'sign', 'documents', and 'schools' have their own dedicated views. + All other apps (assets, interventions, controls, contracts, etc.) use 'asset_management' view. + """ + current_view = None + + # Apps that have their own dedicated view + DEDICATED_VIEW_APPS = ['sign', 'documents', 'schools', 'reporting', 'maps', 'authorizations', 'panoramax'] + + try: + # Get the namespace from the current URL + resolved = resolve(request.path) + namespace = resolved.namespace # e.g., 'schools', 'sign', 'home', 'assets' + + if namespace: + if namespace in DEDICATED_VIEW_APPS: + # These apps have their own dedicated view + current_view = AppView.objects.filter(url_name__startswith=f"{namespace}:").first() + else: + # All other apps (home, assets, interventions, controls, contracts, etc.) + # should show 'asset_management' view + current_view = AppView.objects.filter(code='asset_management').first() + except Exception: + pass + + return { + 'current_view': current_view, + } + + +def linked_accounts(request): + user = getattr(request, "user", None) + + if not user or isinstance(user, AnonymousUser) or not user.is_authenticated: + request.session.pop(SESSION_ALLOWED_IDS_KEY, None) + request.session.pop(SESSION_ORIGIN_ID_KEY, None) + return { + "switchable_accounts": [], + "account_switch_origin_user_id": None, + } + + try: + current_config: UserConfig = user.config + except UserConfig.DoesNotExist: + request.session.pop(SESSION_ALLOWED_IDS_KEY, None) + request.session.pop(SESSION_ORIGIN_ID_KEY, None) + return { + "switchable_accounts": [], + "account_switch_origin_user_id": None, + } + + session = getattr(request, "session", None) + if session is None: + return { + "switchable_accounts": [], + "account_switch_origin_user_id": None, + } + + allowed_ids = session.get(SESSION_ALLOWED_IDS_KEY) + origin_user_id = session.get(SESSION_ORIGIN_ID_KEY) + + if allowed_ids is None or origin_user_id is None: + allowed_ids = compute_initial_allowed_ids(current_config) + origin_user_id = user.pk + else: + allowed_ids = list(dict.fromkeys(int(pk) for pk in allowed_ids)) + if user.pk not in allowed_ids: + allowed_ids.append(user.pk) + + session[SESSION_ALLOWED_IDS_KEY] = allowed_ids + session[SESSION_ORIGIN_ID_KEY] = origin_user_id + + switchable_accounts = ( + UserConfig.objects.select_related("user") + .filter(user_id__in=allowed_ids) + .exclude(user_id=user.pk) + .order_by("user__first_name", "user__last_name") + ) + + return { + "switchable_accounts": switchable_accounts, + "account_switch_origin_user_id": origin_user_id, + } + + +def organization_info(request): + """ + Context processor that exposes organization details from settings. + """ + return { + 'product_name': getattr(settings, 'PRODUCT_NAME', 'StreetUp'), + 'organization_name': getattr(settings, 'ORGANIZATION_NAME', 'Brussels Mobility'), + 'organization_logo_header': getattr(settings, 'ORGANIZATION_LOGO_HEADER', 'common/android-192.png'), + 'organization_primary_color': getattr(settings, 'ORGANIZATION_PRIMARY_COLOR_HEX', '#2f3e8b'), + 'map_default_center': [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)], + 'map_default_zoom': getattr(settings, 'MAP_DEFAULT_ZOOM', 11), + 'map_default_extent': getattr(settings, 'MAP_DEFAULT_EXTENT', None), + 'map_ortho_wms_url': getattr(settings, 'MAP_ORTHO_WMS_URL', ''), + 'map_ortho_wms_name': getattr(settings, 'MAP_ORTHO_WMS_NAME', ''), + 'map_ortho_wms_attribution': getattr(settings, 'MAP_ORTHO_WMS_ATTRIBUTION', ''), + } \ No newline at end of file diff --git a/streetup/common/fixtures/address_data.json.gz b/streetup/common/fixtures/address_data.json.gz new file mode 100644 index 0000000..326c9d6 Binary files /dev/null and b/streetup/common/fixtures/address_data.json.gz differ diff --git a/streetup/common/fixtures/reference_data.json b/streetup/common/fixtures/reference_data.json new file mode 100644 index 0000000..9b80fe1 --- /dev/null +++ b/streetup/common/fixtures/reference_data.json @@ -0,0 +1,442 @@ +[ +{ + "model": "common.thematic", + "pk": 1, + "fields": { + "code": "trafficlights", + "name_fr": "Feux tricolores", + "name_nl": "Verkeerslichten", + "description": "", + "icon": "bi-stoplights", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 2, + "fields": { + "code": "publiclighting", + "name_fr": "Eclairage public", + "name_nl": "Openbare verlichting", + "description": "", + "icon": "bi-lightbulb", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 3, + "fields": { + "code": "roads", + "name_fr": "Roads", + "name_nl": "Roads", + "description": "", + "icon": "bi-fullscreen-exit", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 4, + "fields": { + "code": "sign", + "name_fr": "Signalisation", + "name_nl": "Signalisatie", + "description": "Signalisation verticale et horizontale.", + "icon": "bi-signpost", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 5, + "fields": { + "code": "nature", + "name_fr": "Nature", + "name_nl": "Natuur", + "description": "", + "icon": "bi-suit-club", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 6, + "fields": { + "code": "artworks", + "name_fr": "Patrimoine artistique", + "name_nl": "Artistiek erfgoed", + "description": "", + "icon": "bi-bank", + "color": "#0000aa", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 8, + "fields": { + "code": "structures", + "name_fr": "Ouvrages d'art", + "name_nl": "Kunstwerken", + "description": "", + "icon": "bi-rainbow", + "color": "#000000", + "uses_occupations": true + } +}, +{ + "model": "common.thematic", + "pk": 9, + "fields": { + "code": "controlcenters", + "name_fr": "Centres de contrôle", + "name_nl": "Control centers", + "description": "", + "icon": "bi-building-gear", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 10, + "fields": { + "code": "its", + "name_fr": "ITS", + "name_nl": "ITS", + "description": "", + "icon": "bi-lightning", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.thematic", + "pk": 11, + "fields": { + "code": "dpv_micro_projects_security", + "name_fr": "DPV Micro-projets Sécurité", + "name_nl": "DPV Micro-projecten Beveiliging", + "description": "", + "icon": "bi-exclamation-diamond", + "color": "#000000", + "uses_occupations": false + } +}, +{ + "model": "common.role", + "pk": 1, + "fields": { + "name": "manager" + } +}, +{ + "model": "common.role", + "pk": 2, + "fields": { + "name": "technician" + } +}, +{ + "model": "common.role", + "pk": 3, + "fields": { + "name": "external_manager" + } +}, +{ + "model": "common.role", + "pk": 4, + "fields": { + "name": "controller" + } +}, +{ + "model": "common.role", + "pk": 5, + "fields": { + "name": "operator" + } +}, +{ + "model": "common.role", + "pk": 6, + "fields": { + "name": "admin" + } +}, +{ + "model": "common.role", + "pk": 7, + "fields": { + "name": "observer" + } +}, +{ + "model": "common.appview", + "pk": 1, + "fields": { + "code": "schools", + "name_fr": "Écoles", + "name_nl": "Scholen", + "icon": "bi-building", + "url_name": "schools:index", + "is_default": false, + "order": 30 + } +}, +{ + "model": "common.appview", + "pk": 2, + "fields": { + "code": "sign", + "name_fr": "E-Sign", + "name_nl": "E-Sign", + "icon": "bi-signpost", + "url_name": "sign:index", + "is_default": false, + "order": 10 + } +}, +{ + "model": "common.appview", + "pk": 3, + "fields": { + "code": "asset_management", + "name_fr": "Asset Management", + "name_nl": "Asset Management", + "icon": "bi-database", + "url_name": "home:index", + "is_default": false, + "order": 0 + } +}, +{ + "model": "common.appview", + "pk": 4, + "fields": { + "code": "documents", + "name_fr": "Documents", + "name_nl": "Documenten", + "icon": "bi-journals", + "url_name": "documents:list", + "is_default": false, + "order": 5 + } +}, +{ + "model": "common.municipality", + "pk": 2, + "fields": { + "fid": "", + "name_fr": "Saint-Gilles", + "name_nl": "Sint-Gillis", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((649075.2301120854 669482.1250360702, 649059.0015178479 669471.4521208412, 649078.1638939864 669442.3028995914, 649078.6969874342 669441.4958740631, 649076.1892047069 669439.8465935362, 649123.232814194 669368.0901735846, 649125.8242875555 669369.8115632858, 649157.0726315633 669389.9340645123, 649162.996128121 669393.7487281717, 649160.4646490052 669397.8763706064, 649160.3286232402 669398.098351392, 649155.3196755337 669406.262643931, 649153.1422634695 669409.8123363869, 649151.8620212203 669411.8991555733, 649146.9951002639 669419.8324681926, 649141.099984474 669429.4436356003, 649140.3208370466 669430.7135255642, 649137.7613526069 669434.8861640901, 649136.0890361497 669437.6119279166, 649131.6121887792 669444.9102956818, 649128.9666880626 669449.2229220746, 649127.510412449 669451.5967164187, 649120.179024784 669463.5476811093, 649116.4163124494 669469.682149779, 649107.5666387713 669484.0950003519, 649107.3645019031 669484.4129719593, 649105.9226480941 669486.6045704047, 649098.7080780691 669497.5706619462, 649075.2301120854 669482.1250360702)), ((647019.0251194938 668927.1273376498, 647020.8902201583 668926.191571467, 647022.6523185108 668925.2807927249, 647088.5500320863 668891.7394578874, 647262.4224555991 668803.2398444884, 647270.2918861947 668799.2348298139, 647288.5478850703 668789.9431153933, 647326.7384906241 668766.2539521, 647343.1266083537 668756.0890271162, 647353.8362134257 668750.4773693492, 647367.9490552079 668742.716142882, 647384.5220435794 668733.6022253748, 647400.3675144225 668728.7911648037, 647412.6913180964 668721.4467204474, 647416.8105867768 668718.9912403842, 647419.3227505311 668717.4945574555, 647427.9273359862 668712.165646084, 647430.7275264326 668710.4320003171, 647447.1825289583 668701.2040692071, 647448.2265911509 668700.6302003136, 647456.4770824742 668696.0962362959, 647462.6004472541 668692.7300051531, 647466.0386519869 668690.8404368276, 647468.386791846 668689.5497316441, 647473.6291040351 668686.668389799, 647476.6872861219 668684.9877737265, 647480.2995012457 668683.0022272035, 647483.438688196 668681.276621281, 647489.5660529752 668677.9093904392, 647498.6505939557 668672.9155307392, 647502.8938466097 668670.5830633274, 647525.2251264905 668658.7168605503, 647527.9562791636 668657.2972022033, 647538.365860779 668651.8885042912, 647571.0106849965 668634.9215870108, 647579.3511924945 668630.244634368, 647587.3626798863 668625.7526403335, 647591.6559412061 668623.3441793956, 647595.7161881875 668621.0676891704, 647601.2445239376 668617.9723831862, 647608.5448511264 668606.6013938561, 647615.5750438087 668596.3543578697, 647624.3395307268 668583.5785595068, 647627.1800126063 668579.4379488998, 647629.6104248902 668575.8952820504, 647635.0153417017 668568.0170228734, 647637.3147469052 668564.5403396242, 647639.0760572435 668561.877582234, 647641.581498913 668558.0879273608, 647643.6218584102 668555.0032083838, 647645.596206363 668552.0174803186, 647646.6163861761 668550.4746208262, 647649.5929107737 668545.9730307531, 647653.59561608 668539.9205819592, 647655.7890025845 668536.6038839811, 647658.3374516456 668532.7502348836, 647661.7490527439 668527.5917045965, 647666.0108037749 668521.1462913265, 647666.2788031592 668521.13532314, 647666.2658486274 668520.7613264227, 647678.7250129584 668502.1750380697, 647679.068750515 668501.8448829429, 647683.4581300803 668497.6286558397, 647688.7991136017 668492.4983528921, 647696.8669685681 668484.9664029311, 647697.6200484809 668484.2625009456, 647698.8701809235 668483.0956636335, 647699.3722340334 668482.627728963, 647702.617578019 668479.5971513084, 647702.7545926714 668479.4681691475, 647704.4067675413 668477.9273841191, 647708.4761987829 668474.127913656, 647709.5793156774 668473.098057197, 647709.9753576581 668472.7281087313, 647713.1906980984 668469.7265280315, 647715.8059750652 668467.283869443, 647717.5681614112 668465.6400994514, 647718.9423071082 668464.3552788426, 647721.954625858 668461.5436720347, 647726.1150662528 668457.6592150772, 647728.8523559804 668455.1035723576, 647730.5185323621 668453.547789817, 647732.4317347989 668451.762039515, 647734.7859840608 668449.5633467779, 647735.881100132 668448.539489724, 647739.306462477 668445.3429367334, 647741.4856933695 668443.3062211574, 647742.9488480634 668441.9414120846, 647743.7019277452 668441.2385103597, 647746.3322060944 668438.7828535925, 647748.0203847081 668437.2070738835, 647749.9425880152 668435.4133247029, 647752.381846187 668433.1356429927, 647753.9760149362 668431.6468510145, 647756.9943342899 668428.8292448428, 647759.8416358562 668426.1686163734, 647762.0178660002 668424.1379003007, 647764.0990861796 668422.1951718305, 647765.7112567195 668420.690382161, 647770.4487580226 668416.2670002299, 647771.023818869 668415.7300752464, 647774.2481600478 668412.7194958833, 647778.195643093 668416.7409123189, 647781.543204704 668420.1512654829, 647781.635192739 668420.2442751932, 647781.8151692085 668420.4272941869, 647785.6426505755 668416.2238015318, 647789.9021862913 668411.5453661362, 647797.2791142432 668403.4413438862, 647802.1451018271 668399.9470651525, 647807.0098893024 668396.4535862416, 647816.8438671082 668387.7868621333, 647818.5780395838 668386.258087119, 647830.8542601218 668375.4386796703, 647838.5000201771 668368.7006714167, 647854.2238208186 668352.8827356807, 647860.7185645734 668346.3485882299, 647876.0023145848 668330.9725942174, 647899.849827797 668308.7707007658, 647901.9900682562 668306.6549810912, 647911.7971700296 668296.9602655245, 647924.317576549 668284.5829051054, 647926.7699736251 668281.1552389364, 647926.7831758035 668281.1364407442, 647934.125164447 668270.8752400806, 647936.7415880073 668267.2185961771, 647948.736530022 668250.4522285517, 647952.0990743177 668245.752686094, 647954.3374367952 668242.6229906641, 647976.3697583845 668205.4950076783, 647984.5783648263 668191.6601126797, 647994.8853580653 668174.4854981012, 648037.1935390418 668103.9841831274, 648051.8667758665 668079.5332540908, 648058.3816353374 668068.6770290771, 648061.9434013239 668062.0995135767, 648065.1380876482 668056.1989527857, 648068.5448190266 668049.9064242801, 648072.0725762929 668043.3909125326, 648072.5976890189 668042.4209852138, 648073.7379340081 668040.3131430574, 648074.2200371992 668039.4252097626, 648075.359281825 668037.3203674495, 648077.289696248 668033.7546346491, 648078.9820593904 668030.629868891, 648076.5279491973 668023.4106421046, 648088.1869531616 668014.3091365251, 648099.84695691 668005.2086311354, 648110.1968394564 667997.1999572184, 648114.4872053425 667993.8795069477, 648122.2838701037 667987.8465059621, 648140.8584204898 667973.7488834691, 648146.5916834904 667968.6963241129, 648153.0426169165 667963.0114574883, 648164.5026159973 667953.9589279573, 648167.7959305843 667951.1293527446, 648173.523440591 667946.5160885587, 648181.2361271738 667940.3050794324, 648206.4193583379 667920.1113142171, 648220.6525185725 667909.533134567, 648224.4988320648 667906.6746265157, 648227.9281115765 667904.1260651263, 648228.9541952438 667903.3631963664, 648237.064880759 667897.132235743, 648237.5479133677 667896.8292969826, 648242.7032605069 667893.6039505455, 648247.8236571903 667889.9696038571, 648249.9788241424 667888.4398788401, 648253.8491240204 667885.6923726698, 648254.9212070506 667884.9315094565, 648255.9012830098 667884.2356345141, 648259.0255250243 667882.0180331578, 648264.2529299452 667878.3077001683, 648269.9374481366 667873.6264317863, 648275.3509172937 667869.3701265798, 648280.3313729375 667865.2547677453, 648284.8927885866 667861.4993548471, 648289.0191084733 667858.5678814471, 648294.186542084 667854.6225435842, 648300.3074651569 667863.1541999371, 648302.7813145189 667864.2394881087, 648311.7174787194 667870.5815055295, 648314.7030859714 667873.6428363047, 648340.0477522376 667899.6306439673, 648347.0358332312 667906.7954180206, 648449.7103665314 667862.289247255, 648464.6160243669 667855.8281101339, 648474.5444625197 667851.5243510192, 648476.9075667267 667850.5006463615, 648489.494122194 667845.0442195316, 648508.3159525456 667836.8865721258, 648518.9554220363 667832.2739020481, 648538.5502864438 667823.7803514831, 648554.6943154023 667822.4583140744, 648630.0134506029 667816.2884706984, 648647.2005370107 667814.4185647457, 648650.0085506593 667814.1169068303, 648653.7205687978 667813.7173590648, 648660.2396008401 667813.0141532837, 648667.6476371244 667812.2160558124, 648675.6966085482 667811.9140308709, 648677.2046032175 667811.8572135498, 648725.9687475725 667807.3881469292, 648732.1027657315 667806.8258933052, 648744.8218425608 667805.3344441584, 648747.5648591272 667805.0127786228, 648754.147850669 667804.6415773394, 648760.2828427125 667804.2963217014, 648765.4188360826 667804.0069448533, 648766.2158350622 667803.9620415543, 648766.9788341223 667803.9186341391, 648773.373825716 667803.5599099697, 648780.9388159681 667803.1338277486, 648787.3058552383 667802.3806040997, 648813.4880166155 667799.2837965979, 648830.988124445 667797.2139304935, 648853.3664037851 667793.3906709136, 648861.6014213072 667792.6926723592, 648871.2464601404 667791.722846786, 648876.9394830427 667791.150539991, 648884.6334135276 667791.2124685588, 648885.4123995347 667791.2765620005, 648889.1773556422 667791.3890155733, 648895.7612788583 667791.585808746, 648903.1382622983 667791.2287031882, 648910.6902452813 667790.8636188568, 648916.7431266161 667791.444344081, 648924.7539695235 667792.2133038886, 648931.6188349283 667792.8721263881, 648933.8867905104 667793.0893981231, 648934.0336830281 667793.9734070703, 648936.5308551359 667809.0075591151, 648936.5318541649 667809.0155591542, 648937.537241955 667814.0396305901, 648940.3425313664 667828.0778296981, 648940.5762934511 667830.0408383989, 648942.3165213886 667844.6619030661, 648944.4663334896 667862.713983032, 648947.0197340411 667884.1620778947, 648948.3214092446 667895.0931262679, 648948.7566423486 667901.4421156257, 648949.0371475887 667905.5381087296, 648950.4875878516 667926.7300729086, 648950.7780752328 667930.97406573, 648951.6928789747 667949.1789949562, 648952.2405728829 667960.0049533063, 648952.6116714998 667967.4769237299, 648953.0386464216 667975.9738906764, 648953.4167383729 667983.5008613862, 648953.7728841224 667990.5818338842, 648954.1350156296 667997.780805923, 648954.8313418961 668011.6547518475, 648954.8552851873 668012.1247500522, 648957.5558161674 668015.8460389134, 648962.8500966023 668023.1422051779, 648962.8810912333 668023.1848084918, 648968.2459594446 668030.5781822813, 648983.0545073812 668041.67185876, 648986.9181285449 668044.5662961382, 648989.0358103313 668047.0725266987, 648991.3004698979 668049.7537732405, 649036.2897670936 668102.525675028, 649067.9276898394 668134.3501738751, 649098.066806305 668164.6675062454, 649099.5646133745 668166.1736718393, 649109.593018946 668170.456838616, 649121.54431049 668175.5622290657, 649138.109016701 668185.2361301407, 649147.3332962886 668190.6231887273, 649154.3767461812 668194.7369970034, 649155.7606381136 668195.5451558152, 649164.2249771567 668200.488127116, 649164.4379604441 668200.6131515516, 649168.6726297163 668203.0866374748, 649169.6645522924 668203.6657512888, 649174.4291801243 668206.4492980078, 649174.8921440158 668206.7193511301, 649180.3077210345 668209.8829725223, 649184.241413759 668212.1814238748, 649180.6429848275 668215.9879516347, 649180.0789175568 668216.58487761, 649178.2506995351 668218.5196376776, 649174.5362567179 668222.4491502357, 649172.6870363245 668224.4049075743, 649177.0336532189 668227.3013976668, 649180.7973210296 668229.8103193585, 649182.7611477474 668231.1190393935, 649187.4407347242 668234.2385636945, 649192.0893244975 668237.33708452, 649195.1300560975 668239.3644251905, 649198.1276016831 668242.9377407972, 649208.4030440594 668255.1868226198, 649219.6228690171 668264.201059836, 649221.7247185047 668265.3122990191, 649221.9777004707 668265.4453278169, 649223.7475739167 668266.3795292395, 649224.4414861151 668267.0626041411, 649248.9233914673 668291.1382468787, 649261.8320687024 668276.377993327, 649266.3816597043 668271.1766088669, 649270.0030301511 668267.03609883, 649269.5672857943 668273.2411664649, 649269.5663840178 668273.2559661657, 649269.0184479866 668281.0587997119, 649267.0440791999 668309.1412003812, 649266.6532583262 668320.9567012616, 649266.2371452624 668333.5387892367, 649264.6364512531 668347.6994140832, 649264.2590513492 668351.042325577, 649264.0518315887 668352.8792769609, 649266.1212926354 668365.5143639377, 649266.137281065 668365.6093646428, 649267.9569276293 668376.7214410787, 649267.9819092369 668376.8724421492, 649270.5759804512 668392.7085511592, 649270.6009612154 668392.8665521401, 649270.6249440372 668393.0075532161, 649274.0144233776 668413.7036955587, 649274.0002401944 668415.224674282, 649273.9992232035 668415.3656723422, 649273.9424861304 668421.4855867624, 649273.9262725884 668423.2585619945, 649273.919186239 668423.9755519173, 649274.4061795453 668432.2975035469, 649274.6207183638 668436.1104803465, 649275.5804317782 668455.0223526981, 649275.6992382774 668456.6203464606, 649275.8719566216 668458.9463373509, 649276.4460211932 668466.6713071412, 649276.5718153471 668468.3713004313, 649276.9711649762 668473.7422794522, 649277.8896682913 668486.1024311141, 649278.2750415609 668491.2782109634, 649278.3109822253 668491.7682089934, 649278.739283309 668497.5401863364, 649279.5559526855 668508.5291433585, 649280.1999040693 668517.1891095245, 649280.8797945665 668526.3520735538, 649281.0065871505 668528.0650667915, 649281.5117622003 668534.8780399994, 649281.5207474335 668535.0000395053, 649281.7513701371 668538.1160271987, 649274.6457375182 668543.8230973975, 649268.0188630813 668549.8461212382, 649261.2333724587 668556.0133241409, 649258.7271128116 668558.3289923174, 649248.1460166619 668568.1045914168, 649245.7127644841 668570.3532692639, 649237.5329170445 668577.9101863377, 649233.0734550611 668582.0295959658, 649229.1010434084 668585.7000700738, 649224.8996079648 668589.5825138649, 649220.8851920348 668593.2909824288, 649220.7191747514 668593.4449604513, 649216.5227399651 668597.3214049373, 649212.3583084111 668601.1688536576, 649208.9629564638 668604.3064041939, 649207.9068469955 668605.28226439, 649203.4933897454 668609.3586801792, 649203.3893789739 668609.454666411, 649194.7564842395 668617.4305236832, 649194.4734548505 668617.6924862125, 649190.1710088567 668621.6679167043, 649185.252499146 668626.2112656834, 649180.7290302337 668630.3906669492, 649175.6455031956 668635.0879940883, 649172.4191687611 668638.0685670632, 649182.654629625 668650.1806447894, 649194.3988636389 668664.0788812897, 649202.3012933666 668674.1115045112, 649214.1893882898 668687.4972647827, 649210.9725576587 668690.4455393278, 649199.6503935705 668700.8250418769, 649195.4469613725 668704.6784859593, 649190.9024941724 668708.8438849507, 649186.8310755166 668712.5763465086, 649182.1305922415 668716.8847248885, 649187.7627835233 668723.2343218419, 649188.5256739748 668724.0944026989, 649197.4853873 668734.1973522604, 649198.2242813169 668735.0294305856, 649214.0720058143 668752.8981100457, 649216.7639351807 668757.4608756696, 649219.3648838289 668761.8696323108, 649225.5603224918 668774.429217138, 649226.5970610605 668776.5323149839, 649226.9969603558 668777.3423527397, 649229.4933313193 668782.4025883832, 649232.4895763358 668788.475871216, 649232.8714801158 668789.2499072633, 649236.4955667888 668796.5972493406, 649236.7085133246 668797.0272694654, 649240.215656972 668803.9096034155, 649243.9647418951 668811.2639604314, 649247.3349193007 668817.875281348, 649250.7670816659 668824.6076081627, 649250.8900515635 668824.8496198612, 649254.9030722375 668832.721001971, 649258.1782727388 668839.147313792, 649263.4299909187 668849.4508137909, 649264.9536191879 668852.4389588512, 649266.572224307 668855.6131129616, 649273.1646569958 668866.5420663371, 649265.0792038846 668872.4717162908, 649251.4090995612 668882.4969410187, 649247.8618129606 668885.0984803676, 649261.5005014257 668903.4178872389, 649266.3126044552 668910.5573748993, 649235.8722170586 668957.169109843, 649237.0425688864 668962.4731825701, 649238.2399060513 668967.8972570039, 649240.0558009319 668976.1223699016, 649241.7498626804 668983.800475146, 649242.9822804371 668989.3835517453, 649245.0941114965 668998.9496830008, 649245.4779933698 668999.9057169252, 649250.4374677646 669012.2531552614, 649250.9332152957 669013.4871990834, 649252.2718036027 669016.8193173873, 649258.318943505 669031.8748517983, 649260.6252339775 669037.6180555879, 649262.7365847346 669042.8732421744, 649264.9069169784 669048.2784339322, 649265.0428752254 669048.6164459505, 649265.1108542273 669048.7864519511, 649265.1648375469 669048.9214567132, 649265.9016110857 669050.7545218256, 649260.9401727429 669054.7008737482, 649260.0530943325 669055.406757872, 649258.430951063 669056.6965459886, 649248.1670442027 669064.8602053287, 649218.4604189546 669088.4883253004, 649216.7281239695 669091.0430839265, 649215.0928456171 669093.4538560817, 649215.0442104663 669098.7257824653, 649214.8851480575 669115.8445432766, 649214.6835418199 669137.4772409571, 649213.6534950385 669137.9291111617, 649204.3930744224 669141.9919442676, 649196.112698271 669145.624900883, 649184.9781923642 669150.5104978824, 649175.3507549564 669154.734284807, 649174.6498230601 669155.0421964794, 649171.9326996544 669156.2338541269, 649166.6504595259 669158.5521885556, 649167.2661067309 669161.4402255665, 649168.6802959407 669168.0773105451, 649173.8012724766 669192.8376088878, 649176.5306611501 669206.0337678819, 649177.7519403643 669211.9368390414, 649185.3254692965 669248.5552800788, 649179.8745063221 669256.8775171823, 649133.1632526504 669328.1909805151, 649064.9761973266 669432.2973414432, 649062.4654137966 669430.6550604613, 649042.77993264 669460.7054073123, 649026.6823363478 669450.0446075032, 648971.4341551486 669413.4574297137, 648967.8324686699 669411.0780268593, 648962.5849254549 669407.6114399182, 648957.0064111339 669403.925815949, 648903.6300586096 669368.6658450526, 648889.5422856078 669359.3592689817, 648860.3258307325 669340.0580001634, 648842.5963753099 669328.3460164692, 648789.881968877 669293.523117883, 648784.90040308 669290.2321604221, 648779.1979001436 669286.4649222671, 648769.1867727616 669279.8518019209, 648769.1597751353 669279.8337989005, 648747.0496288497 669265.8363163482, 648731.4749347918 669255.9765675012, 648716.428196665 669246.4506778728, 648697.3677953726 669234.3837374381, 648684.8908420875 669226.4843362563, 648675.1736572164 669220.3332449566, 648663.5114357125 669212.9500351985, 648616.302597634 669183.0626328352, 648603.1676393243 669175.2511510339, 648602.7466726747 669175.00110354, 648597.972995883 669172.6205591589, 648592.7553489994 669170.0199641378, 648590.396508716 669168.8436951365, 648580.0600986112 669164.5995048089, 648576.2063185591 669163.0170609999, 648561.5420008807 669158.275355982, 648546.822539108 669154.7296291023, 648544.469598734 669154.3813502758, 648542.5056485685 669154.090117562, 648526.3530579249 669151.6992034786, 648512.7452662414 669150.8145766058, 648481.031751806 669148.75278512, 648473.2108714989 669148.244850083, 648466.3869760255 669147.8010342554, 648420.9376720332 669144.8466005157, 648411.4168178836 669144.2274623286, 648393.3749941755 669143.0549056849, 648372.1684189477 669141.6767707268, 648371.7314395151 669141.5337199384, 648358.9190409194 669137.3522307432, 648351.6193642853 669135.1293802587, 648339.4948906883 669131.5259664999, 648327.4300144865 669131.246517878, 648325.6230330666 669131.2043009268, 648318.4363623542 669132.2316229399, 648316.5369436612 669132.5031908974, 648314.7419259712 669132.7599715986, 648313.1309100543 669132.990774777, 648304.3716551891 669135.6366870875, 648284.646057208 669141.7932351767, 648261.5743294071 669149.2273644861, 648200.0344648693 669168.4167160023, 648194.1262808453 669170.2999812263, 648189.4390087817 669172.8353851784, 648184.0585116158 669177.2726817495, 648180.3780354061 669181.4321864198, 648177.2024756445 669186.250743577, 648176.4852939335 669187.7956378086, 648174.6438272563 669191.7633662284, 648172.9711450213 669197.5010926798, 648149.3525413596 669303.0429219977, 648146.6430960852 669315.1444437136, 648145.087265583 669322.0981690199, 648141.492346211 669338.1685342973, 648141.4483232875 669338.3605265818, 648136.9019631596 669358.1287308283, 648134.4606956745 669368.7448035323, 648132.5296930871 669377.1419655546, 648121.9632071556 669423.0871162921, 648117.8180547754 669441.1123908767, 648112.7284119017 669463.2445002146, 648108.2140676958 669482.8747102607, 648099.1793756849 669522.1631293838, 648085.0880575385 669583.4356640507, 648080.7287931815 669602.3929014212, 648079.3080556312 669608.5676529239, 648075.4710623139 669625.2549816757, 648068.1302494345 669657.1736976252, 648066.7535342613 669663.1604568241, 648065.5409043593 669668.4332447186, 648055.647761558 669711.452519929, 648052.8703171785 669723.5320359694, 648048.2107946179 669743.7915240178, 648048.193885801 669743.8652210757, 648046.4132599131 669751.6081107367, 648043.4474177466 669764.504393803, 648038.6109029459 669785.5335507084, 648031.6512836057 669815.79733721, 648029.3490863615 669825.8079357239, 648021.6040585376 669859.4845848127, 648021.3679355845 669860.5125436035, 648013.3727765509 669895.2771487149, 648011.0185435846 669905.5157386747, 648009.8689417315 669910.5135384557, 648004.7802775325 669932.6366520831, 647998.8244585507 669958.5354143468, 647995.5904649574 669972.5980507586, 647979.9759882656 669969.1572246363, 647977.1164865271 669981.6257256721, 647970.4916942589 669980.2829493879, 647969.2677326462 669980.034805969, 647937.0775446474 669975.1290138079, 647931.0246390643 669974.6842947585, 647914.4248981811 669973.4633228391, 647905.6080357565 669972.815275474, 647897.8002434783 669971.5373568715, 647896.3782813636 669971.3041895846, 647893.1344374955 669970.2008151764, 647889.2026267597 669968.863361369, 647878.1698737398 669959.242162213, 647871.3306561197 669953.2014199081, 647854.3935936451 669938.2425818816, 647843.687818513 669928.7864203034, 647842.8489906207 669928.2411268172, 647831.9598273606 669921.1571133917, 647823.669393369 669916.9701743304, 647816.56287854 669913.3813694161, 647799.1690661053 669904.5973994901, 647780.5433378906 669895.1912903171, 647761.7216166448 669885.6861598055, 647728.6367842573 669861.3895128535, 647722.4615756071 669856.8543322891, 647695.5529517907 669837.091869027, 647691.6322352334 669826.7265337708, 647691.6710515318 669819.9956249432, 647691.7439255891 669792.1789912512, 647691.7486389469 669790.4200144276, 647690.068458936 669783.7528994487, 647690.2729772727 669779.4689789553, 647690.9115943582 669766.1042270538, 647690.2399446863 669755.0102894739, 647686.6747330289 669748.7079447844, 647684.4702202895 669744.8127316508, 647684.0163030248 669744.1556859029, 647678.9242294925 669736.7991725501, 647671.5860942269 669730.0753829764, 647661.165204108 669721.5012493081, 647656.3078315116 669716.5977326435, 647649.2327455914 669709.4539801822, 647648.2938687253 669708.4908805324, 647638.3641713528 669698.3028267595, 647636.5055361914 669695.3986424112, 647629.5509018417 669684.5279527634, 647625.8166350791 669678.6915825116, 647611.8686999248 669654.2062347118, 647589.5148631264 669637.6187825328, 647570.3451712506 669619.6637286528, 647566.601622042 669616.1573276445, 647558.1576046373 669608.5304195505, 647535.215274563 669587.8089529102, 647534.8373186272 669587.4669122879, 647527.833570044 669585.7850990172, 647514.8355422067 669578.4996440292, 647511.8461030198 669574.0463456244, 647501.8949696985 669559.2243524817, 647497.3585594008 669546.3829792328, 647489.0694646048 669522.9162975497, 647488.0096436054 669521.5011897205, 647478.3902682856 669508.6572111343, 647470.1570584858 669502.6093091017, 647461.9248487374 669496.561407285, 647460.9534703522 669491.4963578216, 647460.6476657377 669489.904342217, 647460.8536031824 669482.172467925, 647462.0868124402 669476.2621921765, 647465.8514770239 669458.2243766072, 647466.0587217459 669447.961535525, 647460.1071997966 669436.1229815854, 647451.6344959895 669430.0420521742, 647437.0503666486 669419.5744526145, 647434.9035684715 669418.0332171893, 647433.2187266946 669416.8250324117, 647427.8825858468 669410.0504858878, 647423.3005505019 669385.8962572722, 647423.2559665052 669382.4732968844, 647423.1613382405 669375.3003797435, 647423.0974298564 669370.4324360135, 647423.342864504 669366.8395123691, 647425.0729233501 669341.5540500298, 647426.8749445605 669324.8084841621, 647378.7174923848 669265.3935366451, 647370.3288260516 669254.8906774125, 647361.9401596821 669244.3888183398, 647332.3978186378 669207.7167902831, 647322.8161523901 669180.8220069718, 647319.3247081225 669164.9237023175, 647319.1132265794 669163.9607899212, 647318.548494935 669157.6688060357, 647318.3392795887 669155.3378120074, 647319.0110836826 669148.6809798563, 647319.0931816429 669147.8700003374, 647320.842271259 669130.5704368834, 647321.5350998492 669123.7106098809, 647321.2255249084 669111.9987279648, 647304.4790850864 669083.6571142743, 647286.472817026 669062.2082606044, 647269.5553841286 669042.0565197336, 647243.6142430577 669011.7988430575, 647232.5903548504 669003.2876482027, 647222.1909579525 668998.9264723492, 647187.522348906 668989.4874849345, 647098.774919249 668965.2582815778, 647062.9204349845 668954.8721686542, 647028.414269649 668941.7892528384, 647019.0251194938 668927.1273376498)))" + } +}, +{ + "model": "common.municipality", + "pk": 3, + "fields": { + "fid": "", + "name_fr": "Ixelles", + "name_nl": "Elsene", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((648487.6996488319 667299.7886003482, 648487.688648686 667299.7905990061, 648487.841723045 667299.1606224324, 648488.2139040092 667297.6276794235, 648490.0467931911 667290.0949599458, 648491.5395175783 667283.9581884386, 648491.5505233716 667283.9091901516, 648491.5625290356 667283.8611919823, 648492.3639177302 667280.5683146364, 648493.130289421 667277.4194319344, 648494.8431208644 667270.3756941517, 648496.4789145318 667263.6519445576, 648498.1497253064 667256.7832003499, 648499.787519784 667250.0524510748, 648501.4463247628 667243.2327050449, 648503.1971743474 667236.0349731188, 648504.0285780217 667232.615100438, 648504.5938520789 667230.2931869813, 648505.3112002725 667227.3432968183, 648506.1105854729 667224.0794190549, 648507.1660934745 667219.7749804085, 648508.5597763031 667213.9907942506, 648510.2295864441 667207.1270499598, 648511.9374152296 667200.1053115046, 648513.5541998638 667193.457559122, 648514.5376769246 667189.4157097366, 648515.1999983067 667186.6928111783, 648518.0243686729 667175.0822437718, 648519.4080398644 667169.3954556949, 648520.8087195563 667163.6366702552, 648522.2314098618 667157.7878881827, 648523.6180828123 667152.0861006173, 648525.1528305204 667145.7513359238, 648533.3065489501 667147.5373091944, 648533.2080777516 667143.1383316588, 648533.1782366777 667141.8163383827, 648533.0648478229 667136.7323643893, 648533.0369955674 667135.5033706301, 648532.919626307 667130.2563974322, 648532.9136574419 667129.9973987307, 648532.7863415603 667124.306427796, 648532.778383749 667123.9554295761, 648532.6401247361 667117.7914610155, 648532.6371402391 667117.6624616615, 648532.5187743503 667112.3874885674, 648532.4949038213 667111.3104940976, 648532.377534802 667106.0615209246, 648532.3646033234 667105.4915238188, 648532.2740889773 667101.4515444473, 648532.2372855216 667099.8165527697, 648532.2362875732 667099.7995527864, 648532.1149428254 667094.3486807138, 648535.2868829841 667094.6290626503, 648543.5437274431 667095.3570568769, 648549.3486179856 667095.8697558406, 648560.426150218 667096.5056923687, 648573.5442515572 667097.258675082, 648580.3221423397 667097.702492408, 648591.3189652383 667098.4218184871, 648593.6169282349 667098.5720955931, 648607.8936983382 667099.5058171769, 648611.1996450751 667099.7222158231, 648614.4675924927 667099.9356099002, 648633.7962811155 667101.2009406409, 648639.978181519 667101.6056860765, 648646.1370823599 667102.0084287392, 648652.486980183 667102.4231944382, 648654.8969413026 667102.581485034, 648658.3848851044 667102.8099056205, 648663.0738096148 667103.1164710211, 648663.4298039187 667103.1395139499, 648670.5896885635 667103.6083773017, 648676.7375896219 667104.0101186298, 648677.4045788242 667104.0541990561, 648682.1045031347 667104.3617657793, 648683.0054886068 667104.4208744183, 648688.13440601 667104.7564928725, 648695.392289136 667105.2313680312, 648702.5831734166 667105.7012351155, 648709.7860573538 667106.173103633, 648716.4569500199 667106.6089080088, 648720.4548856519 667106.8703900796, 648723.5868351719 667107.075767726, 648727.6977689092 667107.3452634092, 648730.5137236755 667107.5286029618, 648736.7736228642 667107.9383577668, 648744.1285043567 667108.4202445941, 648751.1253916938 667108.8780882639, 648754.4903376087 667109.0974940046, 648758.4452738434 667109.3569708732, 648759.5502561879 667109.4281041162, 648760.168246162 667109.469178628, 648773.1030378774 667110.3157383384, 648787.880701118 667112.1065138575, 648805.7574281334 667113.152670603, 648820.1872078718 667113.9964114913, 648827.9040869391 667114.4738422841, 648830.7194428812 667114.6474818686, 648833.767995175 667114.8355495804, 648840.5798885598 667115.2558712184, 648850.8557275921 667115.8911106578, 648854.3966721825 667116.1095377597, 648856.851632103 667116.2748337586, 648875.038335122 667117.5000265553, 648904.099860621 667119.4575304975, 648910.271759849 667119.873274633, 648922.1205663598 667120.6717032182, 648970.8817630663 667124.0165816762, 648985.8614885838 667125.2753856792, 648987.0024689602 667125.3605231772, 649006.649130629 667126.8308906406, 649043.0425039853 667129.5542760475, 649046.3154570491 667129.72067107, 649053.7803499432 667130.1005720235, 649061.1822438458 667130.4764653755, 649061.1882437912 667130.4764661007, 649066.3721614099 667130.8070912109, 649068.0810837593 667131.3372938829, 649071.475929473 667132.3906964822, 649080.6937294978 667133.4248044032, 649092.6284706373 667134.7632388746, 649101.1479961465 667133.1303838285, 649101.7384045197 667133.0199562222, 649109.557715402 667131.5572150134, 649112.9196611361 667131.7786203589, 649124.0814807131 667132.5159660362, 649136.0402873504 667133.3064076641, 649148.3610881281 667134.1208929252, 649149.4830699913 667134.1950281784, 649155.9859648541 667134.6248120979, 649168.3647646798 667135.4433043301, 649175.1866543925 667135.894126691, 649183.4985200629 667136.4431286636, 649195.4823263934 667137.2345732704, 649201.2652328459 667137.6172703764, 649208.6831129697 667138.1071645733, 649207.644432144 667143.8589918892, 649206.9079500523 667147.9318694924, 649205.4820153174 667155.8286324379, 649204.4093127085 667161.7644541503, 649203.2695655829 667168.0762646857, 649202.093795372 667174.5830692778, 649200.2565914671 667184.7537639262, 649198.9007035954 667192.2545386277, 649196.3350220526 667206.4601122278, 649195.4654523396 667211.2729677313, 649193.6212440197 667221.4806612912, 649208.4160433572 667222.1394467562, 649218.3658783098 667222.8336454625, 649216.3935311034 667234.2093138759, 649212.8862376874 667253.5847312585, 649212.8975373486 667253.586832606, 649220.2040168362 667254.9256059815, 649232.1386566961 667257.1120325577, 649239.8734233181 667258.5289571054, 649238.4965160075 667266.1937278227, 649237.4258113555 667272.146549597, 649236.3571078805 667278.0893717007, 649235.3074175577 667283.9211970153, 649234.2767395341 667289.6490254898, 649234.0856135855 667290.7129936673, 649232.9878916435 667296.8118110027, 649232.4785562996 667299.6447262247, 649231.7510783194 667303.6826052067, 649231.3207948145 667306.0775335773, 649230.087984808 667312.920328504, 649228.8041404613 667320.0531148954, 649227.7554513036 667325.8749404529, 649226.7217717278 667331.615768496, 649246.4441869813 667335.1451259133, 649249.6960905107 667335.7275146048, 649269.3013434102 667340.619846612, 649283.6867952574 667344.2095576789, 649298.2172416255 667347.835285943, 649306.3599571312 667349.6522561873, 649318.821521656 667352.4337410086, 649320.9234483202 667352.9019914647, 649322.5743905456 667353.2711881696, 649332.4690448282 667355.479367108, 649338.242843085 667356.7680550461, 649341.7927190317 667357.5604780002, 649342.0198428631 667356.5115141001, 649342.7172233423 667353.2886249732, 649343.2425103261 667350.8577085073, 649343.8268290577 667348.1578014041, 649344.5172058701 667344.9659111797, 649345.1295401922 667342.1340085486, 649345.5667786481 667340.1140780606, 649350.1136160792 667341.1596195834, 649350.118616041 667341.1596201896, 649355.4434255128 667342.3852543454, 649355.6294188376 667342.4282764953, 649355.8124117008 667342.4752982464, 649368.264874349 667346.1077749436, 649381.4193059689 667349.951334783, 649381.4223057047 667349.9533351306, 649389.4829574577 667352.3082909347, 649403.7443413269 667356.4749819536, 649408.1821494934 667357.7725081427, 649417.995725546 667360.6396717299, 649422.8760162672 667357.8792849487, 649427.1822727654 667355.4438260484, 649428.9173760487 667354.463044071, 649437.2328713333 667349.7600889867, 649448.2125252669 667343.5504687304, 649451.1146981479 667341.9088334464, 649460.4342531861 667336.6380046426, 649464.7735115283 667334.1845499715, 649477.7232832925 667326.8561774908, 649482.1135399134 667324.4137289131, 649490.9080540993 667319.5198335596, 649493.2221893867 667318.2321242385, 649501.2306576535 667313.7751302011, 649509.0971173616 667309.3991183466, 649514.7508750786 667311.0347888648, 649521.157600613 667312.8875486762, 649530.3422070332 667315.5446379023, 649536.6419371823 667317.3663849965, 649542.9876652787 667319.2021375288, 649549.3913909865 667321.0538969338, 649550.9139853996 667315.9871233305, 649555.788888379 667299.7658482163, 649557.3064805443 667294.7180738823, 649562.9957011271 667275.7889199666, 649564.4082523329 667271.0901300465, 649567.6014985471 667260.4666049903, 649574.3403813108 667260.9834162495, 649587.8521461968 667262.0200428488, 649596.8979887944 667262.714131817, 649605.8588329236 667263.4012105506, 649620.8285724348 667264.5500126301, 649633.3823540903 667265.5125238706, 649634.6313323192 667265.6086742165, 649637.7802775629 667265.8500532898, 649646.6717248193 667269.8560958166, 649653.8992754733 667273.1129432215, 649662.3877477539 667276.9379384341, 649672.2441350417 667281.3790939859, 649677.9157824673 667283.9347589049, 649692.8098565771 667290.6465049408, 649700.597372407 667294.1564178313, 649711.5806897021 667299.1057053079, 649717.4193268295 667301.7363897034, 649725.4888251874 667305.3733355557, 649728.7436229188 667306.8397170621, 649736.4081465328 667310.29361541, 649750.274284715 667316.5422405838, 649760.9186231159 667321.3394880834, 649771.5019655117 667326.1077283989, 649783.6992075321 667331.6041577797, 649789.4428478135 667334.2158306502, 649815.0977581738 667349.9158018412, 649822.3331688762 667354.3436397247, 649828.1236917281 667357.8873212696, 649829.9150058586 667355.1155674042, 649842.3249236753 667363.1350418366, 649844.020608461 667365.6292313796, 649851.0992926041 667376.0420224391, 649853.1681081429 667379.0846535817, 649859.9916401977 667389.1200157535, 649861.7586499854 667388.8932372088, 649869.5386927882 667387.8972122461, 649870.2426966302 667387.8073004773, 649871.0667579246 667387.2274078191, 649873.8999686405 667385.2337768765, 649874.5620178838 667384.7678631179, 649876.8331868466 667383.1691589889, 649879.8254093627 667381.0635488159, 649881.1885362786 667379.8907282427, 649884.9358854457 667376.6642215606, 649886.4690690517 667375.00342634, 649889.6954552492 667371.5098572874, 649892.4217815046 667368.5582214788, 649894.5260333762 667366.2795026032, 649896.0972214742 667364.5777125233, 649899.2575996183 667361.1561347967, 649898.7191768081 667356.3751090729, 649898.1288093003 667351.1360808349, 649897.9460051156 667349.5140720888, 649897.8471108758 667348.6380673402, 649897.2297727106 667343.1560378207, 649902.6641489597 667339.56174447, 649907.0284513119 667336.6733120524, 649907.2634674035 667336.5193426072, 649907.5644882239 667336.3203817522, 649910.3096782478 667334.5047387889, 649913.4708971414 667332.4131499648, 649913.6169070549 667332.3181689433, 649914.5709731925 667331.6862930473, 649921.4414486568 667327.1421867881, 649923.9706236519 667325.4695158284, 649928.021903959 667322.7900429254, 649932.6917227809 667323.9186139099, 649932.9208099504 667323.170648902, 649933.8821949996 667319.8707971541, 649936.54240671 667309.5162180859, 649937.4108022143 667306.1363555216, 649952.0292852867 667309.2471470073, 649952.3594280913 667308.0251987468, 649952.9506842581 667305.8332914405, 649953.5789554749 667303.512389873, 649957.2085260148 667290.0729589232, 649915.3593002063 667278.7168494115, 649918.6037238446 667266.542358654, 649922.1872963407 667253.093921336, 649925.651816453 667240.0924655152, 649925.7648664981 667239.664483303, 649927.3065425651 667233.881725499, 649928.8232078606 667228.1909638145, 649929.104331187 667227.1360079945, 649930.4139057455 667222.221213818, 649931.993843244 667222.6114072663, 649932.3820232495 667221.073468986, 649933.4315102312 667216.912635901, 649934.6800895468 667211.9628345018, 649938.3567947248 667197.3924194205, 649945.9464805919 667199.3813480516, 649947.9854371466 667191.2086734516, 649950.3135283383 667181.8850450116, 649952.0655412998 667174.1004313971, 649953.8153530854 667166.3256174781, 649955.7212626351 667158.5559230441, 649960.8200594516 667159.824547877, 649965.7908613919 667161.0611569994, 649971.4286368886 667162.4628478183, 649977.1514089049 667163.8865490181, 649982.4371983926 667165.2011966544, 649998.3795634881 667169.1671497677, 650014.0329402529 667173.0610672215, 650020.1286975486 667174.5778138482, 650025.5414820832 667175.9244767884, 650031.9362275575 667177.5152599555, 650038.1509802679 667179.061021048, 650048.5125679972 667181.6382898806, 650059.7021192682 667184.4516597069, 650062.8929914199 667185.2530503214, 650065.875871772 667186.0034154428, 650070.817673622 667187.2460203465, 650073.6385606406 667187.9543656319, 650082.3302122387 667190.1394294492, 650091.7108362964 667192.4975774966, 650102.9523857898 667195.3239531704, 650103.6703570739 667195.5040410385, 650110.4060871485 667197.1978652402, 650113.4049670678 667197.951232184, 650123.9415449421 667200.6005213527, 650126.080459319 667201.1377830409, 650132.1142176775 667202.6545212169, 650135.2360925789 667203.4399031268, 650134.268174778 667211.2137116132, 650132.2362830399 667227.2393123694, 650130.1863742076 667243.4089097077, 650128.7250133059 667254.9366227256, 650126.8782941821 667269.498260225, 650125.7342286925 667278.5230356813, 650125.2928178333 667282.0029490743, 650122.103848223 667307.1543234782, 650120.318186477 667321.2279734178, 650118.5375277207 667335.2756243179, 650115.9010720644 667356.0711076636, 650115.0122446307 667363.07793357, 650113.6579827039 667373.763668272, 650112.9413157215 667379.4115279679, 650112.8031866461 667380.504500883, 650112.1906167254 667385.3303809977, 650111.1946889522 667393.1861860086, 650111.1546515154 667393.503178162, 650110.3499015721 667399.8530206084, 650108.9536010355 667410.8647473429, 650108.3420311759 667415.6896276602, 650101.9631886964 667448.4055394552, 650100.844527144 667454.0393496434, 650098.9243910538 667463.7140238681, 650098.100904082 667467.8608841901, 650097.0742968477 667473.031710075, 650096.091715434 667477.9825434351, 650093.6822897332 667490.1221348951, 650092.4985896688 667496.0829342607, 650087.0903896033 667523.3280178746, 650085.3823790973 667531.9307286092, 650083.627340359 667540.7734314108, 650082.1524678099 667548.2011817378, 650081.4130299899 667551.9280565585, 650079.8491046433 667559.804791876, 650079.77005813 667560.2007785245, 650075.4034732888 667582.20203973, 650075.1263098691 667583.5929928992, 650073.698464285 667590.7897513903, 650072.3966936431 667597.3485312703, 650070.5796183088 667606.5002241057, 650070.3825018771 667607.4911908035, 650068.5454146203 667616.7438803271, 650067.2406422541 667623.3166598484, 650066.7053252115 667626.0145693999, 650066.222038721 667628.4524877133, 650065.5066156801 667632.052366894, 650063.5874793023 667641.7220427165, 650057.8031141291 667650.2646507001, 650057.526467965 667650.6732128271, 650051.8515504906 667655.4570681873, 650044.6952978624 667661.4895293404, 650039.8028516013 667665.6138875901, 649922.3010986521 667764.6613298293, 649916.1325375697 667769.8255142045, 649909.9649763984 667774.9896988055, 649847.2692236223 667827.8514122597, 649837.2115312219 667834.4031038686, 649838.4132456268 667845.0321424023, 649850.3254438522 667917.5288618561, 649854.3967290244 667939.8631323562, 649856.4583111521 667951.5332654781, 649857.7864760568 667958.3983579563, 649859.1156409725 667965.2634505322, 649867.4420603976 668011.186999499, 649871.6564462736 668024.3353816402, 649885.3431170315 668051.0837873411, 649889.7441208311 668059.0662455254, 649891.7456679397 668062.6954538745, 649881.1026218376 668063.87713281, 649760.5370347359 668077.2583172189, 649748.5564707627 668078.5878579542, 649737.9169139332 668079.7685620328, 649736.645880303 668080.1334049295, 649752.3500285391 668094.5281519431, 649777.5270599846 668117.6069523921, 649772.8737045131 668120.8793573364, 649755.2743597673 668133.2581068549, 649751.2850549794 668136.0635967554, 649750.3420088746 668136.5104783959, 649745.9257928806 668138.603924126, 649726.5847413435 668156.9884021434, 649689.6183809026 668187.4476306243, 649688.6152895385 668188.2755011398, 649708.4582669258 668203.8237351212, 649727.3604644423 668225.9357868804, 649732.3697198847 668231.7953149655, 649729.475110565 668237.0508976548, 649718.7468527493 668256.5243510986, 649718.541809261 668256.899321505, 649715.767225268 668261.9359215427, 649704.2287962656 668282.8832582863, 649694.9671554441 668288.8170644387, 649692.9840182093 668290.0878088048, 649680.7761734396 668297.9092352267, 649677.6319557054 668299.9248299414, 649665.4271110406 668307.7442568038, 649661.7888592782 668310.0747878726, 649658.890658468 668311.9334143016, 649649.0179750441 668318.2591418161, 649648.8349623989 668318.3761182306, 649640.8734262984 668323.3520937192, 649639.7053476664 668324.0819434158, 649630.1437039076 668330.0567130424, 649628.4475897074 668331.1164947918, 649623.4704055925 668332.9718706034, 649621.4424168047 668357.9383026559, 649620.9797346962 668363.6361730598, 649619.0068271346 668387.9236205696, 649618.4109481918 668395.2654536248, 649613.0343354646 668400.706734908, 649608.0437668466 668405.7560678311, 649599.9538450754 668413.9409864979, 649561.5065582049 668443.7459655803, 649549.5135077013 668436.637611754, 649534.955660252 668428.0099683295, 649534.7626754914 668427.8959465366, 649530.4670144819 668425.3594614677, 649527.3246049709 668428.9650359934, 649525.296340707 668431.291761382, 649524.5502434865 668432.147660372, 649513.2047652438 668445.1621243472, 649508.2691220405 668450.824456138, 649505.3068359715 668454.2230550938, 649503.3354790324 668456.4847882092, 649508.5929008907 668460.9453643187, 649514.1882855733 668465.6929774554, 649518.0168645675 668468.9413969807, 649523.4892628973 668473.5839966208, 649527.499821849 668476.9874360663, 649535.49794248 668483.7733124248, 649541.6062710274 668488.954981715, 649482.5898208839 668579.5566969138, 649478.5271071496 668585.7451273073, 649478.202050041 668586.2402817362, 649473.6832561631 668593.1234481902, 649471.7885235334 668596.0073825925, 649431.6378736501 668657.1195552454, 649421.9021301202 668672.2181873035, 649359.0409956725 668768.6583702163, 649354.5122043273 668775.5147363016, 649349.3192969741 668783.37600946, 649292.0011665714 668871.0819764782, 649287.447353036 668878.1203374694, 649287.4455527123 668878.1231372198, 649283.7308890581 668883.8646159685, 649266.3126044552 668910.5573748993, 649261.5005014257 668903.4178872389, 649247.8618129606 668885.0984803676, 649251.4090995612 668882.4969410187, 649265.0792038846 668872.4717162908, 649273.1646569958 668866.5420663371, 649266.572224307 668855.6131129616, 649264.9536191879 668852.4389588512, 649263.4299909187 668849.4508137909, 649258.1782727388 668839.147313792, 649254.9030722375 668832.721001971, 649250.8900515635 668824.8496198612, 649250.7670816659 668824.6076081627, 649247.3349193007 668817.875281348, 649243.9647418951 668811.2639604314, 649240.215656972 668803.9096034155, 649236.7085133246 668797.0272694654, 649236.4955667888 668796.5972493406, 649232.8714801158 668789.2499072633, 649232.4895763358 668788.475871216, 649229.4933313193 668782.4025883832, 649226.9969603558 668777.3423527397, 649226.5970610605 668776.5323149839, 649225.5603224918 668774.429217138, 649219.3648838289 668761.8696323108, 649216.7639351807 668757.4608756696, 649214.0720058143 668752.8981100457, 649198.2242813169 668735.0294305856, 649197.4853873 668734.1973522604, 649188.5256739748 668724.0944026989, 649187.7627835233 668723.2343218419, 649182.1305922415 668716.8847248885, 649186.8310755166 668712.5763465086, 649190.9024941724 668708.8438849507, 649195.4469613725 668704.6784859593, 649199.6503935705 668700.8250418769, 649210.9725576587 668690.4455393278, 649214.1893882898 668687.4972647827, 649202.3012933666 668674.1115045112, 649194.3988636389 668664.0788812897, 649182.654629625 668650.1806447894, 649172.4191687611 668638.0685670632, 649175.6455031956 668635.0879940883, 649180.7290302337 668630.3906669492, 649185.252499146 668626.2112656834, 649190.1710088567 668621.6679167043, 649194.4734548505 668617.6924862125, 649194.7564842395 668617.4305236832, 649203.3893789739 668609.454666411, 649203.4933897454 668609.3586801792, 649207.9068469955 668605.28226439, 649208.9629564638 668604.3064041939, 649212.3583084111 668601.1688536576, 649216.5227399651 668597.3214049373, 649220.7191747514 668593.4449604513, 649220.8851920348 668593.2909824288, 649224.8996079648 668589.5825138649, 649229.1010434084 668585.7000700738, 649233.0734550611 668582.0295959658, 649237.5329170445 668577.9101863377, 649245.7127644841 668570.3532692639, 649248.1460166619 668568.1045914168, 649258.7271128116 668558.3289923174, 649261.2333724587 668556.0133241409, 649268.0188630813 668549.8461212382, 649274.6457375182 668543.8230973975, 649281.7513701371 668538.1160271987, 649281.5207474335 668535.0000395053, 649281.5117622003 668534.8780399994, 649281.0065871505 668528.0650667915, 649280.8797945665 668526.3520735538, 649280.1999040693 668517.1891095245, 649279.5559526855 668508.5291433585, 649278.739283309 668497.5401863364, 649278.3109822253 668491.7682089934, 649278.2750415609 668491.2782109634, 649277.8896682913 668486.1024311141, 649276.9711649762 668473.7422794522, 649276.5718153471 668468.3713004313, 649276.4460211932 668466.6713071412, 649275.8719566216 668458.9463373509, 649275.6992382774 668456.6203464606, 649275.5804317782 668455.0223526981, 649274.6207183638 668436.1104803465, 649274.4061795453 668432.2975035469, 649273.919186239 668423.9755519173, 649273.9262725884 668423.2585619945, 649273.9424861304 668421.4855867624, 649273.9992232035 668415.3656723422, 649274.0002401944 668415.224674282, 649274.0144233776 668413.7036955587, 649270.6249440372 668393.0075532161, 649270.6009612154 668392.8665521401, 649270.5759804512 668392.7085511592, 649267.9819092369 668376.8724421492, 649267.9569276293 668376.7214410787, 649266.137281065 668365.6093646428, 649266.1212926354 668365.5143639377, 649264.0518315887 668352.8792769609, 649264.2590513492 668351.042325577, 649264.6364512531 668347.6994140832, 649266.2371452624 668333.5387892367, 649266.6532583262 668320.9567012616, 649267.0440791999 668309.1412003812, 649269.0184479866 668281.0587997119, 649269.5663840178 668273.2559661657, 649269.5672857943 668273.2411664649, 649270.0030301511 668267.03609883, 649266.3816597043 668271.1766088669, 649261.8320687024 668276.377993327, 649248.9233914673 668291.1382468787, 649224.4414861151 668267.0626041411, 649223.7475739167 668266.3795292395, 649221.9777004707 668265.4453278169, 649221.7247185047 668265.3122990191, 649219.6228690171 668264.201059836, 649208.4030440594 668255.1868226198, 649198.1276016831 668242.9377407972, 649195.1300560975 668239.3644251905, 649192.0893244975 668237.33708452, 649187.4407347242 668234.2385636945, 649182.7611477474 668231.1190393935, 649180.7973210296 668229.8103193585, 649177.0336532189 668227.3013976668, 649172.6870363245 668224.4049075743, 649174.5362567179 668222.4491502357, 649178.2506995351 668218.5196376776, 649180.0789175568 668216.58487761, 649180.6429848275 668215.9879516347, 649184.241413759 668212.1814238748, 649180.3077210345 668209.8829725223, 649174.8921440158 668206.7193511301, 649174.4291801243 668206.4492980078, 649169.6645522924 668203.6657512888, 649168.6726297163 668203.0866374748, 649164.4379604441 668200.6131515516, 649164.2249771567 668200.488127116, 649155.7606381136 668195.5451558152, 649154.3767461812 668194.7369970034, 649147.3332962886 668190.6231887273, 649138.109016701 668185.2361301407, 649121.54431049 668175.5622290657, 649109.593018946 668170.456838616, 649099.5646133745 668166.1736718393, 649098.066806305 668164.6675062454, 649067.9276898394 668134.3501738751, 649036.2897670936 668102.525675028, 648991.3004698979 668049.7537732405, 648989.0358103313 668047.0725266987, 648986.9181285449 668044.5662961382, 648983.0545073812 668041.67185876, 648968.2459594446 668030.5781822813, 648962.8810912333 668023.1848084918, 648962.8500966023 668023.1422051779, 648957.5558161674 668015.8460389134, 648954.8552851873 668012.1247500522, 648954.8313418961 668011.6547518475, 648954.1350156296 667997.780805923, 648953.7728841224 667990.5818338842, 648953.4167383729 667983.5008613862, 648953.0386464216 667975.9738906764, 648952.6116714998 667967.4769237299, 648952.2405728829 667960.0049533063, 648951.6928789747 667949.1789949562, 648950.7780752328 667930.97406573, 648950.4875878516 667926.7300729086, 648949.0371475887 667905.5381087296, 648948.7566423486 667901.4421156257, 648948.3214092446 667895.0931262679, 648947.0197340411 667884.1620778947, 648944.4663334896 667862.713983032, 648942.3165213886 667844.6619030661, 648940.5762934511 667830.0408383989, 648940.3425313664 667828.0778296981, 648937.537241955 667814.0396305901, 648936.5318541649 667809.0155591542, 648936.5308551359 667809.0075591151, 648934.0336830281 667793.9734070703, 648933.8867905104 667793.0893981231, 648931.6188349283 667792.8721263881, 648931.5388597475 667792.6711187324, 648930.4451987715 667789.9250139595, 648924.664990731 667775.4104601918, 648923.6533042146 667772.8713632504, 648914.2592165035 667749.2834630887, 648908.550989868 667751.5517510901, 648901.8997258813 667754.1939214877, 648896.8985272899 667756.1812976961, 648890.4834977902 667740.2266812811, 648871.7402557519 667693.6088798345, 648868.6901925862 667686.0245866015, 648866.2729352391 667680.012354211, 648860.9965556671 667666.8878422225, 648858.4093485111 667660.4525810033, 648853.7837657038 667648.9501138981, 648851.8653536745 667644.1779201734, 648849.6140435535 667638.5786928108, 648844.6005799309 667626.1091864401, 648841.8034370377 667619.1529038881, 648847.1556534249 667616.986567931, 648842.5380741042 667605.4551018495, 648841.9622512786 667604.01704372, 648839.3330604943 667597.4487783348, 648838.16242064 667594.5256601576, 648836.930799744 667591.4485358251, 648835.5822145987 667588.0813996684, 648833.2759242812 667582.3211668187, 648821.8744607052 667561.9489509912, 648821.2845920668 667560.893888087, 648812.4585601276 667545.0859470182, 648812.327589502 667544.849933064, 648810.7012410838 667537.0275986872, 648809.1794315462 667529.7076729306, 648801.5680969971 667516.3408590578, 648800.6402999859 667514.7117598356, 648793.6298339008 667502.4010100955, 648801.0301405507 667499.3419291326, 648796.2132036141 667490.807414558, 648791.88015982 667483.1309516281, 648781.6824103912 667465.0638619944, 648778.3051558603 667459.079501094, 648773.1767103068 667446.4709809534, 648770.712457451 667440.4107310103, 648768.5411154715 667435.0735107437, 648768.1082467764 667434.0084668383, 648767.0225757334 667431.3403566972, 648765.0685031925 667432.0781145167, 648764.2554730127 667432.3850137533, 648758.6332643249 667434.5073169642, 648752.9380529922 667436.6566111429, 648752.4308304203 667421.8756669043, 648752.008309509 667409.5757132489, 648751.9036774788 667406.5157248388, 648751.498096689 667394.7137692702, 648748.6225391356 667387.0542821146, 648745.9559131137 667379.9512157984, 648745.5797295719 667364.8362899909, 648745.4404001569 667359.2563173242, 648745.1458200437 667347.4413752411, 648747.7509247538 667346.3906987327, 648743.5008882086 667338.6502458379, 648739.6897522205 667331.708839694, 648738.4607170776 667332.0856880107, 648734.2215961488 667333.3831648441, 648732.2625402466 667333.9829230724, 648727.9734178105 667335.2963937443, 648726.5213763383 667335.7412145478, 648724.7113248467 667336.2939911821, 648724.5063188518 667336.3579658723, 648717.371362133 667311.4802992962, 648712.087615681 667293.0588055376, 648710.675218078 667288.1346735404, 648708.0586418465 667268.11251509, 648706.5622331842 667263.2863721199, 648702.7277484287 667250.920005728, 648696.8315939375 667252.6112787774, 648690.842436725 667254.3315403486, 648683.1682205503 667256.6585932039, 648674.5339614227 667259.4092265479, 648671.9608841619 667260.2292086806, 648667.2077413655 667261.7446214947, 648642.9520142563 667269.4639252475, 648621.554470383 667275.459988602, 648606.648266667 667278.1771633718, 648605.1572463084 667278.4487808004, 648598.5461559147 667279.6541713049, 648597.2521382325 667279.89001286, 648592.6830758301 667280.7224534018, 648586.7249943693 667281.8087238759, 648581.223919225 667282.8110503113, 648575.8338455616 667283.7933903431, 648570.1797683162 667284.823698062, 648564.1686860735 667285.9199620605, 648552.6095280979 667288.0265467888, 648546.4464438454 667289.1497922054, 648540.1123595685 667290.2850168402, 648528.0311957744 667292.4755377924, 648522.2721177187 667293.519532745, 648516.1070340323 667294.6380779907, 648492.2467104938 667298.9641569932, 648487.6996488319 667299.7886003482)), ((649114.7320074495 669488.8968006055, 649107.5666387713 669484.0950003519, 649116.4163124494 669469.682149779, 649120.179024784 669463.5476811093, 649127.510412449 669451.5967164187, 649128.9666880626 669449.2229220746, 649131.6121887792 669444.9102956818, 649136.0890361497 669437.6119279166, 649137.7613526069 669434.8861640901, 649140.3208370466 669430.7135255642, 649141.099984474 669429.4436356003, 649146.9951002639 669419.8324681926, 649151.8620212203 669411.8991555733, 649153.1422634695 669409.8123363869, 649155.3196755337 669406.262643931, 649160.3286232402 669398.098351392, 649160.4646490052 669397.8763706064, 649162.996128121 669393.7487281717, 649157.0726315633 669389.9340645123, 649125.8242875555 669369.8115632858, 649123.232814194 669368.0901735846, 649144.5102716081 669335.6332502908, 649190.566401626 669265.3836945689, 649196.5114508498 669256.3155265213, 649225.4269822674 669250.1300858054, 649254.1825105919 669243.9786254896, 649265.5827199848 669241.5400288277, 649267.5067553463 669241.1282656791, 649272.4188521376 669240.0228710521, 649277.3299489339 669238.917476307, 649298.8623475948 669234.284127315, 649304.1503819311 669225.3748784368, 649309.4394162097 669216.4656296909, 649375.4211655145 669114.8628806203, 649379.2078396532 669109.0314116286, 649377.0741595711 669106.5081873047, 649362.413374455 669089.0336476777, 649364.3675571035 669087.3959040362, 649366.591765192 669085.5301958388, 649378.9929241122 669075.1378227185, 649379.7259925646 669074.5239188869, 649385.5535303274 669069.6966826759, 649398.1076888634 669059.2963281773, 649402.5711097278 669055.5239141819, 649415.0852895647 669044.9475572181, 649417.040473808 669043.2958139195, 649421.2728727278 669039.7193696109, 649421.4078855669 669039.6043873506, 649426.366352954 669035.4140383955, 649426.6873832898 669035.1420805517, 649429.7506695932 669032.573482505, 649423.7145264763 669025.835842703, 649409.3905768782 669009.7073261616, 649409.9269210354 669009.3071959089, 649413.5129162378 669006.6318621787, 649454.7931931579 668976.8362170833, 649460.9196610928 668972.5660098965, 649467.0471290078 668968.2958028476, 649516.4670929014 668932.2682197532, 649520.4944165042 668929.3267427906, 649524.978776602 668926.053325181, 649585.7686320317 668881.8822181029, 649590.902028846 668878.2608832817, 649595.8624122709 668874.7615260503, 649614.3718708692 668861.470927602, 649621.923715209 668862.2868266744, 649627.9205916189 668862.9345406261, 649634.5534548839 668863.6513302776, 649636.4454157999 668863.8565555064, 649641.4173133894 668864.3931474391, 649647.9961778092 668865.1039306576, 649648.4981674786 668865.1579904192, 649648.7351626698 668865.1830186417, 649650.832119363 668865.4102682829, 649655.3240267661 668865.8958030455, 649664.8588302768 668866.925938169, 649669.8359276693 668867.4640306912, 649669.8407275697 668867.4645312665, 649670.0689766714 668865.3795857924, 649671.5086520115 668852.194330147, 649672.0881862178 668846.8860687888, 649673.3344254912 668847.3119133812, 649675.8590024844 668848.1745063027, 649685.5484304539 668851.4846305271, 649693.8811970189 668844.5847237515, 649695.7990510082 668845.6769405983, 649696.2120194761 668845.9129872838, 649697.5209197431 668846.6591352781, 649698.5148440622 668847.2252476518, 649700.1037201884 668848.1544269915, 649703.1074942086 668849.8427669134, 649709.0020451793 668853.2024333543, 649714.2906421619 668856.2180312626, 649719.902214698 668859.4166656827, 649726.468714533 668863.1594080586, 649732.153281618 668866.3990507228, 649737.7058586255 668869.5646784448, 649743.6544055733 668872.9553509206, 649748.8820074638 668875.9349418972, 649753.5886490553 668878.6174739737, 649754.7635595344 668879.2876067772, 649764.6608058881 668884.9287256085, 649766.5547617173 668886.007939708, 649769.1695626269 668887.4982352942, 649835.5763423152 668776.9566897665, 649831.0947067437 668774.2301779166, 649828.3908266546 668772.5848691023, 649822.1793306282 668768.8051637893, 649808.3524482468 668760.3926073356, 649803.4698429069 668757.4220576981, 649761.6822215312 668731.9963533939, 649758.4724811403 668730.0429920425, 649758.1005111882 668729.8169501645, 649752.0351016974 668726.1262672946, 649746.836522188 668722.9626820177, 649769.8839873513 668684.3679603932, 649771.6363267236 668681.4342096641, 649771.137438501 668680.536861209, 649768.9909194338 668676.6759527095, 649717.7664008241 668584.5209762852, 649715.1459882861 668579.8067216582, 649722.9739724423 668571.1217778355, 649723.0279791245 668571.0627851059, 649733.6475139976 668557.6192395464, 649855.88586612 668430.1776908233, 649861.2437918094 668424.5919199261, 649861.5425266916 668424.2804605762, 649857.9590478736 668420.2000748282, 649864.2764705803 668416.2339004166, 649868.668216267 668409.7175234292, 649872.6458915402 668403.8160877423, 649871.7340008236 668402.9719870457, 649868.6153745179 668400.0856426298, 649865.5657400752 668397.262305832, 649867.6019460233 668395.4035795531, 649869.7561636929 668393.4388691284, 649880.1612155044 668383.9442679856, 649881.1683174035 668383.0244033978, 649892.2144336298 668372.9458886675, 649893.7065843077 668371.5850893104, 649904.7837035888 668361.4765790388, 649910.3712680009 668356.3783305659, 649914.7607113788 668352.3729209894, 649910.5563876944 668347.0464754148, 649907.0240831318 668349.8350059594, 649884.4087214854 668321.1876082837, 649890.1280674476 668317.8963524789, 649892.3401974969 668316.6546399195, 649892.621214035 668316.4966764341, 649896.9804704584 668314.048242912, 649914.7275141154 668304.0805493146, 649919.0867704176 668301.6321159016, 649919.2507800373 668301.5401372131, 649942.8761684708 668288.2722082492, 649945.0352953393 668287.0594889403, 649949.1675380773 668284.7390261601, 649949.2355420593 668284.7010349976, 649977.2261860046 668268.9806745285, 649978.1652412035 668268.4527966498, 649993.0251134493 668260.107729203, 649999.4082321352 668256.9887532905, 649982.8039758665 668226.1866220254, 649989.279661498 668222.4927573366, 650003.4875305521 668214.1905886987, 650010.7799763482 668209.9305288248, 650013.2201256496 668208.5038434444, 650028.5540630177 668199.5438206961, 650046.580164478 668189.0111456773, 650051.424460384 668186.1807705979, 650060.0485573832 668184.733844803, 650085.3388414981 668180.4919951588, 650100.7330143125 668177.9099129568, 650116.2841887587 668175.301850444, 650120.1662323445 668174.6503341254, 650121.520579975 668171.6425329074, 650122.6158611521 668169.2096936753, 650124.7154001382 668164.5460018865, 650126.2557955844 668161.1242280346, 650127.4280964845 668158.5204001494, 650130.5739038344 668151.5338620595, 650132.5484106044 668147.1481520258, 650133.2755973204 668145.5322588367, 650136.2303558234 668138.9676928455, 650139.1661093009 668132.4461241191, 650144.6425144628 668120.2829287746, 650150.9951443987 668106.1723624693, 650162.1840146396 668081.31950771, 650164.7056244285 668076.0278752523, 650168.367510029 668068.3424091032, 650172.3004611024 668060.0879825695, 650176.1323876809 668052.0455414086, 650179.3461620547 668045.303015374, 650181.9007768992 668039.9423939021, 650183.0480532912 668037.5325639561, 650186.4358684841 668030.4240661288, 650190.4758406609 668021.9456651537, 650195.1349617218 668012.1673562516, 650201.8125681854 667998.1523472329, 650204.2941648868 667992.945715626, 650204.8132897988 667991.8557927093, 650207.654973177 667985.8922146951, 650208.8412584739 667983.4023908828, 650218.6586653193 667962.4098539408, 650256.700984108 667881.0625347607, 650265.5871590482 667862.0608643607, 650269.6671528865 667853.3744747341, 650273.8451706127 667844.4781000073, 650277.8551472322 667835.9397003339, 650280.1957170889 667830.957050821, 650284.3237221903 667822.1676691488, 650287.4894930148 667815.4261435029, 650290.7372835217 667808.5116302556, 650293.49595496 667802.6380438085, 650293.8030297119 667801.9840898542, 650296.6307178711 667795.9635138625, 650299.4404015082 667789.9819352687, 650329.5275081665 667727.688434085, 650336.9002484927 667712.4234382011, 650339.9557695872 667706.0972959753, 650341.0082179685 667703.9181536802, 650349.0397746164 667690.1293252246, 650367.0980053155 667661.3559239517, 650369.7694821176 667657.100303995, 650373.8622127069 667650.5788864023, 650377.8109174119 667644.2874484987, 650404.0756019003 667602.4401920078, 650412.4170886833 667589.1493826495, 650416.0877428856 667583.2999068659, 650423.7321046345 667571.1209990542, 650425.6734503659 667568.028276535, 650442.2904092319 667541.5506535136, 650445.4799768596 667536.4691101387, 650447.9684198018 667532.5034664752, 650467.9469733433 667500.6733300065, 650471.2325575821 667495.4378013982, 650475.5813305908 667488.5094255093, 650484.0668387503 667474.9886439687, 650488.0125399017 667468.7012108375, 650495.6628988311 667456.5123104636, 650497.1551638809 667454.134525037, 650498.287364936 667452.3306878539, 650511.3916916372 667431.4505734558, 650520.5431156474 667416.8697914807, 650530.2490375715 667401.404290461, 650535.1489066248 667393.5969970592, 650536.0960746721 667392.0871336864, 650543.6074063462 667380.1202175099, 650548.6382981585 667372.1039438096, 650552.8910517785 667365.3285580096, 650555.9936015707 667360.3850062242, 650558.3140127567 667356.6873415271, 650566.0813886744 667344.3114643756, 650568.7928689258 667339.9908565267, 650580.0938592931 667322.0734908957, 650588.0062572598 667309.4866365544, 650588.0932727011 667309.3476491617, 650588.9794292707 667307.9377775226, 650590.7687453689 667305.0910367287, 650596.1206905278 667296.5778122529, 650596.3247264875 667296.2538418192, 650596.7848077451 667295.5219085035, 650597.3068999792 667294.6909841849, 650601.7986930759 667287.5456353968, 650606.8633045774 667274.2696264235, 650607.5614128861 667272.439735508, 650608.3714818994 667270.9309554315, 650614.8829401921 667258.8036197335, 650619.2848355232 667250.7972698726, 650628.8487799125 667233.4046833052, 650633.7917845603 667224.4154143123, 650641.0202533946 667211.2694839117, 650642.1464822857 667209.2206506286, 650643.4027376379 667206.9348366272, 650651.7644359196 667191.7290750615, 650653.9658830973 667187.7244012766, 650657.6846381214 667180.9619524725, 650662.3865928195 667172.4096496871, 650667.2825864843 667163.5063759699, 650673.0787627029 667152.9652362183, 650695.2672630673 667112.6105336384, 650701.8996074707 667100.5495206655, 650706.7645935386 667091.7012450754, 650714.2101020705 667078.1613543483, 650731.8926832904 667046.0019918272, 650747.3278037335 667060.3368159222, 650760.9491458181 667072.9874242835, 650779.380903739 667090.1045985962, 650808.4143747442 667117.0690187538, 650841.5013580231 667147.7969095397, 650850.5602590917 667156.2099734709, 650860.2330860683 667165.1931088711, 650872.011658146 667176.1324905575, 650877.4990093145 667181.0911355462, 650883.4603110322 667186.42383669, 650887.7898042649 667190.295345823, 650893.0801850248 667195.0269677732, 650899.3494513897 667200.6337045692, 650903.5759568217 667204.4142011233, 650907.9714426756 667208.3447174294, 650911.8749860466 667211.8361758228, 650915.9955040982 667215.5216595941, 650919.4081051148 667218.5730601704, 650922.485745274 667221.3254213608, 650923.124670493 667221.8974963295, 650926.4652799722 667224.8848882898, 650938.1099189251 667235.2992539912, 650957.4506595534 667252.5965203088, 650972.8768584703 667266.3933261055, 650975.6635820746 667268.4696572318, 650986.4430338356 667255.0372005374, 650989.8225407831 667250.3836900332, 650995.662416494 667242.3435361483, 651004.5921107852 667227.0198679301, 651005.9333652345 667224.7180680574, 651007.1636212234 667222.4142540169, 651017.9838711249 667202.1598903544, 651027.4714441431 667179.2783900093, 651035.636683161 667159.3696846446, 651041.4265748181 667142.657635704, 651042.6359700031 667139.1658344652, 651043.7893583392 667135.7380252739, 651044.5566165023 667133.4591521976, 651045.1718236823 667131.6302539991, 651049.0938368081 667088.4932963228, 651050.6399907898 667078.4966200647, 651054.7125667147 667056.1154239225, 651058.4779432871 667035.4621672535, 651062.3876989934 667011.5579705435, 651071.5260048438 666990.938407511, 651074.0236352666 666985.2998005374, 651076.4842559956 666979.7471878141, 651077.8896251939 666976.4504106333, 651078.87886001 666974.3435648046, 651080.6303018086 666970.3908405936, 651083.6120538899 666963.6613102572, 651084.7403324039 666961.1664873546, 651086.0306506247 666958.3156898904, 651088.8413439485 666952.1041311886, 651091.3059519435 666946.6565182633, 651092.7743139205 666943.412748899, 651093.7815624209 666941.185907155, 651096.2461701735 666935.7392944237, 651103.9030581977 666918.8154982313, 651105.4744455467 666915.3427453953, 651106.7247535661 666912.580942073, 651107.862034031 666910.0661210219, 651108.9733080291 666907.6092958983, 651112.109080626 666900.6807894122, 651115.3918897039 666893.4243062783, 651116.0540526786 666891.9624105347, 651120.6541863079 666881.7931351813, 651121.4013703156 666880.1422529023, 651123.5058886553 666875.4915845371, 651125.6004045038 666870.8629146736, 651128.5291257635 666864.3903764172, 651129.6724073202 666861.8635567157, 651131.1777779734 666858.5367941353, 651139.0407139261 666841.1580348917, 651139.467819059 666840.2141023186, 651141.765384559 666835.1364650762, 651149.798454695 666816.5857443294, 651151.3621588647 666810.401026608, 651154.1764269884 666799.2625349177, 651156.1603267791 666791.359894034, 651157.8051404689 666784.2291993314, 651159.245918325 666777.4264739966, 651159.4631836982 666775.1355317503, 651160.0869631938 666768.4076995157, 651160.9141265303 666758.3779363229, 651161.3197497242 666753.009058292, 651161.4317505124 666744.4461833728, 651161.6281301307 666732.6373613232, 651161.385989419 666725.3224242938, 651161.9828067252 666718.2735928561, 651162.515492712 666712.3537385417, 651164.9406675724 666702.0471869895, 651171.7084636786 666677.4033861146, 651176.2358999229 666664.6251409182, 651181.1272468955 666652.5669342091, 651183.0747547754 666648.0082470356, 651185.8596808668 666641.4900944699, 651188.6021958436 666635.0711352354, 651189.3033787071 666633.4292479508, 651190.5137066005 666630.4904439045, 651192.9133568786 666624.6618325191, 651195.2169708179 666619.1542045317, 651195.3149964267 666618.9242203101, 651196.9844284749 666615.0424885405, 651197.4765554471 666613.9015675765, 651199.6191002823 666609.0019108455, 651202.2887792672 666602.8953386851, 651204.5793617947 666597.6557058764, 651206.2617892962 666593.8099756023, 651209.1855327846 666587.1214445197, 651211.7691894909 666581.2128589841, 651214.7509474624 666574.3923374744, 651217.8097248924 666567.395828478, 651220.5294157909 666561.1772651611, 651225.1105797881 666550.6990011187, 651229.3946686311 666540.8956897758, 651231.2291348018 666536.6979847593, 651237.2536653914 666522.9139538795, 651239.1041355645 666518.6792516866, 651239.2001597629 666518.4612671222, 651241.6397795327 666512.8786598291, 651243.3312179828 666508.9329331564, 651246.4110161364 666501.7494309512, 651246.5410485079 666501.4574518184, 651249.3797566737 666495.0709076885, 651253.2947330273 666486.2645366014, 651254.6430693431 666483.2307532707, 651255.8583722735 666480.4979485623, 651258.7120834799 666474.0814072348, 651260.158444212 666470.8266397986, 651260.5345378483 666469.9817002607, 651262.3050349456 666465.5219913143, 651263.3043480474 666462.7261593239, 651265.3559912626 666456.9825043749, 651266.016196782 666455.1466152626, 651270.7576736198 666441.9534120162, 651272.799410553 666435.4067668006, 651275.8195003648 666425.724291741, 651276.8579119109 666422.0794764954, 651278.2514722854 666417.1187254112, 651278.5986219241 666415.7967885712, 651279.7221372466 666411.2529965716, 651279.960252172 666410.2410413196, 651283.5900019114 666394.8327233363, 651284.0312145782 666392.9598062651, 651284.0742351342 666392.7788143333, 651284.1772848666 666392.3408337084, 651285.4070522027 666385.6250848969, 651286.370653274 666380.3642817468, 651287.4903515445 666374.2525105134, 651287.8317259564 666371.0055987962, 651287.9898996176 666369.4996397264, 651288.1731011805 666367.7516871835, 651288.6125834022 666363.5698008807, 651289.3844310249 666356.2190006841, 651290.0801946772 666349.5961807799, 651292.1995217241 666329.4137296852, 651292.8182009032 666323.5228899732, 651292.8882789683 666322.8459082601, 651294.572125356 666306.8303444266, 651298.6075541175 666268.41139101, 651298.903879277 666265.5904679081, 651299.2352432773 666262.432553942, 651299.8178815746 666256.894705052, 651300.0421281148 666254.7557633125, 651300.050136864 666254.679765387, 651300.4065265355 666251.2988577484, 651300.9571302272 666246.0610006483, 651301.7249715404 666238.7611999025, 651302.3516588733 666232.797362636, 651305.5281465045 666202.5341883808, 651306.7414783322 666190.9765039507, 651306.8676166042 666189.7765367454, 651307.5383530098 666183.3857112651, 651308.5933101436 666173.343185699, 651309.6426611744 666163.3532587633, 651310.3324173026 666156.7904382407, 651310.6777965768 666153.4985282073, 651313.1600390224 666142.5500036544, 651313.3131154974 666141.8760329713, 651314.7728461083 666135.4373126683, 651314.8128658487 666135.2633203048, 651316.0674936132 666129.7305607498, 651321.8095866053 666111.0915715505, 651327.1429908387 666098.4024485608, 651328.1572579318 666095.9886154088, 651333.7525642578 666084.1035167811, 651334.6727575043 666082.3336625572, 651335.7739888642 666080.2148370472, 651336.2201770885 666079.4036071012, 651336.2930916413 666079.2699185655, 651336.4691262001 666078.951946177, 651339.4736963164 666073.6944152596, 651339.8557641879 666073.0654743748, 651344.9466674746 666064.6932621738, 651346.5339492349 666062.0815078827, 651348.1832418541 666059.3687632135, 651349.1304097784 666057.8119098387, 651350.0655860229 666056.1850558488, 651351.5698692683 666053.570290706, 651354.3663961021 666048.7067274265, 651357.357938225 666043.6871922114, 651360.9905964728 666037.5917567626, 651364.558242973 666031.6043113864, 651366.4855921997 666028.369611072, 651366.8246536193 666027.8006637975, 651368.2289232048 666025.3138839691, 651369.5211709546 666023.0280865575, 651374.1340561594 666014.860810032, 651375.4983177282 666012.4470240242, 651377.7307460455 666008.4943742928, 651385.2912190094 665995.1104835983, 651389.235002299 665988.1290118253, 651393.9049297139 665979.8636371278, 651396.2824019939 665975.6549553928, 651403.7408836066 665962.4529534605, 651405.7132636018 665959.0602172432, 651411.1773166625 665949.6589478086, 651413.6027839746 665945.4872720045, 651422.7325436977 665929.7804918465, 651425.4150610022 665925.163850115, 651436.3981784525 665906.2693162705, 651437.3613640353 665904.6134447926, 651446.1490589002 665889.493616983, 651446.9382108917 665888.137722212, 651450.9589864284 665881.2202582717, 651460.5668403682 665864.6865386008, 651465.1247193936 665856.8481456775, 651471.0898702727 665846.5869398908, 651474.3575008534 665840.9653748124, 651484.421449742 665823.5977137072, 651487.6280689657 665818.0791401137, 651493.6152252821 665807.7749359917, 651496.2607361003 665803.223287556, 651508.1360302599 665782.7848648692, 651526.3835563946 665751.3812859766, 651527.2136131852 665750.8123957247, 651540.3385113527 665741.8171307081, 651542.9326888685 665740.0394735539, 651544.1177700629 665739.2266301704, 651552.8393669231 665733.2507825951, 651558.3967474888 665729.4415167695, 651575.9189471517 665717.4358308384, 651581.1843078275 665713.8275259881, 651588.6148169785 665708.7345067991, 651595.0292563098 665704.3393532797, 651599.0325307042 665701.594881486, 651603.8618615784 665698.2855186125, 651609.6182560975 665694.3402779271, 651616.1977070699 665689.8311456603, 651622.7361553216 665685.3500078118, 651628.8145720137 665681.1848091731, 651633.3148806388 665678.1004023838, 651633.4598906497 665678.0004214972, 651642.351500375 665671.9075933244, 651647.5958601456 665668.31328434, 651660.6677569979 665659.3550062939, 651665.5230901927 665656.0276457192, 651695.3491358324 665635.604571721, 651699.1193969126 665633.0030677532, 651699.3463124378 665632.8480976099, 651732.7137180382 665609.875485251, 651740.4233940969 665603.3834981667, 651745.8479554645 665598.1072105361, 651752.0049671531 665589.0200180993, 651754.2603384946 665585.6853138406, 651756.1566507267 665582.8815624649, 651765.266943912 665587.6727618328, 651775.4211559559 665593.0130989042, 651783.7190671205 665596.0934913275, 651791.9722808615 665599.1572780064, 651800.0397134415 665602.9133406545, 651798.942443904 665605.2701972119, 651810.2604760076 665611.9596889599, 651812.8590897263 665614.0250317495, 651829.8348101688 665622.6312694763, 651842.0748870753 665628.8388834475, 651849.7853058982 665632.74590036, 651936.2587800934 665676.5853164918, 651942.5053249146 665679.75210137, 651952.3366424285 665684.7362536249, 651972.7762237779 665695.0976488914, 651981.8403675765 665701.5687123154, 651995.6646674152 665714.6963362829, 651993.7416257135 665723.4291178854, 651990.33677986 665738.9027312882, 651983.6793721365 665767.4929744969, 651981.8625016403 665774.8037676723, 651977.6266798504 665790.1322843464, 651973.4410895633 665803.5448056102, 651970.4163871484 665809.5464570203, 651960.0800096202 665829.866265922, 651953.6267059353 665841.0595213801, 651950.0432820128 665877.8351309766, 651948.790735482 665890.690994569, 651976.1569855968 665961.1692368278, 651984.121180172 666008.9670735182, 651988.9927707171 666038.2061806787, 651992.9601627855 666065.759309408, 651997.0312710352 666094.0322392529, 651998.6121085206 666112.6811302586, 652007.2990741057 666173.1001941761, 652009.757562324 666186.0442778651, 652011.8936826691 666210.8751372327, 652012.2142498774 666214.607116059, 652012.3469602525 666217.1100922349, 652012.5684774015 666221.2830525618, 652015.600870516 666278.3815101117, 652015.3349917071 666286.0223590016, 652012.472340224 666291.8699322771, 652011.3568498804 666296.1997337554, 652010.3564101576 666300.082555695, 652010.1320041664 666303.61947385, 652010.0729239368 666304.3194559378, 652009.8575533866 666307.548379995, 652010.2298043987 666314.0193218569, 652011.8463857437 666326.2133194185, 652015.2109635681 666346.991386812, 652014.3118056017 666357.097122792, 652018.6573559368 666360.6985750427, 652018.4148439128 666365.1564766448, 652013.8251784942 666362.5699798379, 652013.1214524639 666368.9157977292, 652012.6379535296 666373.2766725904, 652013.76398087 666390.3095368692, 652014.1050833008 666395.4692957648, 652016.9004855142 666437.7591590639, 652014.1845423061 666472.1483004432, 652009.8579492694 666477.5897075268, 652003.234041208 666485.9217996105, 652001.8295478537 666487.6955069675, 651995.5339812101 666495.6457433613, 651985.1977744281 666506.8243546576, 651973.1536939784 666517.0247806053, 651957.1667419435 666526.382756708, 651940.6537537558 666536.0906650434, 651907.3128108634 666556.7290019755, 651900.9064807761 666560.3281090526, 651894.7866653338 666563.7662562226, 651892.5230486336 666565.037940811, 651842.3644995366 666592.8699627739, 651823.1548073823 666601.0563311549, 651805.5248501004 666611.3498743863, 651797.876066036 666619.0027601923, 651789.5332858262 666635.3464233355, 651781.4235550404 666651.2311248406, 651746.5241396526 666736.528292043, 651702.2436700072 666788.6757611982, 651693.3585712361 666799.1394528314, 651682.4940064191 666813.8388256294, 651675.2199584058 666823.6797368507, 651668.646011186 666832.5717533597, 651655.2900853123 666850.642756516, 651656.2210335345 666850.9878719541, 651658.9893794605 666852.0151151682, 651669.2793068525 666855.8334907532, 651675.6439527728 666858.1952795868, 651710.311025078 666871.0615742198, 651716.947642754 666873.6413942522, 651723.5862604794 666876.2212144056, 651805.2767130028 666906.6673161716, 651812.3663176534 666909.320191824, 651819.7219073816 666912.0741001582, 651947.2329270886 666959.1067448319, 651966.260021051 666965.654888089, 651977.0806156273 666968.4261221659, 651985.6532943733 666970.6220999372, 651999.8387629306 666974.2547180258, 652009.5708173449 666973.1128853575, 652014.8488468591 666972.4935184187, 652020.2508771298 666971.8591663627, 652033.203666914 666972.7906814644, 652048.4590610056 666976.9954173053, 652056.2495602234 666980.8022778744, 652064.0820569121 666984.6281432052, 652036.5446021293 667059.8497114156, 652031.154865447 667057.9371041055, 652022.4369960716 667074.7498093672, 652001.9106347025 667113.9877644237, 651991.1371925636 667144.5850079202, 651994.3151466565 667144.7643813047, 651991.4664671846 667150.8539482163, 651977.5060773691 667189.8906800172, 651959.94560973 667221.1771057488, 651916.4357944569 667275.3458806832, 651898.6347720213 667294.6753014583, 651895.5743339859 667298.7798460493, 651881.9122994116 667317.7828036085, 651870.2605227033 667334.3410577633, 651861.5094177785 667344.784778437, 651852.7583124447 667355.2284998011, 651830.8651531657 667384.7502516322, 651808.97199093 667414.2720083033, 651797.9044545292 667428.6483789355, 651876.4278332876 667469.6377358375, 651879.624502242 667463.5312375639, 651881.6649292941 667459.6325578943, 651885.2496321556 667453.1931144106, 651888.8373349981 667446.752671495, 651897.2487688144 667433.5039492783, 651905.6612022236 667420.2542280325, 651932.1671279995 667383.600182145, 651940.1031754921 667373.7623394869, 651951.0534239404 667362.1788230077, 651962.9754729372 667352.2583952418, 651998.0493338575 667316.3531241156, 652033.8809733286 667334.3570938394, 652025.1002288519 667350.0928047688, 652022.6867493097 667354.4184503614, 652005.4233195807 667385.3559144281, 651995.4993478938 667403.1414558244, 651998.0196031564 667418.1005199068, 652000.8209457903 667416.6701752768, 652006.5660383408 667413.7366040616, 652010.7276654753 667421.8876704859, 652011.646850586 667423.6880514347, 652039.380107903 667410.8705479875, 652084.1122430114 667397.9410645416, 652147.4972678183 667393.3556654871, 652205.6811170588 667399.3244852311, 652256.6078897063 667415.1302933024, 652308.6113199882 667442.6060240692, 652353.3127313687 667479.5747022121, 652376.2726530528 667504.8120147532, 652402.9625990578 667538.2656455078, 652406.9492663753 667547.8311706611, 652409.3835747446 667553.6719693041, 652411.3372196739 667558.3593287835, 652419.7118401389 667578.4530129952, 652431.7914299105 667615.9098755716, 652439.5599589798 667654.1642213532, 652441.3258640079 667680.8370307257, 652441.2297948975 667690.0758799769, 652441.2160419292 667691.3977584052, 652441.1337257876 667699.314729228, 652438.1200177704 667731.5288876499, 652431.6010951892 667765.8196005248, 652430.1055581985 667770.552352463, 652428.6090212172 667775.2851042511, 652420.958579943 667796.8558745038, 652409.7558069512 667821.5181772579, 652400.2238867071 667838.7097896471, 652395.1029706193 667846.9480589894, 652389.9820546459 667855.1853281277, 652383.0729972376 667864.7583651963, 652352.8333120808 667898.515271309, 652235.9398294924 668022.6454318597, 652231.7673413472 668027.1438617276, 652227.5958532888 668031.6412916698, 652074.4107059246 668198.6722842439, 652070.2292233189 668203.1197113716, 652066.0477408179 668207.5661384471, 651936.3504416385 668347.4442522061, 651920.9682302763 668367.6250049403, 651917.0006596947 668372.8304254729, 651906.252761974 668364.2592277392, 651738.0120607266 668230.0874374966, 651722.8046218802 668217.9587301585, 651707.5971802918 668205.8310192846, 651540.5173497164 668072.5821512341, 651519.893474866 668056.1348130722, 651500.9154316097 668041.00065972, 651461.7194766637 668009.7442069697, 651449.9332784252 668021.013563891, 651448.0630074095 668023.4932931139, 651450.3627610247 668025.4035534369, 651445.6658232044 668032.1199663614, 651443.8317350561 668034.7425981181, 651392.4454164631 668123.0716792429, 651381.9782856068 668137.8893560899, 651371.7616934432 668152.3498703105, 651357.2314276485 668172.9187586707, 651345.144541998 668190.0280034739, 651303.801254671 668238.4991378282, 651288.4212767109 668256.5299433639, 651284.641770636 668261.1294016641, 651285.7686240063 668262.2815245509, 651211.660781057 668334.7401842393, 651164.5957912857 668380.7486270564, 651165.137464582 668391.9385256378, 651130.319124604 668431.416598398, 651092.4346719676 668472.0452775079, 651050.5234264509 668519.640357877, 651009.8181885807 668567.019598135, 651007.5238399007 668570.9845545066, 650990.4674034687 668600.459700952, 650982.6559388256 668613.4185400429, 650973.7661328892 668629.3372020223, 650968.484724626 668633.1814901294, 650954.9496780181 668643.0336660314, 650944.011806895 668651.206189189, 650938.0302321487 668656.5043695011, 650932.0466571939 668661.8035496644, 650895.92810491 668702.8924701338, 650883.924430515 668717.8997633976, 650871.9197556789 668732.9070571762, 650828.9846972034 668795.5388301271, 650824.621059156 668801.2312075795, 650824.4500340414 668801.4551831753, 650821.5185383352 668805.8437568194, 650818.4600211297 668810.4223120492, 650814.811404361 668815.8817815576, 650812.2539826958 668819.6184110631, 650808.6313854242 668824.9108863227, 650795.9722547487 668843.7670479289, 650799.297388829 668850.8183573309, 650803.1153946613 668858.9147124779, 650808.0751032413 668869.4331736694, 650809.893829795 668873.289642746, 650813.8336042949 668881.6437089232, 650814.4274601878 668882.8147653792, 650818.4404859201 668890.7331467964, 650839.7863046324 668932.8591735769, 650853.803902562 668960.5335026841, 650856.5122378338 668965.9441583967, 650856.6542029982 668966.2277717954, 650859.5045034899 668971.9220408536, 650870.0479548392 668992.6620403439, 650884.5004318515 669021.350405341, 650899.6147772715 669051.1178346329, 650916.7376537055 669084.7164537469, 650937.367700961 669125.0894033574, 650955.7052786201 669161.1651310474, 650957.7157961614 669165.1013206025, 650951.4544415477 669168.5154995251, 650943.8590113522 669172.6565036036, 650940.9208449376 669174.2581183915, 650916.690471738 669187.4679421898, 650910.9821480373 669190.580194092, 650905.6158437422 669193.5054908763, 650900.8185716341 669196.1208622714, 650896.1853087294 669198.6472551895, 650891.2160268606 669201.35560415, 650885.6487109037 669204.3908748064, 650879.6963730818 669207.6360950639, 650872.4549622001 669211.5841464776, 650853.7138985038 669221.800691989, 650848.5856072429 669224.5970204532, 650827.2043925227 669236.2542211947, 650817.9508667004 669241.2980099949, 650817.1328202093 669241.7439029298, 650783.3599699764 669259.5604923754, 650777.0886261994 669262.8686736047, 650743.8798050006 669280.3833392044, 650730.2850588415 669287.5535654081, 650740.9445110896 669333.5262158986, 650742.8558314155 669339.1203703829, 650744.5501539212 669344.7114982372, 650746.5034643672 669350.3876566372, 650746.3455714956 669357.9115292492, 650741.4056304028 669366.1498045331, 650740.6592667586 669374.3064958989, 650740.587574251 669375.0895758678, 650739.8755092941 669384.0963592809, 650741.0259584704 669388.6574350566, 650743.6976797115 669399.2466110033, 650745.2019596512 669405.2097100086, 650745.9626129421 669408.078762156, 650748.4374857068 669417.4069318797, 650749.9368023804 669423.0620345913, 650750.2906752954 669424.1090629682, 650752.9687127799 669432.0392775448, 650754.3842041213 669436.2303909318, 650754.5421474437 669436.6974035901, 650759.3259902578 669454.5507336082, 650760.9471483479 669461.5348319429, 650754.226056263 669471.1498697787, 650749.7193238313 669477.5972247488, 650743.6754068915 669493.266059087, 650741.24577639 669499.5648709878, 650741.0129816564 669506.2667463276, 650740.7839241741 669506.7647111167, 650737.6511380867 669513.5742296425, 650736.9479325748 669515.3471180717, 650732.8667401414 669525.6334706859, 650732.270566028 669527.1353761468, 650732.8400318691 669548.4211408345, 650737.3730480166 669556.4195811894, 650741.9890461252 669564.5660294441, 650744.5934809023 669569.1622823004, 650747.485853254 669574.2665630609, 650750.1532744837 669578.9738219469, 650751.1530575935 669580.7379189642, 650752.8366923119 669583.7090823352, 650755.5860958026 669588.5613490823, 650758.1055492581 669593.0075934668, 650760.9209387059 669597.9748665364, 650763.7343945039 669602.3841473509, 650756.5191549169 669603.9982413666, 650754.4051139113 669604.4719759106, 650750.5516534826 669600.1655663147, 650749.0346263408 669600.4863760942, 650748.1133118806 669594.7733454016, 650744.527782354 669591.0329603505, 650740.1443575673 669586.4604895462, 650735.9229117051 669582.0560360822, 650734.2201350672 669580.2808531387, 650729.2547868436 669575.1013196317, 650725.6962541745 669571.3879372521, 650723.6925172573 669569.2977219063, 650720.8568895012 669566.3404171315, 650719.9019723363 669565.70230936, 650715.0833901577 669562.4837655233, 650700.0566934892 669552.4470692175, 650692.0832953978 669548.7180461515, 650692.0791956519 669548.71614568, 650692.0756958689 669548.7145452733, 650692.0419864632 669548.7956399836, 650690.923974496 669551.4876647118, 650690.5488698155 669552.3909059083, 650689.112468954 669555.8496807406, 650682.0358704269 669552.9099562081, 650682.5711157429 669549.9739635558, 650662.2727048203 669546.2735307504, 650662.0427115093 669546.2315031819, 650660.2367639258 669545.9022867195, 650660.1897653451 669545.8932810962, 650654.7399897366 669544.3436357725, 650650.9341463797 669543.2621850853, 650648.7792350576 669542.6499298939, 650634.110839122 669538.4801927082, 650617.4216447688 669532.7442299451, 650616.8816708961 669532.5581664458, 650621.1635383255 669516.6089162708, 650615.4247513236 669515.1762334945, 650609.5709685485 669513.7155370124, 650609.4639725626 669513.688524278, 650606.769072557 669513.0162036278, 650604.0751725497 669512.3438830917, 650603.9051788166 669512.301862855, 650598.167391898 669510.8691801162, 650595.1425041405 669510.114820163, 650592.3676071499 669509.4224899531, 650586.5658226727 669507.9737995445, 650580.8510348514 669506.5481194459, 650580.4870483957 669506.4570761241, 650577.5641569511 669505.727728271, 650574.8562574702 669505.0524059881, 650569.026474139 669503.5967121604, 650566.0345852625 669502.8503560581, 650563.1156937028 669502.1220086454, 650564.4563947888 669496.1552566439, 650566.6565454651 669486.3616637099, 650566.2346576698 669486.2856130744, 650554.2559793371 669484.3350723714, 650546.9181764172 669483.1402898005, 650534.248394999 669482.0987515532, 650519.0285096056 669482.0868863133, 650503.9154703738 669483.3570162895, 650501.0884630247 669483.5946664917, 650496.5654212856 669484.2261033561, 650486.3633271771 669485.6498331864, 650473.2382060618 669487.4811991871, 650468.2421584786 669488.1905770571, 650418.650685996 669495.227402473, 650411.7576202159 669496.205544324, 650411.6493372635 669496.9068214006, 650410.7838741784 669502.512338195, 650410.1724052739 669506.4762087261, 650417.686216155 669507.595113914, 650436.4257445314 669510.3863713475, 650466.3650100123 669514.689979715, 650490.5514256306 669518.093895429, 650493.8802602894 669519.274286801, 650495.5161013801 669520.5054700624, 650496.5679189884 669521.9695785416, 650511.1600803823 669586.7954624101, 650514.7247062708 669589.7158579315, 650517.0724599125 669591.6391184194, 650521.493209143 669618.6322826488, 650521.3270864476 669619.6712478262, 650521.0730519083 669619.9762124949, 650520.552981372 669620.5991401859, 650519.7249464355 669620.9420341143, 650518.3309272595 669621.1868601759, 650525.6778694722 669654.7752898138, 650525.8377538634 669655.73529595, 650525.8876378515 669656.7052885102, 650525.697531896 669657.6052526956, 650524.7773846546 669658.8951221874, 650481.3408449033 669707.9191309336, 650459.8661033933 669732.1561716399, 650459.8391000042 669732.1861679297, 650458.3989692683 669733.3659757089, 650457.6289269238 669733.7658761308, 650447.3115957808 669737.1445693946, 650444.6495103289 669738.0162322519, 650454.9286918533 669769.4060530156, 650455.2845597606 669770.4920814177, 650475.1891688418 669831.2756674393, 650506.2861751901 669926.2390339515, 650509.6922939273 669936.6405356927, 650507.4731564719 669937.8992515636, 650500.8086436738 669941.6793982583, 650485.1226719549 669950.5763898203, 650477.0141695956 669955.1753515708, 650474.792031864 669956.4360670345, 650470.6318741054 669958.79553434, 650461.4273038161 669964.0153557195, 650451.6855998948 669969.5421082653, 650443.2160750268 669974.3450237208, 650443.0050679723 669974.414997242, 650438.9239315822 669975.7664851422, 650434.4157851965 669977.2239198441, 650425.1424707611 669980.3317558116, 650424.0555344482 669980.6916194175, 650410.3000696133 669985.2878928728, 650400.2197289802 669988.6556276082, 650375.4618922111 669996.9265199781, 650366.2387870462 669999.1270718764, 650364.9806590581 669999.4272152681, 650357.4334911137 670001.2282757784, 650291.7552969994 670022.942033589, 650286.1771100505 670024.7903334817, 650286.1123340029 670024.5962277177, 650283.3621511102 670016.3510828464, 650249.5647795313 669913.9820860532, 650241.7426974284 669890.2884025482, 650239.9573568291 669884.8812578298, 650237.1653882398 669876.424031469, 650231.5382044301 669878.2763207307, 650164.6099956419 669900.4198934096, 650159.0058166732 669902.2151954435, 650153.400637596 669904.0114973244, 650091.8785964729 669924.3438274678, 650084.7033525401 669926.7629323362, 650077.52810858 669929.1820371889, 650001.779602191 669954.1415924951, 649994.6063737733 669956.430698836, 649992.0783540168 669948.4944807184, 649973.0074851185 669890.7688191889, 649970.6943602776 669883.6136333843, 649968.3822354579 669876.4584476774, 649933.7648985141 669847.9596316492, 649918.9414677841 669835.7559970109, 649916.6917060008 669833.9037488942, 649915.755665671 669835.1262195421, 649915.0724632373 669836.0185251283, 649911.5759389878 669840.5850419726, 649887.9343936066 669871.4587759549, 649882.864649133 669877.9460775157, 649880.5464845374 669876.120920791, 649878.0921337771 669874.1885489697, 649813.2777207729 669823.161377561, 649808.102247556 669819.0868084916, 649801.9108320025 669814.5931227516, 649798.3678712051 669814.4766983973, 649729.2056359815 669812.2104145666, 649722.1787137564 669811.9795729297, 649713.4218106117 669811.6925240741, 649610.4639495322 669808.3181923073, 649595.5951140948 669807.8304113903, 649580.7262785437 669807.3436304599, 649579.948352111 669806.7785442665, 649543.9842518484 669780.6662592385, 649520.4969726667 669763.6129564513, 649505.3624039222 669752.6242791653, 649501.3227860212 669749.6908314679, 649496.5050214702 669756.3261665325, 649476.1959125712 669741.8279124347, 649466.2608378512 669734.7358096179, 649439.3333462804 669715.5128203114, 649350.4566291848 669652.0649521111, 649299.0094263536 669615.3372385885, 649294.4618503639 669612.0917335065, 649294.2679683302 669611.9543119612, 649289.3383250764 669608.4601641127, 649240.560721232 669574.9167296442, 649233.0423989226 669569.746891933, 649225.5240766509 669564.5770541985, 649143.8794390487 669508.4339557707, 649143.6904561087 669508.3039347123, 649134.7712285569 669502.4369372003, 649134.4832673068 669502.1330064716, 649131.8934955586 669500.3967172466, 649131.7885047614 669500.3267055175, 649126.275990362 669496.6330898636, 649125.5697525839 669496.1598109918, 649124.9941033125 669495.7739467035, 649124.9821043628 669495.7659453587, 649119.918550517 669492.372379846, 649119.5585823244 669492.1303396537, 649114.7320074495 669488.8968006055)))" + } +}, +{ + "model": "common.municipality", + "pk": 4, + "fields": { + "fid": "", + "name_fr": "Molenbeek-Saint-Jean", + "name_nl": "Sint-Jans-Molenbeek", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((643969.1836977684 671469.320829634, 643969.4134959554 671464.1047721372, 643974.5918780832 671448.083691966, 643975.8609914938 671445.2400421152, 643976.8763339863 671443.4061078527, 643978.8076964942 671440.5497359239, 643983.8834228549 671435.0345715912, 643985.8000580143 671433.5676178128, 643989.5119264675 671430.5754568556, 643992.9216393869 671428.0086942697, 643997.3583037802 671424.4612664358, 643999.3849994091 671422.9031269059, 644002.9486237181 671420.174624734, 644004.9518772641 671418.7152010206, 644013.4215461621 671411.8446147427, 644019.4997502108 671406.6082840739, 644024.4203635583 671402.3542033024, 644030.8209553482 671396.7131163555, 644036.7676381622 671391.4335708946, 644043.573579473 671384.8242255677, 644048.8736332174 671379.5558040519, 644058.430300545 671369.8487465236, 644070.7631854415 671357.5937318294, 644080.6792820834 671347.9291159182, 644088.2131071792 671340.1947523921, 644096.5457784844 671331.6356946095, 644102.812619833 671325.3178018446, 644110.423308588 671317.5277681834, 644116.7307981347 671311.2010803327, 644122.7302749074 671305.2981901888, 644130.5055164535 671297.41393724, 644134.7184488205 671293.1063342565, 644137.2625004731 671290.5389901301, 644142.3015845824 671285.6425326747, 644146.6105329546 671281.4213397193, 644153.9893362718 671274.2107904935, 644161.5550376778 671267.0642622932, 644165.7310062865 671263.1107298612, 644168.9666731018 671259.8943563516, 644175.7060921846 671252.957488126, 644177.0831261312 671251.5309504699, 644180.083997936 671248.0263935827, 644183.8965421813 671243.3233893624, 644187.9248433377 671237.8457017066, 644191.8611935604 671232.4254439987, 644193.1783059669 671230.4153898278, 644195.2566854835 671227.1753640533, 644197.9903613459 671222.7376735951, 644200.0925987003 671218.9086991858, 644202.4705893466 671214.2912890081, 644203.5674434472 671212.1388106906, 644210.9631409765 671191.8139050577, 644212.5190788117 671187.6488705085, 644216.3529221788 671177.2389174151, 644220.0026163021 671167.4983726976, 644225.9524273999 671151.317394864, 644230.1534489716 671140.0116583462, 644231.8277985812 671135.6832802006, 644237.0929739248 671122.5503366813, 644240.4640208994 671114.0305410279, 644245.9537240679 671100.9611432295, 644250.8846911802 671089.0814217121, 644256.653684775 671075.3250270014, 644260.3729487092 671065.6577692863, 644265.9789134517 671049.7217066733, 644267.1910962 671045.0703981604, 644268.7167135528 671039.2138643663, 644271.094729131 671030.0944391612, 644272.5241261051 671024.367332039, 644275.1772092412 671014.2626737831, 644278.8412702482 671000.3036315141, 644280.9204115723 670992.344793923, 644291.0887716423 670968.0650742974, 644295.1030827741 670956.603797417, 644298.7541004362 670946.2000620199, 644302.3587648496 670936.0063980521, 644307.4165885051 670921.6977663456, 644311.1373473094 670912.85273662, 644313.2135932291 670908.2032906972, 644316.0148735605 670902.5067461015, 644330.224877771 670876.7929671615, 644303.3991890688 670860.6874513834, 644290.2702300316 670852.8288845156, 644264.5168414341 670837.9162792665, 644254.7335353696 670833.0841463938, 644246.3236259258 670830.3958257376, 644239.0749860557 670828.5492309295, 644232.0639855784 670826.9556207105, 644225.852555719 670825.8537784057, 644216.7706808089 670824.2384823739, 644213.1102099887 670823.5566171878, 644191.5247560487 670820.8265712172, 644187.9380521029 670820.7383097538, 644184.7615377794 670820.731536803, 644180.6112430868 670820.9100868036, 644178.8348196995 670821.049396162, 644177.0949425325 670821.3378276505, 644170.4433428718 670822.9034237968, 644168.0677772021 670823.7237729195, 644163.3341688683 670825.7623874629, 644161.0505059591 670826.9174625304, 644154.2888360973 670831.5148418214, 644133.9676624625 670854.9541956927, 644132.9318448756 670855.8665408293, 644130.0249874122 670858.4272823541, 644123.7997930467 670863.8520726913, 644119.4932006095 670867.6894313435, 644113.234296234 670872.9950194581, 644107.727332226 670877.8471024521, 644102.5269670506 670882.2256283181, 644094.3130235808 670889.519918052, 644065.5382699718 670877.4311140358, 644057.9894772257 670874.1997143524, 644051.265304393 670871.2800869882, 644046.076616044 670868.8123133957, 644041.8652488736 670866.2334561794, 644037.2938404274 670862.8212887492, 644033.4448848865 670859.1389700691, 644029.785017104 670855.0398796264, 644026.3268857843 670850.1509043123, 644021.3715922864 670842.0901190982, 644017.6470417023 670835.7216139113, 644013.5288318025 670827.6602869941, 644011.5018403116 670823.0257160518, 644009.9365810145 670819.3516654255, 643978.1223598417 670735.6224211998, 643981.67397295 670735.2716434029, 643996.0605885957 670733.8512736214, 644029.8246987219 670712.6320263539, 644042.2504120104 670704.664641113, 644056.0464212246 670693.7811390627, 644075.9932476201 670676.0798982073, 644078.136273639 670665.3766249316, 644072.5601871994 670603.6015046717, 644071.4445445879 670591.2413926338, 644051.1271944281 670558.1298856202, 644073.3590505102 670551.1341983557, 644074.1162852991 670546.6341924742, 644075.1200698317 670540.6695967636, 644092.8301302311 670542.4110118494, 644098.8564856686 670540.1666322404, 644105.0037231849 670537.8774674856, 644108.5258129487 670536.5649402204, 644117.1799061039 670535.3589342482, 644117.4859190002 670535.3159308166, 644122.4058966732 670534.7365171369, 644123.1508712459 670534.6316461663, 644128.7159041894 670533.8473512278, 644129.120962783 670533.7923596061, 644135.4639805843 670532.9390370538, 644145.4681664871 670531.4223141802, 644151.6270627982 670531.0160035298, 644157.3510841067 670530.6376413892, 644171.5900082537 670530.737392537, 644172.7849140891 670530.8554511856, 644184.2478101825 670531.9847813258, 644188.0957549576 670532.3642678284, 644197.9334687059 670533.5583763152, 644198.155516158 670533.5854422972, 644215.7370137083 670535.8114983523, 644221.55195558 670536.5481783734, 644227.823771771 670537.3429918261, 644231.9543894343 670539.8303264362, 644303.6160739525 670554.3336686777, 644320.0815008738 670557.6656563401, 644321.8275399696 670557.7877819776, 644332.9503962391 670558.5681319349, 644333.2013826784 670558.5411221398, 644335.661311666 670558.7414114499, 644350.6179902066 670560.7212394845, 644353.5628245764 670561.0905439554, 644356.4558302144 670561.4528023709, 644362.4926431018 670562.2095085308, 644366.4515795279 670562.1570195613, 644379.9067512924 670560.2076060215, 644387.1008969963 670559.19551521, 644394.3589462861 670558.1504724575, 644394.543903227 670558.1234548193, 644407.7271422705 670556.2120884045, 644413.9092362399 670554.8317226738, 644430.236546873 670551.1797948248, 644430.6714305668 670551.08288788, 644446.9047345116 670548.3178160399, 644458.6120135116 670545.446288095, 644461.0381034098 670545.136500743, 644460.923862941 670546.015514495, 644471.5571948546 670543.7918496268, 644483.8052738945 670541.2293414333, 644492.2345479847 670539.4663680961, 644495.8962694061 670541.5117335622, 644508.2023541773 670539.2421880192, 644513.953459717 670538.0358485283, 644520.1627011143 670536.7337248344, 644525.0125805404 670536.0562306694, 644530.1127372504 670535.3448467212, 644534.4497447822 670535.299922522, 644534.9957314084 670535.2935874676, 644539.4827590572 670534.9758850185, 644544.1428047755 670535.0063581319, 644545.7727215948 670535.4366255624, 644556.7164892461 670536.1548752927, 644564.0824456918 670536.6387833696, 644574.6202116064 670537.8609776251, 644591.7003347301 670535.3080034137, 644599.5346199459 670536.5207566449, 644663.1162580454 670546.362568463, 644666.9854022256 670553.3078882986, 644666.5724862219 670560.9227698052, 644664.7964701599 670593.6500884423, 644664.6971996282 670595.4799703497, 644664.3233791807 670602.7238218213, 644663.0520481135 670629.2533694627, 644662.5928975093 670638.9901349567, 644661.8409944132 670654.93077649, 644637.1393575516 670656.6514965914, 644618.1452393471 670657.9746605913, 644604.0136806646 670676.6475927604, 644600.4898304853 670683.5720343918, 644598.7683608939 670687.1488983622, 644598.1121572198 670688.511680776, 644596.9659311366 670690.8935902918, 644592.450801709 670700.2759587342, 644589.6852138748 670705.8614697064, 644587.1615363912 670711.611167057, 644585.8311639917 670714.6408853518, 644587.8715989143 670718.2361160952, 644588.8701694846 670721.4041891692, 644586.6288063359 670724.9798713466, 644583.580036771 670731.1413403497, 644576.9205497324 670736.1865563035, 644569.3363190637 670746.7714225902, 644566.9938175591 670750.0562169052, 644564.5105341314 670753.5467515201, 644559.6788503078 670766.3939923106, 644558.2824347686 670769.5328211497, 644557.4293164053 670771.449652156, 644553.8332574156 670779.5361483163, 644538.814269218 670813.4498748028, 644537.3897799769 670816.821656925, 644534.7630603989 670823.3182111401, 644526.2515032928 670844.5279338994, 644499.8012033912 670913.1758003848, 644492.317761268 670932.5995907062, 644482.1896262936 670958.8850475738, 644486.625708439 670958.6055790228, 644537.9872052604 670950.4847625393, 644556.5092829742 670949.7749748986, 644557.3241833455 670949.7441922275, 644588.4942762834 670954.8558245619, 644590.0253271584 670955.2840004325, 644595.0341671682 670956.6864957717, 644604.7828018671 670957.711640082, 644616.7496092055 670958.9690447878, 644620.7317818224 670990.6260228321, 644623.1992492774 671010.2481139458, 644642.00303793 671010.8162616899, 644685.3304898866 671012.1265148416, 644697.3824301432 671012.4908226877, 644699.9385958363 671012.4367274148, 644700.0241504534 671012.434817614, 644701.0951088209 671000.0704826056, 644701.7089623488 670992.3825060511, 644703.4149069773 670976.553016311, 644704.4319024305 670968.5222126042, 644707.1353942355 670948.3489038466, 644707.1954833847 670947.583801981, 644708.3214586595 670939.5880507134, 644715.2174274115 670891.0396479032, 644715.6427254321 670888.0778210033, 644716.5466185834 670881.7698590569, 644716.6857192069 670880.8060494357, 644717.534362647 670874.8831953865, 644718.1399783355 670870.6553281023, 644720.0886001108 670857.3997501186, 644721.9340340507 670844.6380727639, 644723.8017535105 670839.6974057297, 644723.9567604167 670839.2884700266, 644724.6080387069 670837.5645721982, 644725.1600622204 670836.1076987373, 644727.0106472254 670831.9319786762, 644730.7645490207 670823.4615065153, 644733.3382880875 670817.6519758559, 644741.0474312356 670800.2557019372, 644745.2585280882 670790.6836998714, 644745.9937473784 670789.087810165, 644746.2588260748 670788.4958501708, 644747.1000461855 670786.6239770253, 644751.961296887 670775.8077500652, 644755.932111697 670777.3102804435, 644756.1701263129 670777.4002274426, 644756.7120045692 670777.6042889212, 644765.1785712565 670788.6331369341, 644767.7673040477 670790.7614140847, 644774.4944657225 670796.8181267518, 644782.4443103593 670797.9830549442, 644794.6670034355 670799.7745620238, 644805.6807749033 670801.3888079658, 644812.3025982942 670802.3606210973, 644817.529485005 670803.1261913911, 644835.062688173 670799.7622436134, 644835.4507095417 670799.6874108055, 644841.6049123598 670798.5060792255, 644844.1508363502 670798.0174288356, 644869.6702494812 670793.1205322128, 644887.6475646738 670789.6707183754, 644900.8557320227 670787.1363245159, 644914.2301614223 670783.6019646694, 644915.8092412682 670783.1840783275, 644926.0453915192 670780.4794135969, 644930.1698541149 670778.2048963371, 644937.7450769737 670775.8618301682, 644945.400706216 670776.3987323432, 644954.6176828222 670776.3767880782, 644956.4636696675 670776.3720475377, 644960.0287316862 670776.363511361, 644976.2964903031 670776.3254452982, 644981.752446352 670776.3120139244, 645002.4404558472 670776.263593355, 645003.5373980417 670776.2606837659, 645008.1633700237 670776.2492337202, 645008.773255594 670776.2483462179, 645014.0632502622 670776.2358551016, 645023.4902079211 670776.2129758112, 645029.0842579206 670776.199720848, 645038.18018043 670776.1777621984, 645051.2630793117 670776.1473175207, 645057.6180663932 670776.1320730222, 645058.9580042757 670776.12919231, 645080.112979379 670776.3517033856, 645080.1944511728 670776.3525130561, 645101.5636669109 670776.5772895637, 645108.8686175388 670776.6381568846, 645125.1294323888 670776.7730475502, 645135.4543424033 670776.8593934281, 645141.0952446793 670776.9059831863, 645147.5881397349 670776.9597140991, 645146.5946919599 670789.8165327762, 645146.5845396585 670790.0994075355, 645146.5645885344 670790.6774769267, 645146.3166464656 670797.684387586, 645146.0467916903 670805.3431263472, 645145.7867380232 670812.6881107448, 645145.5439595643 670819.5639838793, 645145.5529947361 670819.7039829593, 645145.9212949643 670825.5399435521, 645145.992974682 670827.2208881095, 645146.7009152604 670843.8816947815, 645146.7478164164 670844.9847246327, 645147.1247949937 670853.8548830106, 645146.3966040458 670871.8713396424, 645146.8611074382 670874.8372725846, 645148.895592739 670887.8323692104, 645148.9354829758 670888.2133285218, 645147.3304783367 670904.547944886, 645145.8226882446 670919.8865869894, 645145.224833155 670925.9653092587, 645144.8644134243 670929.6343341079, 645142.5485223691 670953.199642831, 645141.6935035791 670961.8984571618, 645141.0025207503 670968.9373146581, 645140.056863815 670978.5589850438, 645143.6034992866 670978.2843706659, 645153.970553947 670976.3147514135, 645161.9698812154 670973.4906228003, 645167.7953705215 670969.3184149141, 645168.3224107525 670968.9405629628, 645177.8220912206 670962.1477092573, 645182.6755320971 670958.5064181968, 645184.6466556488 670957.0626731262, 645188.1160034493 670954.5220018134, 645191.4593158019 670952.0734741961, 645197.445051232 670945.305322267, 645208.6116784947 670923.6499982527, 645209.9861227011 670920.1871709613, 645211.1543108594 670918.1852583472, 645214.9498384324 670913.2728195254, 645218.1634685722 670908.8203049641, 645224.9597149692 670905.9491538026, 645228.9039413568 670904.2826064305, 645232.2089594995 670904.3439584712, 645237.9448165589 670904.451678841, 645243.9857932604 670904.5643954129, 645249.8336517488 670904.6740490334, 645255.8606061507 670904.7858038734, 645263.1566732442 670904.9217092295, 645279.1423462372 670905.2215252027, 645285.4664327062 670905.3393952796, 645290.8764030215 670905.4400369392, 645294.0353399904 670905.4992916058, 645296.9722712894 670905.191645095, 645303.1272952606 670904.5483858975, 645304.9344327187 670904.359643395, 645310.6054115812 670903.766405942, 645311.8104339413 670903.8985473084, 645328.0903278118 670903.3414504367, 645338.559239195 670903.0297393156, 645340.9751827552 670902.9580275202, 645343.2118913147 670898.2482803045, 645353.3738241598 670881.3158087396, 645357.8147080349 670873.7503640509, 645359.7651178072 670870.4276030692, 645361.1234453085 670868.1129173952, 645362.6637471605 670865.488017755, 645364.2351176596 670862.8112825418, 645365.4679937278 670863.053505644, 645367.2329980728 670863.399630522, 645379.7985365972 670865.8660490741, 645380.4774530588 670865.9962079246, 645381.0635422777 670866.0892362682, 645385.0813244878 670866.7237848255, 645385.8083445916 670866.8417895647, 645393.1852236093 670868.0116098018, 645398.2899861248 670868.8222850647, 645403.9238620307 670869.7159020475, 645409.9707692558 670870.6756471886, 645416.0395084736 670871.6382748871, 645422.2074674777 670872.617074151, 645428.3111487076 670873.7848230995, 645432.6141297636 670874.7573207789, 645437.9068474497 670875.6979365627, 645448.6820054173 670882.0970466149, 645451.3657670519 670883.6903030332, 645452.4928013819 670884.3595475124, 645460.2912106147 670888.6524136276, 645467.011004506 670898.3559548458, 645467.7308708966 670898.9110725488, 645481.1396258499 670909.2434000263, 645490.5465197716 670916.4924155375, 645491.510275242 670918.4425824702, 645495.9152278474 670927.3629797172, 645503.7593360739 670933.8793398899, 645503.4822195669 670934.2129022189, 645505.1720813076 670935.0530912065, 645508.5007285814 670937.8233676704, 645513.1862143812 670941.723869433, 645517.9036467656 670945.6504546327, 645522.6562202392 670949.6069635919, 645527.4055991384 670953.5604322256, 645532.211061986 670957.5599868817, 645539.1074228995 670963.3007254743, 645539.3223518222 670963.4796284938, 645542.8730273414 670966.4350487764, 645549.5674008346 670970.6837845057, 645554.7158815865 670973.9493103746, 645556.3098731928 670974.9595655815, 645560.2715127629 670977.4720010201, 645560.8884500393 670977.8640688168, 645565.9950443405 670981.1036700924, 645571.0755485807 670984.3251885273, 645575.9851347575 670987.4396481636, 645581.310745552 670990.8173535491, 645586.428166763 670994.0619160713, 645591.3828341211 670997.2053406838, 645596.4063656882 671000.3919728763, 645602.5288837042 671004.2745659025, 645608.4582980165 671008.0363376874, 645614.2858362506 671011.731858344, 645619.5363710722 671015.0625155636, 645624.6209576711 671018.2870745586, 645630.1596783185 671020.7606581656, 645630.4056779947 671020.8707658602, 645634.7282008713 671024.5981071555, 645636.6618443688 671026.265353519, 645639.5595974262 671028.4487072268, 645645.3339830731 671032.8003723518, 645645.4850219466 671032.9023088728, 645645.5240684568 671032.9283931468, 645648.3508563603 671034.8327023649, 645653.6563889442 671038.3832030743, 645654.9912469847 671039.2763091815, 645665.0516033671 671042.9225739295, 645669.8804183379 671044.6720834045, 645677.087089829 671044.2959856386, 645691.316365417 671043.553688076, 645696.0208472153 671047.432112734, 645696.5857683995 671047.8977733364, 645698.2838679203 671048.1474117315, 645702.2106010183 671048.7248305033, 645702.2237356674 671048.7269120349, 645702.2397084674 671048.7438736958, 645705.2342458747 671051.9861040214, 645705.506315147 671052.281172208, 645708.6549126543 671054.1504802293, 645714.0327315582 671057.3421146732, 645714.0426707588 671057.348035771, 645716.3764392664 671058.667334668, 645726.9629305704 671062.082465341, 645731.84475754 671054.2092368945, 645733.264195381 671051.4774442085, 645734.9694340143 671048.5188087048, 645742.2968657629 671053.4245708142, 645745.2555259463 671055.4058546897, 645747.1093915232 671056.2891026782, 645751.0481932256 671058.3225023705, 645756.8447416634 671061.4332277861, 645781.8888356736 671067.9250743301, 645792.4268796083 671074.1532796868, 645798.0074018689 671077.3487782701, 645803.7160692514 671080.617411072, 645808.7946604256 671083.525974028, 645813.9832897667 671086.524628809, 645819.2698311567 671090.071127524, 645824.4262766833 671093.530732, 645829.214797844 671096.7422962238, 645835.9943356304 671101.2900784158, 645875.4337667546 671103.3456196914, 645881.9915925678 671103.6875147521, 645892.0328232261 671101.1986638885, 645900.8427182374 671094.5906845193, 645912.8410058983 671091.0121616835, 645919.5384111785 671087.927081514, 645921.9125907704 671086.8332992293, 645923.5815280625 671086.0645485041, 645934.3011041959 671081.1258926842, 645971.2548924382 671064.1015928048, 645987.162707192 671056.7725683615, 646008.106804116 671047.1241817725, 646022.9515177889 671040.2860622844, 646025.5016068483 671039.141424891, 646029.147811476 671037.5068461312, 646029.9849412361 671037.1389519196, 646035.7802367662 671034.5897643153, 646036.6802696898 671033.6628454898, 646049.8019331287 671020.1486119715, 646056.2527016476 671013.5045203137, 646061.4421814494 671008.1591388332, 646073.0046753125 670996.251735131, 646078.665274388 670990.6025744556, 646107.8376214136 670962.6623106299, 646108.4385256664 670962.0865508784, 646118.2307190568 670952.4747416908, 646121.6330718675 670949.1342371218, 646151.9970739855 670939.1450648304, 646153.2921578798 670938.719146376, 646156.4822960469 670937.6696242961, 646159.4244582668 670936.7019912807, 646166.2685255196 670934.7137212905, 646170.1306832877 670933.5913209558, 646182.7721948016 670928.8988251043, 646188.5473830383 670926.7545887926, 646188.835476686 670926.648544879, 646194.4406133029 670924.5671672447, 646194.6766975297 670924.4795168284, 646194.7345647851 670924.4580840791, 646201.2659324373 670922.0341024712, 646207.1872089561 670919.8348444224, 646209.9412289733 670918.8132694828, 646218.5735686909 670915.6093910614, 646229.5040907637 670911.5517205455, 646232.3731259828 670910.5631189347, 646235.7592369383 670909.3985818798, 646239.4233228852 670908.1379195518, 646243.2384668267 670906.824476092, 646248.1197137586 670905.145085773, 646249.6107671506 670904.6311920203, 646273.402652565 670896.4432835504, 646283.0689380832 670893.1163307682, 646307.4875320942 670879.4664935551, 646313.8567570561 670876.3053835602, 646313.9037972016 670876.281389551, 646316.4549663218 670875.0156339821, 646320.2691169407 670873.1221990312, 646320.282075222 670873.1151606871, 646329.1946589473 670868.6913340547, 646336.3900139256 670865.1202490025, 646344.3355248729 670861.1762993, 646350.3607359909 670858.186025387, 646356.9811371245 670854.8998271262, 646369.4637955326 670848.7034941253, 646374.4101028387 670846.248122951, 646376.5982646435 670845.162321119, 646378.2093425262 670844.3625259276, 646380.1074536279 670843.4197272321, 646391.7100188371 670837.661202129, 646418.6694789756 670824.2796687841, 646442.3718138419 670812.5146809933, 646455.4555196654 670806.0203435526, 646468.803205855 670799.2030021511, 646484.500236666 670791.1859595451, 646499.6549744981 670783.4450077617, 646499.7360965034 670783.4040580752, 646509.0835950187 670778.6292473192, 646514.7095261429 670776.3304351966, 646521.6101405355 670773.5106634345, 646527.2208575143 670770.7974550948, 646534.0697025345 670767.4853637689, 646552.1147751375 670758.7586923558, 646558.7071190634 670755.57040838, 646560.2392026283 670754.8296426673, 646583.7643451109 670743.4526257685, 646590.4056815869 670740.2405078537, 646590.8157281338 670740.0434398274, 646591.7048727776 670739.6126725735, 646618.2892116907 670726.7579629247, 646644.1106151532 670714.2713160663, 646644.1576552942 670714.2473220443, 646656.964247719 670708.0539052278, 646656.9708154711 670708.0515060498, 646656.9752531388 670708.0499066025, 646743.5648632015 670666.1768778085, 646747.7821311164 670664.1374120169, 646757.7775069363 670659.3036380967, 646763.7888621823 670656.396399502, 646786.6930751641 670645.3202603277, 646808.8412755376 670634.6101050477, 646825.1470717657 670627.0342455469, 646853.7744755455 670613.7317826654, 646860.8679091618 670610.4537986012, 646868.7572926375 670606.8087150229, 646875.4976609302 670603.6945262883, 646883.5989901333 670599.9505894079, 646890.454384154 670596.7834551308, 646897.0246988281 670593.7482848192, 646905.5390912419 670589.8133599816, 646913.9775097198 670585.9144254875, 646923.7270091099 670581.4086165, 646935.0925494543 670576.1570914616, 646941.6633966832 670573.1209610297, 646954.8966856796 670567.0066316308, 646969.2614164224 670560.3694049413, 646969.5543060775 670560.2344019049, 646978.0488173774 670556.3085941409, 646984.1460707894 670553.4912837385, 646989.6152986878 670550.955974157, 646994.9647077578 670548.4785693961, 647005.0741964992 670543.7945654627, 647017.3287030488 670538.1164321953, 647022.6159839616 670535.6671795005, 647027.8583552269 670533.2377611315, 647060.1339160767 670518.2839141337, 647076.6978066372 670510.6090441392, 647076.7958065878 670510.4408981298, 647083.0629936457 670499.8208309693, 647088.91013123 670489.9127038817, 647096.5716836808 670476.92771515, 647102.0477716039 670467.6484950195, 647115.55148985 670444.7604981642, 647115.5575257889 670444.7524989946, 647120.9215874244 670435.6612630882, 647125.2625108476 670428.306841347, 647129.589941897 670420.9724176672, 647135.1505352536 670411.5493695277, 647136.7407219801 670408.8535959768, 647140.9394443694 670411.8649398122, 647146.3289020545 670415.731615199, 647146.3368887793 670415.7375360795, 647153.0292512852 670420.5393954458, 647158.9367828228 670424.7770081583, 647160.8745455628 670426.1661822788, 647171.5535378696 670433.8283222863, 647180.9505685389 670440.5693607649, 647190.737707507 670447.5904424617, 647195.844256859 670451.2540469087, 647220.0782006211 670458.3058222882, 647223.8944602988 670456.5752629664, 647240.1128279499 670443.983454597, 647248.4626794237 670437.1275866432, 647254.5471272157 670433.0603701454, 647269.2804245335 670421.4002109905, 647274.3579153009 670417.3818329012, 647275.4499939301 670416.5170152693, 647280.3951803169 670414.5056349321, 647286.2254199658 670412.1343654776, 647303.7897916548 670400.7377419375, 647316.5030233568 670390.1283658842, 647321.8384730151 670385.6749841555, 647321.7624645754 670394.7108567659, 647321.7523766874 670395.9218797041, 647321.5675714768 670417.9514891796, 647321.526866881 670422.7795810966, 647323.1224593301 670433.9505063165, 647324.9526846553 670440.9315945515, 647331.3459572133 670454.4843444995, 647331.9368762634 670455.7282391784, 647332.4546844747 670456.8193270789, 647333.9353442477 670460.0135430837, 647348.2413803773 670490.6268607599, 647353.2494700612 670498.2153629583, 647369.6063328676 670522.9880437357, 647372.0129702451 670533.7171529628, 647373.8499700405 670541.9012669655, 647375.2493187641 670548.1343138395, 647375.2842680567 670548.2813561226, 647379.6061367083 670564.6645418314, 647381.6341865741 670572.3477253795, 647383.2224203884 670578.3659177994, 647387.382430541 670594.5822064255, 647388.0541226573 670597.1872132119, 647389.2924767184 670601.995299405, 647390.9458037363 670608.413334509, 647392.2941552259 670613.6455884184, 647393.3456919063 670617.7266216528, 647396.3031938995 670629.207787624, 647396.5801381544 670630.2828469161, 647398.1442258222 670636.3528758906, 647399.2897928656 670640.7940757767, 647400.549081563 670645.6860034706, 647402.3611946264 670652.7211697331, 647406.2140128822 670662.6535035633, 647406.5928534663 670663.6306563823, 647410.3196871679 670673.2389793433, 647415.0022282216 670685.3133051699, 647418.0462614307 670693.1625290371, 647419.4438020103 670696.7647302091, 647425.3148558356 670711.9031592665, 647429.3558927549 670719.813622291, 647430.2646469057 670721.5927484321, 647456.8831010745 670757.7714790812, 647464.4400248707 670765.7711639852, 647490.8999422699 670790.5890642892, 647517.2295125966 670824.6337107222, 647532.1685636698 670831.6005376154, 647540.8961272427 670834.9634639904, 647553.2123714486 670839.7100042437, 647553.2175186812 670839.7119248405, 647554.3793794794 670840.0950996566, 647560.0071093633 670841.9517526748, 647566.9947807366 670844.257523513, 647586.8699482897 670850.8148699364, 647594.177459831 670853.2256379909, 647604.2043222112 670855.0799403561, 647614.4528761958 670856.9710689452, 647648.9013518512 670859.6622185793, 647652.9947919048 670859.9817469304, 647662.3762280989 670860.7137461611, 647667.2341484553 670861.0935257145, 647658.9589137668 670921.1136812884, 647657.2614061257 670933.4321991741, 647655.3427174898 670947.3449099741, 647661.072800074 670945.3717049569, 647763.0125636893 670910.2733883675, 647775.2118642457 670906.0728314011, 647785.9525951861 670902.3746522688, 647809.1604692289 670972.9349521752, 647812.1477640411 670996.2660560686, 647817.3847881581 671007.0889499923, 647820.1074658778 671012.7152868767, 647877.597792921 671131.522316668, 647882.8647129134 671140.2738815831, 647883.4100562615 671141.1800159375, 647886.1179619752 671145.6791658526, 647900.1490624086 671168.9944846983, 648036.6534986793 671395.8111608792, 648039.5976077733 671400.7032955226, 648046.9562833435 671412.9312320667, 648048.6732366959 671415.7840439429, 648053.4858524735 671423.7808457513, 648060.0155991387 671434.6304595685, 648186.293208241 671644.4552420685, 648187.2002214626 671645.9625343794, 648191.3280101138 671652.8218345087, 648196.3629893933 671661.1893870402, 648271.4603533181 671785.9709611776, 648297.7344566337 671829.6438830897, 648296.6212813039 671830.3034194404, 648288.1621125138 671835.5648847969, 648284.1773325889 671837.9192506091, 648299.6288599331 671863.5755489349, 648321.4155104834 671899.355380364, 648340.6524020153 671932.1211778957, 648367.3831746669 671977.5607010429, 648392.6463049517 672020.7591137745, 648407.8658898906 672046.029153624, 648419.6794907975 672065.118371115, 648429.4625487768 672080.926259667, 648429.4840222095 672080.9585819086, 648424.1261381998 672084.5500446634, 648401.2533717033 672099.8809877333, 648381.3569705354 672113.2174376359, 648379.7037891686 672114.3252224067, 648382.8151095872 672119.1765852775, 648383.8515096367 672120.7928545914, 648385.1017559152 672122.7427464589, 648383.8866789027 672123.5576291569, 648373.4888509263 672130.5263630658, 648375.933426505 672134.4666195235, 648424.535673932 672212.7877214688, 648424.5876710538 672212.8717269282, 648429.2787779314 672220.3821401942, 648433.7737861195 672227.5786128885, 648475.3577141794 672294.1580280679, 648479.6468631616 672301.0255195732, 648484.180913854 672308.2839169175, 648484.2708831686 672308.473045908, 648523.2617799825 672390.9878188105, 648560.6929929996 672470.554596685, 648561.1848949018 672471.6000858385, 648566.0416274962 672481.9239311585, 648571.305794015 672493.1137372736, 648566.6763436985 672495.4571451806, 648558.9399428967 672499.3731156756, 648555.3709328484 672501.1969789906, 648555.5524717527 672501.5520374608, 648546.4673674162 672506.0300766521, 648546.3378246262 672505.7675035729, 648542.7576358874 672507.5600058181, 648534.8690965335 672511.6599555766, 648534.7202095235 672511.3709804092, 648530.1010524207 672513.7284692967, 648530.2281117939 672513.9779222109, 648519.405525034 672519.4894973111, 648519.2784656611 672519.240044401, 648515.1352147823 672521.3315143893, 648515.261919138 672521.5811272555, 648514.144318536 672522.1481842574, 648508.7822852801 672524.8687781655, 648503.5952764864 672527.5007544474, 648500.8781366551 672528.8792867631, 648500.7514322998 672528.6296738969, 648496.616169216 672530.7181048412, 648496.7428735518 672530.9678777019, 648485.9273895334 672536.4554536595, 648485.800685198 672536.2056808004, 648480.4382952661 672538.9400344065, 648480.5642895676 672539.1901271746, 648469.7296396873 672544.6437810159, 648469.6038228851 672544.3936882704, 648465.4685576116 672546.5001989226, 648465.5952619652 672546.7498117818, 648451.2065083983 672554.0559700582, 648451.0798040453 672553.8063572012, 648446.9514620516 672555.9058287235, 648447.0781664044 672556.155441579, 648436.2594876307 672561.6401367743, 648436.1329607962 672561.3903639427, 648431.4926827952 672563.7347300424, 648431.8703123404 672564.4712484326, 648423.950185449 672568.5087943214, 648420.3738985846 672570.3264164511, 648420.5234947242 672570.6225916105, 648411.5232201788 672575.2196790163, 648411.3794808087 672574.9292645149, 648407.7822534966 672576.7088044519, 648408.1627212357 672577.4603630189, 648400.2177836385 672581.1678292652, 648398.6872955284 672581.9445933038, 648397.7838699471 672580.0944224412, 648380.8620682533 672588.5592970069, 648375.6477217334 672591.2127092168, 648375.42679004 672590.7576470287, 648366.0393776775 672595.4594855601, 648366.2787665552 672595.9371097639, 648361.0748979163 672598.5447636918, 648350.3513787267 672603.8553917481, 648348.309307772 672604.8920902048, 648348.4562408358 672605.1932249729, 648327.3171504454 672615.9185174273, 648327.1680865549 672615.6244223274, 648325.9338625232 672616.2465042798, 648325.1190936083 672616.6571199443, 648325.3160689176 672617.062259721, 648319.1089118699 672620.2112246244, 648312.0770449991 672623.7788038509, 648310.3672721444 672624.6462648218, 648310.1761547582 672624.2372858124, 648308.1200596847 672625.2880623379, 648308.2661059977 672625.582957056, 648287.1298540145 672636.3192891069, 648286.9889560121 672636.0173550979, 648284.9317985711 672637.0460516963, 648285.1317917402 672637.4479918666, 648271.9811659454 672644.132386788, 648271.7701700095 672643.7123654666, 648269.7091053074 672644.7601013578, 648269.8500049732 672645.0481155128, 648246.7867183136 672656.6763609388, 648246.432868334 672655.9841650598, 648236.1483149723 672661.1988470926, 648236.7431448181 672662.4229067429, 648210.5507115556 672675.691709932, 648210.2117721031 672674.9943959526, 648198.3965852321 672680.7152844416, 648197.2882156367 672681.272262422, 648195.0521279583 672682.3954959069, 648194.9641094535 672682.2204470182, 648192.147387596 672683.6392860565, 648191.3858715221 672684.0230284634, 648182.0490097911 672689.0258286336, 648181.2193292299 672689.4506822262, 648181.3607626469 672689.7281365485, 648174.6029867377 672693.0197516887, 648102.5939269215 672729.2438013143, 648095.6605691827 672749.1516922045, 648097.7391226669 672753.2549422429, 648101.9161049839 672761.4993643825, 648103.1238771398 672763.8834864497, 648105.2073995337 672767.9956970447, 648106.2311724836 672769.818682695, 648107.4687861028 672772.0218504271, 648109.3594564574 672775.3881255552, 648111.1241497548 672778.3603095551, 648112.7617782623 672781.1284802019, 648112.8011740553 672781.203364215, 648115.2150767442 672786.3407241803, 648119.4940178704 672795.4470696008, 648119.6439662566 672795.7661245101, 648122.0603521649 672800.9183646692, 648125.4324527818 672807.0467114748, 648143.318154912 672842.4294799594, 648151.0763910179 672857.7907445643, 648157.6747721191 672870.830932278, 648176.6798990521 672909.6928419606, 648179.1594900002 672913.2331078915, 648189.6267693578 672935.5541501965, 648205.5326256391 672969.655732722, 648210.861937787 672974.8763306983, 648213.0110380836 672982.7665888689, 648224.2585918386 673002.2117594428, 648226.9796817397 673009.3649760463, 648228.7427740344 673016.8201516523, 648229.751498255 673027.2251224752, 648230.1030795143 673039.4659925696, 648229.5954348881 673045.3839458078, 648226.1634606714 673061.9862233179, 648222.130142741 673072.463693451, 648218.9034706418 673079.2131432785, 648215.0256522108 673085.1506818488, 648199.1670981417 673108.1894408301, 648234.110746117 673176.6779533386, 648237.3218360465 673182.9702799795, 648213.6908948594 673216.9426827474, 648097.5687221764 673383.8819211111, 648073.4188775201 673400.6566819545, 648021.472620562 673437.1771269811, 648021.4708458397 673437.1748867878, 648021.4021441856 673437.2545575197, 648013.957869191 673445.871153402, 647701.5515778336 673035.5563164292, 647690.1095119822 673020.292215264, 647685.2088371234 673013.7555322784, 647678.6663812349 673005.0289141334, 647575.6954554984 672867.4518562735, 647567.0254850837 672855.8680526074, 647487.8132700287 672750.0338582699, 647445.4180388523 672693.3899875721, 647438.6432780939 672684.3383869838, 647435.2267925314 672679.7740236114, 647447.953408257 672665.4867714279, 647449.9088213295 672661.8240104406, 647453.0384759884 672655.9655769435, 647456.4653309844 672649.5479858561, 647458.2251374468 672650.3481905528, 647461.1557990563 672644.6286112715, 647464.1915242184 672639.0410432381, 647467.2221008203 672633.4624344592, 647469.4816291773 672629.3027559636, 647474.2685909368 672620.4904770538, 647489.1424575985 672588.0526502039, 647492.5112753627 672581.5921723172, 647495.8058814043 672575.2745637812, 647495.9839537581 672574.9336292592, 647498.6385028207 672570.0600866658, 647501.0860424099 672565.5673146213, 647505.6260872391 672556.885083967, 647507.6560087178 672558.005317787, 647512.126940128 672549.8758724516, 647511.2219299336 672549.3997679435, 647515.4968303777 672541.8714119864, 647515.0628724953 672541.6452818518, 647513.786024496 672540.9811343261, 647521.0025876822 672527.7621597161, 647522.1633537464 672528.4062933696, 647523.3193276845 672529.0475464752, 647525.9239674262 672524.4408746794, 647530.9498517129 672515.5505451476, 647534.9817083467 672508.4191150586, 647541.2441985948 672496.8120061964, 647544.8668699203 672490.09860165, 647547.7145861026 672484.8209268234, 647545.6246748136 672483.6606862242, 647548.24513139 672478.8911380741, 647560.0440329805 672462.4986749411, 647565.1639611926 672455.341456986, 647565.1676884983 672455.3428974239, 647565.1699966753 672455.3371377678, 647555.4125473221 672441.8113059471, 647546.5014500304 672429.4585237447, 647546.6594326434 672429.3873437179, 647549.2846066951 672428.2041556347, 647558.4147593447 672424.0888300024, 647626.6099957648 672393.3513710393, 647617.3475405567 672372.840557648, 647611.9689744979 672360.9301189559, 647610.2391991076 672359.8497612394, 647613.1302437332 672349.9703830305, 647612.2884009439 672349.6802041644, 647611.3303011317 672349.350011642, 647612.5007793293 672345.9703516277, 647613.0808219707 672346.1702597493, 647613.2807564976 672345.5803706351, 647613.3539265671 672345.2424633083, 647610.4089433368 672344.5790735001, 647611.4065420108 672339.6811695759, 647612.8284352026 672332.7014205203, 647613.5468406918 672329.1786672618, 647614.4492435736 672324.7508264949, 647615.4217991722 672319.9839580406, 647616.8487696921 672312.1082196414, 647617.6293928224 672307.8093226363, 647618.9542326462 672300.5074853789, 647625.0469257534 672294.2374148087, 647679.4809636998 672223.9598445588, 647696.6125502598 672202.4021632022, 647721.2179182667 672197.0400882304, 647724.2328335223 672197.72044676, 647739.613511349 672191.4524646731, 647811.4411673042 672124.3118548655, 647815.6906121689 672120.3394549713, 647815.7665902127 672120.2684249856, 647812.3221169995 672116.7520489767, 647761.7995973683 672065.1806346915, 647753.2144367041 672058.4667160391, 647714.1757571739 672033.0352545538, 647703.3006566052 672026.530977591, 647629.835672184 671980.911607692, 647625.4901167486 671978.2131951089, 647622.292413844 671983.0777071074, 647617.0974777808 671990.9830592143, 647614.2060173558 671995.380574085, 647610.0924899046 671992.3791533761, 647596.0876475621 671982.1635049535, 647589.6657017582 671991.1946142355, 647586.8002010317 671995.2233367813, 647582.4765211062 671992.1706913168, 647532.873252034 671956.7662479803, 647517.7905425236 671945.8655185312, 647499.6722865464 671933.177404247, 647447.3702593825 671895.4025881109, 647451.8112256872 671887.1243500033, 647454.0986905441 671882.4546057222, 647458.6638552239 671873.1362356733, 647463.8820234693 671862.4830814053, 647456.2642448527 671861.495131918, 647382.7029300993 671851.948430059, 647361.1913414868 671853.0878923782, 647331.3610340446 671848.8436960019, 647297.813215708 671841.4677297155, 647262.7069376074 671834.2504531726, 647262.5347679609 671834.2150727857, 647226.5458235501 671826.7047335518, 647189.7616057886 671822.7504130121, 647153.1548160192 671822.2919093708, 647132.5789865542 671822.1004621108, 647116.7601229647 671821.9524698994, 647050.4176649536 671821.3585300314, 647037.7514850551 671823.0950550254, 647023.2373329204 671825.0852731065, 646969.0636147457 671836.5747289059, 646969.0542127057 671836.5304083427, 646952.3688153985 671840.6153761804, 646995.939424598 671973.5499711883, 647032.2241133413 672076.9231121888, 647033.7690601111 672081.3242026418, 647074.9313355271 672167.1631910616, 647059.2660148592 672180.2461011652, 647052.521196291 672186.4581718331, 647076.9288127302 672230.0295897326, 647084.283372319 672244.2405581103, 647086.5775463108 672248.6735853428, 647082.2436843568 672256.4590501573, 647068.9498134949 672280.8270772985, 647057.4594831894 672300.9784114626, 647051.6938875034 672306.032693835, 646931.7639424056 672352.7396845794, 646920.892504881 672357.0112760541, 646910.0210673116 672361.2828674827, 646908.7907647267 672361.7591926806, 646799.4085028928 672404.1009476944, 646775.782781143 672412.1160671453, 646752.1568817312 672420.1310263864, 646722.6661702512 672461.1789675374, 646707.5067758436 672456.0621473035, 646701.0011702044 672453.8653833547, 646691.7804728753 672450.7534205853, 646689.3866414541 672449.9451394876, 646686.9127612518 672449.1098089963, 646686.0458915209 672448.8167472088, 646682.930023529 672447.7653413309, 646682.3959507803 672447.5852786165, 646673.5343195073 672444.59319808, 646668.7526912944 672442.9796766202, 646666.9167165366 672442.3594610498, 646661.739991758 672440.6118132217, 646661.5300190233 672440.540748566, 646657.0842052897 672439.0402265589, 646655.0272532497 672438.3460650435, 646651.479370459 672437.1486884764, 646647.7875419226 672435.9021750174, 646645.9027569424 672435.2659537229, 646639.3680428779 672433.0602264863, 646634.4492138239 672431.3996489842, 646633.8171657723 672431.1854947885, 646628.7334467942 672429.4700179257, 646621.3868854902 672426.9901154193, 646618.9928766984 672426.1818343578, 646615.4781849497 672424.9953417355, 646613.76911686 672424.4181810966, 646607.9214734372 672422.4445345912, 646607.7995365787 672422.4035602729, 646602.4096444707 672420.583887525, 646600.9258134268 672420.082753333, 646597.8539636864 672419.0452727218, 646595.0410743261 672418.0959825075, 646592.0991485183 672417.1026771485, 646588.5233995668 672415.8961773834, 646586.6954120025 672415.2790027985, 646580.900661285 672413.3223225658, 646579.1658568296 672412.7371589172, 646575.0899362699 672411.3606804671, 646569.9671692044 672409.6320791256, 646569.5282320845 672409.4840275982, 646563.2824748354 672407.3752944611, 646558.5306656957 672405.7707366957, 646557.4188573419 672405.3955661915, 646551.3330200585 672403.341891838, 646550.9031345604 672403.2007213305, 646538.1056113327 672398.9883379545, 646536.1047448782 672398.3290629197, 646531.2959559594 672396.7465780237, 646524.726269701 672394.5837262983, 646521.8663422255 672393.6424303484, 646518.8874952646 672392.6611204456, 646514.0927282142 672391.0834372223, 646512.1578887313 672390.4462499556, 646507.6881095169 672388.9746849294, 646505.5251929088 672388.2624308736, 646500.519388016 672386.6149628878, 646495.9635253106 672385.1144277757, 646493.2407990304 672384.2190679623, 646489.3778648229 672382.9466142479, 646486.2421149606 672381.9142459399, 646483.4451971297 672380.9939174289, 646479.6033843482 672379.7294661952, 646477.3694709516 672378.9941638191, 646471.609680526 672377.0974873401, 646470.9937835864 672376.8943750081, 646465.110991096 672374.958644076, 646463.2061477558 672374.3315403592, 646458.5643789847 672372.8029952104, 646454.8494729433 672371.5804789126, 646451.2935996922 672370.4101412973, 646447.7087953291 672369.2297202977, 646445.1419071485 672368.3844588434, 646440.1501241992 672366.741792607, 646438.1822712888 672366.0935615093, 646432.6065400187 672364.2578667174, 646431.6755948273 672363.9518373851, 646424.9758072224 672361.7460906017, 646406.8636533498 672355.705884612, 646391.3313431996 672350.5271015163, 646374.8208766301 672345.0141237981, 646374.3822943261 672344.8678323234, 646373.7876967423 672344.6692025443, 646358.2071557108 672337.6131560587, 646346.034696245 672334.1347606394, 646334.5670608449 672330.85836837, 646325.3195060071 672328.2163179207, 646308.386155973 672323.3783212487, 646292.7348371235 672318.9054393488, 646289.0852835635 672323.3039853999, 646262.6346228787 672355.2384844412, 646249.3747640027 672371.2435691078, 646236.1138398286 672387.2488134877, 646052.549271626 672608.8189818576, 646011.4854631203 672658.4063737866, 645996.6366378478 672665.5054500861, 645983.6619550813 672671.7078040851, 645962.6118443541 672681.7712134906, 645955.8725284934 672684.9933984464, 645944.1428943641 672690.599769182, 645932.414324404 672696.2074283408, 645924.819945334 672699.8375224773, 645908.4420521468 672707.6674827263, 645905.7792429936 672705.9391871229, 645903.8167094787 672702.3938765165, 645901.4002325127 672698.0286818076, 645900.3901277843 672690.3316941555, 645899.7268910772 672685.2846359096, 645895.0480295834 672683.7101788901, 645881.6026601887 672679.2705546381, 645873.7839793 672676.5526982335, 645863.1698858897 672678.0803788058, 645852.230793046 672679.5971409132, 645829.9904929253 672682.7653405424, 645820.5074444512 672684.1161976795, 645792.3316323322 672683.8899727565, 645788.4596725154 672683.8584730965, 645780.2479481889 672681.6444038746, 645774.7311035082 672680.1578461444, 645760.9176928962 672676.4343293353, 645760.0447809484 672676.2011084008, 645733.6827686416 672669.1641400221, 645681.0674582544 672657.3340289559, 645675.632534258 672656.5433125502, 645674.2926157815 672656.3652354503, 645666.9867030743 672655.3403794975, 645658.7668647176 672654.1773365845, 645654.9160825782 672653.6909647855, 645634.7173593224 672651.1386749267, 645632.9864154181 672650.9203918293, 645625.669663271 672649.9955333686, 645616.046879172 672648.780484313, 645593.1297959217 672650.3127829615, 645591.1467608322 672650.4455057515, 645584.503806331 672650.889671127, 645578.2485719644 672647.8931232989, 645575.6403755028 672646.6435481554, 645607.862927892 672483.3773118188, 645606.7042640524 672472.7213002964, 645603.7031772832 672464.8591165568, 645604.5787058152 672461.0762254939, 645607.1421323641 672448.0107651517, 645608.4729543809 672441.2279637966, 645609.535586854 672435.8151943204, 645610.1019811525 672432.9271359082, 645608.6272712538 672423.0792371997, 645606.2170277826 672417.1009413493, 645604.7004003635 672413.3388855223, 645601.0156715878 672412.1064618317, 645592.2259249442 672409.1664111213, 645583.9283629196 672406.3915369741, 645578.3446499025 672404.52385489, 645572.279934288 672402.4951574816, 645566.3052070276 672400.4965504106, 645560.9744198008 672398.7138573751, 645554.6587309688 672396.6011310702, 645548.9500639391 672394.6915145615, 645542.581304938 672392.5627821091, 645536.9715025973 672390.6720571138, 645530.3759077858 672388.4792984092, 645523.8001937333 672386.2796621339, 645517.8295489989 672384.2829754282, 645511.7607514036 672382.2531574264, 645505.9700833843 672380.3164913994, 645499.1593703679 672378.0387480372, 645492.657670797 672375.8620002301, 645486.9159903984 672373.9434197814, 645479.4242417786 672371.4374380412, 645473.5345335364 672369.4678005605, 645468.186707614 672367.6791854026, 645467.6137643543 672367.4871194996, 645469.0904930336 672362.1553579839, 645471.9736303769 672351.7419036654, 645473.6243845898 672345.7789703152, 645419.1680375715 672327.0227915063, 645408.442579889 672323.3385185619, 645426.4216603087 672312.8137416583, 645455.8504278744 672296.9204706817, 645461.8768980394 672285.0083681173, 645460.8641512112 672283.0362708085, 645464.8975766308 672279.1459166463, 645470.5842429985 672273.6615380388, 645474.5425859176 672269.7740549456, 645478.9990502287 672265.3967169275, 645483.5707248945 672260.907233961, 645488.1042327281 672256.4548660362, 645492.4817001392 672252.1575177307, 645502.344715551 672242.4708059548, 645504.2699207403 672240.5800174196, 645507.6573588252 672237.2524598954, 645507.8064755012 672237.1075193557, 645509.7575986659 672235.1906542225, 645510.6767975837 672234.2878942925, 645521.1188828339 672224.033258333, 645526.1535293031 672219.0887960449, 645522.5367627494 672209.2536015814, 645519.644664594 672210.1481267187, 645511.0716909695 672218.5450870408, 645496.3020259484 672233.0121182023, 645495.7879280535 672233.515091075, 645495.3068488387 672233.9860682563, 645487.3159627478 672241.81302479, 645479.2402667402 672239.1210552312, 645471.0587858204 672236.3930734816, 645462.6470982328 672233.5881052129, 645455.2394813207 672231.1182925026, 645447.4097892117 672228.5073111869, 645424.9388055914 672221.0148043218, 645412.9647318035 672247.2881102037, 645394.5484807496 672241.6859838831, 645389.1575276 672241.326306404, 645377.1047393605 672240.5228011506, 645374.2718797885 672240.333506165, 645366.5688535734 672232.4467617241, 645364.0211740441 672229.8373690491, 645315.2484597072 672205.8358440209, 645311.7515858138 672204.1144878535, 645314.0352401586 672198.6609035244, 645315.5877412631 672194.9560117032, 645317.1862281705 672191.1392866687, 645318.010417453 672189.1714078211, 645318.343501961 672188.3775367682, 645321.0722753778 672181.8598579231, 645322.2535184356 672179.0409915546, 645323.6570547908 672175.6903178804, 645325.9756863475 672170.1535387554, 645330.6809507278 672158.9212704984, 645337.3287945765 672143.0492879493, 645341.5986931137 672144.6288179193, 645347.4913909996 672146.8083740966, 645353.2790143125 672148.949038206, 645359.6227166043 672151.2958461016, 645366.4293151355 672153.8145470901, 645371.9801657539 672155.8671839957, 645377.1469260717 672157.7778568192, 645382.9934763993 672159.9415676118, 645388.4972921101 672161.9761190942, 645396.1349033228 672164.8019553539, 645398.3254838809 672159.8713940803, 645400.2959055286 672155.4366407789, 645407.6019830966 672138.992743905, 645409.2654250852 672135.2489859712, 645410.503702839 672129.031606469, 645411.7098335777 672122.9763012421, 645417.1313508947 672110.0051193725, 645419.2120731486 672105.0115154113, 645420.9075071026 672100.9607326764, 645421.0594388903 672101.0197899628, 645423.2842043806 672095.2791733581, 645425.9449830474 672088.919577136, 645432.2237193246 672073.8974901416, 645436.4609738353 672063.7611732594, 645439.0477027963 672057.6665245863, 645441.7414940932 672051.3229320645, 645455.3130183441 672021.2639989632, 645454.1932545621 672019.193813898, 645453.0734286846 672017.6297019101, 645452.5534384516 672016.966129052, 645447.4177803707 672008.9685067318, 645438.2923556261 671994.7572141874, 645438.3465196379 671994.538343629, 645440.2433201547 671986.9175536332, 645441.6350653919 671981.3217958241, 645446.572501082 671961.4786549062, 645432.8621338671 671957.1481213915, 645431.5470959645 671956.7319704784, 645420.314694296 671953.1836413671, 645415.3317847559 671951.6101094906, 645409.9230854509 671949.7925302422, 645405.3212626649 671948.2459233775, 645401.914490377 671947.1005733409, 645398.7325998037 671946.031249044, 645385.869256638 671941.7076963305, 645384.8593298192 671941.3687006973, 645381.686491369 671940.3022574446, 645352.4507982061 671930.4749103144, 645348.8819760975 671929.2765817137, 645340.0324264764 671926.3024485518, 645315.1676400757 671917.944721875, 645299.0624150987 671912.531838058, 645290.3088888247 671909.588755923, 645282.777195539 671907.2078915997, 645252.4145362509 671897.607447302, 645251.5286734784 671897.33134559, 645236.6303477306 671892.6797151954, 645225.6716693564 671890.4234011862, 645208.3354545522 671883.8623866746, 645171.6711606286 671871.9642638019, 645157.7659244506 671867.4516298518, 645152.1580726113 671865.6319470296, 645144.490429026 671863.1440280926, 645137.3659102261 671860.8323314199, 645123.8674854055 671856.4517441327, 645120.4846701669 671855.3543963628, 645117.517818858 671853.5729474868, 645111.5223513225 671849.9722828977, 645105.5307884258 671846.3746587334, 645087.3782593155 671835.4747265612, 645075.6872791192 671828.4243110735, 645063.5582677727 671821.0829674295, 645061.1364765037 671819.6186991266, 645057.6066427955 671817.4823080954, 645045.4387621435 671810.1190401467, 645039.8602121149 671806.7433821638, 645033.8535668319 671803.1087967597, 645029.4299157815 671800.431186728, 645014.5651418684 671791.435620074, 645009.6919783786 671788.0109267649, 645004.7151645527 671784.5135821116, 644997.2758331011 671778.7546952456, 644997.1278132529 671778.6396391977, 644986.0259654026 671770.0454751122, 644981.3855238268 671766.4520118384, 644976.6298963875 671762.7705360607, 644971.7605030971 671759.0008879501, 644966.7619029998 671755.1313858251, 644959.6216289828 671749.6035714354, 644952.4183489891 671744.0267902253, 644947.7718730447 671740.4302862982, 644942.9784421478 671736.7198464364, 644938.2579565335 671733.0642945133, 644933.1874765494 671729.1387846217, 644924.842278275 671722.678839584, 644924.3593500841 671722.3048672118, 644922.5264363317 671721.3595419497, 644916.7827619571 671718.4000186839, 644914.6470544727 671717.2986595114, 644910.064326165 671714.9371862706, 644908.3544112046 671714.0560347761, 644903.3547647242 671711.4794748514, 644902.1929313503 671710.880304742, 644896.6352640227 671708.0166423206, 644893.2514642425 671706.2722633722, 644889.9817888332 671704.5878971897, 644884.1960503748 671701.605329253, 644883.338253149 671701.1631531894, 644878.0384753855 671698.4326396593, 644876.6457307944 671697.7144036926, 644874.4187948281 671696.566154305, 644872.3769782833 671695.5139256429, 644871.7520446833 671695.1919356553, 644870.0121343756 671694.2957407995, 644860.8188128917 671689.5577512542, 644856.5610646391 671687.3631944426, 644850.9224633772 671684.4576029868, 644851.3881211058 671679.5017263506, 644851.4862285142 671678.4627122708, 644852.2341239864 671670.5078703358, 644852.506586461 671667.6130624503, 644852.7001369572 671665.5499137698, 644841.7673505864 671669.2647213112, 644838.821279686 671670.2653168695, 644831.8163282922 671669.9515272361, 644824.9153242684 671670.1806225356, 644817.8103733761 671670.4157734727, 644814.4524373489 671670.5273721832, 644810.5474479216 671670.6560255327, 644807.1185121379 671670.7695357661, 644799.3344480047 671671.0276455405, 644792.338481738 671671.2597694937, 644785.2906215187 671670.5739798567, 644782.3106300097 671670.2835490729, 644771.2378553415 671669.2062456453, 644764.5780028385 671668.5584216814, 644741.4292691295 671667.528639981, 644718.3005848057 671670.8797605775, 644708.4891146785 671672.6955276281, 644706.4105758704 671675.7984376115, 644703.7973881135 671684.8380025018, 644697.9349401543 671705.1181463439, 644693.0408065066 671722.0472514216, 644691.4943092049 671727.3970739042, 644689.2414037539 671726.6928153506, 644677.9708417213 671723.1704018877, 644673.5770282772 671721.7969376387, 644669.8672688305 671720.6374718928, 644661.8996547477 671718.1475974956, 644655.1508780741 671716.0378629926, 644648.9700756315 671714.106113667, 644642.563503484 671712.1034584387, 644635.7128464036 671709.962672241, 644634.343850983 671709.5345151334, 644623.7122602906 671706.2121750322, 644617.7636174797 671704.3526123648, 644607.8570815707 671701.2564354893, 644607.7630107824 671701.2274646867, 644600.7442479309 671699.2044168727, 644587.8067911214 671695.7150845734, 644542.8765966254 671683.594862089, 644525.5224057261 671678.922939982, 644509.4569909071 671674.5980465702, 644501.9422965442 671672.5752596301, 644495.0585895831 671670.7213454964, 644487.5678567429 671668.7056013176, 644480.1461688875 671666.7066651308, 644472.526398808 671664.6558660752, 644453.2555997707 671704.3059868164, 644429.9093080774 671713.3315630301, 644421.1842324897 671705.7021490217, 644402.1310923632 671694.7297912632, 644389.6809543718 671687.6892056706, 644366.9328874449 671674.7553150133, 644354.8495991806 671668.0442086174, 644337.4548412177 671658.4294697699, 644316.9179052236 671646.7105457837, 644311.671041533 671643.8355606309, 644305.7994381303 671640.5078673263, 644295.3862978657 671634.7375869076, 644289.1460554335 671631.2008925985, 644284.5559080375 671628.5634223815, 644278.4597830313 671625.1209839385, 644260.5766294496 671614.8626759358, 644257.2668691103 671612.947108238, 644248.7979521051 671607.9767283372, 644241.0808608321 671603.484671374, 644238.4029519119 671601.9272539817, 644231.9011121106 671598.1041325063, 644227.987721195 671595.8436976993, 644222.9769282297 671592.9941803486, 644218.7780927917 671590.9477886192, 644214.0089492915 671588.9374884922, 644204.6243169836 671585.3957799538, 644201.6092828979 671584.2829563156, 644196.7227619907 671582.6825565815, 644191.523274394 671581.3755555527, 644187.7701489559 671580.6946438737, 644184.701607319 671580.2462898892, 644180.4306829674 671579.8517140389, 644176.5012451432 671579.6261359474, 644160.1141919845 671578.3554300787, 644154.1673116842 671578.0692960283, 644143.6114906191 671577.8643799082, 644136.1522952329 671577.7908654357, 644125.9404609392 671577.6317490577, 644117.1830592518 671577.4046443223, 644110.3200086324 671576.4904113676, 644104.6062742084 671575.4774346668, 644099.7117156592 671574.3074362657, 644090.2089707521 671571.428600466, 644082.4900518983 671568.837810074, 644077.3655695242 671566.7449173451, 644071.3726743502 671563.941693589, 644065.6226069164 671561.0626604069, 644059.6349471683 671557.5517684916, 644053.1294296505 671553.3691455208, 644036.3105675054 671541.7247801304, 644027.3869084159 671535.3036657431, 644021.6646633464 671531.3574512608, 644017.5676417954 671528.4738121418, 644014.3145528937 671526.1829034935, 644007.8653521257 671521.6015728753, 644000.581830073 671516.2148759856, 643996.6593328033 671512.9845423623, 643993.6409369042 671510.373665888, 643988.0986948254 671504.8425354608, 643981.8657268566 671497.0843162042, 643979.230682401 671493.4117400922, 643976.566917227 671489.4647645799, 643975.1071226532 671486.9711893412, 643973.1702245274 671482.94382031, 643971.769769346 671480.0084984307, 643970.9049405658 671477.593551886, 643969.467358853 671472.0342636416, 643969.1836977684 671469.320829634)))" + } +}, +{ + "model": "common.municipality", + "pk": 5, + "fields": { + "fid": "", + "name_fr": "Jette", + "name_nl": "Jette", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((644722.514665879 675412.4714911185, 644727.9817917418 675358.7086658573, 644731.6210184043 675288.0766325927, 644732.8015739715 675283.3777624567, 644732.9640034928 675270.8788310867, 644733.4558267198 675254.1410882102, 644733.3073614982 675240.9122062505, 644732.6608210967 675220.0740415382, 644733.0627442033 675202.5364540756, 644734.6037238309 675193.8776542933, 644740.735931628 675175.68153357, 644748.0568488068 675157.5145104942, 644753.0884704993 675144.7371043849, 644762.3205443122 675126.3811846597, 644771.2011667143 675109.5943336278, 644777.422794406 675096.3772295975, 644781.5329021391 675087.6897685208, 644786.5235451302 675080.8814357669, 644793.6535957081 675072.4543226389, 644804.1638427015 675060.2676287204, 644811.9240039429 675050.4015598232, 644822.5044794742 675037.2059225204, 644829.1645087996 675028.8088361078, 644836.6441921784 675022.2817510238, 644847.7332005687 675012.1960708838, 644854.4829794001 675005.9088999713, 644862.6226800944 674998.7318170097, 644874.8814612061 674991.2772938967, 644886.3504915487 674982.7718075521, 644897.4884119517 674974.9080383951, 644908.6562341757 674966.3333984965, 644919.4239283052 674959.6897379139, 644923.6449019731 674960.879178466, 644928.7444951291 674963.6277490081, 644939.3657400385 674969.4260565452, 644944.2953556122 674971.5946116244, 644949.8667075962 674970.0152698616, 644953.7172350169 674966.6247024887, 644954.938939619 674959.1159419358, 644956.9814017441 674946.9751522933, 644958.7331838274 674938.9054976784, 644962.776347951 674928.9148438771, 644970.310275918 674912.2129673429, 644973.832082465 674905.6724668206, 644975.7047383421 674899.172734567, 644977.1251713843 674895.3620490229, 644981.4001923507 674877.2905255973, 644983.8621607193 674869.1199950044, 644987.4653904792 674858.3793770954, 644989.8674056003 674849.8486825475, 644995.4007209252 674836.3884705078, 645002.1256638838 674820.737374478, 645006.1065366132 674813.7378921283, 645008.6793697248 674806.1971303205, 645012.9814733728 674796.4868267832, 645014.5530907077 674789.9861005322, 645016.8954062155 674780.4354079108, 645016.8671011545 674773.445539348, 645016.8687360343 674766.954550363, 645017.2823080653 674754.6337349989, 645019.1869558545 674740.0630309739, 645019.557747831 674733.9322422089, 645020.0603352862 674727.0112350032, 645020.4620488825 674720.5614123648, 645023.0327304759 674714.7507175155, 645026.9045188712 674708.6201769024, 645030.8862805659 674701.6107761288, 645032.1881301863 674694.8399411418, 645034.4510818181 674685.4793186466, 645034.1329735388 674678.028300181, 645033.0860287161 674669.2472871474, 645032.247948879 674660.9152549094, 645032.6022882538 674649.68434458, 645032.65235096 674648.074283085, 645034.3026997822 674640.7129336031, 645418.2305755018 674591.0680960478, 645427.9506625887 674589.1239685765, 645439.2446786049 674588.3507347894, 645477.2462527759 674583.4368179757, 645610.9714363541 674550.8506610973, 645716.5327655465 674527.8472180197, 645717.5553762205 674527.5265406372, 645800.8842139923 674501.3882174052, 645815.5719285514 674496.7811804737, 645877.5450878862 674475.4766411429, 645905.6812235144 674464.675081403, 645938.8725479996 674452.2739709623, 645987.6815449804 674433.1230712561, 646015.1967072124 674422.7975597751, 646043.2786771918 674413.5479465423, 646066.974119055 674407.2920101457, 646081.2494882215 674404.2316964791, 646098.4697417989 674401.2079005525, 646115.8968865671 674398.814043518, 646135.6630539154 674396.9515056242, 646154.3842186239 674395.6517150567, 646171.596174625 674395.3107714249, 646189.6859147362 674396.0670436014, 646195.994913354 674396.3417280396, 646212.1585678989 674397.462722372, 646212.8702451355 674392.1339020097, 646280.4483764792 674400.3463576884, 646281.9769778419 674400.5321417178, 646427.347353261 674417.4868490165, 646489.7041042744 674424.6603209907, 646543.9471224934 674430.9008105319, 646545.144270296 674420.8581763728, 646546.3895671999 674410.4094721143, 646546.5077199525 674409.4054965035, 646547.7048667137 674399.3716622437, 646548.3995515596 674393.6228441063, 646545.5477752245 674381.9654909665, 646543.071943294 674371.8363286584, 646534.6030314049 674337.2066818504, 646532.2427112343 674323.047503776, 646526.2526343127 674276.2541488404, 646523.1612630343 674276.6499268442, 646517.1691656166 674234.9785974362, 646526.0533977478 674228.5349515097, 646610.6035998391 674167.210732849, 646646.3062268317 674152.7422879299, 646633.1528022155 674122.719078904, 646623.6965504062 674099.8951254855, 646619.3865941089 674082.5847023899, 646615.060290687 674059.3664999318, 646613.045214878 674042.104356706, 646612.3170802441 674034.8193849607, 646610.1061164294 674001.8145046425, 646606.6342686253 673948.537685207, 646603.8559344612 673901.1068085865, 646601.3880247152 673865.9699196446, 646675.6073751324 673807.9515931456, 646710.738602815 673780.2621020566, 646720.3193718158 673772.233444994, 646723.2047887157 673769.350790763, 646726.0891530063 673766.3650976, 646731.4469296878 673759.2629543045, 646734.5675384107 673754.8582665089, 646737.143838442 673751.2557429213, 646745.1806748629 673736.2247389638, 646746.6568186523 673732.7625594691, 646754.660202688 673713.9891949494, 646754.4563353733 673713.3312577521, 646759.72479135 673700.6450084571, 646801.9366788953 673600.7343612262, 646804.9855600792 673593.5228169728, 646788.3670230831 673589.1698011799, 646713.8387541865 673569.6499012801, 646704.5122450738 673566.6828787755, 646695.1197088787 673563.6947284937, 646626.1128516599 673540.1936301908, 646609.5752270508 673528.6976688787, 646610.0048969928 673527.6822131763, 646613.3023476849 673519.8873466321, 646694.4480105162 673327.9674826724, 646711.8088001296 673286.9590365689, 646730.9681651709 673241.702858383, 646822.5285359055 673026.3745932579, 646829.0964389703 673010.9115721909, 646835.9693718423 672994.7284364966, 646852.6739419876 672955.2000478592, 646861.3045617298 672958.9210544075, 646866.430112537 672961.1315721854, 646875.652660758 672965.1086478127, 646879.3304275027 672966.6921167924, 646879.4823588489 672966.7570945182, 646889.2899344789 672970.8123204573, 646894.630614742 672973.01790452, 646895.1806567782 672973.2459687864, 646895.6101795929 672967.5460871251, 646901.3019758485 672969.5608361113, 646908.6696466609 672972.1676221029, 646909.1741540335 672967.278780127, 646909.5336545393 672963.7847843478, 646909.760940706 672961.5808375372, 646910.4086792355 672955.3030690737, 646910.4196972717 672955.194111675, 646910.7720884467 672951.7780341227, 646913.8259901507 672952.995511464, 646920.8615365776 672955.7993747313, 646921.0825847553 672955.2843675809, 646921.8288222497 672953.5444785017, 646923.2362518996 672950.258607747, 646925.3547643642 672945.314922673, 646927.4343938434 672940.4601918288, 646928.6367552521 672937.6474106327, 646929.5159785525 672935.5825814753, 646929.6558877932 672935.2496423486, 646931.5844316913 672930.6769698411, 646931.9356247353 672929.8450221873, 646932.9968388616 672927.3280603898, 646933.5479533481 672926.017262589, 646934.3211817303 672924.185377835, 646935.0145128451 672922.5424411714, 646938.0472415641 672924.1807110397, 646950.4683486948 672930.8891840555, 646963.4984154727 672937.9266874183, 646975.6522814768 672946.9880609148, 646989.1733112911 672954.020664246, 646995.5259589164 672957.3253987808, 647004.0443142813 672961.7558237711, 647009.8559786612 672964.7800157878, 647027.4727200478 672973.9430130108, 647033.2433849231 672976.9448003713, 647047.0753739637 672984.1393600769, 647061.1227348134 672989.3120493584, 647075.1720482471 672994.4845789542, 647085.6132273899 673000.5878589719, 647088.4549646351 673002.2491455815, 647091.2697240718 673003.8934691154, 647096.3923412047 673006.8870179116, 647103.2438548473 673010.8918054468, 647105.8462447259 673007.2972833613, 647106.2824322421 673006.6943033551, 647110.0549123576 673001.4828221833, 647111.044950833 673001.9078977489, 647114.1456989276 673003.2381796971, 647116.4307035514 673004.2186064263, 647124.0672050836 673007.4974178292, 647130.2837915928 673010.1341438784, 647133.0636343256 673011.3575080369, 647135.9581090093 673006.9388708909, 647137.372503081 673004.7790677485, 647152.775141606 672981.2661712323, 647153.0451669673 672980.8551687944, 647154.1904232501 672979.1054081729, 647177.2509636952 672990.5516022966, 647179.7476935207 672991.7914118341, 647186.1553813145 672994.9710349403, 647186.4093670188 672995.0992243738, 647197.883632963 673000.8514344409, 647198.5545387716 673001.1886322043, 647204.6070683829 673004.224213812, 647210.4047247721 673007.1319258937, 647216.0944683838 673009.9845854933, 647216.7533055908 673010.3107419198, 647222.5199136335 673013.1022915875, 647225.9427832622 673014.7598090833, 647228.5305685336 673016.0130696023, 647239.7318805918 673021.4374104519, 647244.5315161647 673023.8779665423, 647245.349944572 673022.5624011466, 647246.1639346383 673021.2540351247, 647289.950188284 672949.5542597239, 647311.2494482738 672913.3203029754, 647364.0591344318 672814.1728115723, 647365.9465567303 672810.5850817339, 647370.5364781898 672801.9177380437, 647377.5340087153 672788.7057785189, 647392.5573767179 672760.3388061901, 647397.0844488624 672751.7916132665, 647401.6504020322 672743.1711458577, 647410.7943810112 672725.9065326572, 647422.2779716151 672704.2220936177, 647428.3952620623 672692.672087634, 647431.099894001 672687.5663940323, 647432.5896253902 672684.7533268651, 647435.2267925314 672679.7740236114, 647438.6432780939 672684.3383869838, 647445.4180388523 672693.3899875721, 647487.8132700287 672750.0338582699, 647567.0254850837 672855.8680526074, 647575.6954554984 672867.4518562735, 647678.6663812349 673005.0289141334, 647685.2088371234 673013.7555322784, 647690.1095119822 673020.292215264, 647701.5515778336 673035.5563164292, 648013.957869191 673445.871153402, 648000.6109340286 673461.3202329305, 647989.5645271732 673474.1065826287, 647989.00820814 673477.3834384978, 647971.1356543906 673582.6951316779, 647968.9661576792 673595.4267258479, 647962.9399168646 673630.7836784937, 647945.2270576366 673697.965730118, 647941.6763652939 673711.3261081333, 647938.1398789176 673724.6356083995, 647918.6271295609 673798.4234548279, 647912.7195232043 673820.4425089495, 647906.8120947039 673842.4574030389, 647886.8736180122 673915.5723018021, 647860.3434028777 673995.9011994932, 647857.4887545932 674004.4374033855, 647857.3213145921 674004.9378578067, 647866.0391053109 674007.1137826135, 647939.7904865689 674025.5226220933, 647939.772116032 674026.2784521856, 647939.6358544407 674031.872618814, 647939.4760508275 674038.4285328161, 647939.4057816655 674041.2974551411, 647939.4047158501 674041.3044949396, 647939.1373688845 674052.2462713273, 647939.1002717107 674053.7673713699, 647939.0369539288 674056.388297059, 647938.8942741761 674062.2242204489, 647938.6542118929 674072.0740112389, 647938.0412239637 674097.1468020855, 647937.4754960139 674120.3093780645, 647936.9390166476 674142.2820496701, 647936.8173835631 674147.2450244726, 647936.2084969587 674172.1557374189, 647936.3395714115 674179.0796832824, 647936.470823252 674186.00458916, 647934.9619349318 674202.4792773899, 647934.1289361734 674211.5741231852, 647933.8525739856 674214.5850988169, 647933.7363413892 674215.8559516995, 647932.9012415872 674285.5901824434, 647932.7290874552 674296.0740550605, 647932.7149659016 674296.9241247145, 647930.9675772522 674403.693742514, 647928.9786038968 674525.2593762437, 647928.9344491267 674527.9843047708, 647928.8266671759 674534.5242290132, 647928.5648001437 674550.489164231, 647928.5326180868 674552.5141009196, 647928.3342025857 674564.6478805526, 647927.9756287121 674586.5407472085, 647927.7641651615 674599.4645976789, 647925.05667363 674604.9568532961, 647924.0345733708 674607.0303081856, 647921.5617902311 674612.0467571141, 647920.8334641547 674614.0056891029, 647918.0387657469 674621.5231946373, 647917.3814612227 674623.2910971555, 647916.1630864049 674626.5678764696, 647913.438704959 674635.3946580747, 647910.719117377 674644.2091203758, 647909.0756752072 674648.6578363572, 647905.8715315876 674657.3272806295, 647905.7951831617 674657.5347892842, 647905.2064131694 674659.1292618548, 647903.3948273291 674664.0310728762, 647902.6496319335 674666.0498422063, 647902.4065600383 674666.7088461017, 647898.322279847 674677.7611996447, 647897.5300326918 674679.9062019964, 647894.3070679845 674688.6286034045, 647888.6624407014 674703.9058451701, 647883.6868377964 674717.3721462513, 647870.5227876211 674752.9981911442, 647869.3484435476 674756.1780167036, 647866.0350592425 674765.5304808681, 647862.679416454 674775.0019786982, 647859.9704398991 674782.6046137076, 647856.751521185 674791.6420524316, 647853.9976226158 674799.3726806805, 647852.2479824619 674804.2853790689, 647851.2437240242 674807.103308923, 647850.185310687 674810.0751506601, 647847.3935923752 674817.9136132146, 647846.757230542 674819.6995980657, 647846.5732824712 674820.213490585, 647843.6933185847 674828.3000999255, 647839.3359085129 674840.5335283298, 647838.1835681512 674843.7663160306, 647837.6853450403 674845.1652815227, 647837.4884350478 674845.7182120745, 647836.9422718021 674847.2510904047, 647831.4834798146 674862.5752340686, 647822.3395915524 674887.4978737136, 647827.8121399545 674891.5114663367, 647842.1349391339 674902.0152021007, 647853.5979351712 674910.422447292, 647856.7362963827 674932.0986243859, 647858.1831882739 674942.0916660496, 647855.3341015904 674942.5074726245, 647850.2910885422 674943.2438070406, 647836.5139056507 674945.2550579812, 647825.828809866 674946.8148524859, 647813.4906366855 674948.6162019086, 647812.4187012546 674948.7731889283, 647810.6325424972 674957.9228020157, 647808.5183503649 674968.7494787553, 647808.2991872288 674969.8753610635, 647797.2888337375 675026.2705889128, 647795.3087321477 675036.4102486083, 647792.5500160558 675050.5427344145, 647791.322458005 675056.5023665635, 647790.0817571693 675062.5303164398, 647787.7132548508 675076.046855995, 647787.6271111922 675076.5398006879, 647782.9570664128 675103.2000915323, 647782.5048572689 675105.7819312367, 647781.1279815771 675113.6437667435, 647777.5145714695 675134.271125118, 647777.1502828022 675136.3440204951, 647776.8811946259 675137.8858926556, 647776.2916887347 675141.2509079734, 647772.6663784011 675161.9451441825, 647771.6207559337 675167.9129584974, 647770.9092281137 675171.9749121289, 647770.8161577373 675172.5048156158, 647769.5733819721 675179.5985149036, 647734.7330754435 675175.4593991609, 647721.8698981331 675173.8192764088, 647718.8593786092 675173.435390663, 647710.380543869 675172.354361321, 647643.8596898998 675164.6813301663, 647607.5812839674 675160.4969285114, 647603.0962933346 675159.9792643568, 647591.0622187229 675144.3760215465, 647574.5085785012 675124.841223435, 647565.3177540995 675114.832194864, 647554.8060714658 675104.0329322492, 647544.6741236275 675094.2228266144, 647536.9081123116 675087.0720249658, 647527.6659518691 675078.9488920199, 647520.2218213152 675072.6939611629, 647481.9354642555 675050.6866070163, 647443.1882269032 675027.946083912, 647435.5608587092 675040.7379862969, 647432.7081095789 675039.0366935554, 647427.5503558187 675035.7911336143, 647419.4349174025 675031.1361052754, 647416.6412535767 675029.6258579291, 647413.2064962351 675027.8233749382, 647405.4790010091 675023.7784682027, 647389.9219243587 675015.8996802242, 647368.7792470541 675005.4202338067, 647356.5888518572 675000.160712703, 647355.2919296286 674999.600559404, 647339.58971047 674992.8187033951, 647332.9520841483 674990.112957278, 647320.570675526 674985.0665309184, 647292.9520118912 674975.5131635331, 647289.6520722568 674974.4228502585, 647287.9822035618 674974.0426495846, 647285.9122298497 674973.8523180985, 647285.338196195 674973.8632877368, 647283.4031339914 674975.1110387811, 647281.9820523538 674975.3118229074, 647281.3744552516 674975.4627870773, 647280.2890166545 674975.7325716121, 647278.6328796078 674976.33748304, 647277.1299059809 674977.1022516582, 647275.3387863033 674978.4309795294, 647273.7194492612 674980.0778053302, 647272.3162865808 674981.8955359226, 647271.3922219106 674983.4725274934, 647270.5899019227 674985.2213722914, 647269.7946748906 674987.0311773662, 647268.5564690167 674989.8700382131, 647267.0043268302 674990.6458407032, 647266.3202276098 674992.3398204991, 647265.149819409 674995.2396490499, 647263.0613317181 675000.149265684, 647208.3315661569 675129.9203432113, 647176.8651791676 675204.2997659323, 647158.9364665482 675246.3821202999, 647155.6316527564 675253.711724131, 647132.3412681808 675309.260170032, 647108.6049582353 675365.2087167727, 647098.2282023035 675389.6553670316, 647096.6134059541 675393.4858518251, 647094.985293529 675397.347854673, 647096.526853664 675400.6120116061, 647097.5387284642 675402.5740363812, 647098.9253709109 675405.498177664, 647100.0609396829 675409.2022805738, 647100.2448559572 675410.5093303323, 647100.4296598384 675411.8154202113, 647100.5674258717 675413.126304279, 647100.6163159829 675413.9923818082, 647100.6741584367 675415.728371938, 647100.6460641901 675416.1573243039, 647100.6361069607 675416.3074016194, 647100.6181475531 675416.5853167054, 647100.6129508174 675417.0133118868, 647100.6180544508 675417.3942687892, 647100.6409081178 675417.7742678788, 647100.728906764 675418.1302751808, 647100.8799260336 675418.413330913, 647101.0788721575 675418.6784726924, 647101.301783587 675418.9183376554, 647101.6148630967 675419.1714935312, 647108.0283136064 675423.3791180287, 647126.5207969513 675435.0163096146, 647251.7687002128 675518.2978829175, 647268.0993341022 675529.156738374, 647249.824688179 675535.6254765034, 647258.0918739486 675551.2242580277, 647270.3222905889 675574.300572699, 647270.2370884496 675574.3223220474, 647254.5078869939 675578.3286757529, 647252.9348425806 675578.7294391026, 647244.8054941206 675580.7985028591, 647241.1904384801 675581.7199709255, 647226.737138888 675585.4012040724, 647225.2240909192 675585.787094915, 647222.2650917924 675586.5406449973, 647216.5718142199 675587.9900132559, 647209.5387330067 675589.7811340075, 647208.2186323607 675590.1179289651, 647199.0183993474 675592.4607787814, 647191.4173038399 675594.3967884979, 647190.714208732 675594.5757406005, 647184.6630820672 675596.1169040883, 647179.2708519022 675597.4903099497, 647173.3966971582 675598.9863755511, 647167.5236074742 675600.4818012874, 647161.6744807684 675601.9721100058, 647153.3463269913 675604.0929887872, 647147.9370565042 675605.4694325037, 647142.1000000666 675606.9567027135, 647136.0458557262 675608.4988257799, 647130.3417502198 675609.9512326128, 647124.5045162558 675611.4385027845, 647119.1994406761 675612.7898394921, 647116.8833626125 675613.3794699174, 647113.8063229398 675614.163085194, 647110.9272007991 675614.895685221, 647108.4941473751 675615.514421025, 647098.5349048894 675618.0522157858, 647092.7468396593 675619.5265320849, 647080.7864120302 675622.5730366163, 647075.2163221848 675623.9923401487, 647065.110106108 675626.5659565026, 647060.6769324088 675627.6945221925, 647055.0708091549 675629.1227812057, 647049.9377353147 675630.4301393705, 647044.258480273 675631.8764692346, 647038.7123535826 675633.2887357456, 647035.6314087522 675634.0733104988, 647032.8951775919 675634.7699283591, 647026.9450506021 675636.2863043292, 647021.0039765436 675637.7985214436, 647015.2638373736 675639.2608436682, 647009.4136454564 675640.7509921266, 647005.6745139379 675641.7046045093, 647003.5865291345 675642.235383401, 646997.619361881 675643.7546371665, 646996.4623878251 675644.0493724758, 646996.7922711826 675644.8114854527, 647000.961151915 675654.4448621637, 647001.4480802016 675655.5709908297, 647001.8709483184 675656.5489530906, 647001.880885863 675656.5699141055, 647003.6793622515 675660.7260938805, 647008.1531059265 675671.0615811306, 647009.6137135696 675674.4377271382, 647018.2045223033 675694.2865859588, 647021.5175519676 675701.94291717, 647021.7665172197 675702.5179820647, 647024.1108362462 675707.9351764475, 647027.293971278 675715.2894147182, 647030.4762188193 675722.6438128898, 647032.2486098587 675726.7399901059, 647032.8815812822 675728.2030933924, 647036.7874785999 675737.2274839745, 647044.1884715763 675754.3291840957, 647046.2848902168 675759.1733937571, 647048.6442930042 675764.6237097424, 647060.8918570812 675792.9238147727, 647062.7494253345 675797.216000588, 647065.1916980492 675802.8602448922, 647067.6410690176 675808.5195299248, 647072.4418574244 675819.6120102152, 647082.7649544451 675843.465043074, 647085.7672661546 675850.4023034936, 647092.0964492357 675865.0290168233, 647098.2739112852 675879.3016350552, 647100.1763751602 675883.6968654647, 647103.6104278774 675891.6320891608, 647100.4026190581 675893.5385571892, 647097.8840870694 675895.0353537343, 647091.7666115372 675899.3404815315, 647086.7780536793 675903.4518296446, 647082.5057144507 675907.4843863487, 647075.3577744388 675915.5783107359, 647069.6058501482 675923.811604936, 647066.435252544 675929.3542019617, 647061.891506371 675935.0195093739, 647057.3021421492 675939.1489861775, 647056.9616717929 675939.3812621546, 647053.2518228575 675941.9134224238, 647048.4865724898 675944.364814036, 647044.5173424981 675945.8292729168, 647044.060440017 675945.9972152394, 647038.4042796649 675947.2635491567, 647031.4032272052 675948.3565998201, 647019.8779986225 675950.096289283, 646992.2646680366 675954.2458219563, 646952.0660640392 675960.3027522804, 646911.9926162396 675966.1908994896, 646911.7045301963 675966.231823754, 646900.0124633821 675967.8753335719, 646888.3202189396 675969.5198033515, 646888.2802809018 675969.5258783916, 646870.3269857827 675972.1996501148, 646851.4289497476 675974.1113934262, 646834.0908651513 675975.261095548, 646822.621899887 675975.4937665494, 646808.0530019308 675974.926065207, 646775.5314949341 675972.6179390664, 646753.2498631737 675969.2752391036, 646735.102398243 675965.6631288988, 646723.6968194513 675962.6497597145, 646705.2074972952 675957.4435433741, 646694.7598964768 675954.3062929541, 646669.0968799479 675946.5521820389, 646655.239451776 675942.4505230185, 646630.0723456065 675934.8905511728, 646611.2820369017 675929.232302669, 646579.7922666122 675919.8094940524, 646565.7548658338 675915.6078141574, 646527.5363504095 675904.0002022726, 646489.4037235902 675892.5786793211, 646474.532465509 675888.0829803039, 646474.3413056043 675888.0239174503, 646457.2040053256 675882.7397884212, 646438.3577785903 675877.1396130302, 646433.5819652884 675875.7560011495, 646419.0374560182 675871.4993799607, 646404.4689750795 675867.3235550646, 646385.004842967 675861.9892214244, 646367.788227676 675858.0111499268, 646351.9886763516 675854.8151644364, 646340.0630635227 675853.0108401207, 646328.0481120137 675851.69641998, 646316.0162478739 675850.8168734419, 646302.7594384503 675850.136218445, 646282.5165356729 675850.1287984522, 646262.1554525676 675851.362311723, 646251.7034534239 675852.5090300906, 646227.3810486468 675855.8900405582, 646205.3777560642 675860.0774048301, 646188.1783559232 675863.8112774283, 646187.5613530148 675863.9451211126, 646172.3800377136 675866.8502872268, 646159.6509275142 675869.2986359054, 646138.5997130272 675886.016551023, 646135.2407636904 675888.6841162395, 646130.5320110974 675886.061970141, 646104.3436793296 675871.4789329224, 646083.1053959745 675858.8764775087, 646062.0970633359 675845.5041376991, 646040.9587852838 675832.2716206855, 646020.1500738554 675819.4692194284, 645999.001790358 675806.8067754942, 645976.9433833373 675793.3442289148, 645954.4048127683 675781.6616062352, 645932.5163413081 675769.1893105098, 645906.5980122762 675754.4464961741, 645888.7691487853 675746.3744589624, 645865.7707022681 675734.5218673908, 645844.0921188483 675722.849423737, 645806.8246524988 675702.0756046667, 645738.3395304944 675661.1380829363, 645714.3215063076 675646.3855539518, 645684.3036720019 675628.3922069179, 645671.2541900916 675624.5905883688, 645648.6145570218 675622.7182535229, 645594.5844997157 675624.2720525868, 645568.0447624716 675623.1890236421, 645544.2354929714 675617.5162688465, 645519.7566020178 675609.5836171496, 645495.2276942017 675601.3208818476, 645469.4987150924 675593.0479282712, 645449.4296132154 675586.6656855574, 645426.7702149024 675582.2560880723, 645400.350460016 675581.2301895894, 645374.0706929587 675579.1771159671, 645348.6814983898 675575.074081897, 645325.8120274941 675569.9116652862, 645300.8930697879 675562.3688735642, 645260.3832143856 675561.6343159832, 645242.9529348875 675564.9922894773, 645232.8021132221 675572.6510614554, 645227.5810896837 675580.8803139995, 645201.9356399573 675568.6024370211, 644818.1746472394 675443.7035391186, 644722.514665879 675412.4714911185)))" + } +}, +{ + "model": "common.municipality", + "pk": 6, + "fields": { + "fid": "", + "name_fr": "Forest", + "name_nl": "Vorst", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((645296.5995676904 665956.6576601798, 645298.7616684567 665929.9804726618, 645301.512665253 665912.5815366136, 645305.7261661466 665895.8752453024, 645309.6075353643 665882.4092157928, 645314.9910580582 665869.0732789921, 645324.7809484277 665848.5638147561, 645340.1792594638 665820.5992032933, 645346.7460016531 665808.623714368, 645362.8313738141 665776.5008064619, 645365.0421103868 665770.6110173475, 645378.0182809596 665758.9647460722, 645391.2335675734 665747.1042634682, 645391.6355933367 665745.6871984205, 645395.5686624568 665731.8245359652, 645398.1264269681 665722.8100054951, 645400.7775950863 665713.4655677576, 645408.5405362283 665687.8166979962, 645426.9146271498 665643.3688451446, 645428.3852276627 665640.1800739401, 645431.8368808369 665629.6261354273, 645451.0721367458 665570.8131861711, 645455.174652376 665548.5837120912, 645457.0470731225 665529.3640427706, 645454.1293882765 665509.7739033678, 645449.7362059458 665506.1255786933, 645448.9400270815 665505.4642108587, 645449.3900555139 665503.3941951111, 645471.1878551553 665429.7473867174, 645502.1350283056 665367.6513205323, 645516.2154334601 665354.3930207778, 645532.5954244612 665353.714911354, 645633.1670897866 665333.0018152439, 645748.4068764589 665309.2680218567, 645779.3072120565 665303.4205730443, 645855.7372663008 665287.5894621908, 645859.1144386503 665286.8600950975, 645878.3573630588 665282.703293789, 645908.7673572329 665276.1345113887, 645911.7376078432 665273.2350295744, 645908.6488561853 665262.2145759104, 645914.0709262455 665247.1653644429, 645917.9410006107 665245.0458217387, 645930.3929255519 665241.6186339994, 645934.2113975288 665240.5677193325, 645941.1499299105 665239.5754034007, 645948.9514754692 665238.4595092471, 645959.480835384 665241.6906354189, 645967.851850707 665234.2217303732, 645978.2820128051 665232.8029509512, 645983.4520832086 665230.7334835866, 645985.1148746058 665229.3756044785, 645985.9345380012 665251.8357345663, 645986.4677718553 665266.4345951807, 645986.7439414553 665274.0035451011, 645986.9622599736 665279.9766571847, 645997.3392704863 665286.6397466873, 646014.5665678009 665300.0077554772, 646023.5719514876 665305.9857294057, 646035.459682708 665313.8769879098, 646083.6378089165 665343.1315482407, 646110.2392214881 665354.4995389645, 646133.8977529838 665365.4923096747, 646138.5584266535 665367.6717196816, 646143.2870784189 665369.8832974198, 646190.2050371008 665392.6006289737, 646208.218819538 665401.6097145699, 646218.9052998049 665406.0389331843, 646219.3003094654 665405.4089025976, 646221.3486781465 665402.146118828, 646222.2646163532 665401.7643074682, 646232.4163901252 665403.9024365158, 646235.8770890555 665405.3049113352, 646237.8910675602 665406.1201811103, 646238.4469659813 665406.3461645278, 646242.1096494631 665409.3056142834, 646246.3851763328 665412.759052614, 646248.827006679 665414.3863676976, 646250.2769129847 665415.0495727031, 646255.6205694779 665417.4930608467, 646256.046561801 665417.5281502058, 646271.1932492867 665418.792945134, 646274.2918751764 665421.4782508244, 646279.2073651304 665425.7397592108, 646283.3711049489 665427.4002344375, 646290.1228277315 665430.092045055, 646301.6385531186 665439.2083749415, 646304.3962647552 665441.3917243135, 646304.3282466754 665441.6936347316, 646303.9380718328 665443.4526196226, 646309.6619195222 665451.92620022, 646328.2087929255 665469.3332262883, 646335.99402804 665475.5171397245, 646342.53221198 665479.7677984176, 646349.5988662239 665483.5417213164, 646351.8707028317 665484.7549394423, 646359.4273210054 665487.9617225527, 646367.8241272045 665488.9558156667, 646381.030194725 665487.5063225687, 646414.7703816743 665482.9302794706, 646423.7782515759 665484.4583608923, 646444.5363415661 665490.5047470748, 646447.8991123944 665491.4841736364, 646454.3866840419 665494.4618734958, 646462.2490266149 665500.034759474, 646467.5059355925 665500.5892891013, 646486.3505730985 665502.3345157885, 646499.5335124267 665510.2310492861, 646501.6603255102 665511.5042502393, 646508.4688603027 665515.5831016016, 646519.3549552504 665522.1032149574, 646521.0437330795 665523.1154062888, 646557.7579881407 665535.6746572048, 646630.0574759291 665560.4069501938, 646641.8468037834 665564.439342555, 646661.2158327134 665571.6095614977, 646663.3036560236 665572.3818006893, 646663.8936351965 665572.5997882801, 646669.8214699194 665574.7945473939, 646670.0523853936 665574.8795338502, 646675.5570821917 665576.918244494, 646683.7976209994 665579.9681485858, 646684.6694579927 665581.7522406018, 646689.8170754997 665592.2907038867, 646693.6311177948 665600.100226433, 646693.686129083 665600.2111122502, 646698.9838733954 665611.0587114145, 646701.3942460427 665615.9930458373, 646704.665453302 665622.6903511317, 646706.8857715458 665627.2355455058, 646709.4201851961 665632.424773039, 646712.8603299141 665639.4662561984, 646714.8357602136 665643.5093447418, 646719.2678786521 665652.5840526301, 646722.7488377828 665659.7113001212, 646725.6401161118 665665.6104854752, 646745.0391681109 665705.196534435, 646752.2163452156 665719.8433725836, 646768.269301044 665752.6029884877, 646796.4981710925 665810.2070913082, 646808.4561699725 665834.6093150812, 646811.4434514705 665840.7056308081, 646814.4044696903 665846.7475437709, 646817.6538526337 665853.3782872215, 646868.008434196 665928.9376761273, 646896.7167751581 666008.3537013206, 646903.8106519444 666049.8831528166, 646910.0472903735 666084.8555976003, 646910.6830160812 666088.4196429596, 646910.9317009741 666089.8156606881, 646911.5223248384 666093.5225796263, 646912.5445713752 666099.1508134039, 646915.4489201044 666112.9190410114, 646915.3457014299 666115.1569308654, 646922.2845772717 666115.7897356339, 646969.7152954473 666122.4982575345, 647017.1461913704 666129.2056598032, 647021.6600809059 666129.8442615533, 647023.7319865457 666130.1374610395, 647032.5817376871 666131.3885639802, 647042.2096338393 666132.712557177, 647059.911096954 666135.1456439019, 647076.4927401014 666137.4245211893, 647082.4716361816 666137.1433013496, 647115.0495556741 666135.6080763956, 647117.7345919734 666135.4813508259, 647122.1736792083 666135.2719506631, 647122.53538828 666137.0209389211, 647124.5452781047 666146.7330960003, 647136.0034260192 666202.0859919683, 647141.2243506525 666227.3094404048, 647145.1611100069 666246.3276684759, 647146.651849048 666248.3969460712, 647159.091802514 666239.839427044, 647160.3068809431 666239.0035756014, 647168.6726750862 666230.7305384312, 647217.2804762544 666213.1073945459, 647294.2813275533 666201.2014398059, 647320.0195562447 666205.2404132073, 647328.8327727119 666209.1940511174, 647329.2349627818 666209.3745766588, 647326.7754151664 666214.8571262524, 647329.7618447202 666219.1505611386, 647330.6656885918 666220.4504964371, 647334.4200216774 666225.8480123127, 647338.2783510367 666231.3943391828, 647339.8629617685 666233.6725862878, 647340.42695481 666234.4825657706, 647341.7606259806 666236.3998264153, 647343.7993770412 666239.3309214225, 647346.7288394578 666243.5422703763, 647349.3983114663 666247.3795119239, 647352.736659764 666252.1798640176, 647355.1262649918 666255.6141960747, 647356.3649908156 666257.3952867313, 647362.6739743394 666266.4639522393, 647365.157459828 666270.0352542156, 647369.2266209616 666275.8856034899, 647372.870036873 666281.1219878856, 647373.7918049602 666282.4481651271, 647383.1072720509 666295.8390680269, 647385.789699204 666299.6953510751, 647388.6932513282 666303.8697374694, 647393.5842663706 666310.9012936148, 647397.7065032658 666316.7217294908, 647399.371334763 666319.0718255361, 647401.2809122478 666321.6261085763, 647407.8258299667 666330.3757645552, 647408.960573415 666331.8928452262, 647414.1077369095 666338.7754726066, 647416.9802670331 666342.615778109, 647417.8020767663 666344.1637019927, 647419.4038161349 666347.1800254913, 647421.4312780413 666350.9991124477, 647422.1270923859 666352.3092634772, 647427.2508480103 666361.960666534, 647428.5035268439 666364.3188744306, 647431.6209382048 666370.1902327046, 647433.8203574029 666374.3323772709, 647434.0643626206 666374.7924021799, 647434.999035945 666376.5524976169, 647436.3006929645 666379.0057505091, 647437.2154910302 666380.7277639253, 647439.2150559541 666384.7940457539, 647440.3138585786 666387.0271166776, 647441.4744153143 666389.388193817, 647444.9802519368 666390.3485959851, 647451.0430492434 666392.0073714969, 647458.476800323 666394.0431042816, 647463.4875980379 666395.4147191222, 647466.9073507432 666396.3511114406, 647481.1168874248 666400.2417016127, 647494.5362265161 666403.9152411851, 647516.238441983 666409.8577311308, 647519.1283789774 666410.6481427168, 647536.2046010677 666415.324022016, 647545.381153617 666417.8361149596, 647553.7408844952 666420.1250741959, 647586.9795964336 666429.2248883679, 647658.8355359635 666449.2931318976, 647636.3610281817 666555.4916040521, 647634.7549958962 666563.0793575359, 647634.045663759 666566.4283686633, 647665.2821523502 666577.3418573822, 647680.0250894506 666576.6687067995, 647681.278081519 666576.42373523, 647681.6641499706 666576.34890096, 647688.095255537 666575.0696629564, 647702.340391293 666572.2363107825, 647727.1577720441 666567.3011841016, 647730.9659176595 666566.5437182266, 647732.6241387655 666572.2048298678, 647734.1075956635 666577.2680055429, 647738.1529031734 666591.0763755336, 647738.2228090094 666591.3143019335, 647745.9285875547 666617.6180468197, 647749.3452753276 666629.2833193897, 647751.709118723 666637.3536156585, 647759.3889933943 666663.5682384244, 647762.5996901521 666674.5285322606, 647765.3474834152 666683.9059037708, 647765.7092544577 666685.141776876, 647766.8587959149 666689.0670020861, 647768.3944871036 666692.2410785459, 647770.1489796926 666695.8673373032, 647771.462555587 666699.578503484, 647771.5785999997 666699.9084346162, 647773.0770666796 666704.1415385663, 647774.1227105994 666707.0966790103, 647774.6415403518 666708.5616888574, 647774.7274139486 666708.8588166898, 647776.7055319465 666715.6279576594, 647776.961556982 666716.5049410759, 647778.5258876726 666720.6380535694, 647780.5943319564 666726.1012952231, 647780.7671357574 666726.7653104749, 647782.6672713102 666734.038438417, 647784.2095564221 666739.9456147073, 647783.1933104674 666741.9755594241, 647781.1440044281 666745.6341699571, 647776.4703982741 666750.6316610947, 647756.9849001302 666764.4012601285, 647745.0020171334 666772.8697035224, 647744.7238438937 666773.1437486463, 647743.3867660763 666774.4615010112, 647733.7447872132 666783.967332084, 647729.128211616 666788.5187532295, 647720.5632443145 666796.9626392602, 647720.2841840588 666797.2326843071, 647711.480289779 666805.7915812513, 647707.8558772354 666809.3140090762, 647701.2171857933 666815.7672621864, 647697.9327226381 666818.9598130719, 647692.980287519 666823.7742361724, 647687.5135789145 666829.0876351914, 647681.5940182072 666834.8428977552, 647678.2057061938 666838.1363956427, 647676.3733088296 666840.271204632, 647671.6318336574 666845.7995668163, 647670.3075083751 666847.3433998399, 647668.7213359731 666849.1921598513, 647664.2786960391 666855.3226323302, 647661.6493278758 666858.950296439, 647654.5941847659 666868.6863551056, 647647.8630304382 666877.9746151109, 647644.7605499269 666882.255178708, 647628.1907937914 666906.5620102071, 647626.7595451543 666908.6808661852, 647625.5974060768 666910.6195950714, 647623.2699324238 666914.5013724302, 647622.4147430309 666915.9862207726, 647617.4638046373 666924.5886142913, 647614.9291263586 666929.0002832428, 647614.3761023644 666929.9632510096, 647606.4130604081 666947.0221860933, 647606.0058854301 666948.4411673977, 647609.7415449457 666951.1964635814, 647614.8150702982 666954.9391088259, 647618.5537469371 666957.6974453628, 647619.648698367 666958.0095711981, 647676.7964027382 666973.6462242807, 647679.0163440155 666973.9284021467, 647680.8771808154 666974.4166163559, 647682.7531046325 666974.907872309, 647685.6928194218 666978.0041927691, 647712.2991547972 667006.0301347991, 647712.8310507725 667006.5901931837, 647714.5577988385 667009.196296642, 647718.9760020467 667015.8668460572, 647723.2511675039 667022.3202603005, 647725.4286800274 667025.6084916635, 647730.0798968719 667032.6289859025, 647730.5297741881 667033.3080737069, 647740.5841007796 667038.4181796311, 647746.2106402526 667041.2778609227, 647751.6742460008 667044.0544836562, 647752.8282713412 667044.6416551875, 647773.0018853318 667054.8938746505, 647774.2947061819 667055.5509820165, 647778.9356800561 667056.2345639032, 647783.6735663812 667056.9330371018, 647784.3354469987 667057.0302343895, 647784.3624263057 667057.0342375381, 647784.403413942 667057.1552014408, 647785.5879793924 667060.6093945623, 647791.6928569492 667078.4069376375, 647797.0918920181 667094.150493329, 647801.2243861463 667106.1998480986, 647801.8771748906 667108.1018304684, 647802.4396557988 667112.632902015, 647803.7002737931 667121.3027442652, 647804.9475905198 667129.8801056745, 647805.8619165955 667135.5312102253, 647807.2099052548 667143.8572254406, 647807.9443817732 667148.3932773268, 647808.9055329884 667154.331425212, 647810.6423097722 667165.0605078989, 647811.7535142567 667171.9185064469, 647812.013179175 667173.5246047776, 647832.3382345027 667189.4008003629, 647835.3583055832 667188.4882035245, 647844.0455805456 667185.8642881436, 647846.5306513499 667185.1135469676, 647858.5287499453 667182.9719383512, 647869.1029241452 667181.0842398513, 647886.1090759165 667178.0493086484, 647892.8081948045 667176.8529878383, 647901.1202780089 667175.3701394299, 647914.6046043767 667172.9626280452, 647918.1546527412 667172.3291315435, 647930.6577456082 667170.0976630744, 647937.6799989039 667174.0861408617, 647941.2849271602 667176.1337504209, 647948.8043201498 667180.4046847183, 647963.4054363831 667178.2584230449, 647969.300624366 667177.3922448853, 647975.59856392 667176.4659546752, 647982.8596278628 667175.3988191199, 647990.8658441902 667174.2216922742, 647997.6948688201 667173.2185052732, 648000.6609229676 667172.7828583816, 648004.7088503618 667172.1873402906, 648011.7869099743 667171.1472229279, 648014.2680293829 667170.7825583036, 648016.9800785764 667170.3838411716, 648018.5660566387 667170.1510699783, 648023.059158344 667169.4906448754, 648024.4037125686 667172.4267811717, 648024.8746702563 667173.4557889089, 648030.0521886307 667184.7643137567, 648031.2459776541 667186.7964391378, 648035.7800135664 667186.7019345937, 648039.443977242 667186.3913290716, 648044.6140319811 667185.953062145, 648048.52146159 667181.7274750033, 648051.7615479444 667180.0278700083, 648053.0637598021 667179.3449887594, 648058.4119526136 667176.5387607638, 648065.4332608241 667173.6116187681, 648075.6265576085 667169.9218408875, 648086.7510522836 667165.8942583587, 648088.9981039136 667165.0814165975, 648096.3454429151 667162.4213664015, 648103.6505353347 667159.7773109884, 648104.7295958997 667159.3864046129, 648113.9789143234 667156.038589986, 648114.7720122549 667155.7516481996, 648115.5612399587 667153.6978393772, 648123.8623423118 667151.9847775466, 648134.6655096228 667149.8921815932, 648143.4506525018 667148.16221854, 648145.3046686627 667147.7963658274, 648147.2698383991 667147.1017290823, 648153.5879896794 667144.8674711687, 648160.7042054757 667142.3742718863, 648162.3642416601 667141.7925573384, 648168.5474857572 667139.6272426021, 648168.9234435062 667139.4952891349, 648179.0069083953 667135.47366088, 648179.0630004687 667135.4525478355, 648181.7422235132 667133.474007385, 648185.550504915 667130.0994543319, 648187.7022453774 667123.3287266314, 648187.724497557 667121.3108247413, 648187.8135349083 667113.0008190123, 648187.979394719 667097.5879969503, 648188.0047102629 667095.1880183702, 648189.605779941 667094.9141343809, 648197.7948429795 667093.5141769218, 648201.6169207942 667092.0207312724, 648204.2800563097 667090.9799417921, 648205.0190433444 667090.4750751632, 648207.4942858339 667088.7854678973, 648211.8035935358 667085.8439323665, 648213.0367718 667085.045047849, 648220.1441889218 667080.4379440135, 648220.9042967218 667079.9460398493, 648223.2094886504 667078.31433157, 648227.2887597729 667075.3599683195, 648227.9096843577 667075.0709257466, 648231.4539954499 667073.4233676906, 648235.844071054 667071.9090310655, 648240.6942419669 667070.2355513982, 648242.6742934401 667069.7748347772, 648247.2183831348 667068.7153933325, 648249.8894557373 667068.0936416528, 648253.7815750706 667067.186120074, 648257.0075236814 667066.4345166096, 648257.4525249879 667066.3305713123, 648260.3886103303 667065.6469322157, 648263.0205313986 667065.8733292851, 648266.7534644613 667066.194738992, 648272.6403199685 667066.7015281778, 648278.512265735 667067.2071955635, 648281.2311602592 667067.4404831119, 648283.607130984 667067.6457293363, 648284.8462378928 667067.7319587667, 648296.8720179844 667068.5694090101, 648302.8288188515 667068.9851673547, 648308.9258424834 667069.4099026169, 648312.8361300406 667073.8873017525, 648314.3899159459 667075.6674762352, 648318.8319320034 667076.1859302855, 648333.6800410134 667073.2157517133, 648343.0357872267 667074.8939919954, 648347.6996382596 667075.7305504545, 648353.8604733713 667076.8882477479, 648357.5003536302 667077.5726833422, 648368.7581216815 667079.6879505869, 648397.3791953747 667084.3934608449, 648404.803963095 667085.8383489498, 648424.2986487475 667087.1366999602, 648436.4526553565 667087.3701700615, 648442.523446213 667087.4869043565, 648448.1524441848 667087.5955852148, 648452.7434332594 667087.6831805242, 648458.433313113 667087.7928287432, 648464.7973308348 667087.9155185025, 648470.5182729131 667088.0253304831, 648476.7741943948 667088.1449671742, 648485.701873653 667089.3281191378, 648486.6289338497 667089.4501504565, 648487.5618512487 667089.5742624705, 648517.3652133368 667093.0469646615, 648527.5400315269 667093.9451498364, 648532.1150078156 667094.348740723, 648532.2362950674 667099.7995927865, 648532.2373580228 667099.8165527825, 648532.2740814725 667101.4515844446, 648532.3646533202 667105.4915638231, 648532.3775423086 667106.0614809281, 648532.4949488093 667111.310574097, 648532.5187818495 667112.3875285638, 648532.6370727512 667117.6623816537, 648532.6400747288 667117.7915010077, 648532.7783162487 667123.9554695698, 648532.7862615685 667124.3063477882, 648532.913732445 667129.9973587405, 648532.9195588055 667130.2563974208, 648533.0369655568 667135.5034106215, 648533.0648628199 667136.7323643935, 648533.1782066739 667141.8163383777, 648533.2080452634 667143.1382516581, 648533.3065614528 667147.5372691986, 648525.1528480182 667145.7513359309, 648523.618145308 667152.086140623, 648522.2313748639 667157.7878481812, 648520.8087320436 667163.6367502538, 648519.4081098685 667169.3954157047, 648518.0243586729 667175.0822437668, 648515.2000282956 667186.6928511804, 648514.5376319103 667189.4157897318, 648513.5541598651 667193.4575591171, 648511.9374152275 667200.1053115046, 648510.2295689336 667207.1271299514, 648508.559726295 667213.9908742423, 648507.1660234721 667219.7749803992, 648506.1106329686 667224.0794590564, 648505.3111427664 667227.3433368163, 648504.5938720856 667230.2931469778, 648504.0286155136 667232.6151404416, 648503.1971568457 667236.0350131175, 648501.4463347665 667243.2326650491, 648499.7875022868 667250.0524110713, 648498.1498028057 667256.7832003563, 648496.4788945367 667263.6519045576, 648494.8431483728 667270.3756541546, 648493.1303294254 667277.4193919407, 648492.3638677382 667280.5682346306, 648491.5625990313 667283.8612319872, 648491.5505233688 667283.9092301524, 648491.5395125824 667283.9581884377, 648490.0467231924 667290.0949199422, 648488.2138565202 667297.62759942, 648487.8416355469 667299.1606624253, 648487.6887336893 667299.7905590126, 648487.6995613307 667299.788640339, 648492.2466404908 667298.9642369826, 648516.1070465412 667294.6380379901, 648522.2720602088 667293.5195927365, 648528.0311257798 667292.4754977867, 648540.1122945749 667290.2849768352, 648546.4464663549 667289.1497522081, 648552.6095280977 667288.0265067902, 648564.1686635755 667285.9199220599, 648570.1797833121 667284.8236980641, 648575.8337680701 667283.7933503374, 648581.2239842331 667282.8109703212, 648586.7249618638 667281.8087638682, 648592.6830083337 667280.7224533954, 648597.2520982289 667279.8900528515, 648598.5460909114 667279.6542112943, 648605.15716881 667278.448780792, 648606.6481891724 667278.177123362, 648621.5544878944 667275.4599086056, 648642.9519467466 667269.4639852355, 648667.2076938658 667261.7445814876, 648671.9609316663 667260.229168687, 648674.5340489263 667259.4091665599, 648683.1682630548 667256.6585532088, 648690.8425142352 667254.3315003542, 648696.8315214401 667252.6112787668, 648702.7276934291 667250.9200057237, 648706.562308172 667263.2864521293, 648708.058574344 667268.1125150822, 648710.6751630647 667288.1347535319, 648712.0876381914 667293.0587255433, 648717.3713346224 667311.4803392913, 648724.5062763521 667336.3579658673, 648724.711294848 667336.2939911793, 648726.521313834 667335.7412545442, 648727.9734828186 667335.2963137556, 648732.2625377517 667333.9829230766, 648734.2216611552 667333.3831648554, 648738.4606595754 667332.0857679986, 648739.6897047308 667331.708799687, 648743.5008307237 667338.6501658343, 648747.7509922659 667346.3906187454, 648745.1458975459 667347.441335251, 648745.4403701591 667359.256317318, 648745.5797470739 667364.8362899923, 648745.9558281136 667379.951255787, 648748.6226016349 667387.0542421223, 648751.4980466953 667394.713729271, 648751.9036349743 667406.5157648304, 648752.0083470079 667409.5757532546, 648752.4309154149 667421.8757069163, 648752.9380304914 667436.6566111429, 648758.6333293158 667434.5073969706, 648764.2554955077 667432.3850537534, 648765.0684756897 667432.0781145111, 648767.0226457242 667431.3404367028, 648768.1082617898 667434.0083868438, 648768.5410529702 667435.073550738, 648770.712462452 667440.410731013, 648773.1766578052 667446.4709809484, 648778.3052083558 667459.0795010997, 648781.6824653856 667465.0638619972, 648791.8800748141 667483.1310316139, 648796.2131361071 667490.8074945431, 648801.0301330509 667499.3419291333, 648793.6298514027 667502.4010100997, 648800.6402774886 667514.7116798356, 648801.5680444906 667516.3408990494, 648809.1793540508 667529.7076329249, 648810.7012360798 667537.0275986809, 648812.3276044971 667544.8499730618, 648812.4585701197 667545.085987014, 648821.2845645716 667560.893808087, 648821.8744432091 667561.9489109876, 648833.2759267873 667582.3211668194, 648835.582184596 667588.0813996634, 648836.9307697414 667591.448575821, 648838.162418136 667594.5256601513, 648839.3330254918 667597.4488183298, 648841.9622337718 667604.0170837236, 648842.5380441104 667605.4550618452, 648847.1557059224 667616.9865679353, 648841.8035095334 667619.1529438931, 648844.600584927 667626.1092264394, 648849.6139560522 667638.5786927952, 648851.8653861694 667644.1779601783, 648853.7837506991 667648.950153901, 648858.4094035225 667660.4525410075, 648860.9965606652 667666.8878422246, 648866.2729027373 667680.0123542068, 648868.6902425858 667686.0245866093, 648871.7401757574 667693.608879821, 648890.4835652866 667740.2266812904, 648896.8985747818 667756.1813776968, 648901.8996583702 667754.1940014779, 648908.5510248629 667751.5517910944, 648914.2592965154 667749.2833831013, 648923.653234224 667772.8713232453, 648924.6650257327 667775.4104602017, 648930.4451187837 667789.9249339531, 648931.5388897462 667792.6711187352, 648931.6189174354 667792.8720864023, 648924.7538845234 667792.2133438786, 648916.7431116105 667791.4443840788, 648910.690302771 667790.8636588624, 648903.1382273014 667791.2286631883, 648895.7613438569 667791.5858487489, 648889.177413135 667791.3890555755, 648885.4123270315 667791.2766019921, 648884.6334710224 667791.2125085659, 648876.9394480526 667791.150459991, 648871.2464676355 667791.7228867832, 648861.6014337975 667792.6927523564, 648853.3664812912 667793.3906309248, 648830.9880669406 667797.2139704807, 648813.4880566093 667799.2838365986, 648787.305935232 667802.3806441069, 648780.9387934741 667803.133787745, 648773.3737532116 667803.5599099612, 648766.9787916222 667803.9186541343, 648766.2158975529 667803.9620815656, 648765.4189235859 667804.0069448696, 648760.2827902023 667804.2964016937, 648754.1478656728 667804.6415773415, 648747.5649316255 667805.0127786264, 648744.8218300483 667805.3345241535, 648732.1028132302 667806.8259333123, 648725.9687500842 667807.3880669341, 648677.2045632217 667811.8571735427, 648675.6965285342 667811.9141108546, 648667.6476421272 667812.2160158089, 648660.2396433377 667813.01415329, 648653.7205687988 667813.717359062, 648650.0084931588 667814.1169068269, 648647.2006069955 667814.4186447521, 648630.0133730889 667816.2885506828, 648554.6943079112 667822.4582740723, 648538.5502989495 667823.7803114874, 648518.9553870455 667832.2738620475, 648508.3159225379 667836.8866121173, 648489.4940571918 667845.0442595232, 648476.9075092319 667850.5006063579, 648474.544525026 667851.5243110248, 648464.6159793576 667855.8281901269, 648449.7102915358 667862.2892072436, 648347.035853228 667906.7954180256, 648340.0477097409 667899.6306439659, 648314.703155967 667873.6428763175, 648311.7174662271 667870.5814255294, 648302.7813545224 667864.2394481093, 648300.3075101546 667863.1541999449, 648294.1866120922 667854.6225035962, 648289.0191534671 667858.5679214543, 648284.8928460793 667861.4993948557, 648280.3313904345 667865.2548077474, 648275.3509947882 667869.3701665876, 648269.9374856363 667873.6264317891, 648264.2528849583 667878.3076201641, 648259.0254600326 667882.0179931507, 648255.9012180005 667884.2356745064, 648254.921164551 667884.9315494538, 648253.8491590162 667885.6923726713, 648249.9788266453 667888.4398788373, 648247.8236321759 667889.9696838523, 648242.7032480139 667893.6039105442, 648237.5479458676 667896.8293369848, 648237.0649357479 667897.1323157451, 648228.9542752497 667903.3631163791, 648227.9280640796 667904.1260251206, 648224.4988395622 667906.6747065136, 648220.6524560738 667909.5330945626, 648206.4193458428 667920.1112742156, 648181.2360771645 667940.3051594226, 648173.5233705961 667946.5160085523, 648167.7960005837 667951.1293527544, 648164.5025285108 667953.9588479459, 648153.0426619152 667963.0114574961, 648146.5916784813 667968.6963841094, 648140.8583979843 667973.7489234621, 648122.2839576011 667987.8465459693, 648114.4871378518 667993.8794669406, 648110.1967744626 667997.1998772119, 648099.8469319111 668005.2086311368, 648088.1870181727 668014.3090565357, 648076.5279917057 668023.4106021123, 648078.9821043952 668030.6298688931, 648077.2896337473 668033.7545946442, 648075.3592618243 668037.3204074461, 648074.2199996975 668039.4252097548, 648073.7379840097 668040.313143063, 648072.5976565222 668042.4209852152, 648072.0724988031 668043.3908325289, 648068.5448165148 668049.9065042788, 648065.1380376478 668056.1989927752, 648061.9434163198 668062.099513576, 648058.3816453348 668068.6771090728, 648051.8668458773 668079.5331940979, 648037.1934565494 668103.9841831189, 647994.8854280638 668174.4855381119, 647984.5783173218 668191.6601526783, 647976.3698208843 668205.4950476867, 647954.3373943074 668242.6229106633, 647952.0991118185 668245.7526860954, 647948.7365400199 668250.452228551, 647936.741633011 668267.2185961813, 647934.1252144525 668270.8752000849, 647926.7831583027 668281.136440737, 647926.7700211324 668281.1551589449, 647924.3176390581 668284.5828251146, 647911.7971575356 668296.9602255195, 647901.990068261 668306.6549410904, 647899.8498878033 668308.7706607721, 647876.0023795736 668330.9726742224, 647860.7185895704 668346.3485882328, 647854.2238308137 668352.8827756863, 647838.4999676707 668368.7007114111, 647830.854282616 668375.4387596726, 647818.5780495787 668386.2581271239, 647816.8438796082 668387.7868621376, 647807.009934306 668396.4535862473, 647802.1451593301 668399.9470451617, 647797.27914175 668403.4413038925, 647789.9022012835 668411.5454461371, 647785.6427305728 668416.223761548, 647781.8151717202 668420.4272541882, 647781.6352102535 668420.2441951995, 647781.5432772093 668420.1512254899, 647778.1957105875 668416.7409523261, 647774.2480950559 668412.7194158811, 647771.0238213738 668415.730035252, 647770.4488380103 668416.267080237, 647765.7112167082 668420.6904621539, 647764.0991686732 668422.1952118382, 647762.0179384929 668424.137980305, 647759.8415583627 668426.168576365, 647756.9943342762 668428.8293248393, 647753.9759824364 668431.6468510088, 647752.3818636917 668433.1356029962, 647749.9425780135 668435.4133646972, 647748.0204047117 668437.2070338863, 647746.3322011003 668438.7828135933, 647743.7019052473 668441.2384703525, 647742.9488705671 668441.9414120839, 647741.4857608739 668443.3061811631, 647739.3065399871 668445.3428567455, 647735.8811376259 668448.5395297289, 647734.7860240488 668449.5633867858, 647732.4317698075 668451.7619595219, 647730.5184723604 668453.5477898135, 647728.852280985 668455.1035723449, 647726.1151187534 668457.6592150886, 647721.9546108679 668461.5436320341, 647718.9422946062 668464.3552388441, 647717.5681214067 668465.6401794445, 647715.8058925491 668467.2839494282, 647713.1906856033 668469.7264880307, 647709.9752876587 668472.7281487207, 647709.5792431787 668473.0980171872, 647708.4761412825 668474.1279536517, 647704.4067000389 668477.9274241149, 647702.7545326728 668479.4681691404, 647702.6176655282 668479.5971113148, 647699.3722665303 668482.6277289665, 647698.870243422 668483.0956636427, 647697.6199784745 668484.2625409386, 647696.8669435683 668484.9664429248, 647688.7990685961 668492.49835289, 647683.4580425771 668497.6286958292, 647679.0686980085 668501.844922933, 647678.725020469 668502.1749580768, 647666.2659061378 668520.7612464332, 647666.2788181602 668521.135323142, 647666.0107937711 668521.146331323, 647661.7489777462 668527.5916645895, 647658.337536642 668532.7502348949, 647655.7890750928 668536.6038039966, 647653.5956760817 668539.9206219692, 647649.592878275 668545.9730307516, 647646.6163886603 668550.4747008234, 647645.5962938702 668552.0174403263, 647643.6217909028 668555.0032483768, 647641.5814239105 668558.0879273508, 647639.076102251 668561.8775022445, 647637.3148144135 668564.5402996319, 647635.0154291984 668568.0171028791, 647629.6103473967 668575.895242039, 647627.1799601066 668579.4379488899, 647624.339478228 668583.5785194996, 647615.5749938177 668596.3542778668, 647608.544801119 668606.6014738497, 647601.244496423 668617.9724631757, 647595.7161006989 668621.0676091611, 647591.6558962045 668623.344219392, 647587.3626198856 668625.7526403236, 647579.3512025013 668630.2445943722, 647571.0106799916 668634.9216270116, 647538.3658957845 668651.8884242981, 647527.956359161 668657.2972022125, 647525.225191484 668658.7168605532, 647502.8938791184 668670.583023333, 647498.6506664649 668672.9154507512, 647489.5660304802 668677.9093504335, 647483.4387206995 668681.2765812852, 647480.2994462372 668683.0022671957, 647476.6872836258 668684.9877337285, 647473.6291315466 668686.6683098032, 647468.3867118456 668689.549731637, 647466.0386019873 668690.8404768269, 647462.6003997533 668692.7299651487, 647456.4769949737 668696.0962362874, 647448.2266586436 668700.6302403186, 647447.1825414621 668701.204029207, 647430.7274664271 668710.4320403164, 647427.9273909878 668712.1656060861, 647419.3226705268 668717.4945974499, 647416.8105267773 668718.9912403814, 647412.691370595 668721.4467204503, 647400.3674519305 668728.7910847979, 647384.5220885682 668733.6022653806, 647367.9489902015 668742.7161828749, 647353.8361884173 668750.4774493421, 647343.1266533425 668756.0890671227, 647326.7385581313 668766.2538721099, 647288.5478925733 668789.9431153933, 647270.2919061829 668799.234909814, 647262.4223680947 668803.2398444786, 647088.5499970786 668891.7394978866, 647022.6523484987 668925.2808327264, 647020.8902076466 668926.1916514614, 647019.0251145046 668927.1272576512, 646995.8470387304 668904.7059329667, 646989.5531764665 668895.4133127283, 646978.3569209889 668882.2011246565, 646971.1523847909 668870.8585046174, 646955.7809537639 668817.2174116547, 646944.9803101707 668757.6049429819, 646938.5530003327 668740.7676111294, 646931.774532524 668723.010730442, 646924.9038874946 668713.0020535091, 646921.234459781 668708.5987991927, 646917.3987999461 668704.962394776, 646907.5257171965 668698.2561977247, 646903.7408794537 668697.0647260511, 646899.7870694965 668695.8193151243, 646891.98816726 668695.5183962563, 646890.2462029839 668695.4511510216, 646882.2735265426 668693.716311193, 646872.933300348 668695.7161384778, 646859.7899528542 668699.4505318914, 646854.6860015047 668699.2499305857, 646852.4641070993 668698.965671516, 646847.6472218242 668698.3491098871, 646840.2088068094 668693.778372213, 646831.4695537658 668687.3383863016, 646828.93527527 668682.5682318382, 646828.101618964 668679.1990594104, 646825.0013164789 668665.5219202304, 646821.0995765103 668655.0136028226, 646819.297962845 668652.1115095209, 646811.6934182091 668641.6357137578, 646802.1877011004 668631.026735106, 646796.9843745829 668626.256105233, 646781.3740391273 668613.410435682, 646775.0367829906 668607.7398442875, 646764.931417191 668594.6948289868, 646759.3615028096 668585.9882103852, 646754.1246858856 668576.3797236952, 646738.4156541446 668553.394183761, 646730.1778787771 668543.6864640965, 646719.8712761431 668533.1772311585, 646699.0801139422 668510.0941334711, 646696.8755230654 668507.647026847, 646690.7533556715 668500.8492776023, 646677.5134783486 668476.6630885667, 646673.6236967515 668467.0787619773, 646672.4250526163 668464.1266613335, 646666.6417509494 668449.875135949, 646665.1077214807 668441.8360263219, 646665.3429733124 668431.094283153, 646666.2445157872 668419.2195143728, 646674.8843684567 668419.8275660946, 646672.2284732447 668410.938415779, 646667.9053499344 668396.0800314897, 646667.9154659967 668394.6341327413, 646667.4491292544 668389.1970331204, 646666.1491244562 668380.4901204633, 646659.0830541138 668366.122445843, 646655.983225897 668355.8492626232, 646646.2059904075 668341.8923026648, 646643.0037457013 668335.9880068416, 646634.9341194512 668317.0722776705, 646638.790579322 668297.121009986, 646632.1543726441 668274.3275841909, 646630.4691003383 668269.3184951078, 646629.6356820294 668263.9994308089, 646628.2353474152 668258.5612613249, 646623.1668017021 668247.4857777733, 646620.6040348511 668244.9066315008, 646616.9250162566 668237.6491787649, 646612.0389516641 668229.3858377907, 646579.2054742723 668202.5801813334, 646573.6020228256 668198.143567814, 646562.1277179079 668192.9382705048, 646543.8478435033 668185.5641941758, 646533.8406662907 668179.5251588405, 646523.302011634 668160.6761170166, 646522.827451734 668157.1530585171, 646522.3358672084 668153.504079348, 646517.3330886149 668143.0626407887, 646517.059376267 668132.8787167487, 646495.8862054329 668118.7413325263, 646493.3865883477 668115.0100377516, 646461.2157984519 668066.9867599243, 646459.8713145194 668054.6956926696, 646453.6436153742 668044.2761904368, 646450.0702261372 668038.2974334473, 646434.9275103426 668012.9612010017, 646420.0449382651 667994.0617308879, 646332.3957981498 667934.9739681409, 646314.8069787051 667925.7220779083, 646297.7222240014 667916.7361642291, 646295.2796434358 667915.4514105868, 646263.9966152537 667898.9963863789, 646257.4070553904 667895.4316493997, 646174.3782819539 667850.5242862138, 646145.3982234645 667843.4849532889, 646120.696716412 667841.2771113114, 646093.6149641275 667824.3350703431, 646083.8011921084 667814.5010634075, 646082.2039190435 667809.8030068834, 646079.5457598702 667801.9856596916, 646077.9144229698 667797.1565606185, 646073.9869375512 667785.2242291975, 646070.5381674066 667774.7439382502, 646050.712271579 667758.7158623353, 646032.2406432155 667756.2516373513, 645945.4592410768 667741.7726320997, 645928.3955699918 667739.1165713241, 645921.5797149288 667729.6246733101, 645913.1863424326 667717.9359738966, 645912.3309871772 667712.0010587795, 645916.506200602 667686.3458340962, 645918.0152635054 667676.3421223521, 645920.5522642261 667660.0675360457, 645921.60286586 667655.7526570533, 645924.7266137139 667632.613224867, 645924.9429293228 667630.1273518193, 645925.8220448801 667620.1155418381, 645923.3446374042 667614.7402567877, 645919.552884882 667613.5199811675, 645911.7625641181 667596.6192108779, 645888.4872489346 667546.1237913643, 645887.4445409548 667543.8616084142, 645878.0280503295 667523.4326789541, 645870.308136416 667515.8569573971, 645859.0624558405 667504.8195717585, 645853.327272298 667499.1911068053, 645843.6780994653 667493.3199440661, 645825.3860494287 667486.9898498069, 645803.780067776 667479.4824167716, 645789.1811003102 667472.7426806708, 645784.3766408245 667469.2311071116, 645779.6479665263 667465.7747420067, 645778.0980092617 667464.6414499045, 645759.8463306829 667447.3634583652, 645759.3703330478 667446.9224463518, 645740.5746388813 667429.5034329568, 645729.0154650109 667423.1390919122, 645724.516900469 667420.5595465344, 645721.8049433097 667420.2133111367, 645720.0245180528 667407.9391312028, 645720.1389314007 667407.0748323236, 645721.7439148226 667394.9615284714, 645725.2336666583 667389.4559071034, 645736.8785410533 667372.5524641383, 645733.9281737864 667359.2192761751, 645732.2861441956 667351.7961093619, 645727.3877443993 667331.0037192267, 645726.4458094907 667321.7726507233, 645732.3343333091 667317.257381615, 645754.1070669522 667300.5591235494, 645761.3897925752 667294.9750671051, 645761.5948222326 667294.8181325385, 645765.1209779963 667292.1134499973, 645801.5105340144 667268.040969398, 645805.2489498502 667265.5684694562, 645806.5959175646 667264.6615953526, 645859.673863202 667228.9410900963, 645877.6718934617 667218.294332151, 645873.7876219165 667196.8371037212, 645869.5925737068 667188.4256454703, 645860.8212009252 667176.095765378, 645883.011565997 667130.155724085, 645898.1008480396 667118.7065501059, 645913.558390759 667104.7185608087, 645914.076240268 667096.6476512913, 645892.266758953 667028.1047338773, 645855.8324545863 666975.8831685783, 645846.1281638371 666961.9738358478, 645831.9852157988 666946.2123210143, 645757.589112293 666903.0849696414, 645756.1266275019 666902.2374463353, 645752.6364055551 666899.878499598, 645747.4504274206 666892.5798786328, 645742.9240249582 666870.6496245946, 645687.8996433531 666878.3690512679, 645687.8657405151 666878.3743272604, 645629.9591694706 666887.3122853274, 645609.3902580073 666890.5653739952, 645602.7313446831 666871.9511648375, 645579.0754166256 666805.8240434405, 645560.7021264775 666759.390322568, 645559.9092112605 666759.636307872, 645553.3141029708 666727.1087951777, 645550.2840420281 666703.4716973981, 645551.5917657786 666699.7640435783, 645554.3173000388 666692.0362715776, 645557.9080778247 666677.4678626093, 645558.1881890653 666668.4948569302, 645555.1033235238 666645.9763026467, 645552.7556689185 666628.8401853356, 645550.6229531069 666613.2715988613, 645542.2634359653 666543.1512651398, 645538.6710059234 666530.2498448383, 645528.0855335076 666527.3006404667, 645484.0452040965 666528.3779079766, 645477.7147040414 666528.5328502045, 645472.061794664 666528.0321972796, 645466.8943239042 666524.7546664008, 645460.1446375148 666514.0858196662, 645447.1087194144 666473.5947574778, 645436.1647145958 666424.9779348279, 645396.2762297925 666323.3932847962, 645392.6111592348 666318.2058676897, 645388.0346110986 666304.3955451131, 645377.9524342179 666240.4928907779, 645372.5368332717 666163.6579393524, 645372.2338562713 666157.6755600374, 645370.1484349167 666116.4781027319, 645368.2106647266 666101.9532138668, 645340.6106690841 666055.432054148, 645340.133872507 666055.7289561951, 645327.8798466324 666034.26341957, 645314.0961085723 666010.1177318692, 645298.6373775842 665976.3803356368, 645296.8487199472 665969.0056137451, 645296.5995676904 665956.6576601798)))" + } +}, +{ + "model": "common.municipality", + "pk": 7, + "fields": { + "fid": "", + "name_fr": "Woluwe-Saint-Pierre", + "name_nl": "Sint-Pieters-Woluwe", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((652506.0379887512 668512.5401784922, 652509.2572975988 668510.8881098712, 652529.1741969776 668500.6682543505, 652546.6491006536 668491.7013660122, 652556.4815186195 668486.6557413369, 652563.1098935631 668483.2545074502, 652567.1830221101 668481.1639873359, 652573.7924034845 668477.7728308385, 652580.0087793099 668474.5827440878, 652583.6839318859 668472.6971330117, 652589.7503123238 668469.5839869693, 652598.509686358 668465.0889443363, 652602.314055777 668461.6964912306, 652616.3664888544 668449.1623630684, 652627.3164557944 668439.3889411282, 652631.0679252766 668436.0414405271, 652637.761563772 668430.067291446, 652643.533178246 668424.9160594783, 652649.0555586378 668419.9867141552, 652650.4778257887 668418.7179433351, 652654.5381749759 668415.094483385, 652660.0076550986 668410.2132907491, 652666.0332965337 668404.8340919439, 652671.3027147653 668400.1317524329, 652676.5783461138 668395.4234936312, 652683.319913893 668389.4063095055, 652686.1062227255 668386.9186797244, 652705.4080140931 668369.6922833374, 652715.858091832 668360.3647106277, 652721.4456204951 668355.3784121783, 652723.7987874221 668353.277764453, 652726.0390533267 668351.2790217006, 652728.1541697396 668349.3913023354, 652730.2284572638 668347.5395775402, 652732.1046356715 668345.8647864439, 652745.8821875323 668331.2356079973, 652767.4397571055 668308.3455587549, 652778.6620768559 668296.4301130528, 652788.5902713544 668285.8884121856, 652797.5183145988 668276.4096159589, 652800.2595877426 668273.4979855048, 652804.4030959462 668269.0983839842, 652808.5455391016 668264.6991022422, 652820.7459967248 668251.7457058727, 652835.2425936719 668237.1226867113, 652854.377593847 668217.8202831997, 652870.3823901265 668201.6744162859, 652876.9610825595 668195.0372126824, 652883.5397750954 668188.3992088581, 652895.6601237515 668175.8928263171, 652926.7687110375 668143.7939340093, 652971.3446685612 668097.7977879378, 652971.3736042011 668097.7688317748, 652971.3876283391 668097.7537936487, 652976.3331583748 668092.6243716724, 652981.2786883976 668087.4951095603, 653004.1553373048 668063.7711109266, 653005.9025999064 668061.0044760668, 653016.9356342694 668043.5389833689, 653020.3912801226 668038.0674678067, 653023.4947038217 668033.1549427649, 653026.7293231505 668028.0344360295, 653029.9739057106 668022.7128131134, 653030.7743994513 668021.3994858738, 653031.6311709527 668019.9940065538, 653027.789672565 668016.0176044673, 653030.5367806477 668014.8230666014, 653035.4351037554 668012.6926366199, 653045.5065319571 668008.3108507227, 653047.266520288 668007.5440298677, 653067.6223857895 667998.688643805, 653088.5313651753 667989.5922056243, 653092.8155119201 667987.7287385961, 653100.2887609836 667984.4767082315, 653106.8510406929 667981.622484424, 653113.357405651 667978.7912935828, 653122.711702963 667974.7225367314, 653126.8059166374 667972.9409657713, 653135.5221610286 667979.1219172066, 653137.5719590528 667980.281184894, 653142.4258792363 667981.4587048466, 653145.5017008826 667981.9171434962, 653152.9956887914 667981.9749913334, 653154.9927066852 667982.1462658676, 653156.8177300951 667982.3024802101, 653160.4523311564 667984.1758464886, 653161.2903523792 667985.2768913491, 653161.8143845822 667986.3247796764, 653162.1490696821 667986.9929705603, 653163.7689907115 667995.5970493053, 653164.8362898815 668001.7281351383, 653165.6439654086 668004.3041970221, 653167.242702295 668007.3142670548, 653169.402296648 668010.6555992975, 653171.9010721323 668012.627869834, 653185.4177809596 668023.2962137025, 653192.2130516568 668028.6599899117, 653215.6636307916 668047.1695720531, 653221.1212170704 668051.4700040966, 653224.3797954441 668054.0373976503, 653228.3493686512 668057.1648684088, 653229.6932659834 668058.2238942548, 653242.8052310155 668049.3826862808, 653247.2364868851 668046.3093323205, 653251.391879339 668043.4988221313, 653251.8418738437 668043.1939195113, 653256.8463091463 668039.8084774957, 653259.1774046153 668038.2318146378, 653261.9256547939 668036.3721649416, 653271.993282212 668029.5604879666, 653272.2173031666 668029.4085165175, 653275.1986273134 668027.3919363801, 653280.5149599384 668023.7294145388, 653293.2829272854 668014.9342428055, 653294.515923674 668014.1532791387, 653305.5587497812 668007.1617190047, 653307.7098368105 668005.7989516568, 653314.3713187869 668001.5818758681, 653323.2598628044 667995.9540420943, 653332.2403579953 667990.267179722, 653332.4565672853 667990.1300871096, 653338.6128510527 667986.2319868102, 653342.1490618597 667983.9933946104, 653351.9496782734 667977.6395973158, 653362.484484239 667970.8110126426, 653364.7995960525 667969.3101860404, 653367.7148759784 667967.420635473, 653375.2064158568 667962.5646246838, 653384.3008892838 667956.6697767325, 653389.8224007776 667953.0904760397, 653396.2398153567 667948.9312886704, 653399.0379373706 667947.117562953, 653402.1041025562 667945.129951144, 653405.7074192069 667942.7934473082, 653411.0507085894 667939.3301236435, 653420.4004442781 667933.2703468624, 653425.7827864024 667929.7809478784, 653426.4147292305 667929.3711078317, 653433.7433130861 667924.6209949101, 653434.8593312307 667923.8971360798, 653439.4787266392 667920.9026803322, 653442.0569112165 667919.2319663776, 653445.935193874 667916.7183767987, 653449.4923572853 667914.4129065697, 653450.648493139 667913.6631327474, 653461.3285999913 667912.0383371896, 653467.4242678743 667913.5011206607, 653473.6211073546 667914.9867958737, 653475.9709470765 667915.5510670844, 653478.1869566097 667916.0823628637, 653484.4737853616 667917.5901285427, 653490.6095669222 667919.0617168341, 653496.6603290221 667920.5124953663, 653502.8140373535 667921.9892058122, 653504.5130075878 667922.3964419751, 653510.6297973592 667923.8630682345, 653519.1095613976 667925.8970074039, 653520.4014929672 667926.5990717929, 653523.3232930535 667928.1805184373, 653534.9385889549 667933.1256735809, 653534.9744407589 667933.1509575188, 653571.2843399893 667958.6847087807, 653584.2476727747 667971.8521660496, 653592.3786401752 667980.1119103682, 653631.2808841023 668019.62821062, 653660.4935716431 668053.7131926902, 653671.7092891599 668065.0303906642, 653686.4399407186 668075.4409776286, 653701.18515551 668081.187621261, 653713.1025911277 668085.8320145315, 653725.8557407823 668091.5755339819, 653734.6120866444 668096.3496328425, 653739.799799477 668099.7761259064, 653747.1700469562 668104.6439802261, 653756.8452232247 668111.6429624318, 653768.5189297618 668122.6122152923, 653769.7618541453 668123.4673923486, 653784.7765152154 668133.8010483902, 653788.7290962801 668136.5214844736, 653791.5734587177 668141.5848025536, 653792.8712390267 668143.894849459, 653794.2249503323 668146.0418648717, 653795.5523799873 668151.3530007814, 653811.7048833261 668162.5658231924, 653816.4495243808 668166.2933819733, 653847.1403658985 668184.0544121768, 653850.6459078508 668186.0831241603, 653851.8778708717 668186.5624645796, 653874.5617173191 668195.3780107505, 653878.3664084105 668196.8563646283, 653885.6589867717 668201.0731807483, 653894.2983349327 668206.0701477778, 653920.2866252905 668227.5600258645, 653925.8110992057 668232.1286270523, 653945.5749328672 668248.4717388944, 653959.0720277359 668264.5491522318, 653973.8022786187 668278.4678580273, 654008.9662205196 668302.7976702246, 654020.1061317107 668311.2139360739, 654034.2185790678 668324.5364193169, 654085.7836762436 668363.7050052873, 654100.3859835174 668377.4794872657, 654113.8818064481 668387.8049219913, 654182.8067749101 668427.6013653011, 654195.2288963154 668382.7872837482, 654247.4405463756 668399.749099728, 654255.3958236956 668406.2859439933, 654260.5744182384 668408.8025123837, 654262.1293987024 668409.5586830646, 654286.5598623353 668421.4324057339, 654292.0408297434 668421.6179941393, 654310.2536115655 668421.8468064185, 654334.4003508968 668422.1499004324, 654339.8995719122 668421.050544411, 654350.2889181838 668416.6228254228, 654360.4617310874 668408.9960744624, 654366.4546738322 668400.922767167, 654373.1706823955 668391.8756327202, 654378.7895766333 668384.3064365775, 654380.2618046395 668382.3235861885, 654384.5613211995 668376.5321398284, 654388.7619554885 668370.8736405816, 654391.1154254883 668367.704023471, 654402.3370592125 668352.5873870598, 654408.8600963737 668343.8002257678, 654413.2626351897 668338.9118210021, 654432.4143890841 668317.6478347019, 654444.5110745989 668304.2173578833, 654460.3083782017 668286.6777454463, 654468.5113023305 668277.5698168009, 654470.6628332307 668298.4588138325, 654478.9848587764 668298.5527268312, 654570.0391952167 668299.5861942023, 654580.688449716 668288.5985611797, 654797.4400635607 668064.9684443177, 654825.2782526142 668036.2467529622, 654849.7250957309 668011.0246405951, 654870.5251911717 667990.6614275123, 654872.6493851558 667988.5826474869, 654901.862643316 667960.0052604782, 654902.1979769109 667959.6776214641, 654904.4739496718 667957.4514996484, 654953.4951490513 667909.4965252365, 654971.6431081583 667891.7446581298, 654983.9535279945 667879.7032782895, 654998.7380323352 667865.2410789657, 655007.705056963 667856.4690505839, 655061.1667240696 667804.1715506202, 655065.0858144035 667800.3379465613, 655065.2042196975 667800.2219609411, 655078.6637516516 667820.3574000184, 655093.8716716659 667837.2389855066, 655107.4444382425 667848.3033143589, 655134.3083674652 667872.7381635013, 655152.273919204 667892.5971623178, 655158.4024561474 667904.5307220771, 655201.4974172963 667988.4469497361, 655208.8937187378 668002.8486371308, 655220.2064767017 668004.0228755106, 655484.3667809998 668031.4508254535, 655495.1372020701 668027.1960829771, 655500.0454510825 668025.2566559748, 655514.7006153014 668019.4665265615, 655522.1832498845 668016.5102797877, 655554.8726038878 668003.5952133416, 655565.2431016879 667999.4983427239, 655582.5667306616 667992.6533224928, 655587.3079456064 667990.7799551692, 655606.5016119881 667983.1972320838, 655622.9995508671 667974.5321290507, 655634.4641991727 667968.5113946479, 655646.9726302496 667964.771883014, 655655.1657343572 667962.3217915798, 655661.6631319039 667960.3796637473, 655661.9671981946 667960.2886589434, 655666.8711322161 667958.8231465984, 655670.7162482302 667957.6736716684, 655671.9582516555 667957.3018154262, 655672.096350603 667957.2608714068, 655677.019455342 667955.7883212134, 655678.4584893411 667955.3585677594, 655705.6335371353 667928.3037589255, 655715.6475926191 667918.3340834249, 655725.0149812624 667905.6491519971, 655725.2789638196 667905.2922243512, 655725.316067051 667905.2411889047, 655725.3297365782 667905.2231105763, 655725.7151476505 667904.7012378196, 655748.2399646755 667871.0200180418, 655758.9509059587 667862.3612905177, 655773.0417319774 667854.1330231959, 655817.5735687352 667835.7481133372, 655885.3171639511 667801.636006229, 655921.0600409163 667774.9131820289, 655943.8413807763 667745.4500244822, 655965.7144734876 667717.3837133389, 655990.0647852003 667670.6997160706, 656035.642060071 667607.1163353022, 656036.863164538 667604.8374485516, 656041.3802564606 667596.4109753717, 656050.1490217691 667580.0511154959, 656050.6582482134 667578.6891021279, 656055.5758002469 667565.5257455697, 656056.8600323334 667559.2312126548, 657608.779479904 667781.5392814417, 657564.6305838061 667876.942783758, 657522.665286563 667967.7255895433, 657480.8340159124 668058.3853656868, 657438.9405985879 668149.2392084023, 657396.9451289261 668240.0890536893, 657355.0118717848 668330.8829275875, 657313.0857183782 668421.624789126, 657271.1325728035 668512.4538340335, 657229.2332206888 668603.1659120992, 657187.2839772066 668693.974770401, 657145.3418296642 668784.7976958483, 657103.3766051037 668875.626684787, 657019.5329598974 669057.1814336343, 656894.0212630596 669329.0919695349, 656831.9399996896 669463.4743953311, 656926.172319603 669577.4298662515, 656971.2205536807 669631.9065846349, 657002.3581960856 669669.5614323705, 656998.7631838147 669685.972000656, 656994.7949776062 669723.0803766124, 656988.2920303412 669766.8025153344, 656977.3776516505 669812.4938932187, 656964.273014576 669843.7244231587, 656938.0732589115 669896.0341544756, 656920.7593235314 669930.7999315895, 656906.5669227069 669977.357988698, 656916.8125142674 670116.9523959034, 656922.622971429 670180.3997195828, 656931.9531008754 670266.1772356707, 656936.9327613487 670336.5385003176, 656924.259618505 670416.1544470547, 656921.9456556153 670441.1909814582, 656924.4517195264 670474.8790529462, 656794.958654502 670436.2688261892, 656792.0511486832 670435.4017721793, 656759.2137743969 670525.941487659, 656728.8643318835 670609.5392880864, 656706.7753002569 670670.611327422, 656696.2599106417 670717.8697734014, 656682.4479400754 670770.3655578664, 656639.0529673463 670883.95298337, 656622.9829980835 670918.6158258792, 656602.8950163573 670947.0161618183, 656600.4741169945 670954.6038263967, 656550.4935732789 670937.5720228226, 656544.6398946992 670935.6494320115, 656530.168507402 670930.4717766044, 656508.9295588082 670922.8733470205, 656499.228068712 670919.4023171724, 656481.8598469435 670913.1883302958, 656465.3196209619 670907.271398192, 656451.7262808598 670902.4079232329, 656442.4928341781 670899.1038270325, 656406.655586471 670886.2816478076, 656376.5660344168 670875.5172462115, 656363.4856418638 670870.8378301512, 656353.4433663081 670867.2446816001, 656341.4089314964 670862.918185369, 656329.581451076 670858.6669128275, 656315.9421404293 670853.7632731363, 656309.5613343588 670851.4486236285, 656303.1808832353 670849.1338141738, 656279.9369991795 670840.7021968346, 656279.862985398 670840.6751483736, 656264.9427251839 670835.2624026879, 656258.0670732065 670832.7686566709, 656245.9715841038 670828.3803939624, 656240.7728683271 670826.4946796714, 656276.2493005282 670768.5382709075, 656278.5146352648 670764.6315229286, 656280.7812125141 670760.7249350892, 656282.1883294444 670758.455035376, 656298.0832840248 670732.8121118152, 656300.9727919201 670728.1494420012, 656311.4326362992 670711.0627834377, 656319.6680658059 670698.5939126607, 656370.5172838776 670614.9724360532, 656374.1228942786 670609.6957739756, 656377.9646179109 670604.0753018627, 656402.7269174061 670564.4024703124, 656395.9388593262 670565.7167083938, 656395.2029355495 670565.8385415995, 656390.9318796847 670566.5450778672, 656390.0239563994 670566.6950507874, 656385.1477963515 670567.4973557192, 656384.8838506743 670567.5413245849, 656381.6927179483 670568.0670282333, 656367.5546759989 670570.3952808359, 656366.738722462 670570.3262260228, 656366.2866376648 670570.2882534902, 656365.9517008606 670570.2602145661, 656354.4339228937 670569.285756212, 656350.6561692016 670566.6443734412, 656350.0342641819 670566.2092638457, 656349.9472967732 670566.1492541058, 656337.0324723755 670557.1209288314, 656336.6313469755 670556.9338432914, 656336.5184639358 670556.8808704785, 656326.0391522583 670551.9865608237, 656325.1371525961 670551.5654984232, 656324.1590101572 670551.1084273845, 656321.6352916886 670549.937100835, 656318.9253880517 670548.6708732074, 656318.1353832403 670548.3087034777, 656317.6124997326 670548.0696040845, 656315.8706194913 670547.2743662172, 656311.8598935609 670545.4399906487, 656310.3089416728 670545.1647714481, 656300.0222146865 670543.3426228751, 656295.2743989935 670542.5011142995, 656289.4726174653 670541.4734039453, 656289.1385717217 670541.4143253461, 656288.4585881543 670541.2932867861, 656279.3576044186 670542.0882184006, 656278.9745580261 670542.1241734145, 656278.8906002081 670542.1321635536, 656265.5755660663 670543.3886798098, 656264.9404705772 670543.4326053351, 656263.563422596 670543.5444437376, 656263.0504467033 670543.5842235545, 656262.0864429695 670543.655150481, 656255.7775476392 670544.1604901764, 656253.9384663469 670544.3082743711, 656252.788440614 670544.4081393684, 656247.4931545615 670539.5195552151, 656246.7260870305 670538.8114705941, 656244.1764128385 670536.457109335, 656244.1634568205 670536.4451079046, 656240.8507969282 670533.3877824732, 656240.4069200571 670532.9778135108, 656231.2699065873 670524.5427256128, 656230.3336702825 670517.8036235105, 656230.1609989891 670515.966656221, 656228.7955817122 670501.4590784541, 656228.3949268693 670497.2025814991, 656227.0527330673 670482.9434847133, 656227.3578933286 670481.0635335227, 656229.0581250717 670470.5689255976, 656231.5168199142 670456.9112283895, 656228.1070409428 670437.2159204972, 656220.5165525 670416.687169179, 656217.9534489339 670418.4058986715, 656217.0231057116 670419.0297859651, 656216.8810957039 670419.1248087633, 656215.9969057724 670419.7175016471, 656212.6518595376 670421.9602964045, 656212.1088487082 670422.3242306198, 656210.6328909744 670422.8040551585, 656205.4426318094 670424.4923981352, 656194.7110165049 670421.7711632148, 656185.9469398478 670423.9562056966, 656167.6742992171 670421.151051015, 656138.6632637267 670423.5205608641, 656130.9583595856 670405.401846455, 656130.6614846509 670404.7046958426, 656126.8734471374 670395.7963053673, 656117.8969734359 670401.9631993851, 656116.9577525889 670402.6080436637, 656112.823476611 670405.4476942519, 656104.4081893527 670409.1055513938, 656089.2309025106 670412.8008458117, 656043.240872611 670415.0633732068, 656031.4926943016 670417.1488450974, 656014.7732182991 670421.5329115682, 656014.5418025615 670416.55483753, 655896.6343937886 670392.7090079437, 655844.4766004476 670386.0409451226, 655817.1732204839 670390.4595953758, 655767.623094355 670384.9146420811, 655696.2064898551 670342.7724246988, 655698.0200904065 670328.1237851824, 655590.4374310854 670279.4161539985, 655609.3843211576 670253.7395731593, 655609.7743470885 670253.21258376, 655613.118772306 670248.6790977443, 655617.5625213954 670241.974719142, 655620.7250120919 670237.4250513044, 655621.5652437859 670236.3042001848, 655679.7949071333 670158.6787807789, 655692.2366537085 670142.0923907731, 655696.2490147253 670137.4898646767, 655696.4531654381 670137.255970825, 655723.7966591325 670148.2700569605, 655763.6857714701 670162.3747487988, 655806.0548571903 670176.8387771437, 655846.7154135248 670187.5435300041, 655849.4788830051 670183.0189284064, 655857.303279634 670171.6528931851, 655864.671427874 670160.9488792467, 655867.435903667 670156.9332341674, 655883.0642818856 670134.2291603135, 655890.3965695531 670123.5781013279, 655894.5031621037 670117.6115883132, 655898.6108197677 670111.6442753937, 655903.3744231847 670106.456714806, 655908.138026482 670101.2704341704, 655915.2828990577 670093.4903730855, 655922.4277718257 670085.7091919162, 655927.1911978654 670080.5217911112, 655931.954801321 670075.335350278, 655935.7942296517 670071.1539131802, 655936.3773867516 670070.5189465253, 655941.1409903722 670065.3315456234, 655945.9045940207 670060.1441446831, 655953.0492894305 670052.364083106, 655960.253276916 670044.5200288888, 655962.086343173 670042.6762583256, 655963.3735370192 670041.381379436, 655965.2648292677 670039.4825761258, 655969.777174887 670035.1372195652, 655970.4023955446 670034.5342576378, 655975.5397844808 670029.5848190925, 655980.6773509264 670024.6355405288, 655985.5999422646 670019.8940750812, 655985.8138523151 670019.6870617908, 655990.9524837006 670014.738743266, 655996.0900502327 670009.7894645792, 655997.7281921965 670008.2106290631, 656001.2265518189 670004.8400257272, 656011.4226891393 669995.018418287, 656013.3208959769 669993.1885751892, 656016.3161678151 669990.303028985, 656023.6869188172 669983.202828759, 656029.2255000897 669977.8675198477, 656039.5116398181 669967.9579232039, 656047.4444150563 669960.3158728278, 656049.007644157 669958.8099878317, 656058.4976129119 669949.6682915621, 656059.8692995028 669948.3462226557, 656059.9695975524 669948.2494351651, 656061.2647735518 669947.0015967074, 656060.047682877 669946.8363338369, 656059.3863324021 669946.746976198, 656058.1695966137 669946.5826733636, 656057.4738119124 669946.4885116833, 656015.5508302372 669940.8221504877, 656011.1927646867 669940.2656387113, 656003.4210820768 669939.3517655536, 656001.646117392 669939.1435569967, 655996.4740473125 669938.5410292561, 655989.8953184495 669937.7671362864, 655980.0265149453 669936.6080567515, 655969.4235863888 669935.3597710014, 655962.618732349 669934.5130518092, 655955.4619010701 669933.6400511732, 655948.4321582562 669932.7733055371, 655947.1990917202 669932.6162007493, 655941.4341856636 669931.9244835457, 655930.9304761504 669930.7002094489, 655930.2604263333 669930.6271706884, 655920.3076299905 669929.7779992308, 655913.2407652851 669929.124207803, 655906.0858515622 669928.5532854063, 655898.9730017047 669928.0114877933, 655894.2659504471 669927.6528534922, 655891.8401028956 669927.3967283105, 655886.4962096979 669926.8300201093, 655884.8841779778 669926.7168702147, 655882.5002114003 669926.5365894722, 655878.116382461 669926.2050332285, 655877.976513675 669924.6650271887, 655855.6356253946 669924.5983852642, 655845.7144514692 669926.5842382369, 655836.3745088007 669926.1671363941, 655824.6217728597 669924.6547166985, 655769.5394830788 669913.5291724361, 655661.5446587334 669883.6345087745, 655632.7967572438 669876.0819929101, 655572.572143023 669858.9619317885, 655564.421309855 669856.6449712319, 655474.5379709204 669831.093426862, 655466.0133373856 669828.6703438045, 655420.8060498878 669815.819107246, 655416.4184231582 669814.5715514517, 655413.2233718209 669813.6631758779, 655360.8193337056 669799.7960511198, 655328.2934011557 669791.9632615466, 655327.0834281872 669791.6471192988, 655317.5658735129 669789.1599204614, 655307.8582211451 669786.6418192983, 655289.4148707059 669781.8586916528, 655284.3641155208 669780.5480181425, 655278.3362179326 669779.3343472687, 655251.8930573587 669774.0081893887, 655235.0621587527 669773.3652246228, 655227.5491338482 669773.6344055423, 655217.3812476165 669773.9991087341, 655210.1802669632 669774.2572869656, 655208.2432342436 669774.152096482, 655204.7862883243 669773.9656051464, 655202.776304043 669773.856405988, 655196.082349373 669773.3445706582, 655175.0696896291 669771.7380742107, 655173.7807043911 669771.6398410776, 655170.019983023 669770.5685203801, 655158.7853358996 669767.809120578, 655150.009815126 669764.5551383967, 655148.8138907324 669764.0130798806, 655132.6527609826 669756.6780885159, 655121.8015120309 669750.8499595794, 655109.0268389928 669740.8294749049, 655098.4709559632 669731.8692469907, 655083.9916858174 669717.9045919734, 655075.9857624852 669708.8467503338, 655068.292933381 669698.3758379092, 655066.6410649267 669697.3977289557, 655066.6291733198 669697.3906875965, 655053.8091447832 669689.7023422995, 655053.2312496753 669689.3472363295, 655051.8843067525 669688.5200425755, 655051.8804020003 669688.5181221282, 655047.0836595121 669685.5714946439, 655046.8026988773 669685.3985025842, 655046.7666691697 669685.376418472, 655042.2381081898 669682.5959016779, 655042.1071234209 669682.5149267389, 655041.992112399 669682.4438736163, 655037.6323454027 669679.7273764923, 655035.1805532353 669678.2002169563, 655029.4101141463 669674.7174782322, 655024.2564528305 669671.4999309088, 655023.4464096173 669670.9687988132, 655022.4684653594 669670.3266076422, 655011.7994657549 669663.3244350266, 654974.5025157937 669638.8451598012, 654973.4177252956 669638.1331967058, 654971.225785061 669636.6607882822, 654967.2742603475 669633.9873806494, 654964.7344508355 669632.2691729742, 654961.0986717845 669629.8097212082, 654958.4918965304 669628.5115020912, 654951.9462907288 669625.2797108638, 654951.130375092 669624.877537225, 654951.0813883009 669624.8525716134, 654946.9785594521 669622.8271408174, 654942.4548782778 669620.823579845, 654931.377790269 669621.6382555722, 654931.007879525 669621.6652513474, 654929.4438954121 669622.0250622765, 654927.2558164131 669622.4617583212, 654915.5904990515 669624.7923509711, 654910.6663377875 669626.220713023, 654889.290859326 669554.2298349552, 654887.7704797864 669548.9807400267, 654886.2210021465 669543.6305225786, 654885.0136355758 669539.2014577746, 654884.3754294069 669532.3854412705, 654884.3011087246 669525.8144495473, 654885.0416755633 669520.8595404299, 654886.2482174458 669517.0867562756, 654888.3486566673 669513.2011190617, 654890.154000118 669510.1152798723, 654893.7433147116 669506.1008601767, 654897.1276520864 669503.3252853784, 654900.5130543475 669500.5506706338, 654904.2081388228 669498.236088567, 654907.9025131769 669495.9226263472, 654914.0966449907 669493.2553833458, 654920.907791345 669491.8741223306, 654928.3130366927 669490.4039724108, 654930.3979878838 669489.9762231202, 654939.7871905837 669488.0523920414, 654992.7508443593 669479.3286602402, 654999.706005362 669477.4035801336, 655000.1584886827 669474.0062219575, 655001.2492811 669465.8139391872, 655001.8808393257 669461.07005336, 655003.0725650107 669438.1103849947, 655004.2727861248 669418.4255702886, 655005.7144130339 669404.8039336985, 655009.7906270365 669385.302496966, 655011.3458574361 669374.7078883497, 655012.7969920507 669364.8161415241, 655012.7005952873 669351.1602430333, 655010.294448621 669335.6039277297, 655007.0237829381 669323.970678192, 655003.7967794822 669315.4214084719, 654999.9587554764 669307.4711019201, 654995.9415762334 669300.0286102296, 654991.9023861149 669294.126103305, 654990.7834397985 669292.5381446737, 654985.5404531161 669285.0974289558, 654983.7115557187 669283.1263499461, 654981.1169989138 669280.5451058659, 654978.5142758965 669277.9747407731, 654965.2307891875 669265.7551988641, 654958.9475854065 669268.1385988483, 654947.0381736488 669272.2802012013, 654944.9961443893 669272.9898746684, 654944.5392311645 669271.7269515699, 654941.8320217455 669273.2294999541, 654937.7278061718 669275.5089971535, 654936.6907910287 669276.0848700979, 654932.8395275733 669278.2243982102, 654932.2704295801 669278.5403284773, 654927.4341616253 669281.2258158247, 654924.103003481 669283.0753275608, 654923.77408 669283.2312874767, 654923.3899513487 669283.4133206633, 654915.2636068879 669287.2633702052, 654914.4855867041 669287.6312753755, 654908.708213882 669290.3675310846, 654907.9723911863 669289.6114508831, 654905.0643077561 669290.6421393253, 654904.565338637 669290.8129590553, 654897.7840805515 669293.1321399135, 654857.6929846491 669312.689323754, 654849.3738237298 669315.4633584274, 654843.6746412306 669317.4165891578, 654827.35788681 669323.4875711317, 654798.0486668866 669336.3851185952, 654787.197912468 669343.1277776301, 654776.8462019059 669350.5874485648, 654767.5912235026 669358.3352458583, 654752.9051408764 669369.0954938239, 654752.4801725867 669369.406320797, 654749.205883442 669372.1910286327, 654744.9006184341 669373.9825037061, 654732.6122628952 669368.794059777, 654721.1204727096 669350.2408735896, 654675.3568437991 669366.9572345355, 654647.2068177641 669377.2398926098, 654641.3610390694 669375.9752139953, 654634.2733318574 669374.4423912641, 654632.2933716467 669374.1000806289, 654631.0672345837 669373.8832978512, 654636.7091322822 669383.781470328, 654642.0146221432 669397.1549304641, 654642.6363686718 669398.963946815, 654647.2404461392 669414.8574606837, 654647.5732625001 669416.3384460325, 654649.7549103658 669428.5195488924, 654653.2825491647 669448.2167402508, 654653.729390615 669450.6188504657, 654678.3164596445 669578.4596042299, 654686.2853089125 669621.0431932873, 654701.643146123 669682.9814045476, 654704.1036079337 669687.8146128061, 654699.2615171798 669688.6740279348, 654669.4529043647 669693.167554281, 654665.0190831048 669693.8359796772, 654649.8029057519 669696.1301448487, 654631.9407257248 669698.6599922216, 654603.6403914319 669704.0764075422, 654597.782266952 669705.1977785141, 654597.7400663274 669706.3497623964, 654597.4110162373 669715.2637174045, 654595.918909645 669907.2817069246, 654595.8893537046 669911.1235922381, 654598.2817183329 669916.9348718094, 654591.1734201644 669920.0079150791, 654566.5399420089 669933.8549304573, 654560.5495424712 669936.8332282623, 654555.2014020511 669939.2234478025, 654549.902076327 669941.5919133442, 654545.6250796051 669942.9862693431, 654532.8474349639 669947.8717379208, 654521.0480558776 669952.1592485283, 654511.7578399541 669954.5790751115, 654508.7187989843 669955.2948649577, 654499.3616802566 669957.5916444731, 654485.2034146566 669959.7200097939, 654470.0973893134 669961.1241074015, 654453.6883783464 669962.2402111453, 654440.9375011801 669961.3126102714, 654441.5483810762 669954.2877815429, 654442.684226959 669938.7490060693, 654441.9502107454 669920.92314537, 654439.8574595316 669910.573860112, 654438.5142100292 669903.9327940488, 654432.159092995 669888.1822587061, 654413.6393929281 669869.8082280089, 654384.4003191215 669880.9225327447, 654360.8896809607 669878.1027863579, 654335.3757601944 669878.6147656391, 654322.2069128883 669878.878145846, 654309.7379450208 669879.1286499796, 654184.3345424426 669881.6435246145, 654180.8184596507 669881.7141427426, 654174.9312135087 669881.406881038, 654174.6525419919 669881.3924478209, 654164.9567261562 669880.8861722008, 654164.0706572466 669880.8401465911, 654160.9300273008 669886.6776260315, 654160.898958284 669886.7357016709, 654157.6321000685 669886.6663511297, 654083.2448245663 669885.0854588434, 654050.0570596472 669884.3805292471, 654038.6297129506 669884.1375282708, 654033.872223447 669884.0364650113, 654028.1692474859 669883.5047305445, 653908.4543215297 669872.3471161808, 653896.8096625861 669871.0348215896, 653885.0198114834 669869.7063494402, 653833.0287450292 669864.1030718619, 653824.1109579654 669863.1599081788, 653815.7771349767 669862.2779355026, 653782.0737315929 669858.5679221997, 653773.2089702935 669858.025720422, 653765.570007324 669857.557748256, 653739.2425686647 669853.8416014235, 653721.0380453495 669851.3445172482, 653704.8926031132 669848.0304161136, 653685.9021273718 669843.8972495272, 653623.4554967558 669827.5537571509, 653614.2068454896 669825.1376078657, 653604.4290863514 669822.5825152464, 653590.84569937 669818.9538474306, 653576.010291268 669814.8459916771, 653561.7337884052 669811.0713208512, 653497.534058158 669794.2596286562, 653486.8923448669 669792.2884236437, 653476.4176545334 669790.3469989167, 653360.4626608877 669779.4989450758, 653349.9567804006 669778.6615028979, 653346.0259278879 669778.348223165, 653343.3556603829 669778.135097264, 653332.0031184922 669777.2295517366, 653298.9118669104 669773.9055224961, 653279.1891116791 669772.1520782481, 653248.4615182179 669771.1662293817, 653218.9557474769 669770.8626018389, 653157.7161387423 669771.1350606447, 653142.068190981 669771.1952138388, 653118.2172142946 669772.1042656963, 653089.597586976 669771.2036355232, 653075.9286726796 669770.4099641298, 653065.3668865241 669769.5187565667, 653057.7309621748 669768.8747857139, 653037.6881931217 669768.5094440277, 653032.6891669111 669768.0747548474, 653027.6723909328 669767.6391035067, 653018.1046783122 669764.5299695474, 652998.7476874685 669757.1897705925, 652989.1842880755 669753.6225641577, 652979.6896773349 669750.7515165885, 652969.2088229043 669749.927198907, 652961.3388994863 669749.7622331129, 652958.9098341294 669749.710895027, 652947.7917201989 669752.2755709391, 652925.6392058344 669757.9718045659, 652901.0572963726 669766.4531387889, 652890.7880454338 669769.9963043152, 652870.3523193464 669777.0476079744, 652849.312540459 669784.3069935582, 652841.1280784843 669788.8139209007, 652822.8733928109 669804.0573725107, 652821.6271777728 669807.0752552703, 652820.3919095437 669809.0661142623, 652814.6629493494 669818.298234621, 652809.5560248389 669825.316543528, 652805.6196636745 669829.6369557296, 652801.1452463901 669832.922276607, 652796.9700266562 669835.24380832, 652793.8438927328 669836.982437761, 652781.3384973719 669840.643723232, 652768.6964853664 669840.7572035594, 652765.1135606406 669840.1847703778, 652755.7309695714 669838.6855560568, 652746.3104014146 669834.0675029428, 652740.2928242522 669831.1178448044, 652737.5760046709 669829.785529645, 652736.3044863872 669829.0692235169, 652646.277657513 669778.350088812, 652632.9446425315 669770.8384800176, 652634.925100021 669766.899742729, 652634.6590673215 669766.5728348809, 652624.7604647862 669754.5897162786, 652614.023121162 669741.5906703533, 652593.1770152701 669718.1484593417, 652586.0920121975 669710.1808284387, 652579.0290161686 669702.2378401497, 652566.3757166425 669688.0085002886, 652570.6633504982 669682.1422358694, 652607.8570455452 669631.2614904767, 652610.808365193 669627.8059046344, 652615.2059646281 669622.6566016547, 652622.7189148911 669613.8595755241, 652626.4265419923 669609.0061831567, 652627.0004829805 669608.2552248238, 652629.022865945 669605.5335936957, 652630.9371351604 669602.957867099, 652631.3481092239 669602.40480579, 652635.2258229377 669597.1834395865, 652635.6888123918 669596.5595057075, 652640.3735617312 669590.2540945685, 652640.9625948707 669589.4621786451, 652646.6395407938 669581.8180289252, 652648.6557103627 669579.1043966184, 652648.8788613818 669578.8033084704, 652648.9848446741 669578.6614035842, 652651.0722023114 669575.852741388, 652652.1682515932 669574.3768977688, 652653.3764975726 669572.7509901299, 652661.407623511 669561.9391756305, 652662.2217604865 669560.8432917271, 652666.6295684971 669554.9088802245, 652667.7696443277 669553.3740027611, 652669.1697959079 669551.495162273, 652672.5972999979 669546.8977305877, 652688.9479028654 669524.4711063063, 652689.4929111649 669523.7231841373, 652698.074217166 669511.951449258, 652705.2903393068 669502.054399055, 652712.3522175327 669493.6613074056, 652716.4269106479 669488.8180375551, 652750.5962463564 669448.2047348047, 652768.741658708 669427.7933320107, 652775.7983824522 669420.9832539669, 652787.4815008936 669410.5497937871, 652810.8498277927 669388.4969690933, 652821.5710826585 669378.0403499593, 652830.4060451542 669369.2196363343, 652837.0516720427 669362.4895048864, 652842.9092991444 669356.4703067846, 652851.8635915517 669345.6845151233, 652861.2457977197 669333.6789527675, 652870.8693938557 669320.2172802497, 652876.250369268 669311.7220584983, 652881.0202007501 669303.36084009, 652885.4561860468 669294.7495043789, 652887.5479696871 669288.9797620614, 652891.327404907 669275.6314539285, 652892.5709391019 669271.2406283377, 652894.747000378 669261.1831174638, 652895.584862013 669254.4932353055, 652896.2260711462 669244.6094548985, 652895.6313368377 669233.3657034795, 652894.7252440532 669216.2327185364, 652893.5532247734 669199.386737215, 652893.3967546624 669197.1381103918, 652893.0600694695 669192.2978187744, 652892.5670921329 669185.2049004296, 652891.1617362587 669170.7748563015, 652890.27370485 669161.6539190616, 652889.7913060519 669156.7028913891, 652889.3628027346 669152.3038623665, 652888.775628233 669146.2680375354, 652888.0833213382 669139.161975296, 652887.0805844559 669128.8690410665, 652886.2056721911 669119.8841036553, 652885.1309573107 669108.8470114578, 652885.5944753308 669103.6732219433, 652886.0421749294 669098.6803879025, 652886.1162877977 669097.8552887315, 652887.0324808365 669087.6416264036, 652887.6191651623 669081.0937914868, 652889.3330985525 669071.8830914855, 652891.0463220414 669062.671431332, 652892.5231465045 669054.7347641658, 652894.6285176445 669043.5401398095, 652894.7444956701 669042.926242664, 652898.8509669716 669021.2179312014, 652898.9131103499 669021.0560210488, 652902.9352057106 669010.5905781483, 652906.9384576747 669000.3702492496, 652910.2194181144 668992.0997248172, 652913.9761434273 668984.7694044489, 652928.0592386187 668957.3623801045, 652930.9330229978 668950.565864632, 652932.7134696539 668946.3550999896, 652940.4539382153 668933.2442228618, 652944.1404360143 668928.4338571121, 652947.7340879176 668923.7442783287, 652951.0744107523 668919.3847441453, 652955.0121073348 668914.2462531638, 652960.0564445257 668911.1869064551, 652964.7457589973 668908.3446336722, 652965.2548612745 668908.0357395979, 652968.117940003 668906.3010702912, 652969.8829343895 668905.2302588186, 652974.4772802762 668902.445893599, 652983.1044648984 668899.0818640083, 652991.7328920721 668895.7170344363, 652997.3690874261 668893.5187066719, 653001.9171914662 668893.0552629872, 653011.7331652822 668892.0564636033, 653014.7292020167 668891.7507500572, 653018.198233139 668891.7873288897, 653024.0631394674 668891.8478770035, 653026.3051233426 668891.8711876934, 653027.6571295896 668891.8854309259, 653033.5120959706 668891.9461378431, 653037.8821096622 668891.9916254656, 653061.6902582705 668896.0165275082, 653049.6358527736 668887.8015846617, 653019.6948950508 668867.3977713933, 653014.644398098 668863.8922503241, 653009.5947886744 668860.3865694538, 653005.0731909324 668857.2862267598, 652983.7961720794 668842.5878639072, 652962.629043596 668827.7314382709, 652933.4014256464 668807.580196858, 652925.2472575767 668801.9322534415, 652916.3410878606 668795.7642269615, 652851.342708577 668750.9691927655, 652843.9062904979 668745.7973312788, 652834.4071491923 668739.189242228, 652776.7332250236 668699.261916725, 652770.495728063 668694.805230994, 652764.2482921332 668690.340544321, 652697.4042450924 668644.3901389355, 652691.6006798155 668640.3895003323, 652685.798179555 668636.3888619712, 652629.5861920961 668597.5466847111, 652623.0657478076 668593.2189260899, 652616.6612012325 668588.96830038, 652594.7890478492 668574.1588557485, 652576.245664994 668561.2238640934, 652561.1580792752 668550.6991719583, 652523.7102641071 668524.9851485435, 652506.0379887512 668512.5401784922)))" + } +}, +{ + "model": "common.municipality", + "pk": 8, + "fields": { + "fid": "", + "name_fr": "Woluwe-Saint-Lambert", + "name_nl": "Sint-Lambrechts-Woluwe", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((652199.5487253526 670358.1414385941, 652203.6174374435 670351.5229690942, 652240.5903016694 670292.0091050185, 652256.6630551225 670260.4054928562, 652260.6768935012 670252.5131090744, 652264.511770966 670244.9715795668, 652271.1892509289 670231.841657987, 652282.4985048315 670187.9955021349, 652284.8464917594 670178.8128457313, 652287.1586091535 670169.7532236103, 652305.7791388233 670096.8311499773, 652303.9590068089 670089.8850760497, 652301.1717258001 670084.0708710813, 652286.0596845341 670052.5462876275, 652275.4363606202 670030.3861653627, 652272.1101503832 670023.4479065704, 652279.7503536001 670020.9597534618, 652288.2527022486 670018.1909094282, 652339.0543191005 670001.6463113626, 652348.7925363267 669999.3435372973, 652358.5288012191 669997.0388429738, 652372.9894918829 669990.7826120988, 652384.3572598365 669983.0781738824, 652395.9630726611 669975.2126465336, 652514.2724885807 669907.7589695568, 652535.9097315996 669896.1787155214, 652546.9320919127 669883.6741798609, 652560.3868888167 669866.631137935, 652572.5926194568 669851.1719169314, 652585.7974605742 669834.4467572275, 652591.6692770909 669827.0085530019, 652591.9414228059 669826.6645917218, 652597.0019350349 669829.5371720226, 652600.9456984486 669831.7757043164, 652602.1256326708 669832.4457596416, 652602.7045956649 669832.7738260565, 652608.2102455407 669835.8994175665, 652610.5289370427 669837.215683556, 652613.2377452336 669838.7531143166, 652618.2024144533 669841.5716039217, 652623.5130060724 669844.5862932932, 652631.8355539459 669849.3101284482, 652632.6633537719 669849.7803034601, 652641.1771191016 669852.5382717969, 652650.1298005552 669854.1014320734, 652656.9018471082 669853.144201165, 652662.8369728387 669852.3060652824, 652669.6839251684 669851.3378037111, 652671.1619821737 669851.1290290131, 652677.9940242226 669850.1639655157, 652682.8270417317 669849.4805714, 652684.662101678 669848.8177674335, 652691.4444392624 669846.3675997579, 652698.0295672666 669843.9894066253, 652698.2887264702 669843.8955199608, 652701.0367012202 669842.9029133348, 652703.1307427486 669842.1471825996, 652703.4009073626 669842.0491373494, 652704.2529375291 669841.7412469105, 652705.4569627459 669841.3054017437, 652707.915957424 669840.4157979487, 652713.8372123538 669838.2735193298, 652722.0894799487 669835.2875403381, 652724.0365457032 669834.5897105581, 652725.679543174 669834.0019216839, 652734.3148619015 669830.9411108169, 652737.5760046709 669829.785529645, 652740.2928242522 669831.1178448044, 652746.3104014146 669834.0675029428, 652755.7309695714 669838.6855560568, 652765.1135606406 669840.1847703778, 652768.6964853664 669840.7572035594, 652781.3384973719 669840.643723232, 652793.8438927328 669836.982437761, 652796.9700266562 669835.24380832, 652801.1452463901 669832.922276607, 652805.6196636745 669829.6369557296, 652809.5560248389 669825.316543528, 652814.6629493494 669818.298234621, 652820.3919095437 669809.0661142623, 652821.6271777728 669807.0752552703, 652822.8733928109 669804.0573725107, 652841.1280784843 669788.8139209007, 652849.312540459 669784.3069935582, 652870.3523193464 669777.0476079744, 652890.7880454338 669769.9963043152, 652901.0572963726 669766.4531387889, 652925.6392058344 669757.9718045659, 652947.7917201989 669752.2755709391, 652958.9098341294 669749.710895027, 652961.3388994863 669749.7622331129, 652969.2088229043 669749.927198907, 652979.6896773349 669750.7515165885, 652989.1842880755 669753.6225641577, 652998.7476874685 669757.1897705925, 653018.1046783122 669764.5299695474, 653027.6723909328 669767.6391035067, 653032.6891669111 669768.0747548474, 653037.6881931217 669768.5094440277, 653057.7309621748 669768.8747857139, 653065.3668865241 669769.5187565667, 653075.9286726796 669770.4099641298, 653089.597586976 669771.2036355232, 653118.2172142946 669772.1042656963, 653142.068190981 669771.1952138388, 653157.7161387423 669771.1350606447, 653218.9557474769 669770.8626018389, 653248.4615182179 669771.1662293817, 653279.1891116791 669772.1520782481, 653298.9118669104 669773.9055224961, 653332.0031184922 669777.2295517366, 653343.3556603829 669778.135097264, 653346.0259278879 669778.348223165, 653349.9567804006 669778.6615028979, 653360.4626608877 669779.4989450758, 653476.4176545334 669790.3469989167, 653486.8923448669 669792.2884236437, 653497.534058158 669794.2596286562, 653561.7337884052 669811.0713208512, 653576.010291268 669814.8459916771, 653590.84569937 669818.9538474306, 653604.4290863514 669822.5825152464, 653614.2068454896 669825.1376078657, 653623.4554967558 669827.5537571509, 653685.9021273718 669843.8972495272, 653704.8926031132 669848.0304161136, 653721.0380453495 669851.3445172482, 653739.2425686647 669853.8416014235, 653765.570007324 669857.557748256, 653773.2089702935 669858.025720422, 653782.0737315929 669858.5679221997, 653815.7771349767 669862.2779355026, 653824.1109579654 669863.1599081788, 653833.0287450292 669864.1030718619, 653885.0198114834 669869.7063494402, 653896.8096625861 669871.0348215896, 653908.4543215297 669872.3471161808, 654028.1692474859 669883.5047305445, 654033.872223447 669884.0364650113, 654038.6297129506 669884.1375282708, 654050.0570596472 669884.3805292471, 654083.2448245663 669885.0854588434, 654157.6321000685 669886.6663511297, 654160.898958284 669886.7357016709, 654160.9300273008 669886.6776260315, 654164.0706572466 669880.8401465911, 654164.9567261562 669880.8861722008, 654174.6525419919 669881.3924478209, 654174.9312135087 669881.406881038, 654180.8184596507 669881.7141427426, 654184.3345424426 669881.6435246145, 654309.7379450208 669879.1286499796, 654322.2069128883 669878.878145846, 654335.3757601944 669878.6147656391, 654360.8896809607 669878.1027863579, 654384.4003191215 669880.9225327447, 654413.6393929281 669869.8082280089, 654432.159092995 669888.1822587061, 654438.5142100292 669903.9327940488, 654439.8574595316 669910.573860112, 654441.9502107454 669920.92314537, 654442.684226959 669938.7490060693, 654441.5483810762 669954.2877815429, 654440.9375011801 669961.3126102714, 654453.6883783464 669962.2402111453, 654470.0973893134 669961.1241074015, 654485.2034146566 669959.7200097939, 654499.3616802566 669957.5916444731, 654508.7187989843 669955.2948649577, 654511.7578399541 669954.5790751115, 654521.0480558776 669952.1592485283, 654532.8474349639 669947.8717379208, 654545.6250796051 669942.9862693431, 654549.902076327 669941.5919133442, 654555.2014020511 669939.2234478025, 654560.5495424712 669936.8332282623, 654566.5399420089 669933.8549304573, 654591.1734201644 669920.0079150791, 654598.2817183329 669916.9348718094, 654595.8893537046 669911.1235922381, 654595.918909645 669907.2817069246, 654597.4110162373 669715.2637174045, 654597.7400663274 669706.3497623964, 654597.782266952 669705.1977785141, 654603.6403914319 669704.0764075422, 654631.9407257248 669698.6599922216, 654649.8029057519 669696.1301448487, 654665.0190831048 669693.8359796772, 654669.4529043647 669693.167554281, 654699.2615171798 669688.6740279348, 654704.1036079337 669687.8146128061, 654701.643146123 669682.9814045476, 654686.2853089125 669621.0431932873, 654678.3164596445 669578.4596042299, 654653.729390615 669450.6188504657, 654653.2825491647 669448.2167402508, 654649.7549103658 669428.5195488924, 654647.5732625001 669416.3384460325, 654647.2404461392 669414.8574606837, 654642.6363686718 669398.963946815, 654642.0146221432 669397.1549304641, 654636.7091322822 669383.781470328, 654631.0672345837 669373.8832978512, 654632.2933716467 669374.1000806289, 654634.2733318574 669374.4423912641, 654641.3610390694 669375.9752139953, 654647.2068177641 669377.2398926098, 654675.3568437991 669366.9572345355, 654721.1204727096 669350.2408735896, 654732.6122628952 669368.794059777, 654744.9006184341 669373.9825037061, 654749.205883442 669372.1910286327, 654752.4801725867 669369.406320797, 654752.9051408764 669369.0954938239, 654767.5912235026 669358.3352458583, 654776.8462019059 669350.5874485648, 654787.197912468 669343.1277776301, 654798.0486668866 669336.3851185952, 654827.35788681 669323.4875711317, 654843.6746412306 669317.4165891578, 654849.3738237298 669315.4633584274, 654857.6929846491 669312.689323754, 654897.7840805515 669293.1321399135, 654904.565338637 669290.8129590553, 654905.0643077561 669290.6421393253, 654907.9723911863 669289.6114508831, 654908.708213882 669290.3675310846, 654914.4855867041 669287.6312753755, 654915.2636068879 669287.2633702052, 654923.3899513487 669283.4133206633, 654923.77408 669283.2312874767, 654924.103003481 669283.0753275608, 654927.4341616253 669281.2258158247, 654932.2704295801 669278.5403284773, 654932.8395275733 669278.2243982102, 654936.6907910287 669276.0848700979, 654937.7278061718 669275.5089971535, 654941.8320217455 669273.2294999541, 654944.5392311645 669271.7269515699, 654944.9961443893 669272.9898746684, 654947.0381736488 669272.2802012013, 654958.9475854065 669268.1385988483, 654965.2307891875 669265.7551988641, 654978.5142758965 669277.9747407731, 654981.1169989138 669280.5451058659, 654983.7115557187 669283.1263499461, 654985.5404531161 669285.0974289558, 654990.7834397985 669292.5381446737, 654991.9023861149 669294.126103305, 654995.9415762334 669300.0286102296, 654999.9587554764 669307.4711019201, 655003.7967794822 669315.4214084719, 655007.0237829381 669323.970678192, 655010.294448621 669335.6039277297, 655012.7005952873 669351.1602430333, 655012.7969920507 669364.8161415241, 655011.3458574361 669374.7078883497, 655009.7906270365 669385.302496966, 655005.7144130339 669404.8039336985, 655004.2727861248 669418.4255702886, 655003.0725650107 669438.1103849947, 655001.8808393257 669461.07005336, 655001.2492811 669465.8139391872, 655000.1584886827 669474.0062219575, 654999.706005362 669477.4035801336, 654992.7508443593 669479.3286602402, 654939.7871905837 669488.0523920414, 654930.3979878838 669489.9762231202, 654928.3130366927 669490.4039724108, 654920.907791345 669491.8741223306, 654914.0966449907 669493.2553833458, 654907.9025131769 669495.9226263472, 654904.2081388228 669498.236088567, 654900.5130543475 669500.5506706338, 654897.1276520864 669503.3252853784, 654893.7433147116 669506.1008601767, 654890.154000118 669510.1152798723, 654888.3486566673 669513.2011190617, 654886.2482174458 669517.0867562756, 654885.0416755633 669520.8595404299, 654884.3011087246 669525.8144495473, 654884.3754294069 669532.3854412705, 654885.0136355758 669539.2014577746, 654886.2210021465 669543.6305225786, 654887.7704797864 669548.9807400267, 654889.290859326 669554.2298349552, 654910.6663377875 669626.220713023, 654915.5904990515 669624.7923509711, 654927.2558164131 669622.4617583212, 654929.4438954121 669622.0250622765, 654931.007879525 669621.6652513474, 654931.377790269 669621.6382555722, 654942.4548782778 669620.823579845, 654946.9785594521 669622.8271408174, 654951.0813883009 669624.8525716134, 654951.130375092 669624.877537225, 654951.9462907288 669625.2797108638, 654958.4918965304 669628.5115020912, 654961.0986717845 669629.8097212082, 654964.7344508355 669632.2691729742, 654967.2742603475 669633.9873806494, 654971.225785061 669636.6607882822, 654973.4177252956 669638.1331967058, 654974.5025157937 669638.8451598012, 655011.7994657549 669663.3244350266, 655022.4684653594 669670.3266076422, 655023.4464096173 669670.9687988132, 655024.2564528305 669671.4999309088, 655029.4101141463 669674.7174782322, 655035.1805532353 669678.2002169563, 655037.6323454027 669679.7273764923, 655041.992112399 669682.4438736163, 655042.1071234209 669682.5149267389, 655042.2381081898 669682.5959016779, 655046.7666691697 669685.376418472, 655046.8026988773 669685.3985025842, 655047.0836595121 669685.5714946439, 655051.8804020003 669688.5181221282, 655051.8843067525 669688.5200425755, 655053.2312496753 669689.3472363295, 655053.8091447832 669689.7023422995, 655066.6291733198 669697.3906875965, 655066.6410649267 669697.3977289557, 655068.292933381 669698.3758379092, 655075.9857624852 669708.8467503338, 655083.9916858174 669717.9045919734, 655098.4709559632 669731.8692469907, 655109.0268389928 669740.8294749049, 655121.8015120309 669750.8499595794, 655132.6527609826 669756.6780885159, 655148.8138907324 669764.0130798806, 655150.009815126 669764.5551383967, 655158.7853358996 669767.809120578, 655170.019983023 669770.5685203801, 655173.7807043911 669771.6398410776, 655175.0696896291 669771.7380742107, 655196.082349373 669773.3445706582, 655202.776304043 669773.856405988, 655204.7862883243 669773.9656051464, 655208.2432342436 669774.152096482, 655210.1802669632 669774.2572869656, 655217.3812476165 669773.9991087341, 655227.5491338482 669773.6344055423, 655235.0621587527 669773.3652246228, 655251.8930573587 669774.0081893887, 655278.3362179326 669779.3343472687, 655284.3641155208 669780.5480181425, 655289.4148707059 669781.8586916528, 655307.8582211451 669786.6418192983, 655317.5658735129 669789.1599204614, 655327.0834281872 669791.6471192988, 655328.2934011557 669791.9632615466, 655360.8193337056 669799.7960511198, 655413.2233718209 669813.6631758779, 655416.4184231582 669814.5715514517, 655420.8060498878 669815.819107246, 655466.0133373856 669828.6703438045, 655474.5379709204 669831.093426862, 655564.421309855 669856.6449712319, 655572.572143023 669858.9619317885, 655632.7967572438 669876.0819929101, 655661.5446587334 669883.6345087745, 655769.5394830788 669913.5291724361, 655824.6217728597 669924.6547166985, 655836.3745088007 669926.1671363941, 655845.7144514692 669926.5842382369, 655855.6356253946 669924.5983852642, 655877.976513675 669924.6650271887, 655878.116382461 669926.2050332285, 655882.5002114003 669926.5365894722, 655884.8841779778 669926.7168702147, 655886.4962096979 669926.8300201093, 655891.8401028956 669927.3967283105, 655894.2659504471 669927.6528534922, 655898.9730017047 669928.0114877933, 655906.0858515622 669928.5532854063, 655913.2407652851 669929.124207803, 655920.3076299905 669929.7779992308, 655930.2604263333 669930.6271706884, 655930.9304761504 669930.7002094489, 655941.4341856636 669931.9244835457, 655947.1990917202 669932.6162007493, 655948.4321582562 669932.7733055371, 655955.4619010701 669933.6400511732, 655962.618732349 669934.5130518092, 655969.4235863888 669935.3597710014, 655980.0265149453 669936.6080567515, 655989.8953184495 669937.7671362864, 655996.4740473125 669938.5410292561, 656001.646117392 669939.1435569967, 656003.4210820768 669939.3517655536, 656011.1927646867 669940.2656387113, 656015.5508302372 669940.8221504877, 656057.4738119124 669946.4885116833, 656058.1695966137 669946.5826733636, 656059.3863324021 669946.746976198, 656060.047682877 669946.8363338369, 656061.2647735518 669947.0015967074, 656059.9695975524 669948.2494351651, 656059.8692995028 669948.3462226557, 656058.4976129119 669949.6682915621, 656049.007644157 669958.8099878317, 656047.4444150563 669960.3158728278, 656039.5116398181 669967.9579232039, 656029.2255000897 669977.8675198477, 656023.6869188172 669983.202828759, 656016.3161678151 669990.303028985, 656013.3208959769 669993.1885751892, 656011.4226891393 669995.018418287, 656001.2265518189 670004.8400257272, 655997.7281921965 670008.2106290631, 655996.0900502327 670009.7894645792, 655990.9524837006 670014.738743266, 655985.8138523151 670019.6870617908, 655985.5999422646 670019.8940750812, 655980.6773509264 670024.6355405288, 655975.5397844808 670029.5848190925, 655970.4023955446 670034.5342576378, 655969.777174887 670035.1372195652, 655965.2648292677 670039.4825761258, 655963.3735370192 670041.381379436, 655962.086343173 670042.6762583256, 655960.253276916 670044.5200288888, 655953.0492894305 670052.364083106, 655945.9045940207 670060.1441446831, 655941.1409903722 670065.3315456234, 655936.3773867516 670070.5189465253, 655935.7942296517 670071.1539131802, 655931.954801321 670075.335350278, 655927.1911978654 670080.5217911112, 655922.4277718257 670085.7091919162, 655915.2828990577 670093.4903730855, 655908.138026482 670101.2704341704, 655903.3744231847 670106.456714806, 655898.6108197677 670111.6442753937, 655894.5031621037 670117.6115883132, 655890.3965695531 670123.5781013279, 655883.0642818856 670134.2291603135, 655867.435903667 670156.9332341674, 655864.671427874 670160.9488792467, 655857.303279634 670171.6528931851, 655849.4788830051 670183.0189284064, 655846.7154135248 670187.5435300041, 655806.0548571903 670176.8387771437, 655763.6857714701 670162.3747487988, 655723.7966591325 670148.2700569605, 655696.4531654381 670137.255970825, 655696.2490147253 670137.4898646767, 655692.2366537085 670142.0923907731, 655679.7949071333 670158.6787807789, 655621.5652437859 670236.3042001848, 655620.7250120919 670237.4250513044, 655617.5625213954 670241.974719142, 655613.118772306 670248.6790977443, 655609.7743470885 670253.21258376, 655609.3843211576 670253.7395731593, 655590.4374310854 670279.4161539985, 655698.0200904065 670328.1237851824, 655696.2064898551 670342.7724246988, 655767.623094355 670384.9146420811, 655817.1732204839 670390.4595953758, 655844.4766004476 670386.0409451226, 655896.6343937886 670392.7090079437, 656014.5418025615 670416.55483753, 656014.7732182991 670421.5329115682, 656031.4926943016 670417.1488450974, 656043.240872611 670415.0633732068, 656089.2309025106 670412.8008458117, 656104.4081893527 670409.1055513938, 656112.823476611 670405.4476942519, 656116.9577525889 670402.6080436637, 656117.8969734359 670401.9631993851, 656126.8734471374 670395.7963053673, 656130.6614846509 670404.7046958426, 656130.9583595856 670405.401846455, 656138.6632637267 670423.5205608641, 656167.6742992171 670421.151051015, 656185.9469398478 670423.9562056966, 656194.7110165049 670421.7711632148, 656205.4426318094 670424.4923981352, 656210.6328909744 670422.8040551585, 656212.1088487082 670422.3242306198, 656212.6518595376 670421.9602964045, 656215.9969057724 670419.7175016471, 656216.8810957039 670419.1248087633, 656217.0231057116 670419.0297859651, 656217.9534489339 670418.4058986715, 656220.5165525 670416.687169179, 656228.1070409428 670437.2159204972, 656231.5168199142 670456.9112283895, 656229.0581250717 670470.5689255976, 656227.3578933286 670481.0635335227, 656227.0527330673 670482.9434847133, 656228.3949268693 670497.2025814991, 656228.7955817122 670501.4590784541, 656230.1609989891 670515.966656221, 656230.3336702825 670517.8036235105, 656231.2699065873 670524.5427256128, 656240.4069200571 670532.9778135108, 656240.8507969282 670533.3877824732, 656244.1634568205 670536.4451079046, 656244.1764128385 670536.457109335, 656246.7260870305 670538.8114705941, 656247.4931545615 670539.5195552151, 656252.788440614 670544.4081393684, 656253.9384663469 670544.3082743711, 656255.7775476392 670544.1604901764, 656262.0864429695 670543.655150481, 656263.0504467033 670543.5842235545, 656263.563422596 670543.5444437376, 656264.9404705772 670543.4326053351, 656265.5755660663 670543.3886798098, 656278.8906002081 670542.1321635536, 656278.9745580261 670542.1241734145, 656279.3576044186 670542.0882184006, 656288.4585881543 670541.2932867861, 656289.1385717217 670541.4143253461, 656289.4726174653 670541.4734039453, 656295.2743989935 670542.5011142995, 656300.0222146865 670543.3426228751, 656310.3089416728 670545.1647714481, 656311.8598935609 670545.4399906487, 656315.8706194913 670547.2743662172, 656317.6124997326 670548.0696040845, 656318.1353832403 670548.3087034777, 656318.9253880517 670548.6708732074, 656321.6352916886 670549.937100835, 656324.1590101572 670551.1084273845, 656325.1371525961 670551.5654984232, 656326.0391522583 670551.9865608237, 656336.5184639358 670556.8808704785, 656336.6313469755 670556.9338432914, 656337.0324723755 670557.1209288314, 656349.9472967732 670566.1492541058, 656350.0342641819 670566.2092638457, 656350.6561692016 670566.6443734412, 656354.4339228937 670569.285756212, 656365.9517008606 670570.2602145661, 656366.2866376648 670570.2882534902, 656366.738722462 670570.3262260228, 656367.5546759989 670570.3952808359, 656381.6927179483 670568.0670282333, 656384.8838506743 670567.5413245849, 656385.1477963515 670567.4973557192, 656390.0239563994 670566.6950507874, 656390.9318796847 670566.5450778672, 656395.2029355495 670565.8385415995, 656395.9388593262 670565.7167083938, 656402.7269174061 670564.4024703124, 656377.9646179109 670604.0753018627, 656374.1228942786 670609.6957739756, 656370.5172838776 670614.9724360532, 656319.6680658059 670698.5939126607, 656311.4326362992 670711.0627834377, 656300.9727919201 670728.1494420012, 656298.0832840248 670732.8121118152, 656282.1883294444 670758.455035376, 656280.7812125141 670760.7249350892, 656278.5146352648 670764.6315229286, 656276.2493005282 670768.5382709075, 656240.7728683271 670826.4946796714, 656245.9715841038 670828.3803939624, 656258.0670732065 670832.7686566709, 656264.9427251839 670835.2624026879, 656279.862985398 670840.6751483736, 656279.9369991795 670840.7021968346, 656303.1808832353 670849.1338141738, 656309.5613343588 670851.4486236285, 656315.9421404293 670853.7632731363, 656329.581451076 670858.6669128275, 656341.4089314964 670862.918185369, 656353.4433663081 670867.2446816001, 656363.4856418638 670870.8378301512, 656376.5660344168 670875.5172462115, 656406.655586471 670886.2816478076, 656442.4928341781 670899.1038270325, 656451.7262808598 670902.4079232329, 656465.3196209619 670907.271398192, 656481.8598469435 670913.1883302958, 656499.228068712 670919.4023171724, 656508.9295588082 670922.8733470205, 656530.168507402 670930.4717766044, 656544.6398946992 670935.6494320115, 656550.4935732789 670937.5720228226, 656530.6731928784 671012.3514279305, 656528.9702346335 671018.7764643279, 656511.2211154273 671085.7416049497, 656493.8513414576 671178.4461305003, 656517.3229349748 671185.3448274167, 656516.2417776319 671277.0434247813, 656516.2347859003 671277.6392598333, 656515.5959206374 671278.0559822146, 656514.4626870531 671278.7756846679, 656511.7350414565 671280.5380734104, 656507.4930357058 671283.3428377935, 656501.967787485 671287.063605721, 656497.637023577 671289.9464391724, 656492.8207631182 671293.1600935307, 656492.2189974296 671293.5565003955, 656489.4673924986 671295.2950464835, 656488.1980119218 671296.0646526667, 656487.6944154872 671296.352431767, 656480.7140195331 671300.2119084996, 656478.9840152493 671301.1137798857, 656475.4466091624 671302.9883931094, 656473.2045009052 671304.0718033318, 656468.59442259 671306.3717281304, 656467.6522089456 671306.8202548043, 656460.8868427417 671310.001041377, 656459.5668973497 671310.5800829624, 656455.6537472135 671312.2832934116, 656449.7381483187 671314.7555442908, 656449.1058694937 671315.009708569, 656446.9450864937 671315.8273701415, 656445.5342795359 671316.2550421477, 656443.0637694255 671317.0073879444, 656434.2368516006 671319.3962588583, 656430.9752156619 671320.2821111871, 656425.701751599 671321.6958845176, 656417.3832313746 671323.7228974, 656416.8830263742 671323.8479580218, 656413.0700589293 671324.91950456, 656408.0017809132 671326.3523417508, 656405.5486681239 671327.0843697132, 656402.1221133445 671328.1679613646, 656400.5826136736 671328.6512979231, 656398.2574722383 671329.4265405509, 656394.5802385852 671330.9512198041, 656392.4984244129 671331.925209509, 656389.6469208526 671333.3850257973, 656387.0893592306 671334.8053567414, 656385.1571632437 671335.891363165, 656382.9420123764 671337.1709351427, 656380.6327681489 671338.6162549537, 656377.326081623 671340.7414533555, 656375.786170317 671341.713746452, 656374.8228137878 671342.3411493936, 656367.9230788737 671347.0839892564, 656362.7826567059 671350.7393624795, 656361.7206025795 671351.440353357, 656358.0193138125 671353.7683841686, 656353.7408246076 671355.8870689195, 656350.3410166984 671357.4514603149, 656343.0334330602 671360.3000256306, 656338.4805616466 671362.1585200889, 656332.8422771716 671364.1076069388, 656331.8769979909 671364.4785314362, 656330.8790658775 671364.7910525189, 656321.6594581003 671367.7417934366, 656319.8719909014 671368.2468608149, 656314.2032034254 671369.9666656863, 656308.7266570758 671371.5972136618, 656305.1671615992 671372.637590012, 656299.3249779287 671374.1880957704, 656296.6469715685 671374.950977171, 656293.3493091856 671375.7933055275, 656289.2377984496 671376.7937775721, 656285.9216859519 671377.5524243537, 656280.1894176011 671378.7255055956, 656274.6461788617 671379.9520485735, 656249.7597951097 671384.9574223419, 656245.7330245614 671385.3244687184, 656240.8173445882 671385.6706112531, 656229.5264336122 671386.2067266715, 656223.7429889806 671386.3378891965, 656211.5887897719 671386.3220674889, 656210.2114043088 671386.3055864748, 656206.9848510385 671386.2005697635, 656202.5656986661 671385.9877342939, 656195.2657897018 671385.4115243772, 656189.4176291849 671384.9281236366, 656185.5595602152 671384.6195144762, 656182.0683615528 671384.4738270927, 656176.032327861 671384.6817595348, 656171.9752064275 671385.0377624321, 656166.6982290798 671386.1478573417, 656163.205126301 671387.1202418947, 656152.7170000318 671390.9199287295, 656149.4132269113 671392.3648052072, 656139.9096438006 671396.5119644849, 656133.3991791717 671399.490368345, 656129.7886627043 671401.156806719, 656122.2483049548 671404.5298051436, 656114.8398841348 671407.4234626276, 656107.5609165482 671409.8265789592, 656097.5322214017 671413.0514420572, 656088.2084414262 671415.6898331981, 656087.648592049 671415.849285407, 656082.5929238524 671417.1451542412, 656074.7343003214 671419.073644831, 656061.372729621 671422.144192039, 656056.851711871 671423.229885996, 656052.979983662 671424.3223772262, 656047.7004407264 671426.0418963752, 656044.8003414716 671427.1261833606, 656044.558753675 671427.2304738542, 656037.9860296933 671429.8413526972, 656036.7083298028 671430.3967166162, 656031.3693588927 671433.3589398853, 656030.4102669882 671433.9335416103, 656022.4160465307 671438.8341549492, 656017.1240924804 671441.972702536, 656011.3346267022 671444.9625519098, 656007.4402689656 671446.8111950383, 655999.2379414415 671450.8556295662, 655993.426666295 671453.6469177015, 655987.4503503565 671456.1522681955, 655979.5082009794 671458.9623423902, 655974.1140703367 671460.0207324519, 655970.2333421843 671460.6594657288, 655956.4721688625 671462.655412609, 655953.8994727759 671462.9127043755, 655943.4995535131 671463.7097000757, 655935.0369097161 671464.0346098557, 655890.9553312003 671465.1378318272, 655878.8974484593 671465.4840732467, 655869.6341289295 671465.6272889475, 655866.7787077453 671465.6215089028, 655861.9294556116 671465.5105321407, 655859.0218625555 671465.3941866719, 655856.0441697589 671465.1715936122, 655847.0172224242 671464.2620449835, 655843.132706111 671463.8883050048, 655835.1118008487 671463.1577549246, 655831.8598836755 671462.8384899033, 655820.7247965763 671461.8203709892, 655814.2837813413 671461.3156074759, 655806.1899278866 671460.494482714, 655802.1280780773 671460.1643940523, 655795.6636015248 671459.8328949591, 655787.6573410188 671459.7386477896, 655779.2552153507 671460.011390134, 655771.9663393712 671460.4943455477, 655765.3379071837 671461.2383389216, 655760.3307660262 671461.8534092112, 655757.6783386304 671462.3679648734, 655750.8087515773 671464.0757621574, 655747.2344922057 671465.2794814265, 655744.9560195848 671466.1033600643, 655737.260718129 671469.2534471375, 655734.4601718442 671470.5020596001, 655729.750147286 671472.8009936601, 655728.5278168432 671473.4259613934, 655718.6649866337 671478.8772098047, 655712.9497007369 671481.9064170349, 655704.2469122437 671486.6154109575, 655699.031665172 671489.4357201078, 655694.9634946121 671491.5820927848, 655688.385155693 671495.0671923084, 655678.2333499907 671500.5769651406, 655668.6735666752 671505.5392934529, 655650.8420434449 671514.5058726047, 655640.4895923192 671520.6327365047, 655609.4969468904 671539.6947707059, 655587.7290376972 671534.7280145024, 655459.3140364416 671639.761279564, 655451.9053645063 671651.0055753865, 655446.9955323024 671659.0633183934, 655439.8719654627 671671.4975593159, 655435.7756196269 671679.4821212537, 655435.5383433921 671682.2463908911, 655435.0900572625 671689.3635608958, 655435.2080197287 671696.2476812195, 655436.8228058118 671711.1496797835, 655439.2741300628 671723.0681229327, 655441.6765060128 671734.6233630149, 655442.7350466306 671739.8578500582, 655461.5385402011 671765.220726197, 655454.8997211455 671801.0396424714, 655449.7599986197 671817.1348945401, 655445.652711079 671827.6752750077, 655441.7478209528 671836.2318554937, 655435.8180325722 671843.1290039663, 655433.8239587627 671845.1157871188, 655419.030341888 671858.7308503814, 655413.9008490428 671862.9309967774, 655407.854553616 671867.3035108779, 655402.7261044392 671870.1326281368, 655392.6617208432 671874.8625732093, 655366.8838904088 671882.5573135976, 655357.2066234417 671885.8061970879, 655346.0960711307 671890.4356962835, 655322.1627938721 671902.9458218697, 655331.1138160804 671919.214777152, 655368.819099742 671976.5198499905, 655364.8582514251 671979.2409492644, 655363.3967866017 671980.245084505, 655355.1679924523 671985.8971642164, 655345.3815511095 671992.3578490413, 655300.3413036996 672022.0952040064, 655292.5228067439 672026.8932599529, 655282.1041411207 672033.1039500555, 655276.8249577722 672036.2493663263, 655271.4884324884 672039.47957498, 655266.2589579745 672042.5448378858, 655261.2667686028 672045.8832473522, 655255.760387252 672049.0195161669, 655235.0211946532 672061.5919468444, 655229.8678548046 672064.764258073, 655224.754468541 672067.7936952587, 655219.5340416005 672070.8989588814, 655214.4257864112 672074.0714355463, 655204.099940491 672080.3810159004, 655198.919806849 672083.4763644544, 655193.7725006834 672086.6577962957, 655183.5018726885 672092.8275443843, 655180.9446506925 672094.3091431466, 655174.2101789945 672098.7642927833, 655163.8914519005 672104.9039552377, 655150.0496593991 672113.1402946304, 655139.7389099569 672119.3629973332, 655127.6292885231 672126.6334672868, 655114.9214627986 672134.336816646, 655106.3519464012 672139.543730481, 655098.6295105309 672144.2467516358, 655087.5077312908 672151.0983012721, 655051.6025893803 672172.9699111838, 654953.5357740233 672232.5010114752, 654832.5586244535 672019.2546108117, 654830.8106110956 672016.1734638205, 654294.4281947133 672316.8402145957, 654296.539704684 672320.2310001163, 654382.8662065283 672458.8539712342, 654301.0362734813 672490.0766118631, 654288.8297034072 672494.3522200653, 654267.7417865886 672501.739048488, 654251.9943214979 672507.2551784338, 654242.5744207265 672510.554972003, 654241.4586150219 672510.9594737216, 654240.0012868561 672511.4877731138, 654234.4309560716 672513.5074027936, 654225.6419002143 672516.6939936165, 654218.1005373262 672519.4279390932, 654212.2105963466 672519.253149379, 654205.9906852172 672519.0683999042, 654199.7537342107 672518.8831683747, 654193.3620043086 672518.6932781946, 654186.0009537307 672518.4747912111, 654185.7577838805 672518.4366821771, 654178.8654268152 672517.3614592387, 654094.2074832934 672502.9459207545, 654078.4511609443 672500.3176405859, 654075.3109012608 672499.793745858, 654060.1556412226 672474.4332538726, 654053.8685460902 672463.9125849783, 654051.2991904758 672459.6134315901, 654046.741978644 672452.4626965666, 654041.8398631033 672445.449348868, 654023.3063982831 672413.7052428762, 654014.5863708177 672396.203191544, 654005.8672313384 672378.7054601834, 653997.710108611 672362.333548783, 653976.8488111391 672320.4663598144, 653973.9425510144 672314.6330892466, 653970.00631391 672306.7336285908, 653960.7924215285 672288.2416038499, 653959.7088517576 672286.0674080336, 653949.638007082 672265.8564115559, 653932.4648687042 672231.3905756172, 653928.6526781576 672223.7391280178, 653927.9371604249 672222.3030515112, 653926.621296369 672219.6619508133, 653906.9167001126 672180.1168036018, 653902.1377103264 672170.5254124617, 653889.2937108398 672144.7480385762, 653880.5877174996 672127.2749472011, 653880.0908304598 672126.2779340384, 653872.4897370603 672109.5131299477, 653865.320483935 672094.599402582, 653862.2772780448 672088.2691568696, 653857.5404252019 672078.4145699245, 653840.3145232074 672042.578806655, 653830.6768493906 672022.5297755538, 653825.1872264507 672011.1101482954, 653763.0633821078 671881.873425662, 653752.7199700477 671860.355360238, 653750.0213878537 671854.7416318599, 653746.8193614234 671848.0806496927, 653669.6587595412 671796.058056412, 653632.1716791161 671770.7839979981, 653526.9973081021 671699.8745324506, 653440.4785282024 671631.8942490135, 653375.8677536424 671581.1336715125, 653345.3429773558 671557.1524735305, 653343.1247955943 671555.4098121863, 653341.9608596198 671554.5197548511, 653336.9634602955 671550.6980452128, 653331.9639312588 671546.874575341, 653214.763873602 671457.2466723076, 653054.0208637145 671326.5357953028, 653025.0747599505 671302.9975179872, 653040.6925422117 671279.2636617044, 653051.4605240868 671262.8990595418, 652982.9488750804 671195.091125049, 652980.8081719182 671174.6731137903, 652979.6036063362 671163.1759425242, 652979.2679799987 671159.975928213, 652978.3610982005 671151.3229296028, 652959.2939165166 671153.1135056904, 652950.7139580052 671153.1124398697, 652933.6312410857 671151.7173697711, 652919.4425008111 671150.0796212768, 652902.0873101443 671142.8156078322, 652905.7267448856 671130.9470419168, 652931.7301435991 671059.7360028903, 652935.4092792872 671049.8464645877, 652939.0074496524 671040.1719543526, 652956.4210799149 670991.7356518429, 652963.2092929892 670972.8555366134, 652948.090831187 670960.636804882, 652844.1952865701 670876.6665541539, 652811.4885499608 670850.1052855786, 652790.2366805238 670832.8463183306, 652759.5708146475 670807.9417699202, 652657.9022443993 670725.3917980671, 652651.0588974457 670719.8349979116, 652646.555649343 670716.1786313909, 652641.7547641337 670712.2809500826, 652568.0593235652 670652.4443356334, 652545.3255759864 670634.8127118653, 652540.0940988112 670630.755138939, 652520.6491057818 670615.6727009376, 652507.2757929466 670605.3000944062, 652501.5118686624 670600.8294992731, 652482.3737451932 670585.9862977857, 652462.8426765146 670570.8380905745, 652457.7782117246 670566.9104575424, 652435.6003474473 670549.7088208328, 652205.692965699 670363.1282258579, 652199.5487253526 670358.1414385941)))" + } +}, +{ + "model": "common.municipality", + "pk": 9, + "fields": { + "fid": "", + "name_fr": "Watermael-Boitsfort", + "name_nl": "Watermaal-Bosvoorde", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((651655.290035311 666850.6427565103, 651668.6459961776 666832.5717933584, 651675.2199459113 666823.6796568527, 651682.4940564134 666813.8388256336, 651693.3586212338 666799.1394928406, 651702.243594999 666788.6758411875, 651746.5241321537 666736.5282920401, 651781.423480039 666651.2310848306, 651789.5332333223 666635.3464633292, 651797.8760935294 666619.0028401931, 651805.5248201018 666611.3498743849, 651823.1547223874 666601.0563311443, 651842.3644645375 666592.8700027632, 651892.523011137 666565.037940811, 651894.7866078336 666563.7662562155, 651900.9064132767 666560.328069042, 651907.3128958689 666556.7289619847, 651940.6537762454 666536.0907450435, 651957.1667019437 666526.3827567002, 651973.1536589757 666517.024780594, 651985.1977594354 666506.8242746582, 651995.53390372 666495.6456633563, 652001.8296303563 666487.6954869773, 652003.2340262098 666485.9217596069, 652009.857991774 666477.5897075303, 652014.1846147903 666472.1483804503, 652016.9004030119 666437.7591590505, 652014.1050608056 666395.4692557683, 652013.7639708853 666390.3094568712, 652012.6379310323 666373.276632591, 652013.1214074614 666368.9157977299, 652013.8252109837 666362.57005984, 652018.4148489117 666365.1564766483, 652018.6572934525 666360.6984950412, 652014.3118331073 666357.0970827977, 652015.2109635635 666346.9914268126, 652011.8463882463 666326.2132794178, 652010.2297168947 666314.0193618435, 652009.8575383826 666307.548379995, 652010.0728614336 666304.3194959314, 652010.1320491654 666303.6195138586, 652010.356460153 666300.0826357, 652011.3569348791 666296.1997737618, 652012.4723027319 666291.8698922786, 652015.334964214 666286.0223190001, 652015.6008505153 666278.3815101145, 652012.5684799071 666221.2830125596, 652012.3469102558 666217.11009223, 652012.214252378 666214.6070760533, 652011.8937651742 666210.875097244, 652009.75759233 666186.0441978729, 652007.2991315931 666173.100274179, 651998.6120385156 666112.6811702502, 651997.0312560231 666094.0323192467, 651992.9602127838 666065.7593294102, 651988.9927457223 666038.2061806794, 651984.121117676 666008.9670335096, 651976.1569955946 665961.1692368321, 651948.7908079902 665890.6909545795, 651950.0431970163 665877.8350909646, 651953.626730942 665841.0594813807, 651960.0799821176 665829.8663059177, 651970.416409653 665809.5463770231, 651973.441002061 665803.5448055982, 651977.626622346 665790.1323243357, 651981.8625291507 665774.8036876764, 651983.6794021314 665767.4930145012, 651990.3367523581 665738.9027312868, 651993.7416282202 665723.4290378868, 651995.6646399212 665714.6962962807, 651981.8403325729 665701.5687523119, 651989.5244956182 665683.3766768505, 652001.362783245 665655.3489201245, 652009.6249730937 665645.1970785, 652015.0806705861 665638.4935514263, 652032.5779976841 665619.0115831747, 652074.2457660554 665560.5144966788, 652100.5519946001 665523.3994706324, 652107.9412215267 665511.980328222, 652116.4986702031 665499.7034021668, 652125.4981289394 665487.1063678579, 652133.8666724068 665473.7473395064, 652145.7721321677 665452.3908008875, 652157.7377547416 665431.1101096821, 652169.3449414346 665411.8434579992, 652182.2670153115 665394.2249616096, 652191.5744143487 665381.7860450143, 652199.7548705377 665369.3839566561, 652200.355990841 665368.472986594, 652218.6234727767 665337.3211906546, 652238.7835833691 665302.0804956091, 652255.4671387647 665271.8054766706, 652269.0063521373 665244.0929711687, 652273.8535851638 665234.4375351515, 652272.7605086067 665225.9676431487, 652270.6864300284 665209.8923526225, 652268.4200947377 665180.8231135457, 652266.7107736762 665158.8947429714, 652265.8322805299 665146.1247140288, 652250.8366655859 665119.7629419086, 652258.4566452763 665118.5627156874, 652259.6395317551 665103.5884475497, 652259.9829848919 665099.2400058398, 652262.425733098 665068.3122782565, 652279.7640747933 664848.7502156927, 652280.7286163134 664844.2263271185, 652281.384656013 664844.1833642668, 652285.2087143968 664843.9318139775, 652287.7096882414 664843.7681080914, 652296.0726002936 664843.2191633502, 652297.1216258436 664843.1503241378, 652299.7038416301 664841.9725410305, 652302.8998816927 664840.5149885116, 652304.0669998551 664839.9831227128, 652307.7104117011 664836.4365008919, 652312.4169882803 664831.8549611651, 652315.5361848064 664828.8183592629, 652321.333985697 664814.2710215383, 652329.2652576416 664794.3719280002, 652337.5377854174 664773.6159140555, 652349.2240534583 664778.5772630646, 652366.5569746783 664785.9362235179, 652393.3435230327 664797.3093941623, 652399.2635144795 664796.8500763327, 652409.2015810452 664796.0801815372, 652410.8684866978 664795.9514136224, 652424.3805228127 664794.9028907203, 652424.7484810676 664794.8739731241, 652431.4626089369 664794.3537868669, 652431.9805549261 664794.3136865542, 652433.0606445193 664794.2298110271, 652436.584558715 664793.9572571313, 652436.8619534998 664791.3142486273, 652442.4763132425 664737.7939266241, 652444.5186570056 664718.3311189656, 652445.1518651566 664716.2793516924, 652447.732820139 664707.9046081663, 652448.9543118988 664703.9447485068, 652451.9225428 664694.317089605, 652452.0164766883 664694.0100604002, 652452.8869162047 664691.1871204488, 652464.5447070594 664683.5635052104, 652465.1127476026 664683.1925307448, 652467.6069783228 664681.56193851, 652462.5374224298 664661.1293504171, 652461.8857872253 664658.502234799, 652460.2385141271 664651.8639636101, 652460.263786632 664641.0190050113, 652460.2701320598 664638.4490853854, 652460.2817598757 664633.2930860042, 652408.8855487512 664613.1820229157, 652388.7306420456 664605.2956913464, 652353.2066766184 664591.3955804801, 652346.122301516 664585.9267595352, 652341.6367967807 664582.4633596807, 652334.3665980921 664576.8505169689, 652337.3893986845 664575.3942260065, 652383.3910298533 664553.2350596996, 652378.3641234097 664548.5583168275, 652370.5465420614 664541.2855702083, 652364.5663976724 664535.7219965461, 652326.7059349474 664500.5004821991, 652271.3395291634 664448.9920813387, 652252.6187629958 664440.1948280262, 652139.190190439 664386.8974109971, 651990.5218413228 664317.0407516889, 651987.1660688806 664315.4643524702, 651978.8216313663 664311.5422397384, 651979.3433227752 664305.4965304965, 651980.5811611821 664291.1606509668, 651982.6099786906 664267.6667684794, 651983.246851055 664260.285870484, 651984.9702220305 664248.2639725618, 651987.019046015 664233.9681890628, 651988.263120552 664225.2894405486, 651988.5955951072 664220.7353916893, 651989.0073659349 664215.0935102552, 651989.4120243423 664209.554508166, 651989.9342793459 664206.1785638544, 651990.6199606615 664201.7496769829, 651991.6326729717 664195.2077049938, 651991.8727336127 664194.5258121515, 651992.2148914258 664193.5508108563, 651992.9551383398 664191.4419345956, 652062.8675218774 664074.3696765187, 652063.2437161373 664073.7509999529, 652087.2613836343 664034.260813699, 652087.5164997239 664033.8418031706, 652089.683833003 664030.2780535424, 652093.4822824255 664023.1034507821, 652159.679569692 663912.2525409878, 652161.712866277 663908.8962962239, 652202.6489491987 663840.3090328274, 652213.4290214189 663822.246280869, 652217.4357973144 663815.5337848234, 652649.4582499157 663127.3377663621, 652652.6871922524 663122.3947706321, 652658.1152469369 663113.8373023146, 652659.0863386217 663112.3954920152, 652663.4832285896 663105.9268689149, 652664.466399501 663104.4080998163, 652684.4950013809 663073.4593194932, 652716.3486385337 663024.23603509, 652867.132560031 662791.2359653643, 652867.6117161495 662790.5120196687, 652877.7483791406 662775.2042086758, 652877.8493941907 662775.0522201277, 653005.1790362343 662604.6737723238, 653001.2120992553 662603.4083419598, 653094.4983371319 662427.7500639702, 653368.6906804441 662535.968077601, 653990.4414174347 662781.3600739626, 654283.1734369079 662936.3820941829, 654323.0235988448 662957.9166745843, 654543.4823068792 663074.6812406611, 654644.552858278 662891.8323851496, 655831.7363106239 663572.9592443488, 655846.1400099968 663583.1640726936, 655944.2399307643 663652.6660454591, 655995.3278542128 663683.6458960176, 656220.5202407779 663844.8962862621, 656577.7258519033 664097.7678318567, 656614.0676653021 664123.28595729, 656667.017903514 664160.4688132696, 656672.370312456 664164.0404497137, 656677.7299011156 664166.9439282025, 656684.7646982787 664170.0687150264, 656697.6920109331 664175.4682415957, 656761.1762514064 664202.4603057246, 656780.6261334324 664210.696562663, 656845.4385123986 664238.4017376156, 656988.4183356682 664299.3547077829, 657158.7537871898 664372.1791389063, 657162.2991483931 664373.6948921384, 657155.6322512014 664375.5064527595, 657088.1664431106 664393.9886872441, 657051.165523175 664404.6783895842, 657027.605785798 664411.7337828927, 656949.4494346569 664435.8639064651, 656908.1833470535 664446.9411255007, 656896.8350271011 664449.3094356231, 656889.4165304091 664450.2071552349, 656880.8020757145 664450.8540210739, 656871.5200680697 664451.3280123118, 656867.6100994456 664451.5155705508, 656849.0340605205 664452.0734732117, 656819.9282738841 664452.4532291517, 656765.2324915043 664452.6259400818, 656663.5791015105 664453.5485521073, 656632.6492186171 664453.4210645643, 656604.6033199892 664453.4478215876, 656598.4183523812 664453.5952834326, 656584.687401962 664454.9935687705, 656574.334177949 664457.0506321163, 656568.8626566819 664458.0352907199, 656563.8742587803 664459.2619227392, 656556.3795683135 664462.0493852134, 656533.3949116387 664469.2387613113, 656466.203762702 664491.6350847094, 656386.6889859701 664518.9810353486, 656353.504820705 664529.7122847335, 656347.3295526623 664531.8854985227, 656337.1206305053 664535.478731001, 656336.7892261727 664535.5953330994, 656326.508946525 664539.2131973958, 656305.2419845718 664547.5818011643, 656284.6182660626 664554.5613636279, 656259.6444852947 664562.3505116951, 656218.7490926994 664577.2658315721, 656197.9694017672 664583.8314579597, 656143.6605163389 664602.3452722601, 656123.4528896463 664609.0069807683, 656108.760308937 664614.3753923742, 656104.29193395 664616.0080049885, 656099.3896609909 664628.8721985118, 656067.1447423813 664713.4851706789, 656025.6334916004 664819.0070953226, 655978.6953018152 664940.1833877095, 655958.207102097 664994.2288581193, 655936.0134668486 665051.4741247274, 655883.0248838635 665184.453539037, 655808.8541756256 665370.5954933944, 655808.4651593266 665371.5714462227, 655729.2892803929 665339.7278108634, 655705.8987552216 665328.249279457, 655690.0121457838 665317.0565342868, 655655.8415158195 665300.0749155, 655639.6623213616 665293.9280358301, 655617.1349641287 665288.5156522457, 655402.8910918294 665256.0770951222, 655401.1450638747 665255.8239433619, 655271.3420822507 665236.964725402, 655270.0380675575 665237.8616979837, 655215.8287937165 665275.1574916055, 655122.1355250403 665339.6169926245, 655120.4324597502 665340.7876804468, 655023.1768416153 665407.698632352, 655020.6937812794 665409.407152493, 654959.4952544825 665451.5106576099, 654954.5978314398 665454.8800259903, 654953.5244066336 665455.6184650905, 654952.6925778338 665456.1908514034, 654939.7797050008 665465.0753171543, 654507.7773659754 665762.2867910245, 654501.9891459296 665766.5362217505, 654494.366312006 665772.132326765, 654490.2490372912 665775.1549050043, 654488.9469673578 665776.1107589809, 654481.3821805596 665781.664950655, 654480.3341511071 665782.29171357, 654473.8658002914 665786.1570709798, 654469.5254603645 665788.7508261409, 654465.186185477 665791.3441014425, 654455.3333906907 665798.75803634, 654453.884157871 665799.8487538076, 654453.1361095938 665800.4107499133, 654449.1278199108 665803.4272603934, 654431.57229221 665816.6372918918, 654424.8888508762 665821.6655026061, 654391.6729456024 665846.6598599032, 654322.8271066829 665898.4631895258, 654282.7837929721 665928.5935880897, 654264.0630547219 665942.6806137757, 654275.785887657 665961.2967267076, 654257.90313315 665967.867706356, 654273.5074165564 665999.2013284501, 654279.6988325429 666038.1835644419, 654267.4990276567 666045.2731946575, 654259.0214528703 666050.2002147994, 654253.7211492826 666053.2796021038, 654248.3317351188 666056.4109790424, 654224.2463203981 666070.4081937089, 654207.6177390434 666075.9443918032, 654199.0750762712 666081.9344328999, 654200.1067515004 666093.715386519, 654195.800110963 666116.4476423425, 654194.4819078506 666126.4253160186, 654193.7719285531 666135.4401862929, 654193.061954254 666142.8851525112, 654193.162868213 666152.7610631405, 654194.326864546 666160.763029751, 654198.7316649985 666162.4354971701, 654197.0089103341 666169.6272346008, 654200.2989376697 666178.0854305523, 654213.8927245857 666196.2408220429, 654236.8812924699 666216.5061874086, 654242.8359769713 666227.6636905245, 654251.1289758511 666253.0633881635, 654251.6715256755 666256.6744516114, 654250.8707569444 666263.3861358834, 654247.8754488843 666274.1677773481, 654246.8078888562 666279.4405666242, 654247.3582361469 666284.6856944781, 654247.6729491494 666287.6775390529, 654248.6352740031 666292.9036725354, 654256.0030988553 666320.247249961, 654261.122991356 666354.4185115905, 654267.0809687758 666381.5208173386, 654267.2738082914 666382.3977898366, 654267.1365531516 666388.9196292848, 654267.1299491655 666389.2337054068, 654266.4368326962 666390.6105750602, 654261.7611377361 666399.8941255363, 654256.2705340008 666410.7953295638, 654256.0175392305 666411.2992965525, 654247.6206047544 666427.972121108, 654243.6462619371 666430.691814529, 654236.4022401058 666440.7758722682, 654225.7774777319 666463.8773443805, 654223.7552298185 666466.9401297956, 654223.6945098866 666467.0713217546, 654209.6337392633 666497.6492967922, 654221.6499064746 666546.3910931115, 654222.2720710671 666579.7169049598, 654223.3559155811 666590.099960085, 654223.2280846166 666596.4787614257, 654222.178502971 666601.3145961921, 654217.9488184446 666607.9340603465, 654210.941303145 666612.7743544289, 654208.8023222918 666621.4169096461, 654171.2163225211 666630.9516844035, 654151.4884964337 666635.9564851209, 654151.0828991353 666636.059479075, 654132.0404170877 666640.8912371931, 654115.2820602407 666645.1434143223, 654102.1497610097 666648.474882987, 654089.1383421678 666651.7762852011, 654087.1604119637 666652.27814054, 654068.9889438255 666656.8880763799, 654062.3067952263 666658.5833172459, 654061.156744989 666658.8751865883, 654059.3776195477 666659.3269844548, 654052.8905493297 666660.9721644607, 653992.6192251572 666676.2631890818, 653971.706762723 666681.5687399304, 653948.2812149967 666687.3659973344, 653800.8938534653 666723.8407252644, 653784.0955159186 666728.6617874962, 653780.4598071988 666725.6063651599, 653772.3577794982 666718.76951366, 653767.703147504 666714.8739842452, 653763.1887285281 666711.0735099094, 653763.0605869137 666710.9654964493, 653759.8199521935 666708.2401959802, 653736.4113368196 666688.9638133628, 653731.6688439313 666685.058194738, 653719.2423514844 666681.8038496205, 653703.5384569385 666682.1599686864, 653696.2725067459 666682.4812105587, 653689.0063796345 666682.8024510606, 653683.5794716269 666683.3237408344, 653662.0294106703 666684.291170746, 653568.2168623789 666685.8944400861, 653562.8014221191 666682.4239378903, 653560.6467488906 666687.1516344323, 653541.2946510714 666724.903842045, 653538.1389297592 666730.7754092513, 653536.6096254461 666733.6212794689, 653533.081774358 666740.2897542192, 653532.624785884 666741.2096906892, 653525.487139984 666755.5547783732, 653510.816614683 666786.824715776, 653505.8497297082 666793.9379782679, 653499.007791504 666803.7370722167, 653491.6729562057 666819.5160374503, 653479.4405615675 666841.5383617901, 653478.8457947585 666847.3923029384, 653478.4504098826 666851.2762107177, 653479.3015839321 666866.0090924335, 653475.2392099526 666887.1043700005, 653467.2045092433 666885.9344953009, 653466.1184110624 666885.7902114936, 653458.0955903557 666884.7273769367, 653457.2750261779 666884.6188033975, 653448.9928836739 666883.5223393281, 653441.7070266808 666882.5554288898, 653430.4152616552 666881.0581419244, 653425.8653774109 666880.0317085198, 653415.335716375 666877.6554804621, 653413.4178924755 666877.2223040962, 653409.8239614975 666876.6708554735, 653385.8705752783 666872.9911723558, 653354.3208369364 666871.8376208646, 653337.6446701041 666874.7356579421, 653330.9755523362 666875.8948728346, 653308.3139380317 666881.9590582455, 653305.9158554326 666882.6008530552, 653298.8956839353 666884.9579321663, 653272.9388516536 666893.6879420236, 653270.9117368729 666894.3696190839, 653261.3305129913 666897.5934306935, 653256.7883203559 666899.1208862232, 653249.1680864545 666902.4860425736, 653247.7975777284 666904.3714602753, 653245.3324373309 666907.7624923412, 653242.6767345493 666913.6040313458, 653240.6478359816 666912.8289264514, 653238.1599459125 666911.8785706897, 653233.7087721893 666922.9329558117, 653223.5964899806 666942.8684927509, 653222.1211386824 666945.7762850479, 653220.0511436281 666955.0149680513, 653215.169712194 666976.8101256597, 653114.5188141903 666989.5382996383, 653089.839354776 666986.7554759661, 653077.2299224694 666983.0041439388, 652979.7453515627 666942.6533882472, 652956.8965364059 666932.8607951602, 652892.8202109532 666905.0836784126, 652870.301308033 666897.321340418, 652854.891828302 666893.5996098125, 652829.2522452112 666891.0866002672, 652697.8512367455 666885.7673252219, 652650.6343019303 666883.8559748147, 652657.4263564444 666952.4559418306, 652657.7654958512 666960.6657840514, 652651.6477275129 667027.5841205154, 652647.6963164698 667040.8434728417, 652645.7841181991 667059.3229869357, 652645.0404602797 667091.0624470104, 652644.3568317075 667122.5019183918, 652646.4651286813 667136.6219620011, 652644.9124440079 667160.5715196616, 652646.3015324288 667168.4014895544, 652646.9604484251 667177.2715196856, 652611.7863183757 667178.0803741267, 652603.2706164973 667178.2763401383, 652583.0915045071 667172.0480013768, 652574.4058927775 667169.5460667736, 652561.120402112 667165.4996194416, 652551.328819974 667162.7874795235, 652522.3684045571 667160.3771051882, 652482.0498157109 667158.2204461345, 652458.0143643382 667155.0797763658, 652423.4659727772 667143.0978557738, 652421.7759976179 667143.1818179877, 652408.1358347614 667137.8502312973, 652398.5451236407 667135.6341092295, 652388.9544124809 667133.4181472781, 652364.0546760134 667124.4973065094, 652347.7818911653 667114.2054902866, 652324.9558805984 667099.0541073086, 652311.2730174109 667089.9726557642, 652291.9246389859 667067.9928237256, 652284.8491348586 667063.8509311393, 652282.5962974533 667062.5308448889, 652274.7036306455 667061.1168131876, 652261.0957991184 667060.2672170923, 652241.9046709428 667063.1769031959, 652208.4118101809 667064.2098867344, 652189.5343611345 667060.6227497493, 652185.2065079447 667059.7992107003, 652179.8819015961 667056.7517481368, 652175.4951358957 667054.24118822, 652174.547178418 667053.6991645099, 652170.9434849462 667051.6357303102, 652149.3211461166 667039.2594455505, 652117.0956248022 667011.0480745826, 652086.1644692833 666988.5946897321, 652072.735736552 666986.1821011163, 652064.0819719168 666984.6281431967, 652056.2495777176 666980.8023178766, 652048.4590710083 666976.9953773081, 652033.2037294023 666972.7907614679, 652020.2508771289 666971.8591663613, 652014.8488118587 666972.493558418, 652009.5708198363 666973.1129253553, 651999.8387954201 666974.254798028, 651985.6533493733 666970.6221399393, 651977.0805981265 666968.4261221645, 651966.2600710548 666965.6548081017, 651947.2329770931 666959.1067448347, 651819.721827375 666912.0741401433, 651812.3662826426 666909.3202718162, 651805.2767954965 666906.667396178, 651723.5862579927 666876.2211344099, 651716.9476077508 666873.6414342445, 651710.3110875771 666871.0615742297, 651675.6438927613 666858.1953595735, 651669.2792743595 666855.8334507425, 651658.9894619624 666852.0150551837, 651656.2209710379 666850.9878319427, 651655.290035311 666850.6427565103)))" + } +}, +{ + "model": "common.municipality", + "pk": 10, + "fields": { + "fid": "", + "name_fr": "Uccle", + "name_nl": "Ukkel", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((645985.1148746058 665229.3756044785, 645987.7616366361 665227.2140042525, 645997.8609822963 665229.9041234461, 646010.7710188972 665229.6157060278, 646022.1011261665 665227.9669524862, 646031.2680863045 665227.5701004773, 646034.7710383422 665227.4185085865, 646033.911513756 665223.6083498421, 646039.4219433932 665220.7490063903, 646046.5719155711 665219.4698447669, 646057.3118776664 665218.9910960672, 646066.0620459124 665217.3222026358, 646070.1722748795 665215.2326120929, 646072.8926048368 665212.3729442258, 646073.2232614368 665206.633014515, 646088.5927573662 665201.5649094378, 646092.9040274838 665191.1153885692, 646108.0740195902 665188.976203975, 646118.786505898 665185.2256300908, 646120.2945967601 665184.6976483336, 646121.9359883401 665172.3078277125, 646127.9098922091 665173.6856345426, 646138.6602118448 665177.1339051833, 646142.0503738794 665176.1543047022, 646143.5309875843 665173.849369562, 646144.1507504757 665172.884486942, 646139.1415467199 665167.0940166491, 646140.9618006845 665164.0742449404, 646153.4126608575 665156.2656554353, 646157.8637136671 665146.5363065796, 646159.9543070985 665141.956414884, 646159.5345058468 665140.6165334855, 646156.2458206385 665130.1659689375, 646154.4663326081 665124.7758718729, 646151.2074639319 665115.8357024761, 646150.0780920904 665110.9554381738, 646148.0687219017 665105.745313415, 646148.369316655 665100.5452170842, 646149.859568898 665099.2756373088, 646154.2692339635 665100.186064798, 646159.0988370595 665102.7766918187, 646161.958670176 665104.4568549353, 646166.5980423111 665109.6175256713, 646170.2278330022 665111.55793685, 646174.057735934 665111.418382769, 646182.0680515553 665108.5892494153, 646186.2880252787 665107.5598255746, 646188.9980938816 665108.8700533404, 646190.2875977345 665112.0301857968, 646193.5372455876 665114.96046735, 646200.786638948 665119.4313653193, 646206.006111047 665124.302025162, 646209.51610974 665123.1123596962, 646215.2467328737 665117.9732140838, 646222.0777608429 665109.1039768861, 646225.5481609681 665105.0844023679, 646229.2384367413 665102.9248431628, 646234.868238795 665103.7354130468, 646237.5478981079 665107.0057865133, 646239.3775108989 665110.3259809114, 646241.3073209998 665111.4261991633, 646244.3773837559 665111.026558195, 646249.2776683407 665108.8570596817, 646254.1380093658 665105.2574843746, 646259.608134877 665102.4981353168, 646262.8880503599 665103.1687528508, 646267.0178508825 665105.1391420322, 646270.2476866764 665106.0195125979, 646273.5076920242 665105.4598945782, 646277.6180896981 665101.9603915274, 646280.4786143654 665097.8609064936, 646282.9690079236 665094.151136296, 646285.9791479622 665093.1514916347, 646288.3485330845 665097.9615006367, 646290.8079430026 665101.0120097578, 646294.3478847387 665103.2423289662, 646298.6177563203 665103.5228237096, 646305.1879862976 665101.0336821008, 646309.2180084678 665100.2440756626, 646311.3285183203 665096.8445000215, 646311.9192953493 665090.3945241405, 646312.839611086 665086.0644150123, 646317.3098424411 665083.6851085168, 646322.0299371816 665083.245660487, 646324.0501569192 665080.8559887845, 646325.3907126838 665077.166085064, 646324.2012492159 665073.0960488569, 646320.8217648666 665067.9056038082, 646320.6220605997 665066.5756678418, 646321.5521515302 665065.1057041755, 646324.2126720515 665062.1860299115, 646326.5128082474 665059.7863108402, 646333.6737591933 665051.3172708908, 646336.4442446615 665047.6176136759, 646338.7548524666 665042.2577519984, 646339.315513339 665035.3180152309, 646338.6962207791 665030.867887479, 646338.3365930693 665026.5478692452, 646339.6868590026 665024.7781161168, 646342.9871195093 665023.0484297761, 646346.447172995 665020.9488440575, 646349.5175544772 665017.8892182217, 646349.7598617494 665016.224295532, 646349.2799865541 665014.0642514775, 646347.7903387193 665011.7640906393, 646347.5605159408 665009.9640737269, 646349.0209386967 665007.2941783355, 646351.3113156386 665003.3845463304, 646353.3219879356 664997.7147313831, 646355.8315034127 664992.9251296886, 646359.2219112455 664989.8954609087, 646363.0119549136 664987.6259944913, 646365.6644173114 664986.7805478704, 646367.4021946732 664986.2265131811, 646368.472348731 664984.6165665487, 646368.9930852348 664980.5366494501, 646365.9136225219 664974.9363215792, 646367.0039954396 664971.9764646677, 646372.1941215415 664969.927159997, 646379.7348169726 664963.7979911969, 646383.4348956009 664962.7885073917, 646385.8346456338 664964.5287772247, 646388.9246716653 664964.9190547717, 646392.9346472598 664964.0395263288, 646396.0247697872 664962.139896376, 646402.1957884637 664953.8606598192, 646402.5165051988 664948.1207284471, 646401.8874326205 664940.8507748677, 646404.6481476644 664934.2511321749, 646409.377932181 664927.0016421974, 646412.1686484774 664921.85207507, 646414.1292925015 664915.9622553608, 646414.4496355079 664911.8623149848, 646413.0206102406 664904.3132298074, 646414.5913772517 664897.6932886905, 646414.3419797363 664893.3433633677, 646413.0222961319 664890.2233867855, 646412.4431298448 664884.1932722059, 646415.1537448775 664877.9736215076, 646418.384210976 664875.0738532358, 646420.5247476434 664869.9442102511, 646422.7251854928 664865.5945699879, 646423.1458978038 664860.8045650045, 646420.1065869031 664854.8943234638, 646419.9769857981 664852.1544832877, 646422.2576833316 664844.5847098707, 646421.8380220624 664842.0745946915, 646417.5591983385 664833.534143238, 646417.7295718364 664829.2941861309, 646416.3319289159 664811.6539594715, 646419.1625173917 664805.5645619702, 646427.7031915596 664798.3555149639, 646428.8734069406 664797.1457377076, 646432.0841531128 664791.4261423987, 646431.6747261278 664786.456041783, 646430.2052082547 664782.0658146681, 646430.6353838139 664779.7759571703, 646434.5960782118 664773.916449865, 646440.3260010263 664774.6371126374, 646442.8862049448 664771.9675050303, 646444.2566690425 664767.5376085604, 646445.7766292123 664759.7877475169, 646445.1276313597 664752.8979494413, 646443.698585439 664744.0386313162, 646439.340620081 664735.3082517361, 646437.4617270684 664726.9979983056, 646439.5421447752 664722.9081825707, 646444.9814509323 664719.6089933094, 646446.5319300944 664716.3991111282, 646446.362878429 664708.1092164998, 646444.8333803976 664703.5989830653, 646439.6050398272 664698.6684816191, 646438.4157786295 664692.9083745705, 646441.7775205859 664668.3688989925, 646444.1877793059 664666.8391076818, 646449.287899656 664666.3897834077, 646456.9076772954 664666.88058713, 646458.5476931032 664665.9908627387, 646460.6182534375 664661.2111295507, 646460.0690748371 664655.2610179707, 646460.939491137 664652.6011336606, 646466.7200987168 664646.5719188104, 646469.1804774306 664643.012224325, 646476.1616937174 664632.4631736244, 646475.9924470279 664625.8030300698, 646474.5132105919 664619.5628918642, 646472.3640000013 664614.112751449, 646469.7544004065 664609.7924713563, 646465.7050524493 664605.9520212052, 646459.2654273387 664602.7912893546, 646456.3157411724 664601.4319136523, 646455.0458748891 664598.8117801766, 646454.8362139602 664597.6218422519, 646461.3978379862 664582.5025275144, 646458.7999233238 664565.0824799279, 646455.9110893526 664555.0721182971, 646453.3716423844 664550.4118482631, 646449.9121018613 664546.7413858525, 646448.5275200777 664544.5509967223, 646448.1924661435 664544.0213606332, 646447.2928810313 664541.241271113, 646447.6132695316 664536.761332703, 646448.4740773727 664531.3313823023, 646448.1445574627 664527.5712044018, 646446.464826204 664523.6412704024, 646443.4654361925 664519.3507049005, 646440.2958612939 664516.2405932, 646436.3975032361 664511.4299259898, 646435.2578012315 664509.8099622586, 646434.7079686443 664507.841909012, 646438.1493879077 664495.3324571799, 646438.1498940752 664491.1024002201, 646437.1905166616 664486.6323929466, 646436.2011368192 664480.7222300081, 646436.5717645951 664476.1822977717, 646436.5916623744 664476.0323808996, 646437.7523829726 664471.1024626571, 646437.8322800559 664470.9125529757, 646439.472913716 664466.3426885772, 646441.3934889301 664461.5530179215, 646442.4236016931 664460.8131417193, 646447.5733885282 664462.1236534117, 646455.802935539 664465.0645943721, 646459.4127351698 664466.45508657, 646468.6322124776 664470.7060555473, 646471.7621548906 664469.7864244868, 646475.7223442802 664468.1468938756, 646476.8428017999 664464.4571241745, 646477.1530198492 664463.387086049, 646477.2734372589 664459.8071194672, 646477.3435942597 664457.9471377137, 646477.4239512335 664455.3972408872, 646477.9739064259 664454.8573077612, 646483.8945423969 664448.9780280766, 646488.3948272353 664446.1284867979, 646489.255121726 664444.9885930056, 646490.6555447475 664440.8788581182, 646490.805843356 664438.2688097417, 646490.7857967769 664438.1788878978, 646489.2761586425 664435.3187278678, 646488.1765745273 664432.188536985, 646488.0267497615 664430.8386068816, 646488.7169511638 664428.6286191125, 646489.2972175219 664426.948695688, 646489.4472205675 664426.8087138876, 646493.4776052408 664422.9892032011, 646494.8878333836 664421.4893752987, 646496.1179256133 664419.7794475843, 646502.2986771426 664413.2302816944, 646504.8590645146 664410.510594116, 646506.0290987982 664409.3307365434, 646506.8993499592 664408.0508446583, 646508.3796285955 664405.5811101403, 646508.1796882163 664404.7410914438, 646502.510472515 664395.0988845408, 646501.7409952751 664393.790322167, 646500.1913338099 664391.6501535933, 646499.6421152984 664384.5501282038, 646500.9828864225 664379.0603938246, 646505.7738536601 664370.7808356623, 646505.8343255973 664366.7909443008, 646495.6446953802 664363.5697771435, 646487.0553799173 664358.9688035251, 646487.9655598704 664357.7589958942, 646491.846320811 664350.9094041681, 646493.3065229616 664348.5996664381, 646493.5866516049 664348.2997006276, 646495.1869731284 664345.3799024887, 646496.3672157866 664343.4398902069, 646496.6871423809 664342.820090757, 646496.7576001341 664339.9300346059, 646497.2377082264 664338.6601772973, 646499.617647366 664338.3404557002, 646501.6276483905 664338.280529685, 646501.9077471099 664338.2306425178, 646503.4378984813 664337.2807455426, 646504.96793335 664335.8210913144, 646505.3680629932 664335.4211399953, 646506.0281809802 664333.9311448014, 646506.8983961967 664331.4712582115, 646507.2886399837 664330.6213875026, 646506.808989654 664326.5912703554, 646504.2994464592 664324.2010696856, 646502.2195493642 664322.4608359637, 646503.3405127553 664314.5610027756, 646504.3613321953 664308.4310697495, 646504.6321158148 664301.6911323614, 646503.5526917893 664296.2310321493, 646504.5731226571 664293.341244085, 646506.4134218496 664290.4313913796, 646510.3538217647 664286.5619471282, 646513.664152147 664283.7522647402, 646514.9846251836 664280.2823541698, 646516.7354074933 664273.4126692513, 646519.4857670526 664270.2930031685, 646521.9859471761 664267.8733047798, 646525.7560816274 664267.3337452768, 646529.4059236121 664267.8442469675, 646530.6360522942 664267.3143123178, 646536.8362225486 664264.6251249849, 646540.046915067 664259.3655220954, 646542.2374237572 664254.9357168562, 646543.3182089435 664247.565956153, 646543.1483799923 664245.7758646224, 646541.7485722603 664244.7457867429, 646539.8688791523 664243.1255758298, 646536.7489831321 664241.6751400927, 646534.4092293009 664239.924956356, 646533.6693744843 664238.7847156529, 646534.0595271275 664237.2148481726, 646538.3304534 664230.1854564522, 646538.9908176437 664226.6455493716, 646537.4013202983 664222.1853852223, 646533.2717332229 664219.9148360667, 646523.6022129487 664216.373729442, 646521.3524520936 664214.6134762871, 646515.7628156962 664211.8928397334, 646515.6434082892 664207.0429282138, 646517.1044277846 664197.9231398702, 646521.7653400282 664189.2336410112, 646522.315489661 664188.5537080116, 646521.4757127154 664187.3336161296, 646520.3865778131 664179.9636036094, 646516.9969726685 664176.2931470271, 646513.0574613357 664172.7527060395, 646512.9675271222 664172.2726978128, 646512.5478245318 664170.1126590563, 646513.6683190321 664166.122887508, 646518.1187263895 664161.8034239665, 646524.2290560723 664159.3340645309, 646530.339648383 664153.1948019471, 646529.4707016716 664145.0747383902, 646528.9709784072 664142.6546915136, 646528.6717541578 664136.4146854524, 646526.1869158333 664133.7524893443, 646525.5820783993 664133.1044221392, 646522.3123891184 664131.5240500055, 646517.6024859855 664129.863511139, 646516.1026482132 664129.6532580751, 646513.6623162522 664131.8129649612, 646510.8521827944 664133.0926329842, 646506.6523026356 664132.3321491522, 646501.7725201813 664130.781510091, 646501.46274676 664129.6216395077, 646501.292940573 664127.6416289563, 646502.2634320061 664123.7916793543, 646502.1751167603 664109.8117337154, 646502.475976789 664102.4118028121, 646505.7483991536 664082.6823535233, 646510.0397317951 664071.2629039198, 646513.6909926045 664059.94321953, 646515.751547177 664055.7234779061, 646515.8515064493 664055.4935705554, 646515.8717304746 664054.103659303, 646515.8726694973 664046.2736154787, 646517.6428939742 664043.0538355507, 646527.5724426708 664046.6549702527, 646531.7925049339 664046.3754608327, 646542.4530251051 664040.8268022622, 646545.0933824107 664037.3170444391, 646549.6139029784 664032.9876683131, 646563.1054331531 664019.3292946181, 646569.7264239612 664010.9600201658, 646576.3972786808 664002.7008308745, 646580.6177247519 663999.2213358036, 646584.3381081482 663996.1518608913, 646590.2386946225 663990.2224915602, 646594.3789965068 663987.5129036122, 646597.2295697172 663983.523331974, 646594.7598101117 663980.9331374937, 646592.9903327941 663976.4428727642, 646594.0507836726 663972.8630118304, 646593.3113449815 663968.2530270066, 646592.6615154902 663966.8329581381, 646591.1918524551 663963.6627222104, 646591.712179951 663961.5227921753, 646592.6123160283 663959.7029046942, 646595.4825796111 663957.2931681154, 646599.062789521 663955.2836719608, 646601.5834024792 663951.2140623408, 646609.2155104767 663931.7750340652, 646602.8332286385 663918.1661962591, 646603.4437098417 663914.676202706, 646605.7541009191 663911.1366462196, 646610.9442110979 663909.226375845, 646618.0141870542 663908.9771156642, 646624.1041981365 663910.167735538, 646626.3438884093 663910.0580753878, 646628.0942103269 663908.5082050264, 646628.3044634162 663906.2381595547, 646627.3849512131 663902.3782303965, 646626.8154830211 663898.3781824027, 646627.7061685378 663892.4783120002, 646630.2267851454 663886.8986288557, 646634.8972597631 663882.8391877617, 646638.8872455708 663882.8796495057, 646645.4773553514 663881.8802569186, 646647.8775163564 663880.1906223294, 646649.2877282171 663877.35079826, 646648.7281303885 663873.9307487437, 646645.2378498865 663869.0604464582, 646646.3790531206 663858.1606272333, 646653.710056394 663851.1215071417, 646653.7703651713 663847.021532398, 646645.3511829558 663841.6805818727, 646644.6114468483 663839.5504257818, 646645.3923123438 663833.220464421, 646645.7231985278 663825.5806968305, 646641.4333144153 663814.5612097073, 646643.2339284273 663809.5515204242, 646644.3645596615 663805.4015897708, 646645.5749369259 663800.8417501593, 646649.9056482532 663795.5613147134, 646654.0359779026 663791.6418099744, 646658.2765677198 663787.4423191992, 646661.6369408937 663783.2526465354, 646662.9474309168 663780.1428919425, 646663.1881716837 663773.792864256, 646662.75885187 663767.4829139638, 646664.619380512 663763.15306238, 646673.1100238292 663757.2641420759, 646675.509337103 663753.7743500822, 646676.4197465406 663750.6645447769, 646676.5614837259 663749.889043517, 646677.4705200491 663744.9146036957, 646678.6127022026 663725.9147933259, 646675.5137110499 663717.4245410594, 646669.7445070598 663711.2538134072, 646672.3356324851 663702.3941396961, 646688.4481925402 663671.8461724728, 646688.3925450398 663668.1725771037, 646688.3291481142 663663.9961023552, 646687.9248328179 663659.3507896818, 646687.550296293 663655.0461194229, 646694.2926581873 663633.7469611788, 646722.6850651251 663603.5194443961, 646724.0812352741 663602.3250488238, 646728.7875624242 663598.298883033, 646754.2070902108 663576.553074907, 646787.5593706729 663547.555877943, 646900.3434931181 663388.996304214, 646905.1128890297 663385.976859232, 646908.6931796918 663383.3073568374, 646910.4534761189 663381.4774834372, 646913.1638360315 663377.417644611, 646921.7254781328 663362.6988235232, 646925.1552249009 663357.2893103287, 646926.3165082311 663344.7683543948, 646927.757810616 663335.8085417366, 646928.4882748001 663330.9086375413, 646929.678139764 663324.2487903188, 646931.6389569987 663316.959112968, 646932.8800891419 663307.599278145, 646932.6912154956 663298.8792778616, 646934.1508314076 663292.6093804362, 646936.0314925656 663287.6596879734, 646941.9326160799 663277.2994290971, 646945.2629443 663274.9998160733, 646952.8029652758 663274.5206807122, 646963.1029717505 663272.0717860888, 646968.8534452899 663270.1724490939, 646981.9427415454 663261.190448935, 646988.3325725541 663256.2809519053, 647005.0269598253 663246.5166453668, 647032.5123913768 663227.7731133334, 647037.4869212827 663223.7340114998, 647039.0733636783 663219.6457220525, 647039.7690732183 663215.929809888, 647040.8834676002 663213.9733017118, 647042.5184498664 663212.1458127885, 647053.1699377077 663202.0460933453, 647053.3421267661 663196.0226062088, 647052.9031039889 663192.1715244228, 647053.1759286588 663190.6203590131, 647056.8721981402 663179.9841648346, 647057.7096220503 663178.6309835154, 647060.1580501776 663175.581190039, 647060.9552742165 663174.9353625699, 647064.4012996118 663172.902160885, 647066.2104596945 663172.1196493721, 647071.4798444036 663170.7832945543, 647072.360628148 663170.4873958644, 647074.7447847889 663169.0526314419, 647076.1143411355 663167.7692707187, 647077.8783112557 663164.9136384185, 647078.163253414 663161.4936782678, 647078.4376795151 663159.909873002, 647079.950026124 663157.9508499942, 647086.6451180107 663153.348904777, 647088.517436104 663151.3542429181, 647088.9524401022 663150.2549349365, 647090.1911092802 663146.6110841221, 647091.0674982864 663143.0129518084, 647091.9808993478 663138.6481053153, 647098.3600351067 663132.6442065182, 647099.3732979794 663131.8073639908, 647104.6598087301 663128.2823750864, 647108.0801912193 663126.9397284851, 647113.1367550964 663125.6039887024, 647116.1784354436 663122.1243431802, 647116.5582716687 663120.7403893826, 647113.8501946657 663117.632406518, 647108.8090039972 663113.6475191102, 647105.6365996721 663110.4642834886, 647105.2996695989 663109.3036074211, 647104.4837377424 663106.113120879, 647104.0967847062 663103.2159627073, 647105.5116423956 663100.1337305859, 647106.3983786958 663099.0483940438, 647109.4385142127 663096.6053860789, 647111.9242812648 663095.9706310974, 647116.3323423127 663095.4687352324, 647120.5547303704 663095.0452180101, 647124.562161551 663093.198319128, 647128.2011480657 663089.5253018453, 647130.7134553329 663083.6982402704, 647131.2492827454 663082.5642236904, 647132.5580541529 663080.4752572196, 647132.9480751494 663080.0058626621, 647136.7929838005 663076.1359090916, 647138.9313353396 663074.448796418, 647140.4167506772 663073.5611276497, 647143.7693310142 663072.1997528243, 647150.4344009898 663066.330764726, 647155.2837871547 663059.4073314256, 647156.2270862585 663057.3405630487, 647158.319492529 663053.9569682776, 647160.5269455106 663053.2913013125, 647164.2988778327 663052.5714128495, 647170.4391302434 663052.2164337498, 647174.1500542554 663051.071419122, 647179.2694142355 663048.5820876377, 647182.1732337967 663046.1150634295, 647193.7546361345 663038.9755973185, 647200.6274666096 663035.3314276241, 647205.8563604349 663031.2262712725, 647216.5707823784 663020.3078324555, 647221.1160866076 663016.5550772428, 647223.2094399913 663015.6239574729, 647227.2331593722 663014.1240986874, 647232.6956701454 663012.7272836344, 647234.448710901 663012.1927643401, 647238.4474792816 663010.0596237627, 647240.4388341958 663008.6036932303, 647243.4923694211 663005.4373267103, 647245.0380040716 663002.6843876368, 647247.8579233717 662996.8018391744, 647249.6083874814 662994.0817233163, 647253.4824539527 662992.76120689, 647254.5943451946 662992.4301341114, 647258.9758586006 662988.3426402177, 647260.5437357227 662986.6053817732, 647264.5343612613 662981.342805264, 647265.906908683 662978.8064858956, 647269.2596083892 662970.5627216822, 647271.8304250058 662963.8060658213, 647273.0399726661 662961.734847016, 647275.1240996678 662959.3014883627, 647278.7074229454 662957.9850984544, 647285.8629410984 662957.85039316, 647286.3319249145 662957.5810069897, 647287.3863616575 662956.793128317, 647302.388578883 662937.3630274767, 647304.1064578478 662934.0243483458, 647307.1014958431 662924.325024751, 647308.9112914002 662916.7970827321, 647313.6350408391 662906.6635561831, 647318.5251750288 662901.8535598804, 647320.7943642999 662898.5186231659, 647326.5698942357 662887.5461770226, 647330.3238858363 662883.9928492776, 647328.9291245132 662879.3840579604, 647327.688642913 662877.4439200193, 647325.2881146242 662874.8039713623, 647318.4421551448 662869.0363225527, 647320.3370913876 662866.4146220188, 647333.8577489652 662843.8654732587, 647337.8495212306 662837.9267357711, 647342.1072308912 662830.1462711991, 647344.1539844491 662825.5873105592, 647343.9429749504 662820.8666538257, 647342.8857964738 662817.8929383747, 647341.6763032742 662815.0926453788, 647339.7383815661 662809.1317945934, 647339.2995163661 662806.9210281898, 647340.099998844 662804.2766431016, 647341.8895295247 662801.4178105679, 647344.9026335714 662797.9339978005, 647346.5766480335 662794.7815525484, 647347.0134636379 662790.43344873, 647346.3655598151 662787.7682592884, 647347.8467923388 662786.2417096458, 647350.7870672654 662783.211807782, 647351.4225913074 662782.5566808641, 647352.795003641 662781.1424386726, 647357.7357270108 662779.2626818964, 647433.2964524676 662750.5144895297, 647436.1583940791 662749.425695361, 647439.034538687 662748.3111428185, 647443.8673420779 662746.4382530161, 647454.4037979393 662742.3540124173, 647458.0345245469 662740.9464256677, 647462.410780197 662739.2501237466, 647478.3500144698 662733.0701775684, 647484.6390847659 662730.6321731162, 647491.7303088172 662727.8835398442, 647497.9013369487 662725.4919218177, 647504.4657205212 662722.947068462, 647510.631601033 662720.5568897127, 647513.0753389657 662719.6086876226, 647522.2128629136 662716.0665266763, 647527.3186751985 662714.0615072361, 647559.9663012751 662701.2408983669, 647612.0284808109 662681.6356116913, 647618.066725924 662679.3615772288, 647622.3293715832 662677.7564611399, 647623.7075240851 662677.3158175144, 647628.54792086 662675.7682067232, 647660.7039250226 662665.4878543904, 647661.6768325346 662665.1767647305, 647666.6599144089 662663.826769661, 647707.32209713 662652.8102584878, 647776.6133653818 662634.4967499832, 647786.001387529 662632.0155845117, 647889.9310127917 662604.5473138673, 647929.1811076833 662594.1735610812, 648025.2400187257 662568.7854961122, 648026.8579699971 662568.3578408891, 648031.731844412 662576.2044768386, 648036.5578836558 662580.368067699, 648038.3427397558 662582.4167514212, 648039.9292342177 662585.2585324582, 648042.44107283 662590.6773790811, 648045.1182990461 662593.494964762, 648052.8454845775 662598.2232073048, 648064.1696855307 662605.8150747631, 648067.6335917461 662608.5454108501, 648070.6975225357 662613.3396988915, 648076.7584679098 662624.3932269998, 648080.087602679 662630.4806669747, 648088.4094385923 662643.6981469145, 648098.006697385 662656.110820273, 648099.6847501288 662657.7211019418, 648106.329537275 662661.009100499, 648111.6993542467 662662.8212658301, 648119.7590315475 662665.6894344788, 648128.1450726136 662670.4898024789, 648139.9123575978 662677.5891370253, 648150.8009787212 662683.6064460209, 648157.4962048926 662686.6493309019, 648164.7826200341 662690.173406908, 648168.5871339615 662691.5287442802, 648178.6452789501 662694.2837134238, 648181.968949795 662695.6389930279, 648186.2980538899 662697.7812735566, 648190.9770634008 662700.9288762521, 648198.6732126812 662706.8306019065, 648202.5985672793 662709.9518740666, 648206.8285242666 662712.9472628435, 648209.8246933909 662714.4450631685, 648214.5835828391 662716.0750754122, 648217.8880293953 662717.881232844, 648228.1982202488 662725.0610331164, 648233.5374434212 662728.8728754533, 648237.5208048963 662730.8141545827, 648240.9564965109 662732.0091277817, 648248.8236996642 662733.9510338581, 648251.5124541445 662735.3447973024, 648255.2465390201 662739.3266867907, 648255.8640207401 662742.5576414816, 648255.3033879376 662744.7977343905, 648251.5841695219 662751.466408134, 648251.5328894332 662754.2664023547, 648252.5002102158 662759.0521193207, 648253.9770558056 662762.9722974465, 648255.4537184979 662765.4670353562, 648257.3381809685 662767.2491822102, 648260.8529236433 662767.9113649484, 648263.7053407577 662767.7590679282, 648268.2899098549 662766.5885790432, 648272.4160805664 662764.756434927, 648279.0892559862 662760.7861967308, 648285.1002826033 662757.4777989867, 648286.6793449206 662757.1725488044, 648287.9019167644 662757.4781358584, 648289.0731908017 662759.260196988, 648289.8369339795 662762.4676493539, 648290.0913675166 662764.7585603249, 648291.8738229624 662767.5589351308, 648295.4898979829 662769.8502503451, 648300.6634393281 662772.8191929825, 648304.1019540492 662774.1533667077, 648306.7064893394 662774.7804000237, 648309.6614788846 662774.8071553794, 648312.3000557177 662774.2801125841, 648314.7539077234 662773.3309674965, 648317.5509372699 662771.7756235612, 648320.7173186536 662770.4046440652, 648322.6169316117 662770.193832457, 648326.2842371761 662771.5656337182, 648331.3496774322 662774.5723234349, 648333.3986217068 662775.3743299887, 648335.3233899294 662775.5734414918, 648338.044645335 662775.341768691, 648346.1756733138 662772.3574658673, 648351.9170380962 662770.8655159682, 648356.1728024213 662770.339467619, 648360.6807585508 662770.0897696521, 648364.1872286327 662770.1735513278, 648369.4046065696 662771.7178590699, 648374.455035292 662774.0963869372, 648381.0080593275 662777.3512557215, 648386.016750722 662779.9384186605, 648388.4086587636 662780.4345464254, 648391.0031767255 662780.2327783904, 648392.9573497379 662779.4584532104, 648397.438621223 662776.7652314613, 648401.85262899 662774.0044815874, 648403.7058013817 662773.2637442553, 648406.367429982 662772.9274242413, 648409.4672125672 662773.3319571035, 648413.6785819989 662774.4436638212, 648416.0032088739 662775.0164234957, 648422.1351674028 662775.6568409834, 648431.0557688772 662776.577113865, 648434.7153726531 662777.0525940381, 648439.277864698 662777.9080228825, 648445.7550606257 662779.9923223145, 648449.750445584 662780.8462429835, 648452.586832512 662781.1515441359, 648454.5999150451 662780.5420260296, 648457.5280899763 662779.2010977418, 648460.425698375 662778.1649658632, 648463.3535264129 662778.2264379353, 648469.7888332565 662778.8368119302, 648473.2021383989 662778.7402623317, 648476.4291104855 662778.3107302273, 648479.0105470389 662777.6659204355, 648482.9751509574 662776.1921566966, 648486.6630868912 662774.2882795502, 648492.287242312 662770.3574345616, 648495.245759503 662767.6297894175, 648499.4209220941 662762.8398898797, 648501.3540097626 662760.2902014735, 648503.3256798886 662757.1998775235, 648504.7180249407 662752.3954033537, 648505.5228040947 662747.9743786583, 648506.1044197959 662742.4814867808, 648507.2218539574 662738.8643399279, 648508.4285692582 662736.7209642844, 648512.8973720791 662731.4518997375, 648513.9698085327 662730.0542681863, 648516.6118266335 662724.5499439264, 648517.6801589227 662720.3934309413, 648518.2430823087 662712.4173359144, 648518.7885803736 662707.6321198621, 648519.6769452569 662704.8130256822, 648521.7123939737 662701.2539091089, 648523.0208923785 662699.9463859249, 648524.7288781301 662699.1477109254, 648528.1085390887 662698.3853968437, 648534.4682970494 662695.5168801767, 648539.137412761 662693.8720007347, 648547.3801981907 662691.439710474, 648555.0066313945 662689.6523063527, 648560.5212709355 662688.6563287148, 648562.8151050672 662688.4679643076, 648566.725953207 662688.2473142147, 648569.7144002925 662687.5470330898, 648571.8544497641 662686.4778498671, 648575.5438207628 662684.4502924595, 648577.2779797678 662684.4505008564, 648580.3029945446 662686.6633452347, 648583.0697581121 662688.8023985592, 648587.349193924 662690.68339358, 648590.3005654187 662691.2736685023, 648594.1006412205 662691.1635651442, 648597.3105802371 662689.2097101279, 648598.0487439098 662686.7022778388, 648599.2023510798 662680.4513739742, 648600.8266758817 662677.2050078663, 648602.4976360918 662675.8138480996, 648606.8596433828 662674.4694117208, 648608.6697174994 662673.4491488129, 648613.2175225846 662670.9916142887, 648621.1459091179 662667.4991255299, 648626.616657144 662665.4436220286, 648629.7962643903 662664.7897638065, 648631.5726672672 662667.7339785056, 648633.4891074876 662672.2204507004, 648639.3336380657 662673.3427534836, 648645.5988171786 662674.6519869067, 648650.4607556926 662675.4524114951, 648655.4413435059 662675.5875700536, 648660.9601795208 662675.1173530208, 648665.9407686887 662673.7725109117, 648671.4597672821 662671.9570132749, 648679.1973394024 662667.0265807728, 648682.0333753427 662664.3618003167, 648684.5734308922 662660.9777839676, 648686.9016030874 662658.6513426409, 648691.0500772347 662655.859839803, 648694.6901179939 662655.1833168451, 648696.3833669027 662654.4220799345, 648697.9073641759 662652.9838623714, 648698.5482527963 662651.9948189122, 648699.8893455132 662651.2288996838, 648702.4892423904 662652.049852441, 648704.8428310291 662652.3784553787, 648707.0598351758 662651.9684815542, 648709.167412101 662650.8197742291, 648713.6836074721 662647.0180750194, 648715.4352737116 662646.0883650279, 648731.2589799784 662641.0051436522, 648744.3961873347 662636.4279996668, 648751.454038835 662633.6238861551, 648765.2912126511 662628.2993057325, 648772.8054279665 662625.3661268917, 648778.3008836403 662623.0179858193, 648783.6164291968 662620.0640627907, 648795.9535312894 662610.6055396808, 648812.7109638558 662598.9454661143, 648815.7050545815 662595.609023881, 648823.7231581918 662591.4525445831, 648826.6445392136 662589.0500141002, 648830.3877185399 662582.4005399024, 648832.0844438613 662580.7796227566, 648839.2705560086 662578.5603245142, 648840.2759887909 662574.9449231892, 648844.7703737722 662573.8998623253, 648859.0257767882 662574.0929343916, 648871.9662465797 662568.6223251699, 648880.0644365501 662562.7124141001, 648889.6885006371 662558.92732748, 648898.3274142431 662559.832685405, 648906.7931825621 662561.4359429834, 648918.0123110608 662564.461772112, 648930.819710231 662567.3610719442, 648941.4651837806 662568.7549912313, 648945.3069687661 662565.7335306163, 648951.0226444136 662563.8460557492, 648955.6633476941 662557.9040491121, 648958.0583227288 662556.522415789, 648959.9972426539 662557.1199290073, 648965.4230863116 662559.4476220909, 648972.0067736178 662557.1911311521, 648973.1850665938 662558.1727133028, 648974.3088730462 662559.109008877, 648976.496861485 662562.3255937817, 648979.6286346704 662565.3277318873, 648981.9023484358 662567.1291261507, 648984.4766239657 662568.4588761909, 648987.3081568639 662569.2739367774, 648992.4140074187 662569.8319903165, 649003.7841561857 662570.1763959837, 649005.414551259 662569.5762713616, 649006.6161880817 662568.3327347966, 649008.1611729522 662565.0738780582, 649010.2638836344 662561.6005281371, 649012.2809363827 662558.9849285171, 649013.2139664472 662558.1691999858, 649013.9973495826 662557.4841735733, 649016.5272597299 662557.2699172142, 649019.7177794386 662557.3811804095, 649022.4087583793 662557.9915839739, 649027.0140200892 662560.3767786709, 649029.3214858645 662561.9520968806, 649032.845526109 662563.5219611829, 649035.7068633626 662564.46406546, 649038.8824191398 662565.0223672021, 649041.8311701242 662565.2654414781, 649045.5580778102 662565.47660918, 649050.128876461 662565.1608378403, 649053.2582236228 662564.7396133224, 649056.915003433 662563.6155716222, 649059.6928198553 662562.4210243184, 649063.103646438 662560.804952709, 649069.4994388106 662557.9218785904, 649074.6738101774 662555.4914581184, 649077.0179605057 662554.6077389385, 649079.8040710642 662553.8125528911, 649082.413355846 662553.3266258431, 649087.1011464893 662552.8409483725, 649097.4939388279 662552.1349957661, 649104.0679639283 662551.9858650353, 649108.3973188234 662552.0503849383, 649115.5812256769 662551.9230874554, 649116.7960355805 662551.8315532539, 649123.7357327398 662550.0053451174, 649409.1148104606 662474.9054574282, 649421.3940002477 662471.7264479896, 649465.3042141035 662460.3582662296, 649588.020319414 662428.5881567781, 649560.486333041 662380.8897603517, 649627.2719131848 662333.6431559668, 649646.4275816506 662320.091836931, 649649.4397091899 662329.0528486306, 649649.6313384336 662329.6227922793, 649655.8786286139 662348.2082033417, 649656.2982692283 662349.3896950628, 649657.6908018026 662353.3103466968, 649659.1571485532 662357.4386874387, 649663.3562163106 662369.2604850624, 649664.4002608561 662372.1999737762, 649665.7120591834 662375.8932555056, 649666.686370654 662378.6362556306, 649669.532037296 662387.3274873139, 649670.8143438388 662391.2441258027, 649672.3943825547 662396.0700810994, 649675.7927521439 662406.4500211824, 649676.8317983351 662409.6239096341, 649677.0143645764 662410.2812123238, 649678.7983326542 662416.7073540527, 649682.6732070501 662430.6657557151, 649683.7537008423 662434.5582100373, 649684.7905491156 662438.2930589432, 649682.9426291356 662451.3571729328, 649682.9113002643 662452.0934900582, 649682.75732313 662455.7358759637, 649681.9180851729 662475.0042784174, 649680.6365778383 662490.8737436741, 649670.0278125459 662495.6223959922, 649657.9554938775 662501.0260733268, 649628.8148582822 662502.6377772908, 649606.1956272284 662509.1699496234, 649605.7998624317 662511.634864932, 649604.4736595422 662519.895035165, 649601.9422215003 662535.660989241, 649634.6796545372 662524.0293862105, 649692.7722237228 662503.3888154334, 649725.046152005 662492.2278757319, 649732.7463539136 662489.565116677, 649799.282808655 662466.5554731215, 649944.6744595616 662414.942307119, 649996.7171676672 662396.7766713887, 650384.7539914947 662263.4465632483, 650583.9262310211 662193.0073995914, 650688.9163868767 662156.8654436466, 650756.9254850973 662133.0291141727, 650787.4396061511 662122.0076329467, 650814.5734522573 662112.4841309781, 650867.9260864253 662094.0246555111, 650868.111442478 662090.7326620823, 650870.0605471663 662055.9528904755, 650872.5892049333 662010.8459395394, 650908.1324576753 662010.8454370649, 650920.429548396 662010.5738579297, 650921.0016101686 662010.6889702473, 650931.0202536181 662012.5502359684, 650939.946959398 662014.1692834975, 650947.1038573448 662015.5911480654, 650953.193407151 662017.4460008898, 650981.3168959632 662027.5556571748, 650981.4785800079 661979.8495239008, 650981.6747753798 661961.9613009419, 650981.2997946703 661946.8722200103, 650981.7327596799 661921.8382317867, 650981.9197159939 661896.7760525594, 650981.9129252328 661871.05496578, 650981.6317680117 661865.2600220859, 650981.8443450875 661835.3498223351, 650981.8960480758 661804.4877577159, 650981.9865380444 661745.325399384, 650982.024062807 661740.3823398995, 650982.0817481895 661733.145351716, 650982.0827715958 661708.8391528118, 650982.046085955 661698.2299762389, 650982.116311695 661663.9539772107, 650982.2971428257 661649.7769705481, 650982.531742971 661635.7049311795, 650982.5763921329 661630.5618716176, 650982.635565577 661605.3716757058, 650982.6850268818 661576.8715423612, 650983.9146065133 661554.7035888069, 650987.0644880862 661531.5169811993, 650993.6415700391 661496.1844980374, 651001.0076193858 661465.2790194595, 651649.19866847 661757.6367368026, 652064.5960056366 661947.355097149, 652493.5522375118 662146.6470252314, 652542.888142925 662171.1302037662, 652678.5123492769 662234.2129571286, 652978.7457850841 662382.0647797191, 653094.4983371319 662427.7500639702, 653001.2120992553 662603.4083419598, 653005.1790362343 662604.6737723238, 652877.8493941907 662775.0522201277, 652877.7483791406 662775.2042086758, 652867.6117161495 662790.5120196687, 652867.132560031 662791.2359653643, 652716.3486385337 663024.23603509, 652684.4950013809 663073.4593194932, 652664.466399501 663104.4080998163, 652663.4832285896 663105.9268689149, 652659.0863386217 663112.3954920152, 652658.1152469369 663113.8373023146, 652652.6871922524 663122.3947706321, 652649.4582499157 663127.3377663621, 652217.4357973144 663815.5337848234, 652213.4290214189 663822.246280869, 652202.6489491987 663840.3090328274, 652161.712866277 663908.8962962239, 652159.679569692 663912.2525409878, 652093.4822824255 664023.1034507821, 652089.683833003 664030.2780535424, 652087.5164997239 664033.8418031706, 652087.2613836343 664034.260813699, 652063.2437161373 664073.7509999529, 652062.8675218774 664074.3696765187, 651992.9551383398 664191.4419345956, 651992.2148914258 664193.5508108563, 651991.8727336127 664194.5258121515, 651991.6326729717 664195.2077049938, 651990.6199606615 664201.7496769829, 651989.9342793459 664206.1785638544, 651989.4120243423 664209.554508166, 651989.0073659349 664215.0935102552, 651988.5955951072 664220.7353916893, 651988.263120552 664225.2894405486, 651987.019046015 664233.9681890628, 651984.9702220305 664248.2639725618, 651983.246851055 664260.285870484, 651982.6099786906 664267.6667684794, 651980.5811611821 664291.1606509668, 651979.3433227752 664305.4965304965, 651978.8216313663 664311.5422397384, 651987.1660688806 664315.4643524702, 651990.5218413228 664317.0407516889, 652139.190190439 664386.8974109971, 652252.6187629958 664440.1948280262, 652271.3395291634 664448.9920813387, 652326.7059349474 664500.5004821991, 652364.5663976724 664535.7219965461, 652370.5465420614 664541.2855702083, 652378.3641234097 664548.5583168275, 652383.3910298533 664553.2350596996, 652337.3893986845 664575.3942260065, 652334.3665980921 664576.8505169689, 652341.6367967807 664582.4633596807, 652346.122301516 664585.9267595352, 652353.2066766184 664591.3955804801, 652388.7306420456 664605.2956913464, 652408.8855487512 664613.1820229157, 652460.2817598757 664633.2930860042, 652460.2701320598 664638.4490853854, 652460.263786632 664641.0190050113, 652460.2385141271 664651.8639636101, 652461.8857872253 664658.502234799, 652462.5374224298 664661.1293504171, 652467.6069783228 664681.56193851, 652465.1127476026 664683.1925307448, 652464.5447070594 664683.5635052104, 652452.8869162047 664691.1871204488, 652452.0164766883 664694.0100604002, 652451.9225428 664694.317089605, 652448.9543118988 664703.9447485068, 652447.732820139 664707.9046081663, 652445.1518651566 664716.2793516924, 652444.5186570056 664718.3311189656, 652442.4763132425 664737.7939266241, 652436.8619534998 664791.3142486273, 652436.584558715 664793.9572571313, 652433.0606445193 664794.2298110271, 652431.9805549261 664794.3136865542, 652431.4626089369 664794.3537868669, 652424.7484810676 664794.8739731241, 652424.3805228127 664794.9028907203, 652410.8684866978 664795.9514136224, 652409.2015810452 664796.0801815372, 652399.2635144795 664796.8500763327, 652393.3435230327 664797.3093941623, 652366.5569746783 664785.9362235179, 652349.2240534583 664778.5772630646, 652337.5377854174 664773.6159140555, 652329.2652576416 664794.3719280002, 652321.333985697 664814.2710215383, 652315.5361848064 664828.8183592629, 652312.4169882803 664831.8549611651, 652307.7104117011 664836.4365008919, 652304.0669998551 664839.9831227128, 652302.8998816927 664840.5149885116, 652299.7038416301 664841.9725410305, 652297.1216258436 664843.1503241378, 652296.0726002936 664843.2191633502, 652287.7096882414 664843.7681080914, 652285.2087143968 664843.9318139775, 652281.384656013 664844.1833642668, 652280.7286163134 664844.2263271185, 652279.7640747933 664848.7502156927, 652262.425733098 665068.3122782565, 652259.9829848919 665099.2400058398, 652246.5317778224 665102.285308295, 652102.6690039654 665134.8545211034, 652064.7533680439 665142.1590363432, 652024.8177514994 665149.852435359, 651975.5060883265 665159.3525761937, 651939.4534341887 665167.0302644204, 651901.8483549327 665175.037687589, 651885.0516789414 665178.6142198398, 651851.0953354451 665185.8448360657, 651587.3871462592 665241.7904351257, 651571.4999963334 665245.1533199004, 651555.6137340087 665248.5163645003, 651509.1703049332 665258.3472553836, 651485.1219633976 665263.4383518161, 651480.8088395273 665264.3515479376, 651467.344771783 665267.2017875711, 651106.3225538003 665343.6225067413, 651081.2210981015 665348.9361737141, 651001.2012132233 665365.8746311092, 651000.2611622985 665366.0735476828, 650971.371478745 665372.0243143596, 650968.4238815438 665372.6312872746, 650942.4585429779 665377.9789170352, 650729.7262100363 665421.7930284996, 650709.9830267711 665425.859390188, 650686.1718548652 665430.7632959147, 650543.8643153675 665460.0725505317, 650539.3381943699 665461.0049139557, 650535.917200245 665461.7094230277, 650534.8291136235 665461.9332796069, 650524.446139959 665464.0719109172, 650476.0885880349 665474.0316148301, 650472.6054683381 665474.7490754684, 650467.3175093606 665475.8384580554, 650464.2248232691 665481.5330735896, 650471.6976571425 665482.9359696995, 650497.2815787845 665487.7383519397, 650507.7872450666 665489.7096445779, 650510.5231306533 665490.2240820543, 650526.6045794195 665493.2421834702, 650541.6849643284 665496.0731143289, 650546.9057597693 665497.0528366929, 650551.7726330145 665497.9664328232, 650565.0672551019 665500.4622506395, 650577.712605825 665502.8351037572, 650577.8636527817 665502.8639235025, 650590.5581690781 665505.2465432135, 650601.6777849888 665507.3339171546, 650616.3188574406 665514.6698986185, 650626.2259990523 665519.6331056169, 650641.223997315 665527.1470544616, 650646.4633414333 665531.8368496761, 650651.7827284031 665536.5975353464, 650651.4386576202 665544.7003477523, 650649.5052364845 665590.2599675554, 650648.8685346406 665605.2699222222, 650648.2316548635 665620.2837168039, 650645.3744419314 665623.5534116614, 650641.2428000838 665628.2827746334, 650602.9325533361 665618.4937599257, 650575.5676560577 665611.957217843, 650570.7618840984 665610.6965892029, 650568.0340061543 665610.0162322822, 650566.2900685916 665610.2220821814, 650549.8430953725 665612.1648720516, 650548.5711303797 665612.3146242131, 650532.1752550872 665612.9495846565, 650525.854362315 665613.1937121198, 650497.0785115616 665614.3068819294, 650489.4765757858 665631.2858270918, 650480.2267107985 665639.6935422536, 650473.0750939002 665646.1936995082, 650465.9835577357 665651.9687467909, 650462.614150621 665654.7122940484, 650457.6738158448 665658.0836322807, 650450.0432877537 665663.2904499753, 650449.1763338494 665663.7613742088, 650446.473226912 665665.2301332525, 650444.0590468147 665667.0267692399, 650440.0736967312 665669.9951942952, 650439.3826377211 665670.5091815327, 650427.3718407003 665678.5774918974, 650420.7086029509 665681.4046042896, 650414.6412404478 665685.585710487, 650413.967221817 665686.0947399214, 650409.6527571647 665689.3520801611, 650409.3307477627 665689.5950768908, 650405.091580806 665692.5274680898, 650401.5523441148 665694.9761131507, 650401.1502791959 665695.2539791948, 650396.2899333753 665699.1533250312, 650392.7416281827 665701.7378083054, 650391.8285017685 665702.4037656107, 650388.7903686899 665704.6163173772, 650384.2380505796 665707.9326656072, 650379.5866803157 665711.3211604459, 650376.6484870964 665713.4606055012, 650370.7778842067 665718.3599740772, 650367.8777505339 665719.8695062721, 650365.6015046706 665723.0021549684, 650357.3761660669 665734.3181497501, 650352.9186998901 665740.0442612153, 650348.698939289 665745.4648050067, 650343.1262780622 665752.501164305, 650337.2244869898 665759.9542784389, 650333.9099331435 665764.140865744, 650332.3338736425 665766.1307304647, 650329.7864018008 665769.3483024555, 650318.5626630156 665793.5476918239, 650315.6479388911 665799.8324036979, 650324.9456560741 665802.4355423956, 650335.597988539 665805.4190188082, 650339.4807935328 665806.5064078477, 650336.9887875195 665816.450042569, 650334.999871315 665824.3828709498, 650330.7318699822 665841.4091248617, 650327.4933104148 665854.3297695171, 650325.9586482969 665860.4533841832, 650325.1343137614 665863.7403031321, 650308.8520410872 665859.9111236008, 650294.6334734604 665856.5663755302, 650287.0469121314 665854.7832988873, 650286.360359397 665857.7036771037, 650284.5235717864 665865.5299646879, 650283.3891433703 665870.3587564218, 650281.7654153578 665876.1676396866, 650280.7108834446 665880.8063626391, 650279.9614521973 665884.1003310224, 650274.4156602274 665908.4835046115, 650270.3608298504 665924.0617884736, 650266.7629335067 665940.6654086089, 650265.7120069091 665949.3151563416, 650263.8682764607 665955.8249671388, 650262.7607597637 665959.7347254015, 650257.186889077 665993.3639374379, 650256.4958973583 665993.3156859806, 650255.727748195 666003.2545716192, 650254.7053337641 666013.9343344748, 650252.9523164872 666032.2628878922, 650252.316270828 666040.3228870386, 650251.3298489643 666052.822637652, 650250.4166740872 666062.8403416146, 650249.4073463356 666073.9220228206, 650248.8651818763 666083.5968992737, 650248.4528551436 666086.5788964232, 650248.4058190432 666086.572810258, 650248.154841459 666086.5407773531, 650247.4051005302 666092.4323022837, 650246.9437077444 666096.0586068623, 650243.3454674009 666131.9237113503, 650242.7932202698 666133.8546600905, 650234.2760046141 666171.0991032238, 650227.4629609584 666197.7480331218, 650224.671730436 666207.3774944309, 650220.7706078319 666218.4970356296, 650212.535616764 666242.2786075514, 650211.5235325764 666245.2014868854, 650253.9244074965 666248.9119973576, 650266.2772186147 666249.9926541828, 650268.7770676651 666250.2110613731, 650283.6756992049 666251.5149312528, 650295.6399618032 666324.251804306, 650299.0944051142 666345.2541351569, 650286.3305822846 666377.8430246739, 650281.266646351 666404.1001877469, 650277.6894598629 666422.6475406252, 650276.1675440031 666430.5362254773, 650259.5488587297 666446.4369361344, 650255.6556512327 666447.7192939707, 650237.9282706648 666453.5580368235, 650200.661363976 666465.8320820541, 650164.0912684248 666478.1830832293, 650164.8585602667 666484.4061628061, 650165.1073498146 666486.4202556688, 650161.8779584537 666489.3268615481, 650156.9834566234 666493.73116368, 650151.977119033 666498.2354508827, 650148.0266530444 666501.790920044, 650136.4566196052 666512.2032856818, 650119.2049377146 666528.0211263335, 650118.3248284584 666528.6398897532, 650109.055101151 666535.1607419865, 650107.2350607157 666536.4405009584, 650102.673687405 666539.6488569326, 650095.673107691 666544.5778499804, 650072.4620025152 666556.7848948633, 650070.5580863636 666555.899702927, 650054.1263174947 666596.5551884818, 650052.6769723399 666600.1420138819, 650051.9547189756 666601.7428686178, 650051.3075672626 666603.1765343149, 650051.2315779844 666603.3448432389, 650048.5577608503 666610.6594811993, 650043.3640672247 666624.8705404729, 650035.9112595659 666648.7994691738, 650027.5678711766 666679.2781483649, 650025.6788836481 666687.1498408394, 650023.3986731917 666696.650429718, 650022.4283403833 666700.6953918005, 650022.0051519464 666702.4572429531, 650021.0145989227 666706.3801236021, 650012.6707240352 666739.4357418607, 650000.1048524983 666790.5337117593, 649996.1748730703 666806.8201147048, 649994.0818438878 666815.4927355412, 649992.1210408926 666822.7625054199, 649985.6492750054 666846.5834037919, 649985.0094678702 666848.937862831, 649981.4163904114 666862.1608159665, 649975.3530373044 666890.5196467153, 649975.0768957357 666891.6237624149, 649947.9933477528 666999.959423401, 649945.0488428774 667011.7349524358, 649942.0133700416 667024.2533437709, 649937.2251185377 667044.0616927254, 649935.5874200029 667050.8393490772, 649972.2509153744 667059.7217721954, 649969.0505588321 667095.9031348552, 649963.5345433227 667121.8321390044, 649962.0738000723 667128.6970166304, 649959.246262676 667141.986467584, 649957.9925982129 667147.8812996474, 649955.721297634 667158.5559630456, 649953.8152805879 667166.3255774704, 649952.0654612945 667174.1004913866, 649950.3135108403 667181.8850050102, 649947.9853946489 667191.2086334465, 649945.9465605806 667199.3814280608, 649938.3567972373 667197.3923394226, 649934.6801320512 667211.9627945067, 649933.4315502291 667216.9126759074, 649932.3820382474 667221.0734689902, 649931.9938432365 667222.6114872636, 649930.4139782458 667222.2212138271, 649929.1043411831 667227.1360479967, 649928.823235367 667228.1908838209, 649927.3065425691 667233.8816855025, 649925.7648114986 667239.664483298, 649925.6518714505 667240.0924655189, 649922.1873263288 667253.0940013381, 649918.6038038391 667266.5423986632, 649915.3592776979 667278.716929408, 649957.2084535174 667290.0729189147, 649953.5789604837 667303.5123498744, 649952.9506942696 667305.8332114433, 649952.3593630824 667308.0252787347, 649952.0292477862 667309.2471470023, 649937.4108647099 667306.1363955295, 649936.5424942063 667309.5162580995, 649933.8821550055 667319.8707171505, 649932.9207849592 667323.1705689005, 649932.6917227915 667323.9185339126, 649928.0218789526 667322.7901229219, 649923.9706936436 667325.4695958341, 649921.4414986526 667327.142226796, 649914.5708856924 667331.686293036, 649913.6169345566 667332.3181689461, 649913.4708421371 667332.4131899556, 649910.3097032418 667334.5047787897, 649907.5644757232 667336.3204217494, 649907.2634148975 667336.5194225953, 649907.0283888144 667336.6733120439, 649902.6640714674 667339.5617044601, 649897.2297102131 667343.1560378101, 649897.8471033874 667348.6379873402, 649897.9460426078 667349.5141520909, 649898.1288492959 667351.1360808405, 649898.7191143021 667356.375149068, 649899.2576046193 667361.1561347946, 649896.0971939629 667364.5777925197, 649894.525953377 667366.2795025919, 649892.4217940016 667368.5582614831, 649889.6954227426 667371.509897286, 649886.4691515577 667375.0033863491, 649884.935900456 667376.6641415647, 649881.1885262865 667379.8906882398, 649879.8253768545 667381.0636288096, 649876.8332143463 667383.1691589918, 649874.5619328768 667384.767903108, 649873.8999861431 667385.2337368772, 649871.0666979224 667387.2274478113, 649870.2426816286 667387.8073404765, 649869.5387127917 667387.8971722517, 649861.7586674767 667388.8933172096, 649859.9916452004 667389.1199757563, 649853.1680381406 667379.0846535718, 649851.0993051031 667376.0420224398, 649844.0206784572 667365.6292313923, 649842.3249811843 667363.1349618457, 649829.915085856 667355.1155674141, 649828.1236192277 667357.8873212618, 649822.3331588812 667354.3435997246, 649815.097783166 667349.9158818405, 649789.4427803177 667334.2157906417, 649783.6992400435 667331.6040777825, 649771.5020205075 667326.1077684024, 649760.9187081151 667321.3394880926, 649750.2743397175 667316.5422405902, 649736.4081915392 667310.2935754184, 649728.7436854193 667306.8397170671, 649725.4888926925 667305.3732555655, 649717.4193568358 667301.7363097061, 649711.5806797001 667299.1057053072, 649700.5973074106 667294.1564178221, 649692.8098015707 667290.6465449338, 649677.9158149728 667283.9347189063, 649672.2441550321 667281.3791739873, 649662.3876602451 667276.9380184228, 649653.8992504831 667273.1128632185, 649646.6717223299 667269.856015818, 649637.780235066 667265.8500132869, 649634.6312623218 667265.6086742096, 649633.3823940901 667265.5125238734, 649620.828569927 667264.5500526308, 649605.8588354277 667263.4012105506, 649596.8979038036 667262.7140518099, 649587.8521287047 667262.0200028494, 649574.3403538177 667260.9833362481, 649567.6015060546 667260.4665249924, 649564.4082773222 667271.0902100479, 649562.9957136317 667275.7888799686, 649557.3063930393 667294.7181138681, 649555.7888858772 667299.7658882176, 649550.9138979041 667315.9870833176, 649549.3914184853 667321.0538969374, 649542.9876702814 667319.2020975302, 649536.6419621906 667317.3663050014, 649530.3422245418 667315.5445979044, 649521.1575256174 667312.8875086656, 649514.7509375789 667311.0347488725, 649509.0970948552 667309.3991983439, 649501.2307226482 667313.7751702096, 649493.2221643927 667318.2320842342, 649490.9081391003 667319.5197935689, 649482.1135649093 667324.4137289181, 649477.7232007853 667326.8562174801, 649464.7734490316 667334.1845099608, 649460.4342031807 667336.6380446333, 649451.1147206409 667341.9088734429, 649448.2126002711 667343.5504287403, 649437.2328613354 667349.7600889825, 649428.9173785488 667354.4630440703, 649427.1822127696 667355.4437860391, 649422.8759837734 667357.8792449444, 649417.9956855328 667360.6397517214, 649408.1821344926 667357.7725081391, 649403.7442938226 667356.4750219494, 649389.4829624693 667352.3082109346, 649381.4223257034 667349.9533351341, 649381.4193084582 667349.951414783, 649368.2648193485 667346.1077749365, 649355.8124892059 667342.475258257, 649355.6294938264 667342.4283565046, 649355.4434805114 667342.3852943517, 649350.11867103 667341.1597001925, 649350.1137010709 667341.1596995898, 649345.5668461523 667340.1140380705, 649345.1296026955 667342.1339685585, 649344.5172483776 667344.9658711825, 649343.8267515631 667348.157761397, 649343.2424578303 667350.8576685045, 649342.7173033396 667353.2886249794, 649342.0198803601 667356.5115541051, 649341.7927340316 667357.5604780016, 649338.2428580818 667356.7680550461, 649332.4689848297 667355.4793671009, 649322.5744155472 667353.2711481702, 649320.9233683179 667352.9019914548, 649318.8214866592 667352.4337410036, 649306.3600021307 667349.6522561916, 649298.217296625 667347.8352859487, 649283.6868127603 667344.2095576796, 649269.3013409133 667340.6198466106, 649249.6961480017 667335.7275946112, 649246.4442669787 667335.1451259218, 649226.7217617356 667331.6157284946, 649227.7554913062 667325.8749404586, 649228.8041404547 667320.0531948962, 649230.0879548162 667312.9202885005, 649231.3207923069 667306.0776135746, 649231.751158316 667303.6826452166, 649232.4784988018 667299.6446862196, 649232.9879041445 667296.8118110041, 649234.0855760886 667290.7129536638, 649234.2766920372 667289.6489854855, 649235.3074025597 667283.921157014, 649236.3571178872 667278.0893317006, 649237.4258388446 667272.1466295979, 649238.4965135102 667266.1936878206, 649239.8734008144 667258.5289571012, 649232.1387166986 667257.1120325655, 649220.2039768298 667254.9256459738, 649212.8975873418 667253.5868526102, 649212.8862276847 667253.5847712542, 649216.3936135969 667234.2093538864, 649218.3659208027 667222.8336854675, 649208.4159683493 667222.1395267436, 649193.621189028 667221.4806212854, 649195.4653798459 667211.2729277221, 649196.3349895509 667206.4601122221, 649198.9006585882 667192.2546186192, 649200.25658147 667184.7537239254, 649202.0938453842 667174.5829892842, 649203.2694880767 667168.0763046773, 649204.4092527042 667161.7644941432, 649205.4820553154 667155.8286724407, 649206.9079600405 667147.9319494931, 649207.6443571463 667143.8589518799, 649208.683057977 667138.1071245654, 649201.2652753455 667137.6172703806, 649195.4822414035 667137.2344932655, 649183.4985225599 667136.4431286651, 649175.1866868952 667135.8941266946, 649168.3647621894 667135.4432243301, 649155.9859348603 667134.62477209, 649149.4831499907 667134.1950281884, 649148.3610131392 667134.1208129174, 649136.0402273587 667133.3063676576, 649124.0815357146 667132.5159660411, 649112.9196286395 667131.7786203547, 649109.5576553968 667131.5572550085, 649101.7384920166 667133.0199762314, 649101.1479411596 667133.1303038221, 649092.628525648 667134.7631788803, 649080.6936844996 667133.4247843989, 649071.4759544713 667132.3907164857, 649068.0810662494 667131.3373538773, 649066.3721739153 667130.8070712157, 649061.1881912894 667130.4765260944, 649061.1821563393 667130.4765253628, 649053.7803349439 667130.1005920193, 649046.3155020501 667129.7206510757, 649043.0424489797 667129.5543360426, 649006.6491856283 667126.830910647, 648987.0024039603 667125.3605431644, 648985.8614535739 667125.275445675, 648970.8817930752 667124.016521679, 648922.120506357 667120.6717632111, 648910.2716873599 667119.8732146258, 648904.0999356143 667119.4575905052, 648875.0383351245 667117.5000065531, 648856.8516271092 667116.27477376, 648854.396664683 667116.1095177582, 648850.8557725835 667115.8911706635, 648840.5799260609 667115.2558512234, 648833.7679376783 667114.8355095739, 648830.7194228848 667114.6474618644, 648827.9041394365 667114.4738422896, 648820.1872703679 667113.9964314984, 648805.7574506385 667113.1526106072, 648787.8806336278 667112.1064538497, 648773.1030303788 667110.3157183412, 648760.1682811612 667109.4691986308, 648759.5502361839 667109.4281641134, 648758.4453163396 667109.3569908781, 648754.4903251082 667109.097474004, 648751.1253342015 667108.8780282554, 648744.1285743597 667108.4202246047, 648736.7736253596 667107.9383777662, 648730.5136561719 667107.5286629505, 648727.697841409 667107.3452834184, 648723.5868301619 667107.0758277219, 648720.4548931621 667106.8703300803, 648716.4569475213 667106.6088880122, 648709.786072364 667106.1730436344, 648702.5832384059 667105.701295124, 648695.3922966402 667105.2313080332, 648688.1344385054 667104.7565128739, 648683.0054811082 667104.420854419, 648682.1045056409 667104.3617057793, 648677.4045588191 667104.0542590512, 648676.7375246133 667104.0101786255, 648670.5897335703 667103.6083173088, 648663.4298539215 667103.1394539583, 648663.0737946107 667103.1165310204, 648658.384852605 667102.8098856135, 648654.8968563017 667102.5814650226, 648652.4869776758 667102.4232544361, 648646.1370173647 667102.0084087313, 648639.9782215153 667101.6057460778, 648633.7963511078 667101.2010006466, 648614.467622493 667099.9356299016, 648611.1997225704 667099.7222758359, 648607.8936608282 667099.5058771726, 648593.6168557283 667098.5721555825, 648591.3189777406 667098.4217984864, 648580.3221423335 667097.7025524102, 648573.5442365529 667097.25869508, 648560.4261202081 667096.5057523624, 648549.3486904933 667095.8696958512, 648543.5436624509 667095.356996867, 648535.2868729769 667094.6291226489, 648532.1150078156 667094.348740723, 648527.5400315269 667093.9451498364, 648517.3652133368 667093.0469646615, 648487.5618512487 667089.5742624705, 648486.6289338497 667089.4501504565, 648485.701873653 667089.3281191378, 648476.7741943948 667088.1449671742, 648470.5182729131 667088.0253304831, 648464.7973308348 667087.9155185025, 648458.433313113 667087.7928287432, 648452.7434332594 667087.6831805242, 648448.1524441848 667087.5955852148, 648442.523446213 667087.4869043565, 648436.4526553565 667087.3701700615, 648424.2986487475 667087.1366999602, 648404.803963095 667085.8383489498, 648397.3791953747 667084.3934608449, 648368.7581216815 667079.6879505869, 648357.5003536302 667077.5726833422, 648353.8604733713 667076.8882477479, 648347.6996382596 667075.7305504545, 648343.0357872267 667074.8939919954, 648333.6800410134 667073.2157517133, 648318.8319320034 667076.1859302855, 648314.3899159459 667075.6674762352, 648312.8361300406 667073.8873017525, 648308.9258424834 667069.4099026169, 648302.8288188515 667068.9851673547, 648296.8720179844 667068.5694090101, 648284.8462378928 667067.7319587667, 648283.607130984 667067.6457293363, 648281.2311602592 667067.4404831119, 648278.512265735 667067.2071955635, 648272.6403199685 667066.7015281778, 648266.7534644613 667066.194738992, 648263.0205313986 667065.8733292851, 648260.3886103303 667065.6469322157, 648257.4525249879 667066.3305713123, 648257.0075236814 667066.4345166096, 648253.7815750706 667067.186120074, 648249.8894557373 667068.0936416528, 648247.2183831348 667068.7153933325, 648242.6742934401 667069.7748347772, 648240.6942419669 667070.2355513982, 648235.844071054 667071.9090310655, 648231.4539954499 667073.4233676906, 648227.9096843577 667075.0709257466, 648227.2887597729 667075.3599683195, 648223.2094886504 667078.31433157, 648220.9042967218 667079.9460398493, 648220.1441889218 667080.4379440135, 648213.0367718 667085.045047849, 648211.8035935358 667085.8439323665, 648207.4942858339 667088.7854678973, 648205.0190433444 667090.4750751632, 648204.2800563097 667090.9799417921, 648201.6169207942 667092.0207312724, 648197.7948429795 667093.5141769218, 648189.605779941 667094.9141343809, 648188.0047102629 667095.1880183702, 648187.979394719 667097.5879969503, 648187.8135349083 667113.0008190123, 648187.724497557 667121.3108247413, 648187.7022453774 667123.3287266314, 648185.550504915 667130.0994543319, 648181.7422235132 667133.474007385, 648179.0630004687 667135.4525478355, 648179.0069083953 667135.47366088, 648168.9234435062 667139.4952891349, 648168.5474857572 667139.6272426021, 648162.3642416601 667141.7925573384, 648160.7042054757 667142.3742718863, 648153.5879896794 667144.8674711687, 648147.2698383991 667147.1017290823, 648145.3046686627 667147.7963658274, 648143.4506525018 667148.16221854, 648134.6655096228 667149.8921815932, 648123.8623423118 667151.9847775466, 648115.5612399587 667153.6978393772, 648114.7720122549 667155.7516481996, 648113.9789143234 667156.038589986, 648104.7295958997 667159.3864046129, 648103.6505353347 667159.7773109884, 648096.3454429151 667162.4213664015, 648088.9981039136 667165.0814165975, 648086.7510522836 667165.8942583587, 648075.6265576085 667169.9218408875, 648065.4332608241 667173.6116187681, 648058.4119526136 667176.5387607638, 648053.0637598021 667179.3449887594, 648051.7615479444 667180.0278700083, 648048.52146159 667181.7274750033, 648044.6140319811 667185.953062145, 648039.443977242 667186.3913290716, 648035.7800135664 667186.7019345937, 648031.2459776541 667186.7964391378, 648030.0521886307 667184.7643137567, 648024.8746702563 667173.4557889089, 648024.4037125686 667172.4267811717, 648023.059158344 667169.4906448754, 648018.5660566387 667170.1510699783, 648016.9800785764 667170.3838411716, 648014.2680293829 667170.7825583036, 648011.7869099743 667171.1472229279, 648004.7088503618 667172.1873402906, 648000.6609229676 667172.7828583816, 647997.6948688201 667173.2185052732, 647990.8658441902 667174.2216922742, 647982.8596278628 667175.3988191199, 647975.59856392 667176.4659546752, 647969.300624366 667177.3922448853, 647963.4054363831 667178.2584230449, 647948.8043201498 667180.4046847183, 647941.2849271602 667176.1337504209, 647937.6799989039 667174.0861408617, 647930.6577456082 667170.0976630744, 647918.1546527412 667172.3291315435, 647914.6046043767 667172.9626280452, 647901.1202780089 667175.3701394299, 647892.8081948045 667176.8529878383, 647886.1090759165 667178.0493086484, 647869.1029241452 667181.0842398513, 647858.5287499453 667182.9719383512, 647846.5306513499 667185.1135469676, 647844.0455805456 667185.8642881436, 647835.3583055832 667188.4882035245, 647832.3382345027 667189.4008003629, 647812.013179175 667173.5246047776, 647811.7535142567 667171.9185064469, 647810.6423097722 667165.0605078989, 647808.9055329884 667154.331425212, 647807.9443817732 667148.3932773268, 647807.2099052548 667143.8572254406, 647805.8619165955 667135.5312102253, 647804.9475905198 667129.8801056745, 647803.7002737931 667121.3027442652, 647802.4396557988 667112.632902015, 647801.8771748906 667108.1018304684, 647801.2243861463 667106.1998480986, 647797.0918920181 667094.150493329, 647791.6928569492 667078.4069376375, 647785.5879793924 667060.6093945623, 647784.403413942 667057.1552014408, 647784.3624263057 667057.0342375381, 647784.3354469987 667057.0302343895, 647783.6735663812 667056.9330371018, 647778.9356800561 667056.2345639032, 647774.2947061819 667055.5509820165, 647773.0018853318 667054.8938746505, 647752.8282713412 667044.6416551875, 647751.6742460008 667044.0544836562, 647746.2106402526 667041.2778609227, 647740.5841007796 667038.4181796311, 647730.5297741881 667033.3080737069, 647730.0798968719 667032.6289859025, 647725.4286800274 667025.6084916635, 647723.2511675039 667022.3202603005, 647718.9760020467 667015.8668460572, 647714.5577988385 667009.196296642, 647712.8310507725 667006.5901931837, 647712.2991547972 667006.0301347991, 647685.6928194218 666978.0041927691, 647682.7531046325 666974.907872309, 647680.8771808154 666974.4166163559, 647679.0163440155 666973.9284021467, 647676.7964027382 666973.6462242807, 647619.648698367 666958.0095711981, 647618.5537469371 666957.6974453628, 647614.8150702982 666954.9391088259, 647609.7415449457 666951.1964635814, 647606.0058854301 666948.4411673977, 647606.4130604081 666947.0221860933, 647614.3761023644 666929.9632510096, 647614.9291263586 666929.0002832428, 647617.4638046373 666924.5886142913, 647622.4147430309 666915.9862207726, 647623.2699324238 666914.5013724302, 647625.5974060768 666910.6195950714, 647626.7595451543 666908.6808661852, 647628.1907937914 666906.5620102071, 647644.7605499269 666882.255178708, 647647.8630304382 666877.9746151109, 647654.5941847659 666868.6863551056, 647661.6493278758 666858.950296439, 647664.2786960391 666855.3226323302, 647668.7213359731 666849.1921598513, 647670.3075083751 666847.3433998399, 647671.6318336574 666845.7995668163, 647676.3733088296 666840.271204632, 647678.2057061938 666838.1363956427, 647681.5940182072 666834.8428977552, 647687.5135789145 666829.0876351914, 647692.980287519 666823.7742361724, 647697.9327226381 666818.9598130719, 647701.2171857933 666815.7672621864, 647707.8558772354 666809.3140090762, 647711.480289779 666805.7915812513, 647720.2841840588 666797.2326843071, 647720.5632443145 666796.9626392602, 647729.128211616 666788.5187532295, 647733.7447872132 666783.967332084, 647743.3867660763 666774.4615010112, 647744.7238438937 666773.1437486463, 647745.0020171334 666772.8697035224, 647756.9849001302 666764.4012601285, 647776.4703982741 666750.6316610947, 647781.1440044281 666745.6341699571, 647783.1933104674 666741.9755594241, 647784.2095564221 666739.9456147073, 647782.6672713102 666734.038438417, 647780.7671357574 666726.7653104749, 647780.5943319564 666726.1012952231, 647778.5258876726 666720.6380535694, 647776.961556982 666716.5049410759, 647776.7055319465 666715.6279576594, 647774.7274139486 666708.8588166898, 647774.6415403518 666708.5616888574, 647774.1227105994 666707.0966790103, 647773.0770666796 666704.1415385663, 647771.5785999997 666699.9084346162, 647771.462555587 666699.578503484, 647770.1489796926 666695.8673373032, 647768.3944871036 666692.2410785459, 647766.8587959149 666689.0670020861, 647765.7092544577 666685.141776876, 647765.3474834152 666683.9059037708, 647762.5996901521 666674.5285322606, 647759.3889933943 666663.5682384244, 647751.709118723 666637.3536156585, 647749.3452753276 666629.2833193897, 647745.9285875547 666617.6180468197, 647738.2228090094 666591.3143019335, 647738.1529031734 666591.0763755336, 647734.1075956635 666577.2680055429, 647732.6241387655 666572.2048298678, 647730.9659176595 666566.5437182266, 647727.1577720441 666567.3011841016, 647702.340391293 666572.2363107825, 647688.095255537 666575.0696629564, 647681.6641499706 666576.34890096, 647681.278081519 666576.42373523, 647680.0250894506 666576.6687067995, 647665.2821523502 666577.3418573822, 647634.045663759 666566.4283686633, 647634.7549958962 666563.0793575359, 647636.3610281817 666555.4916040521, 647658.8355359635 666449.2931318976, 647586.9795964336 666429.2248883679, 647553.7408844952 666420.1250741959, 647545.381153617 666417.8361149596, 647536.2046010677 666415.324022016, 647519.1283789774 666410.6481427168, 647516.238441983 666409.8577311308, 647494.5362265161 666403.9152411851, 647481.1168874248 666400.2417016127, 647466.9073507432 666396.3511114406, 647463.4875980379 666395.4147191222, 647458.476800323 666394.0431042816, 647451.0430492434 666392.0073714969, 647444.9802519368 666390.3485959851, 647441.4744153143 666389.388193817, 647440.3138585786 666387.0271166776, 647439.2150559541 666384.7940457539, 647437.2154910302 666380.7277639253, 647436.3006929645 666379.0057505091, 647434.999035945 666376.5524976169, 647434.0643626206 666374.7924021799, 647433.8203574029 666374.3323772709, 647431.6209382048 666370.1902327046, 647428.5035268439 666364.3188744306, 647427.2508480103 666361.960666534, 647422.1270923859 666352.3092634772, 647421.4312780413 666350.9991124477, 647419.4038161349 666347.1800254913, 647417.8020767663 666344.1637019927, 647416.9802670331 666342.615778109, 647414.1077369095 666338.7754726066, 647408.960573415 666331.8928452262, 647407.8258299667 666330.3757645552, 647401.2809122478 666321.6261085763, 647399.371334763 666319.0718255361, 647397.7065032658 666316.7217294908, 647393.5842663706 666310.9012936148, 647388.6932513282 666303.8697374694, 647385.789699204 666299.6953510751, 647383.1072720509 666295.8390680269, 647373.7918049602 666282.4481651271, 647372.870036873 666281.1219878856, 647369.2266209616 666275.8856034899, 647365.157459828 666270.0352542156, 647362.6739743394 666266.4639522393, 647356.3649908156 666257.3952867313, 647355.1262649918 666255.6141960747, 647352.736659764 666252.1798640176, 647349.3983114663 666247.3795119239, 647346.7288394578 666243.5422703763, 647343.7993770412 666239.3309214225, 647341.7606259806 666236.3998264153, 647340.42695481 666234.4825657706, 647339.8629617685 666233.6725862878, 647338.2783510367 666231.3943391828, 647334.4200216774 666225.8480123127, 647330.6656885918 666220.4504964371, 647329.7618447202 666219.1505611386, 647326.7754151664 666214.8571262524, 647329.2349627818 666209.3745766588, 647328.8327727119 666209.1940511174, 647320.0195562447 666205.2404132073, 647294.2813275533 666201.2014398059, 647217.2804762544 666213.1073945459, 647168.6726750862 666230.7305384312, 647160.3068809431 666239.0035756014, 647159.091802514 666239.839427044, 647146.651849048 666248.3969460712, 647145.1611100069 666246.3276684759, 647141.2243506525 666227.3094404048, 647136.0034260192 666202.0859919683, 647124.5452781047 666146.7330960003, 647122.53538828 666137.0209389211, 647122.1736792083 666135.2719506631, 647117.7345919734 666135.4813508259, 647115.0495556741 666135.6080763956, 647082.4716361816 666137.1433013496, 647076.4927401014 666137.4245211893, 647059.911096954 666135.1456439019, 647042.2096338393 666132.712557177, 647032.5817376871 666131.3885639802, 647023.7319865457 666130.1374610395, 647021.6600809059 666129.8442615533, 647017.1461913704 666129.2056598032, 646969.7152954473 666122.4982575345, 646922.2845772717 666115.7897356339, 646915.3457014299 666115.1569308654, 646915.4489201044 666112.9190410114, 646912.5445713752 666099.1508134039, 646911.5223248384 666093.5225796263, 646910.9317009741 666089.8156606881, 646910.6830160812 666088.4196429596, 646910.0472903735 666084.8555976003, 646903.8106519444 666049.8831528166, 646896.7167751581 666008.3537013206, 646868.008434196 665928.9376761273, 646817.6538526337 665853.3782872215, 646814.4044696903 665846.7475437709, 646811.4434514705 665840.7056308081, 646808.4561699725 665834.6093150812, 646796.4981710925 665810.2070913082, 646768.269301044 665752.6029884877, 646752.2163452156 665719.8433725836, 646745.0391681109 665705.196534435, 646725.6401161118 665665.6104854752, 646722.7488377828 665659.7113001212, 646719.2678786521 665652.5840526301, 646714.8357602136 665643.5093447418, 646712.8603299141 665639.4662561984, 646709.4201851961 665632.424773039, 646706.8857715458 665627.2355455058, 646704.665453302 665622.6903511317, 646701.3942460427 665615.9930458373, 646698.9838733954 665611.0587114145, 646693.686129083 665600.2111122502, 646693.6311177948 665600.100226433, 646689.8170754997 665592.2907038867, 646684.6694579927 665581.7522406018, 646683.7976209994 665579.9681485858, 646675.5570821917 665576.918244494, 646670.0523853936 665574.8795338502, 646669.8214699194 665574.7945473939, 646663.8936351965 665572.5997882801, 646663.3036560236 665572.3818006893, 646661.2158327134 665571.6095614977, 646641.8468037834 665564.439342555, 646630.0574759291 665560.4069501938, 646557.7579881407 665535.6746572048, 646521.0437330795 665523.1154062888, 646519.3549552504 665522.1032149574, 646508.4688603027 665515.5831016016, 646501.6603255102 665511.5042502393, 646499.5335124267 665510.2310492861, 646486.3505730985 665502.3345157885, 646467.5059355925 665500.5892891013, 646462.2490266149 665500.034759474, 646454.3866840419 665494.4618734958, 646447.8991123944 665491.4841736364, 646444.5363415661 665490.5047470748, 646423.7782515759 665484.4583608923, 646414.7703816743 665482.9302794706, 646381.030194725 665487.5063225687, 646367.8241272045 665488.9558156667, 646359.4273210054 665487.9617225527, 646351.8707028317 665484.7549394423, 646349.5988662239 665483.5417213164, 646342.53221198 665479.7677984176, 646335.99402804 665475.5171397245, 646328.2087929255 665469.3332262883, 646309.6619195222 665451.92620022, 646303.9380718328 665443.4526196226, 646304.3282466754 665441.6936347316, 646304.3962647552 665441.3917243135, 646301.6385531186 665439.2083749415, 646290.1228277315 665430.092045055, 646283.3711049489 665427.4002344375, 646279.2073651304 665425.7397592108, 646274.2918751764 665421.4782508244, 646271.1932492867 665418.792945134, 646256.046561801 665417.5281502058, 646255.6205694779 665417.4930608467, 646250.2769129847 665415.0495727031, 646248.827006679 665414.3863676976, 646246.3851763328 665412.759052614, 646242.1096494631 665409.3056142834, 646238.4469659813 665406.3461645278, 646237.8910675602 665406.1201811103, 646235.8770890555 665405.3049113352, 646232.4163901252 665403.9024365158, 646222.2646163532 665401.7643074682, 646221.3486781465 665402.146118828, 646219.3003094654 665405.4089025976, 646218.9052998049 665406.0389331843, 646208.218819538 665401.6097145699, 646190.2050371008 665392.6006289737, 646143.2870784189 665369.8832974198, 646138.5584266535 665367.6717196816, 646133.8977529838 665365.4923096747, 646110.2392214881 665354.4995389645, 646083.6378089165 665343.1315482407, 646035.459682708 665313.8769879098, 646023.5719514876 665305.9857294057, 646014.5665678009 665300.0077554772, 645997.3392704863 665286.6397466873, 645986.9622599736 665279.9766571847, 645986.7439414553 665274.0035451011, 645986.4677718553 665266.4345951807, 645985.9345380012 665251.8357345663, 645985.1148746058 665229.3756044785)))" + } +}, +{ + "model": "common.municipality", + "pk": 11, + "fields": { + "fid": "", + "name_fr": "Schaerbeek", + "name_nl": "Schaarbeek", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((649185.1160702296 672214.3830949513, 649227.6921865133 672198.5281627327, 649237.1925469979 672194.9913603467, 649319.0586532966 672164.5086783681, 649323.5938254271 672162.8192498581, 649339.672435241 672156.8332758559, 649340.2864585669 672156.6043532268, 649387.6232539496 672138.9783171155, 649394.2735061328 672136.5021548741, 649402.9678357759 672133.2652500923, 649431.2289074057 672122.7418098423, 649498.6615059235 672097.6333206672, 649514.2101066305 672091.84428342, 649528.212647683 672086.6300508777, 649535.7069359547 672083.8499966685, 649550.7955164211 672078.2519007676, 649570.470273245 672070.952383382, 649576.8695193968 672068.5781908007, 649583.7287831404 672066.0340562199, 649597.234302549 672061.0237601093, 649604.8315947846 672058.2047185544, 649610.6438183032 672056.0484517689, 649622.9942933146 672051.4660097203, 649626.2324178731 672050.2644181782, 649637.03383315 672046.2577806035, 649647.1252211679 672042.5140534069, 649658.2586492706 672038.3834575614, 649665.6269225706 672035.732385923, 649675.4862929056 672032.1466285097, 649677.5893718832 672031.3818935505, 649684.3726266819 672028.9147484025, 649708.2123684606 672021.51173908, 649717.162855447 672017.01188008, 649732.1034368065 672011.410764289, 649733.190479142 672011.0029013794, 649735.5015690975 672010.1361928243, 649738.8747003482 672008.8716181903, 649745.8249707152 672006.2664946279, 649752.2032188778 672003.875298903, 649756.6973936755 672002.1908655853, 649761.2215697368 672000.4944360413, 649765.1257216102 671999.0309283062, 649771.8029813706 671996.5277701976, 649778.6002457888 671993.9796271862, 649785.209502947 671991.5014604506, 649791.4527457727 671989.1612475329, 649798.1770073768 671986.6400952308, 649804.6622596312 671984.2089127671, 649811.4625241212 671981.6597699865, 649817.8797737733 671979.2535788972, 649824.1290167745 671976.9113665951, 649829.8602425465 671974.7620918138, 649837.9415608837 671971.7331145668, 649839.9961591493 671966.6934193919, 649860.1889247415 671959.3699720857, 649861.5759773001 671958.8671474181, 649871.2643445797 671955.3533720906, 649880.716702837 671951.9255668647, 649887.7671933466 671947.5254772784, 649905.1668016841 671941.6366719424, 649926.9445629353 671934.2664185893, 649940.6680425466 671929.6221492708, 649949.7833611035 671926.537198753, 649942.1542260515 671903.2645057647, 650053.8290469232 671866.120575902, 650065.1721528067 671897.7166403027, 650065.3400598298 671897.6502615628, 650071.629422538 671895.1655579563, 650088.1800725561 671888.9676500657, 650090.936180741 671887.9359984357, 650106.7108002669 671882.0279922631, 650106.936809306 671881.942020841, 650120.8803566088 671876.7217830773, 650126.5155778455 671874.611495251, 650128.4176524574 671873.8997356205, 650131.0707566335 671872.9060709064, 650132.1898006559 671872.4862123312, 650136.1049542723 671871.0207070909, 650137.2589996572 671870.5878529323, 650141.4121625966 671869.033377754, 650142.2371949698 671868.7244820079, 650150.9445367772 671865.4635823048, 650156.854768755 671863.2503291165, 650158.4458311982 671862.6545301557, 650159.1588591697 671862.3876202517, 650165.9251247584 671859.8534751972, 650172.3753778634 671857.4382901791, 650176.0465220273 671856.0627540231, 650201.3645270867 671846.5819655135, 650204.1266366666 671845.5483158686, 650209.4359014343 671843.1139938667, 650239.7404124099 671829.2198633675, 650244.8496670991 671826.8775156921, 650246.9117699332 671825.931778969, 650266.9767701228 671816.7323405632, 650271.6485631859 671812.4395587357, 650276.3247567081 671808.1425774641, 650287.7067882459 671799.0860691492, 650302.0920917096 671787.6409541927, 650336.1801801324 671760.5184200886, 650339.6804971596 671757.7338785785, 650348.1532647233 671750.9919883453, 650351.5195695385 671748.3144292341, 650361.4334745993 671740.3677281945, 650373.2075492375 671730.931270712, 650374.8677007791 671729.6004881964, 650376.909887044 671727.964755711, 650383.5684947895 671722.6276279541, 650386.3767510606 671720.3769957976, 650390.3341123003 671717.204514156, 650394.626504029 671713.7640763713, 650406.203560308 671704.4865925958, 650411.8608764557 671699.9528334643, 650417.8736250332 671695.1341208402, 650422.5620526387 671691.37773475, 650425.7483433291 671688.8241519602, 650428.6096043022 671686.5315265942, 650432.5989682713 671683.3340489331, 650436.8403549738 671679.9366042253, 650440.6777050333 671676.8611066161, 650444.7960806903 671673.5606457777, 650448.8904541788 671670.2791817792, 650453.0068294583 671666.9817206229, 650457.1362061065 671663.6722611644, 650461.105568065 671660.4917807315, 650465.2589468483 671657.1633243709, 650465.1699741932 671656.9413156543, 650465.9028055333 671656.647608642, 650481.7724526745 671643.9303856207, 650486.1010473258 671640.4619520771, 650486.5988927179 671640.0630172266, 650491.4253328558 671636.1947488138, 650498.8150053121 671630.283715653, 650506.1536731981 671624.4126757598, 650516.1635840753 671616.4049852324, 650526.071485682 671608.4782812458, 650538.3326009971 671598.6688850456, 650547.0623947761 671591.6860268463, 650551.092776182 671588.3384551858, 650555.1221575524 671584.9910833888, 650557.9284142512 671582.7334505125, 650567.8393208254 671574.7597470167, 650569.8665062144 671573.12901219, 650580.7575023575 671564.3664367552, 650585.3469231187 671560.675037514, 650586.3710171895 671559.8511716399, 650593.7206923583 671553.9381341874, 650603.2325659771 671546.2863797941, 650616.0747455308 671535.9540613465, 650619.7950871295 671532.9615484417, 650642.1111364122 671515.0073698823, 650663.7201202194 671497.6231981433, 650678.6074867569 671485.6462463039, 650691.4956696035 671475.2779326206, 650693.1068175404 671473.9811434181, 650699.940444593 671468.4840374365, 650709.8103503331 671460.543328591, 650712.5696034972 671458.3236895242, 650716.2189382944 671455.3881668708, 650727.2619515365 671446.5036112466, 650738.2679350509 671437.8650484608, 650743.361390121 671433.8677135342, 650750.6890448015 671428.116670282, 650751.5821245837 671427.4157868808, 650757.2226285391 671422.9885232951, 650761.1239771286 671419.9260326254, 650766.3974481471 671415.7877210492, 650771.8239329151 671411.5284294195, 650774.4221649285 671409.4897685753, 650777.1634098056 671407.3381263892, 650781.7358181726 671403.7497232072, 650782.5228884908 671403.1318259409, 650787.4993329105 671399.2264754568, 650793.2548469317 671394.7092266263, 650795.7130665118 671392.7795474415, 650801.7964221549 671387.9028423477, 650802.1526546916 671387.6172888954, 650808.5912428236 671382.4552301695, 650818.025104527 671374.8914627145, 650822.3935033972 671371.3900334006, 650829.5511571473 671365.6509684488, 650835.0566598407 671361.237687605, 650839.3960561539 671357.7582544233, 650843.1493988043 671354.7497446614, 650847.2597741312 671351.4542815193, 650851.1881328222 671348.3047945838, 650859.5738983107 671341.5828897281, 650865.7984666924 671336.5916025883, 650872.0230351398 671331.5995154069, 650877.393523894 671327.3062165233, 650881.8999338731 671323.7048047974, 650886.2283278683 671320.2443698414, 650890.6387292385 671316.7189455623, 650895.561177388 671312.7825881151, 650900.0895895498 671309.1621791987, 650904.3059733509 671305.7907295375, 650908.755378307 671302.2333102694, 650913.507810903 671298.4329305274, 650918.1222307251 671294.7445327354, 650922.3686171952 671291.3490869051, 650926.8050208313 671287.8026658379, 650932.0855013779 671283.5803549098, 650942.3464348454 671275.3776937778, 650954.5708331818 671271.4952308182, 650958.4339591222 671270.2677165443, 650960.8640382865 671269.4960220784, 650964.6101603078 671268.3064930756, 650970.3933487837 671266.4692201854, 650973.0444351116 671265.6275534919, 650976.5855505802 671264.5019987011, 650979.0716315802 671263.7123112595, 650982.1437316401 671262.7366974868, 650989.8229817945 671260.2976629164, 650994.5921370973 671258.7832624703, 651000.2473214223 671256.9859734226, 651004.1214474887 671255.7564604349, 651007.5715598199 671254.6608941468, 651020.8389918132 671250.4475619434, 651030.1492950573 671247.4897322711, 651033.8544156685 671246.3131979954, 651041.6776704038 671243.8281813475, 651045.271787411 671242.6866331078, 651046.519827977 671242.2907899709, 651049.5359262397 671241.3321690763, 651052.7010294342 671240.3255669081, 651055.5271213185 671239.4289221059, 651056.9325319089 671235.9701281289, 651063.4291530441 671230.5239776227, 651069.013686925 671225.842607811, 651079.1556050295 671226.0228492579, 651087.5141326645 671229.514839878, 651094.6097269043 671232.5186804674, 651101.7063211702 671235.5225212105, 651129.6897557986 671247.0838394873, 651151.7975027843 671256.3544600343, 651159.7878001694 671252.6819752133, 651172.3915269476 671246.8890764526, 651198.0972700656 671231.232379, 651205.8898977332 671225.6653879635, 651213.6835252378 671220.0993969806, 651235.6316793376 671201.2252689211, 651255.9140969606 671180.2529558096, 651264.3001018438 671171.537066979, 651273.0811540418 671162.4102303819, 651336.2801148568 671101.764551966, 651342.0527434534 671096.2833112248, 651347.8243719056 671090.8030703051, 651460.3976225256 670983.9248665362, 651470.8627987341 670973.6752441686, 651481.8590346044 670962.9046915533, 651494.0294022704 670950.984293279, 651502.099309151 670943.0793552474, 651519.0962186948 670926.432591648, 651551.4801624801 670892.1808753362, 651556.8338255725 670886.425584229, 651557.0937577612 670886.1462186441, 651562.1874886329 670880.6702930777, 651575.4100325607 670867.2300363514, 651586.3422234841 670856.824470856, 651598.1674247101 670846.2850157224, 651604.361033158 670840.9358231928, 651610.5546415824 670835.5866306094, 651683.4576834583 670773.5951215962, 651699.7622577648 670759.7312432749, 651707.018958453 670753.5601874649, 651709.8183559487 670750.1265614557, 651729.5538944951 670723.1339234338, 651738.6869467696 670710.6424550456, 651748.2253721869 670698.3517341479, 651755.536464835 670688.9297144562, 651793.4333557016 670629.9208878388, 651799.3195783145 670619.4927027005, 651804.7326664161 670610.2024492135, 651854.8566843771 670524.6363538881, 651864.9711344014 670503.816778896, 651874.5244483389 670484.1521245555, 651875.1856085891 670482.7902176833, 651875.5287196637 670481.8522681256, 651877.9645085338 670475.1906262384, 651881.2095595422 670466.3151033003, 651892.9694231673 670433.5868607708, 651900.5984643985 670407.8320619416, 651902.8504189523 670399.7554214682, 651905.1023734774 670391.6787809826, 651917.2145243834 670348.0927159946, 651918.0318720134 670345.1508465661, 651919.3994974807 670339.8632688788, 651920.4015557735 670335.9888317662, 651999.4821334124 670388.9484152575, 652006.0295614202 670393.4031772626, 652011.0981186881 670396.8517671876, 652118.2845875825 670471.1702614278, 652123.696105136 670474.9218962298, 652127.7778382578 670468.6374570624, 652195.5199301764 670364.3438772646, 652199.5486478531 670358.1414385842, 652205.6930157113 670363.1281458677, 652435.6003824451 670549.7088208363, 652457.7782017265 670566.9104175409, 652462.842704026 670570.838010583, 652482.373785195 670585.98629779, 652501.5119061605 670600.8295392767, 652507.2757404455 670605.3001144013, 652520.6490282738 670615.6727809248, 652540.0941213187 670630.7550589454, 652545.32560849 670634.8126718709, 652568.059381065 670652.4443356433, 652641.7548516353 670712.2809500933, 652646.5555618309 670716.1787113767, 652651.0589024429 670719.8350379124, 652657.9022043927 670725.3918380636, 652759.570852156 670807.9417299237, 652790.2367355266 670832.8463183384, 652811.4885774672 670850.1052055828, 652844.1952590577 670876.6666341483, 652948.0907886882 670960.6368048771, 652963.2092679826 670972.8555766091, 652956.4210574245 670991.7355718414, 652939.007384648 671040.1719943449, 652935.4092517844 671049.846464587, 652931.7300936098 671059.735922886, 652905.7267473767 671130.9470819162, 652902.0873526536 671142.8155278378, 652919.4424158104 671150.0796212627, 652933.631161088 671151.7173697598, 652950.7139229996 671153.1124798662, 652959.293886509 671153.1135456897, 652978.361018199 671151.3229295921, 652979.267987492 671159.9759682122, 652979.6036063326 671163.1759825235, 652980.8082369283 671174.6730338016, 652982.9488050817 671195.0911250398, 653051.460484083 671262.8990595376, 653040.692587222 671279.2635817114, 653025.0748349588 671302.9974379998, 653023.1025076233 671305.823168715, 653005.1945337731 671331.4867234413, 652997.5532643624 671342.4367799298, 652969.0685291463 671383.2558891769, 652963.2465607147 671391.5989938013, 652963.0048204926 671391.9454607727, 652957.4245920525 671399.9422983713, 652954.205056208 671404.5568584771, 653025.5581998221 671451.7003285545, 653032.4716330075 671456.268049358, 653049.3002537335 671467.3871474661, 653121.2963611169 671514.9556976622, 653077.492873841 671579.5596993637, 653054.0952080147 671611.3745184503, 653035.1262334911 671637.1679392157, 652991.9074481528 671695.9340614007, 652984.9873602759 671705.3441199978, 652983.0530562059 671707.9738568539, 652975.813917757 671717.8178719952, 652974.2266681643 671719.9756560539, 652971.2692029529 671723.99725368, 652967.7096429437 671728.837669382, 652953.1874465494 671747.8297998657, 652944.2490770423 671759.6635863174, 652883.3237296597 671840.3223127295, 652878.8150369977 671846.2916003254, 652828.789343679 671912.5209042674, 652823.1071687381 671920.0447321874, 652822.8744328885 671920.3530005666, 652766.8242215377 671898.0182112736, 652752.2095440284 671892.2416407502, 652745.1682922437 671889.4585877421, 652736.5298327706 671901.9734044707, 652655.1560616053 672019.8522547269, 652646.3249235072 672029.619070389, 652636.5146588207 672040.4697546097, 652630.5188858359 672047.100950355, 652569.1710124993 672114.5617230969, 652565.3664745329 672119.1583092107, 652561.4349527914 672123.6997816563, 652557.8244753485 672127.8712975176, 652552.4767680481 672134.0494804479, 652545.408832957 672142.2145327431, 652545.294801664 672142.4855163178, 652538.3418974732 672150.3795851934, 652534.4903876291 672154.8281687935, 652531.3259684729 672158.4847445133, 652526.4896767536 672161.0751206211, 652521.7133884404 672163.6345044655, 652521.3843686212 672163.8104620271, 652517.0181051227 672166.1488987815, 652512.1808130469 672168.7402747723, 652507.4185253921 672171.2916604477, 652502.7832453289 672173.7750625117, 652497.7639420639 672176.4634150473, 652496.1078418904 672177.3512014144, 652486.0482336777 672182.7399037981, 652486.7439897562 672184.8209734801, 652488.4893781703 672190.0391483008, 652489.6979547366 672193.6522693502, 652495.6048851217 672211.3157609311, 652498.0861038762 672217.9810155832, 652496.534385352 672219.8809066897, 652486.4314621394 672232.2504466893, 652455.933160595 672269.5923415093, 652439.9248997878 672289.1921869542, 652438.5877107674 672290.8300069837, 652426.4195927306 672300.5724114758, 652420.3677675824 672307.7195991795, 652403.9225042316 672327.3093905732, 652388.0367996249 672350.6492212368, 652381.9537633421 672359.5863905906, 652366.7221667399 672381.9663107943, 652362.1523873893 672388.6796868512, 652350.0293186873 672406.4910316694, 652342.8250886054 672417.0760481064, 652338.4854235568 672422.8014609455, 652333.7366957157 672429.06581846, 652328.9879675547 672435.3312759793, 652321.6398405347 672445.0252818479, 652277.642090803 672503.0713321086, 652272.3592824767 672510.040618546, 652271.7177785911 672510.9356315017, 652271.2901093101 672511.5324734662, 652260.5863751445 672526.4689210817, 652255.0119714641 672534.2493646878, 652250.4887380165 672540.5626509296, 652246.2280471664 672546.5080728173, 652211.2913764027 672595.2652325236, 652208.3528990404 672599.3659338362, 652202.8690079559 672607.0191898003, 652188.8377269143 672626.6012861077, 652184.363999443 672632.8436791517, 652177.6495699215 672645.0837448828, 652166.6702316925 672665.0962172216, 652119.3161337216 672751.4095300137, 652118.6049818199 672752.7064310906, 652074.7766150477 672832.5953348713, 652058.4111126104 672862.4250586404, 652049.4501936516 672878.7588123119, 652040.4554045217 672893.9845707671, 652033.471991954 672906.0006052745, 652032.2288493632 672908.0640340181, 652022.0227571198 672925.0050280229, 651992.4108574698 672975.1229409566, 651989.11007245 672981.7844766832, 651985.8102873171 672988.4460125323, 651969.2651430056 673023.589671208, 651963.1936365397 673036.357813079, 651957.2321296221 673049.1259686251, 651938.133291748 673090.0932628163, 651930.38323449 673107.7461583671, 651928.32469074 673112.4338651559, 651923.2809661917 673127.238119775, 651918.2909265622 673144.7053593313, 651914.1979262279 673161.8017130159, 651912.9692962253 673167.1835169615, 651911.1303531978 673175.2392234865, 651895.6689963981 673238.1257945023, 651893.4869505606 673247.0644510763, 651891.3039048312 673256.002107503, 651866.8589940502 673349.3133134532, 651863.1434144881 673362.8277419186, 651860.7953943625 673371.5533791899, 651844.415385918 673431.5058554105, 651841.5814040741 673443.3095069213, 651835.8592138584 673467.1425011961, 651835.3477633208 673469.2823201576, 651832.456547007 673481.3789619936, 651811.7655710056 673549.6738313427, 651800.217950059 673580.7611408454, 651794.9552996737 673594.9293702435, 651744.782565659 673729.9720181861, 651739.8807892109 673745.1852826974, 651733.5550527198 673760.1163721874, 651730.7141563239 673767.8069550996, 651727.8742598972 673775.497538108, 651719.0295381495 673799.3272406806, 651701.4122549718 673873.1494398164, 651699.5933943427 673880.8171501751, 651697.7755336771 673888.4848606458, 651687.6753593876 673934.5422232085, 651681.5083845808 673943.3843820604, 651677.4697460374 673949.1755311706, 651675.3414094903 673952.2275408393, 651652.7636671764 674004.6323950686, 651635.6891465412 674044.2638394899, 651632.7423936374 674051.0134163646, 651629.7966407771 674057.761993349, 651615.2839012175 674091.2709067763, 651607.0119382102 674108.8797295674, 651591.0554313748 674140.3704787982, 651586.3503776557 674149.8268135878, 651578.1474054254 674167.5016438356, 651572.9283883297 674176.650916927, 651538.0375850954 674237.8180558122, 651529.9455498263 674247.2189679247, 651524.0537958723 674254.0641757774, 651522.7845201776 674256.5359968022, 651499.5604763053 674301.7447215451, 651457.4477557306 674380.8588055747, 651435.7900593503 674423.9947427143, 651431.1601467383 674432.3870947264, 651412.8305320062 674465.6135289747, 651381.0442575782 674523.2340780904, 651380.7223716943 674523.1169388617, 651204.340027357 674458.9287492712, 651106.6163454894 674423.3656480919, 651092.6539635642 674418.2849479868, 651084.4673713618 674414.8999546466, 651076.2817792642 674411.5149614608, 651063.5635978532 674404.6005324672, 651053.2692064455 674399.4901907922, 651052.337774213 674398.9140794147, 651052.3330745492 674398.911178852, 651046.9136684964 674395.5624308162, 651038.2032137574 674390.9693825549, 651033.7624958135 674388.5918484555, 651025.7150238502 674384.1328818761, 651021.9363653439 674381.2014351869, 651011.7982583666 674373.5462349856, 651007.7574972411 674380.5016711534, 650977.4068608243 674360.3730546305, 650964.2273039264 674347.9095162596, 650964.0576225419 674347.7487964553, 650963.7183597566 674347.4274568603, 650959.300845745 674343.2509413549, 650958.4399423106 674342.4358409729, 650956.4771621074 674340.5806120876, 650955.5932611618 674339.744509021, 650947.4791254682 674332.4636594885, 650944.5324394212 674329.8193146801, 650939.4279833015 674325.2387174045, 650933.3166345118 674319.7550023423, 650922.921723027 674310.5967846947, 650918.4931798448 674306.7562654456, 650917.8232487428 674306.1771868803, 650898.7132154122 674289.6499460635, 650896.9383980263 674288.1157379618, 650896.1482981392 674289.015631534, 650893.3809484045 674292.1662587613, 650852.2842705335 674255.83244806, 650850.963409554 674254.6641934762, 650852.2465803736 674253.1283669894, 650789.6691624272 674197.8640447756, 650792.3665200919 674194.648409419, 650782.1735998772 674185.5882176461, 650783.5507758444 674184.004403315, 650768.6003264845 674171.0086529095, 650767.3451698464 674172.4194839785, 650756.4263028008 674162.9272057469, 650748.6803354027 674171.6391631163, 650741.5190946885 674165.2733260703, 650742.7022517726 674163.8614860458, 650726.125972407 674149.4525458529, 650724.83581128 674150.9033721869, 650699.7964983874 674128.3824486319, 650697.7217020808 674126.6822049479, 650611.5771698693 674056.0770910826, 650597.7165338981 674044.7174644358, 650570.2152418152 674022.1772375203, 650557.873457639 674012.0617896165, 650500.3911257143 673964.9490474409, 650489.2792210508 673955.8537442808, 650476.6964618736 673945.5532687826, 650435.265549777 673911.6404115063, 650415.8707648311 673895.7649382724, 650411.2609201433 673891.9915980138, 650407.5852831934 673888.9831672522, 650394.5987415687 673850.7878937265, 650387.452195498 673829.7701931314, 650349.4485116905 673717.9924884354, 650346.5966228773 673709.6048114666, 650343.843499038 673701.5079441107, 650332.1476466558 673667.1068087504, 650324.2174588221 673643.7840392478, 650318.9793166878 673628.3765311376, 650312.1587357606 673608.3158696855, 650287.0796318537 673534.5524392797, 650277.9558686558 673507.71755577, 650265.5252789055 673471.1573526056, 650265.1604083502 673470.0843173007, 650264.6205394752 673469.0042603137, 650263.9756432574 673468.1581880926, 650263.0207280724 673467.484075737, 650262.3757658887 673467.1923983209, 650262.2907708674 673467.1539881211, 650260.8307945702 673467.0138080178, 650258.5177944227 673467.1085197183, 650243.5507934375 673467.7216541972, 650238.2908009621 673467.8709991925, 650233.0508214771 673467.9103477089, 650227.8308573707 673467.8196999297, 650224.03088879 673467.7092287856, 650216.4409670664 673467.3582889597, 650211.9510244327 673467.0577338712, 650202.9911557396 673466.3166274911, 650189.0314242626 673464.6249088013, 650177.1716993398 673462.7934523934, 650175.1517499257 673462.4562051236, 650168.4819190401 673461.3423902193, 650166.0809944829 673460.8190980331, 650158.7122262524 673459.2112013143, 650157.977250647 673459.0401119771, 650154.8463545125 673458.3117314033, 650146.3726357048 673456.3397014122, 650135.6130448381 673453.3983977397, 650130.2132495173 673451.9277434484, 650124.8234565422 673450.4370905972, 650119.4736800151 673448.8064440293, 650113.4239621095 673446.7157152235, 650107.4342641373 673444.4549954758, 650101.4745767206 673442.1042803102, 650096.8048262554 673440.2237203208, 650092.1650815872 673438.2931645316, 650089.633229835 673437.1648619575, 650081.2057229793 673433.4118548236, 650068.6765195294 673427.3003626593, 650055.177448913 673420.1187607988, 650043.589248279 673413.941385928, 650042.4383277459 673413.3272493884, 650036.4287360165 673410.1765359355, 650030.4691547508 673406.9358295507, 650025.2595384066 673403.9552134912, 650020.0999325078 673400.8846045029, 650015.4482958313 673398.0490561352, 650014.9803323635 673397.7640009667, 650010.500690579 673394.9634735584, 650001.6414243635 673389.2124326047, 649995.2419730803 673384.90168222, 649988.9025345932 673380.4809403552, 649982.6731133802 673375.9102135748, 649971.9241865304 673367.3989655857, 649964.8449380604 673361.4181473346, 649956.7858403835 673354.2172196636, 649947.8369049456 673345.6961947068, 649939.647942713 673337.3652620163, 649935.61846662 673333.1548041798, 649927.5795751304 673324.2238959651, 649925.5798662107 673321.8736712949, 649922.28036944 673317.8033024569, 649919.0708805169 673313.6629454466, 649908.7025954637 673299.7517974013, 649884.8766077182 673267.1891658037, 649883.5288365101 673265.3320171211, 649877.2479026237 673256.6783242788, 649872.2286849169 673250.3477650299, 649869.8700214903 673247.6334997287, 649869.7690358739 673247.5174883672, 649868.9591273882 673246.7873953196, 649868.0891691777 673246.4772908005, 649868.0761693723 673246.4762892053, 649866.1592061598 673246.2570544162, 649838.2117325802 673243.1446307111, 649803.4164016306 673239.2693786356, 649796.3905384768 673238.487521468, 649787.304715569 673237.4754129996, 649781.7151245142 673236.8528310758, 649770.6310405344 673235.6183788476, 649751.3494163351 673233.4710265892, 649749.7574473517 673233.2938323761, 649745.5958632655 673255.1881069023, 649742.724080051 673270.2966061967, 649738.394391669 673293.0738511947, 649734.9602585484 673311.1462521906, 649728.3791717526 673345.7701041151, 649727.9999365164 673347.7630379719, 649739.8302798229 673369.4352837603, 649751.7056130802 673391.1905346463, 649705.7849428626 673415.9666356065, 649700.4061669837 673405.9770705468, 649698.6315709047 673402.6808841383, 649695.1123621038 673404.6144316758, 649684.2607184296 673410.5750364321, 649670.6279096175 673418.0642834622, 649645.940444971 673431.6251086782, 649643.0980786065 673426.457809168, 649641.5283286049 673423.6036437729, 649638.4141229544 673417.9413156636, 649616.1900778613 673377.5369748349, 649603.0480052098 673353.6685909564, 649589.5705833859 673332.7471371908, 649567.9693939337 673301.9147813212, 649543.2915868529 673268.0390777412, 649539.9611388673 673263.5837118113, 649536.4117273181 673258.8343218607, 649529.5288669944 673249.6365656321, 649501.9506406727 673211.0506538475, 649493.681968324 673200.3517425467, 649479.3092767257 673181.7501588736, 649477.0876096943 673179.0749121213, 649463.8515935447 673163.1384422341, 649455.2576982767 673154.3294727224, 649453.0710194655 673143.3693128964, 649452.7602069225 673141.8142901488, 649452.0246513784 673138.1272363976, 649451.3130810144 673134.5631843786, 649448.463801961 673120.2869761577, 649447.289511394 673114.4018903767, 649445.941325229 673107.6507918461, 649444.7140668491 673101.4987022338, 649443.464821494 673095.2386110032, 649443.1290241386 673093.5575864669, 649431.2105132648 673056.5554900183, 649430.4738447841 673053.8154267909, 649428.9265421623 673048.0512940803, 649428.8715673237 673047.8432894028, 649428.1568892741 673045.182228095, 649426.146795006 673037.6960557065, 649421.2761124711 673026.9095646676, 649417.3327394084 673013.4802140784, 649410.052742559 672988.6915670104, 649410.0307516453 672988.6165650593, 649406.7161192422 672977.3282705559, 649404.4150530032 672969.6229648294, 649403.7896185733 672967.430009901, 649358.50454707 672808.6589383247, 649355.8589696514 672799.3905065723, 649352.4331233293 672787.3884065195, 649334.784612549 672725.5568617827, 649314.1633309331 672653.5850563019, 649295.5711957373 672588.663430939, 649295.0574281183 672586.7433873205, 649280.9181180261 672533.9486869862, 649263.8855159846 672470.3507424655, 649254.4993578446 672438.6599131706, 649233.2377096362 672369.8180064332, 649214.5355563256 672305.0763734434, 649185.1160702296 672214.3830949513)))" + } +}, +{ + "model": "common.municipality", + "pk": 12, + "fields": { + "fid": "", + "name_fr": "Evere", + "name_nl": "Evere", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((651305.1030212701 674626.7038038368, 651381.0443075721 674523.2341580954, 651412.8305370116 674465.613488974, 651431.1601392342 674432.3870947278, 651435.7900743489 674423.9947827157, 651457.4478182293 674380.8588055824, 651499.5604263025 674301.7447215387, 651522.7846001773 674256.5359968113, 651524.0538233712 674254.0641757788, 651529.9456248198 674247.2190479332, 651538.0376425931 674237.8180558207, 651572.9283433319 674176.6509169192, 651578.1473279347 674167.5015638284, 651586.3503326488 674149.8268935821, 651591.0554913771 674140.3704388066, 651607.011988207 674108.8797295731, 651615.2838537097 674091.2709867693, 651629.7966207707 674057.7620333462, 651632.7424536432 674051.0133363729, 651635.6891740431 674044.2638394941, 651652.76359218 674004.6323750587, 651675.3413994793 673952.2276208373, 651677.4697835294 673949.1755911742, 651681.5083695869 673943.3843020583, 651687.6753393863 673934.5422632042, 651697.7754711744 673888.484860636, 651699.5933993456 673880.8171501759, 651701.412214971 673873.1494398107, 651719.0295631457 673799.3272806841, 651727.874269898 673775.4975381073, 651730.714101329 673767.8069150925, 651733.5549977334 673760.1162921824, 651739.8807042236 673745.1852026868, 651744.7825456562 673729.9720181811, 651794.955342163 673594.9294502485, 651800.2179150629 673580.7611008404, 651811.7656035045 673549.6738313448, 651832.4565895173 673481.3788820007, 651835.3477058179 673469.282380147, 651835.8591563635 673467.1424611926, 651841.5813515786 673443.309446912, 651844.4154559119 673431.5058954197, 651860.7954193654 673371.5533791913, 651863.143344479 673362.8278219058, 651866.8589340447 673349.3133534448, 651891.3039623299 673256.0021075094, 651893.4870155528 673247.0645310848, 651895.6690038943 673238.1258345087, 651911.1303981851 673175.2393034915, 651912.9693537125 673167.1835969659, 651914.1979287249 673161.8017130173, 651918.2909490563 673144.7053993335, 651923.2809161883 673127.2381597678, 651928.3247057284 673112.4339451559, 651930.3831844807 673107.7462383594, 651938.1332942521 673090.0932628142, 651957.2321621306 673049.1258886307, 651963.1935865425 673036.3577730705, 651969.2650605036 673023.5896711982, 651985.8102523236 672988.4459325287, 651989.1100524521 672981.7844766775, 651992.4109174582 672975.1230209616, 652022.0227521188 672925.0050280215, 652032.2288643641 672908.0640340202, 652033.4719619654 672906.0005252736, 652040.4553370274 672893.9845307543, 652049.4501636423 672878.7588923085, 652058.4110401077 672862.4250986334, 652074.7766700409 672832.5953748777, 652118.6049293199 672752.7064310842, 652119.3161462139 672751.4095700145, 652166.6702866994 672665.0961572308, 652177.6495799212 672645.0837648829, 652184.3640594389 672632.8437391588, 652188.8377869099 672626.6013061119, 652202.8689579517 672607.0192097904, 652208.3528240506 672599.3658738276, 652211.2914588952 672595.2653125328, 652246.2280646594 672546.5081328189, 652250.4887555208 672540.562630933, 652255.0120114707 672534.2493246921, 652260.5864151418 672526.4689610839, 652271.2901568096 672511.5324934769, 652271.7178235938 672510.935591506, 652272.3592349704 672510.0406385396, 652277.642088309 672503.0712721079, 652321.6399055436 672445.0252218564, 652328.9879350468 672435.3313359744, 652333.7366107282 672429.0657384506, 652338.4854635613 672422.8014209525, 652342.8251160986 672417.07608811, 652350.0292961888 672406.4909916666, 652362.1523073865 672388.6796868434, 652366.7221142296 672381.9663907874, 652381.9536783393 672359.5863905807, 652388.0367521177 672350.649301229, 652403.9225117324 672327.3093905739, 652420.3678425793 672307.7195991873, 652426.4196427286 672300.5724114808, 652438.5876757695 672290.8300069795, 652439.924972294 672289.1921069634, 652455.933118095 672269.5923415029, 652486.4313871271 672232.2505266787, 652496.5343353591 672219.8808466861, 652498.0860813716 672217.9810555775, 652495.604880132 672211.3156809325, 652489.6979747467 672193.6522093558, 652488.4894456745 672190.0391283085, 652486.7439897502 672184.821033478, 652486.0482561877 672182.7398438002, 652496.1077893912 672177.351221408, 652497.7639645615 672176.4634350501, 652502.78325782 672173.7751225096, 652507.418595386 672171.2917204548, 652512.1808530388 672168.7403347787, 652517.0180201168 672166.1489587709, 652521.3844286158 672163.8104820341, 652521.7133559391 672163.6345244633, 652526.4896367515 672161.0751406148, 652531.3259159724 672158.4847245077, 652534.4904501268 672154.8281888012, 652538.3418224704 672150.3795851835, 652545.2948691655 672142.4854763248, 652545.4088554448 672142.2146127467, 652552.4767755511 672134.04948045, 652557.824492846 672127.8713175211, 652561.4349652901 672123.699801657, 652565.3664020392 672119.1582492, 652569.1709424909 672114.5618030891, 652630.5189108385 672047.1009103585, 652636.5147463253 672040.4697146196, 652646.324838512 672029.619030379, 652655.156069112 672019.8522147269, 652736.5298052679 671901.9734444693, 652745.1682372392 671889.458627735, 652752.209491516 671892.2417207403, 652766.8242965406 671898.0182112835, 652822.8744103876 671920.3530005616, 652823.1071487427 671920.0447121874, 652828.7892661883 671912.520844259, 652878.8150394959 671846.2916403253, 652883.3236996594 671840.3223327282, 652944.2490945456 671759.6635663209, 652953.1874115586 671747.8297398629, 652967.7096404464 671728.8376293805, 652971.2692529436 671723.997313685, 652974.2266906695 671719.9756360524, 652975.8139677478 671717.8179319972, 652983.0530237131 671707.9737968517, 652984.9873677737 671705.3441399992, 652991.9074056552 671695.9340413937, 653035.1262109965 671637.167919217, 653054.095238014 671611.3744984531, 653077.4928713369 671579.559759361, 653121.2963536183 671514.9556776623, 653049.3002587399 671467.3871274646, 653032.4716205143 671456.2680093566, 653025.5582073234 671451.700308556, 652954.2049812194 671404.5567984665, 652957.4246570441 671399.942358382, 652963.0047654837 671391.9455207656, 652963.2465607116 671391.5989938006, 652969.0684641415 671383.2559491699, 652997.5532518581 671342.4367999262, 653005.1944762608 671331.4868034335, 653023.1025851269 671305.8231687242, 653025.0747599505 671302.9975179872, 653054.0208637145 671326.5357953028, 653214.763873602 671457.2466723076, 653331.9639312588 671546.874575341, 653336.9634602955 671550.6980452128, 653341.9608596198 671554.5197548511, 653343.1247955943 671555.4098121863, 653345.3429773558 671557.1524735305, 653375.8677536424 671581.1336715125, 653440.4785282024 671631.8942490135, 653526.9973081021 671699.8745324506, 653632.1716791161 671770.7839979981, 653669.6587595412 671796.058056412, 653746.8193614234 671848.0806496927, 653750.0213878537 671854.7416318599, 653752.7199700477 671860.355360238, 653763.0633821078 671881.873425662, 653825.1872264507 672011.1101482954, 653830.6768493906 672022.5297755538, 653840.3145232074 672042.578806655, 653857.5404252019 672078.4145699245, 653862.2772780448 672088.2691568696, 653865.320483935 672094.599402582, 653872.4897370603 672109.5131299477, 653880.0908304598 672126.2779340384, 653880.5877174996 672127.2749472011, 653889.2937108398 672144.7480385762, 653902.1377103264 672170.5254124617, 653906.9167001126 672180.1168036018, 653926.621296369 672219.6619508133, 653927.9371604249 672222.3030515112, 653928.6526781576 672223.7391280178, 653932.4648687042 672231.3905756172, 653949.638007082 672265.8564115559, 653959.7088517576 672286.0674080336, 653960.7924215285 672288.2416038499, 653970.00631391 672306.7336285908, 653973.9425510144 672314.6330892466, 653976.8488111391 672320.4663598144, 653997.710108611 672362.333548783, 654005.8672313384 672378.7054601834, 654014.5863708177 672396.203191544, 654023.3063982831 672413.7052428762, 654041.8398631033 672445.449348868, 654046.741978644 672452.4626965666, 654051.2991904758 672459.6134315901, 654053.8685460902 672463.9125849783, 654060.1556412226 672474.4332538726, 654075.3109012608 672499.793745858, 654074.7129171505 672499.6939944992, 653987.8170907723 672610.8653900578, 653892.9558629186 672745.5180799104, 653854.3687399834 672798.3246877663, 653766.9933813347 672917.8978050824, 653757.3225929892 672921.5509761295, 653754.89302322 672922.6823455727, 653751.6098001968 672924.3091250696, 653742.7016206303 672928.9257424384, 653728.9331963158 672936.1567756359, 653721.0541656648 672954.9263604704, 653718.6392900595 672961.1255287783, 653711.6255743873 672973.5573546566, 653703.1243257436 672988.2942568182, 653691.4493547962 673006.3472153097, 653680.3382260406 673021.8826614943, 653670.879681987 673035.315931128, 653667.9673434396 673039.2958551527, 653679.3256852218 673052.5201959594, 653724.5352510524 673105.1559167737, 653940.3740764068 673413.5347221694, 653948.824756703 673425.6084643871, 653951.5139887428 673429.4511642075, 654020.5104986411 673523.1003369084, 654009.0418378181 673531.1294095697, 654005.2515544584 673533.7805980464, 653972.5626632784 673556.6662790533, 653921.300626109 673592.5545124479, 653916.9122953813 673595.627063508, 653898.5132440234 673604.1967150186, 653893.2878927301 673606.6299680664, 653761.9337493987 673667.8072847509, 653730.7711297171 673690.9072355917, 653714.6997872998 673702.8211474987, 653697.4284397806 673721.9358100805, 653687.2672202258 673733.1814936441, 653674.2224937567 673747.6527106498, 653662.9650725214 673760.0771270912, 653656.8004185501 673766.8993436056, 653643.6726124644 673781.4276700391, 653631.599131525 673794.7900601589, 653549.8527046463 673851.0703395703, 653543.7451580797 673855.2756275395, 653507.9293996153 673879.9340228855, 653493.1502029899 673881.2110571135, 653444.5605448479 673885.4101087438, 653440.7985609467 673885.7355589094, 653440.056316379 673886.778498413, 653428.0094439058 673903.700867357, 653416.9776758734 673919.1963337233, 653405.4259273482 673935.4237296501, 653400.2211604923 673942.7350249309, 653339.6295886558 674027.8479405841, 653321.3827174234 674053.4793897492, 653281.3382279817 674109.7309270483, 653278.8059663299 674113.2877041277, 653266.9740689433 674129.90802183, 653242.2660508825 674164.613635754, 653229.1060620667 674183.1009734999, 653217.532109497 674199.3594060246, 653208.181697348 674212.4941396688, 653164.7611157746 674188.317899028, 653147.1731166792 674178.523792502, 653077.2291764754 674139.5784554776, 653074.3263308582 674137.9630678107, 653059.6321289517 674129.7813079718, 653056.8954140549 674128.2569802172, 653028.5769204168 674112.488628732, 653019.9624544937 674107.6925570532, 653015.3967078314 674105.1499302604, 653002.394517888 674097.9104531211, 652979.4868027533 674085.1557095317, 652977.7229096395 674084.173418265, 652966.8525501165 674078.1205962865, 652955.5281721477 674071.8147599399, 652950.22655361 674068.8632449515, 652943.0456514726 674085.8850967538, 652930.7153422623 674115.1164511718, 652913.8868685189 674155.0078014414, 652908.5336678126 674167.4171171198, 652902.7841832397 674180.7454162055, 652897.0123274899 674194.1255520859, 652871.1377410452 674235.2420085219, 652861.2649276119 674259.6205868234, 652824.0883211583 674318.3654177366, 652823.8728949664 674321.022809692, 652823.207064589 674329.2353012041, 652819.9285979831 674360.2005683875, 652812.667286562 674418.5032087318, 652808.2943085374 674445.3224548848, 652773.4950583031 674529.1075524739, 652728.4248324421 674621.591143716, 652714.6019479594 674647.789219395, 652690.1076704003 674685.7858754542, 652664.1050532893 674708.4723842506, 652656.8027220725 674716.1296560961, 652650.7936321021 674722.430619389, 652644.6825465176 674731.3769891269, 652613.9614817859 674776.3528147115, 652590.1095499537 674811.27053315, 652587.6631977817 674814.8522806527, 652567.4398541631 674843.7994570399, 652565.670161085 674842.2493292218, 652537.5271410281 674867.3656306829, 652477.3903921618 674927.3475960621, 652481.7994196352 674936.3779927853, 652458.6932668291 674949.1052577725, 652431.4860622694 674964.0915145576, 652427.7965048578 674961.0411598743, 652425.7168507923 674956.6409361787, 652406.6458934573 674965.4884136802, 652415.8232245344 674989.9192809386, 652338.754548922 675065.3191732252, 652166.3767856552 674992.1733375351, 652162.863854686 674990.6828320356, 652097.514667398 674963.0709163578, 651868.3421675119 674866.2391252202, 651761.888953293 674821.2594566634, 651305.1030212701 674626.7038038368)))" + } +}, +{ + "model": "common.municipality", + "pk": 13, + "fields": { + "fid": "", + "name_fr": "Auderghem", + "name_nl": "Oudergem", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((651797.9044245335 667428.6483789341, 651808.9720209234 667414.2720883026, 651830.8651931707 667384.7502116336, 651852.7583624431 667355.2284998096, 651861.5093302788 667344.7848184272, 651870.260475213 667334.3409777619, 651881.912286925 667317.7827236084, 651895.5743664921 667298.7798060563, 651898.634727016 667294.6753014548, 651916.4357394595 667275.3458806754, 651959.945562239 667221.1770657417, 651977.5060873661 667189.8906800137, 651991.4664146877 667150.8539082077, 651994.3150816484 667144.7644213005, 651991.1371675639 667144.585007911, 652001.9105997034 667113.9877244238, 652022.4369785688 667074.7498093651, 652031.1548079557 667057.9370241018, 652036.5446871408 667059.8496314269, 652064.0819719168 666984.6281431967, 652072.735736552 666986.1821011163, 652086.1644692833 666988.5946897321, 652117.0956248022 667011.0480745826, 652149.3211461166 667039.2594455505, 652170.9434849462 667051.6357303102, 652174.547178418 667053.6991645099, 652175.4951358957 667054.24118822, 652179.8819015961 667056.7517481368, 652185.2065079447 667059.7992107003, 652189.5343611345 667060.6227497493, 652208.4118101809 667064.2098867344, 652241.9046709428 667063.1769031959, 652261.0957991184 667060.2672170923, 652274.7036306455 667061.1168131876, 652282.5962974533 667062.5308448889, 652284.8491348586 667063.8509311393, 652291.9246389859 667067.9928237256, 652311.2730174109 667089.9726557642, 652324.9558805984 667099.0541073086, 652347.7818911653 667114.2054902866, 652364.0546760134 667124.4973065094, 652388.9544124809 667133.4181472781, 652398.5451236407 667135.6341092295, 652408.1358347614 667137.8502312973, 652421.7759976179 667143.1818179877, 652423.4659727772 667143.0978557738, 652458.0143643382 667155.0797763658, 652482.0498157109 667158.2204461345, 652522.3684045571 667160.3771051882, 652551.328819974 667162.7874795235, 652561.120402112 667165.4996194416, 652574.4058927775 667169.5460667736, 652583.0915045071 667172.0480013768, 652603.2706164973 667178.2763401383, 652611.7863183757 667178.0803741267, 652646.9604484251 667177.2715196856, 652646.3015324288 667168.4014895544, 652644.9124440079 667160.5715196616, 652646.4651286813 667136.6219620011, 652644.3568317075 667122.5019183918, 652645.0404602797 667091.0624470104, 652645.7841181991 667059.3229869357, 652647.6963164698 667040.8434728417, 652651.6477275129 667027.5841205154, 652657.7654958512 666960.6657840514, 652657.4263564444 666952.4559418306, 652650.6343019303 666883.8559748147, 652697.8512367455 666885.7673252219, 652829.2522452112 666891.0866002672, 652854.891828302 666893.5996098125, 652870.301308033 666897.321340418, 652892.8202109532 666905.0836784126, 652956.8965364059 666932.8607951602, 652979.7453515627 666942.6533882472, 653077.2299224694 666983.0041439388, 653089.839354776 666986.7554759661, 653114.5188141903 666989.5382996383, 653215.169712194 666976.8101256597, 653220.0511436281 666955.0149680513, 653222.1211386824 666945.7762850479, 653223.5964899806 666942.8684927509, 653233.7087721893 666922.9329558117, 653238.1599459125 666911.8785706897, 653240.6478359816 666912.8289264514, 653242.6767345493 666913.6040313458, 653245.3324373309 666907.7624923412, 653247.7975777284 666904.3714602753, 653249.1680864545 666902.4860425736, 653256.7883203559 666899.1208862232, 653261.3305129913 666897.5934306935, 653270.9117368729 666894.3696190839, 653272.9388516536 666893.6879420236, 653298.8956839353 666884.9579321663, 653305.9158554326 666882.6008530552, 653308.3139380317 666881.9590582455, 653330.9755523362 666875.8948728346, 653337.6446701041 666874.7356579421, 653354.3208369364 666871.8376208646, 653385.8705752783 666872.9911723558, 653409.8239614975 666876.6708554735, 653413.4178924755 666877.2223040962, 653415.335716375 666877.6554804621, 653425.8653774109 666880.0317085198, 653430.4152616552 666881.0581419244, 653441.7070266808 666882.5554288898, 653448.9928836739 666883.5223393281, 653457.2750261779 666884.6188033975, 653458.0955903557 666884.7273769367, 653466.1184110624 666885.7902114936, 653467.2045092433 666885.9344953009, 653475.2392099526 666887.1043700005, 653479.3015839321 666866.0090924335, 653478.4504098826 666851.2762107177, 653478.8457947585 666847.3923029384, 653479.4405615675 666841.5383617901, 653491.6729562057 666819.5160374503, 653499.007791504 666803.7370722167, 653505.8497297082 666793.9379782679, 653510.816614683 666786.824715776, 653525.487139984 666755.5547783732, 653532.624785884 666741.2096906892, 653533.081774358 666740.2897542192, 653536.6096254461 666733.6212794689, 653538.1389297592 666730.7754092513, 653541.2946510714 666724.903842045, 653560.6467488906 666687.1516344323, 653562.8014221191 666682.4239378903, 653568.2168623789 666685.8944400861, 653662.0294106703 666684.291170746, 653683.5794716269 666683.3237408344, 653689.0063796345 666682.8024510606, 653696.2725067459 666682.4812105587, 653703.5384569385 666682.1599686864, 653719.2423514844 666681.8038496205, 653731.6688439313 666685.058194738, 653736.4113368196 666688.9638133628, 653759.8199521935 666708.2401959802, 653763.0605869137 666710.9654964493, 653763.1887285281 666711.0735099094, 653767.703147504 666714.8739842452, 653772.3577794982 666718.76951366, 653780.4598071988 666725.6063651599, 653784.0955159186 666728.6617874962, 653800.8938534653 666723.8407252644, 653948.2812149967 666687.3659973344, 653971.706762723 666681.5687399304, 653992.6192251572 666676.2631890818, 654052.8905493297 666660.9721644607, 654059.3776195477 666659.3269844548, 654061.156744989 666658.8751865883, 654062.3067952263 666658.5833172459, 654068.9889438255 666656.8880763799, 654087.1604119637 666652.27814054, 654089.1383421678 666651.7762852011, 654102.1497610097 666648.474882987, 654115.2820602407 666645.1434143223, 654132.0404170877 666640.8912371931, 654151.0828991353 666636.059479075, 654151.4884964337 666635.9564851209, 654171.2163225211 666630.9516844035, 654208.8023222918 666621.4169096461, 654210.941303145 666612.7743544289, 654217.9488184446 666607.9340603465, 654222.178502971 666601.3145961921, 654223.2280846166 666596.4787614257, 654223.3559155811 666590.099960085, 654222.2720710671 666579.7169049598, 654221.6499064746 666546.3910931115, 654209.6337392633 666497.6492967922, 654223.6945098866 666467.0713217546, 654223.7552298185 666466.9401297956, 654225.7774777319 666463.8773443805, 654236.4022401058 666440.7758722682, 654243.6462619371 666430.691814529, 654247.6206047544 666427.972121108, 654256.0175392305 666411.2992965525, 654256.2705340008 666410.7953295638, 654261.7611377361 666399.8941255363, 654266.4368326962 666390.6105750602, 654267.1299491655 666389.2337054068, 654267.1365531516 666388.9196292848, 654267.2738082914 666382.3977898366, 654267.0809687758 666381.5208173386, 654261.122991356 666354.4185115905, 654256.0030988553 666320.247249961, 654248.6352740031 666292.9036725354, 654247.6729491494 666287.6775390529, 654247.3582361469 666284.6856944781, 654246.8078888562 666279.4405666242, 654247.8754488843 666274.1677773481, 654250.8707569444 666263.3861358834, 654251.6715256755 666256.6744516114, 654251.1289758511 666253.0633881635, 654242.8359769713 666227.6636905245, 654236.8812924699 666216.5061874086, 654213.8927245857 666196.2408220429, 654200.2989376697 666178.0854305523, 654197.0089103341 666169.6272346008, 654198.7316649985 666162.4354971701, 654194.326864546 666160.763029751, 654193.162868213 666152.7610631405, 654193.061954254 666142.8851525112, 654193.7719285531 666135.4401862929, 654194.4819078506 666126.4253160186, 654195.800110963 666116.4476423425, 654200.1067515004 666093.715386519, 654199.0750762712 666081.9344328999, 654207.6177390434 666075.9443918032, 654224.2463203981 666070.4081937089, 654248.3317351188 666056.4109790424, 654253.7211492826 666053.2796021038, 654259.0214528703 666050.2002147994, 654267.4990276567 666045.2731946575, 654279.6988325429 666038.1835644419, 654273.5074165564 665999.2013284501, 654257.90313315 665967.867706356, 654275.785887657 665961.2967267076, 654264.0630547219 665942.6806137757, 654282.7837929721 665928.5935880897, 654322.8271066829 665898.4631895258, 654391.6729456024 665846.6598599032, 654424.8888508762 665821.6655026061, 654431.57229221 665816.6372918918, 654449.1278199108 665803.4272603934, 654453.1361095938 665800.4107499133, 654453.884157871 665799.8487538076, 654455.3333906907 665798.75803634, 654465.186185477 665791.3441014425, 654469.5254603645 665788.7508261409, 654473.8658002914 665786.1570709798, 654480.3341511071 665782.29171357, 654481.3821805596 665781.664950655, 654488.9469673578 665776.1107589809, 654490.2490372912 665775.1549050043, 654494.366312006 665772.132326765, 654501.9891459296 665766.5362217505, 654507.7773659754 665762.2867910245, 654939.7797050008 665465.0753171543, 654952.6925778338 665456.1908514034, 654953.5244066336 665455.6184650905, 654954.5978314398 665454.8800259903, 654959.4952544825 665451.5106576099, 655020.6937812794 665409.407152493, 655023.1768416153 665407.698632352, 655120.4324597502 665340.7876804468, 655122.1355250403 665339.6169926245, 655215.8287937165 665275.1574916055, 655270.0380675575 665237.8616979837, 655271.3420822507 665236.964725402, 655401.1450638747 665255.8239433619, 655402.8910918294 665256.0770951222, 655617.1349641287 665288.5156522457, 655639.6623213616 665293.9280358301, 655655.8415158195 665300.0749155, 655690.0121457838 665317.0565342868, 655705.8987552216 665328.249279457, 655729.2892803929 665339.7278108634, 655808.4651593266 665371.5714462227, 655808.8541756256 665370.5954933944, 655883.0248838635 665184.453539037, 655936.0134668486 665051.4741247274, 655958.207102097 664994.2288581193, 655978.6953018152 664940.1833877095, 656025.6334916004 664819.0070953226, 656067.1447423813 664713.4851706789, 656099.3896609909 664628.8721985118, 656104.29193395 664616.0080049885, 656108.760308937 664614.3753923742, 656123.4528896463 664609.0069807683, 656143.6605163389 664602.3452722601, 656197.9694017672 664583.8314579597, 656218.7490926994 664577.2658315721, 656259.6444852947 664562.3505116951, 656284.6182660626 664554.5613636279, 656305.2419845718 664547.5818011643, 656326.508946525 664539.2131973958, 656336.7892261727 664535.5953330994, 656337.1206305053 664535.478731001, 656347.3295526623 664531.8854985227, 656353.504820705 664529.7122847335, 656386.6889859701 664518.9810353486, 656466.203762702 664491.6350847094, 656533.3949116387 664469.2387613113, 656556.3795683135 664462.0493852134, 656563.8742587803 664459.2619227392, 656568.8626566819 664458.0352907199, 656574.334177949 664457.0506321163, 656584.687401962 664454.9935687705, 656598.4183523812 664453.5952834326, 656604.6033199892 664453.4478215876, 656632.6492186171 664453.4210645643, 656663.5791015105 664453.5485521073, 656765.2324915043 664452.6259400818, 656819.9282738841 664452.4532291517, 656849.0340605205 664452.0734732117, 656867.6100994456 664451.5155705508, 656871.5200680697 664451.3280123118, 656880.8020757145 664450.8540210739, 656889.4165304091 664450.2071552349, 656896.8350271011 664449.3094356231, 656908.1833470535 664446.9411255007, 656949.4494346569 664435.8639064651, 657027.605785798 664411.7337828927, 657051.165523175 664404.6783895842, 657088.1664431106 664393.9886872441, 657155.6322512014 664375.5064527595, 657162.2991483931 664373.6948921384, 657232.2445544736 664403.5989707056, 657314.0410517425 664437.807889609, 657416.0881965354 664481.2233749805, 657463.6773121704 664501.6896163065, 657478.8686794606 664508.1423426439, 657571.325353707 664547.2945274318, 657623.1454930407 664569.4191606488, 657655.255546619 664583.1788856619, 657675.4455471368 664591.9030470626, 657811.0647106303 664649.8952275661, 657851.5155740331 664667.0056411308, 658004.1368056795 664732.4946906308, 657985.6064196486 664744.7635644716, 657965.8932844963 664756.1603065651, 657959.1388115484 664760.0635191713, 657961.9682731167 664764.9567823929, 657902.8751550285 664809.8778902038, 657895.0986292933 664815.3779499432, 657860.9383835219 664835.197052953, 657882.5028174035 664864.368327153, 657886.7182519229 664870.2476588389, 657878.5049094695 664875.6788694245, 657569.2331327125 665080.1880679162, 657236.0450016655 665300.9744622486, 656819.8399874627 665576.483197668, 656735.006764019 665632.647439497, 656401.9544562399 665853.1148751572, 655985.3293600937 666128.9134304621, 655818.3853460161 666239.43005568, 655527.7153789472 666431.8437969214, 655563.1647420621 666441.2959643546, 655666.8277072171 666468.9368228503, 655744.2952193865 666475.0127068717, 655781.8021020874 666460.6087959398, 655800.203509309 666452.3492881135, 655814.1782965251 666445.3582415786, 655823.4514253292 666440.397710176, 655828.4620672535 666438.6859621246, 655838.2833598639 666436.3505960973, 655840.03421808 666436.2808722886, 655852.6410502305 666438.0849890456, 655925.4797191682 666454.7123777028, 655939.4628431391 666464.3951551085, 655952.8721643437 666478.184240739, 655960.1087471799 666487.7121853128, 655974.6871436079 666508.505354882, 656004.773651013 666553.3308180433, 656044.427952068 666609.7075182146, 656059.4746082387 666625.7967754769, 656066.6958707983 666632.7612176723, 656078.1042640087 666638.809974976, 656091.2541844564 666642.619902333, 656096.9346876558 666642.5909386993, 656101.6877257419 666641.3282793696, 656118.2750502115 666637.9151195603, 656127.8456324865 666636.2736421811, 656143.0031491576 666633.3177590655, 656162.5149418996 666629.9473261675, 656189.7401660645 666626.1853047145, 656244.4843528968 666640.0069286367, 656247.5988203468 666640.9083951415, 656261.3499527276 666644.46608781, 656262.0194540948 666644.6748021984, 656264.0556029149 666647.2001360393, 656268.9884632092 666654.8697639223, 656271.0886522311 666657.7098229452, 656272.639198448 666659.9418237613, 656277.6321984986 666663.344446426, 656280.2459927964 666665.322969212, 656282.33872699 666669.6761374664, 656282.821568204 666675.3559948562, 656282.5781766146 666678.7309851847, 656279.176711034 666694.3370579642, 656278.9588026807 666695.3370267518, 656263.7084530221 666695.3353011502, 656230.3335041162 666687.8451738023, 656208.7097035868 666683.2837169195, 656190.0855834751 666680.6705866851, 656154.704721103 666679.1702882109, 656100.5129834624 666681.1778037192, 656087.3884058755 666682.2515261935, 656069.7064977413 666676.1284659839, 656047.2792557408 666658.534071101, 656028.6602318486 666641.1292222682, 656015.4972373726 666624.6761786571, 656009.2321588717 666616.2203339938, 656003.1888380286 666607.2397178006, 655938.9962910258 666533.3211938803, 655929.312628469 666521.4344315064, 655920.6531112329 666514.2975115088, 655908.216377197 666509.0035165449, 655877.463879727 666503.6916332655, 655863.5851506158 666502.8872064976, 655839.6143867045 666510.964789671, 655804.8777105603 666528.6689542748, 655763.0403076428 666551.3131232947, 655736.6425304451 666564.4539465612, 655706.1914341571 666583.2448388921, 655693.457214002 666591.541288206, 655667.7739201693 666620.9559890616, 655650.0858611043 666640.0314890519, 655632.7854858316 666675.580990466, 655618.0086799837 666702.9874693776, 655613.7367517027 666712.596524452, 655598.3195186098 666732.5109852481, 655592.9205931609 666744.1162197083, 655587.4461398424 666754.9804909977, 655579.0817315395 666763.606697316, 655571.4721592858 666769.7736455295, 655572.1254484826 666770.4983527919, 655579.6049973596 666778.7964029927, 655593.4915679258 666793.0226423249, 655615.0662350011 666815.659916171, 655694.2390992879 666896.4497469363, 655767.2485143965 666970.8880801592, 655849.4772187419 667004.3338511863, 655868.4930869192 667035.7415321677, 655918.0926956711 667068.9936007718, 655941.6694920637 667095.0530347156, 655970.6943055014 667135.9081828473, 656021.4070776238 667183.71260811, 656093.3413910423 667195.6063865379, 656151.6860979507 667237.208201352, 656203.7553531418 667275.446633451, 656258.1287353594 667325.6164561413, 656185.4037305774 667404.2910720533, 656176.8187902877 667412.1210894012, 656116.3005041465 667457.8256373393, 656102.5643440296 667470.3796840202, 656092.507798385 667480.8922392328, 656087.9951604884 667487.6588849309, 656083.2107700392 667495.9552497178, 656076.0823119226 667510.456428491, 656067.2823740923 667530.7829794853, 656055.2024865482 667555.2060923016, 656053.4529978664 667558.7431510764, 656056.8600323334 667559.2312126548, 656055.5758002469 667565.5257455697, 656050.6582482134 667578.6891021279, 656050.1490217691 667580.0511154959, 656041.3802564606 667596.4109753717, 656036.863164538 667604.8374485516, 656035.642060071 667607.1163353022, 655990.0647852003 667670.6997160706, 655965.7144734876 667717.3837133389, 655943.8413807763 667745.4500244822, 655921.0600409163 667774.9131820289, 655885.3171639511 667801.636006229, 655817.5735687352 667835.7481133372, 655773.0417319774 667854.1330231959, 655758.9509059587 667862.3612905177, 655748.2399646755 667871.0200180418, 655725.7151476505 667904.7012378196, 655725.3297365782 667905.2231105763, 655725.316067051 667905.2411889047, 655725.2789638196 667905.2922243512, 655725.0149812624 667905.6491519971, 655715.6475926191 667918.3340834249, 655705.6335371353 667928.3037589255, 655678.4584893411 667955.3585677594, 655677.019455342 667955.7883212134, 655672.096350603 667957.2608714068, 655671.9582516555 667957.3018154262, 655670.7162482302 667957.6736716684, 655666.8711322161 667958.8231465984, 655661.9671981946 667960.2886589434, 655661.6631319039 667960.3796637473, 655655.1657343572 667962.3217915798, 655646.9726302496 667964.771883014, 655634.4641991727 667968.5113946479, 655622.9995508671 667974.5321290507, 655606.5016119881 667983.1972320838, 655587.3079456064 667990.7799551692, 655582.5667306616 667992.6533224928, 655565.2431016879 667999.4983427239, 655554.8726038878 668003.5952133416, 655522.1832498845 668016.5102797877, 655514.7006153014 668019.4665265615, 655500.0454510825 668025.2566559748, 655495.1372020701 668027.1960829771, 655484.3667809998 668031.4508254535, 655220.2064767017 668004.0228755106, 655208.8937187378 668002.8486371308, 655201.4974172963 667988.4469497361, 655158.4024561474 667904.5307220771, 655152.273919204 667892.5971623178, 655134.3083674652 667872.7381635013, 655107.4444382425 667848.3033143589, 655093.8716716659 667837.2389855066, 655078.6637516516 667820.3574000184, 655065.2042196975 667800.2219609411, 655065.0858144035 667800.3379465613, 655061.1667240696 667804.1715506202, 655007.705056963 667856.4690505839, 654998.7380323352 667865.2410789657, 654983.9535279945 667879.7032782895, 654971.6431081583 667891.7446581298, 654953.4951490513 667909.4965252365, 654904.4739496718 667957.4514996484, 654902.1979769109 667959.6776214641, 654901.862643316 667960.0052604782, 654872.6493851558 667988.5826474869, 654870.5251911717 667990.6614275123, 654849.7250957309 668011.0246405951, 654825.2782526142 668036.2467529622, 654797.4400635607 668064.9684443177, 654580.688449716 668288.5985611797, 654570.0391952167 668299.5861942023, 654478.9848587764 668298.5527268312, 654470.6628332307 668298.4588138325, 654468.5113023305 668277.5698168009, 654460.3083782017 668286.6777454463, 654444.5110745989 668304.2173578833, 654432.4143890841 668317.6478347019, 654413.2626351897 668338.9118210021, 654408.8600963737 668343.8002257678, 654402.3370592125 668352.5873870598, 654391.1154254883 668367.704023471, 654388.7619554885 668370.8736405816, 654384.5613211995 668376.5321398284, 654380.2618046395 668382.3235861885, 654378.7895766333 668384.3064365775, 654373.1706823955 668391.8756327202, 654366.4546738322 668400.922767167, 654360.4617310874 668408.9960744624, 654350.2889181838 668416.6228254228, 654339.8995719122 668421.050544411, 654334.4003508968 668422.1499004324, 654310.2536115655 668421.8468064185, 654292.0408297434 668421.6179941393, 654286.5598623353 668421.4324057339, 654262.1293987024 668409.5586830646, 654260.5744182384 668408.8025123837, 654255.3958236956 668406.2859439933, 654247.4405463756 668399.749099728, 654195.2288963154 668382.7872837482, 654182.8067749101 668427.6013653011, 654113.8818064481 668387.8049219913, 654100.3859835174 668377.4794872657, 654085.7836762436 668363.7050052873, 654034.2185790678 668324.5364193169, 654020.1061317107 668311.2139360739, 654008.9662205196 668302.7976702246, 653973.8022786187 668278.4678580273, 653959.0720277359 668264.5491522318, 653945.5749328672 668248.4717388944, 653925.8110992057 668232.1286270523, 653920.2866252905 668227.5600258645, 653894.2983349327 668206.0701477778, 653885.6589867717 668201.0731807483, 653878.3664084105 668196.8563646283, 653874.5617173191 668195.3780107505, 653851.8778708717 668186.5624645796, 653850.6459078508 668186.0831241603, 653847.1403658985 668184.0544121768, 653816.4495243808 668166.2933819733, 653811.7048833261 668162.5658231924, 653795.5523799873 668151.3530007814, 653794.2249503323 668146.0418648717, 653792.8712390267 668143.894849459, 653791.5734587177 668141.5848025536, 653788.7290962801 668136.5214844736, 653784.7765152154 668133.8010483902, 653769.7618541453 668123.4673923486, 653768.5189297618 668122.6122152923, 653756.8452232247 668111.6429624318, 653747.1700469562 668104.6439802261, 653739.799799477 668099.7761259064, 653734.6120866444 668096.3496328425, 653725.8557407823 668091.5755339819, 653713.1025911277 668085.8320145315, 653701.18515551 668081.187621261, 653686.4399407186 668075.4409776286, 653671.7092891599 668065.0303906642, 653660.4935716431 668053.7131926902, 653631.2808841023 668019.62821062, 653592.3786401752 667980.1119103682, 653584.2476727747 667971.8521660496, 653571.2843399893 667958.6847087807, 653534.9744407589 667933.1509575188, 653534.9385889549 667933.1256735809, 653523.3232930535 667928.1805184373, 653520.4014929672 667926.5990717929, 653519.1095613976 667925.8970074039, 653510.6297973592 667923.8630682345, 653504.5130075878 667922.3964419751, 653502.8140373535 667921.9892058122, 653496.6603290221 667920.5124953663, 653490.6095669222 667919.0617168341, 653484.4737853616 667917.5901285427, 653478.1869566097 667916.0823628637, 653475.9709470765 667915.5510670844, 653473.6211073546 667914.9867958737, 653467.4242678743 667913.5011206607, 653461.3285999913 667912.0383371896, 653450.648493139 667913.6631327474, 653449.4923572853 667914.4129065697, 653445.935193874 667916.7183767987, 653442.0569112165 667919.2319663776, 653439.4787266392 667920.9026803322, 653434.8593312307 667923.8971360798, 653433.7433130861 667924.6209949101, 653426.4147292305 667929.3711078317, 653425.7827864024 667929.7809478784, 653420.4004442781 667933.2703468624, 653411.0507085894 667939.3301236435, 653405.7074192069 667942.7934473082, 653402.1041025562 667945.129951144, 653399.0379373706 667947.117562953, 653396.2398153567 667948.9312886704, 653389.8224007776 667953.0904760397, 653384.3008892838 667956.6697767325, 653375.2064158568 667962.5646246838, 653367.7148759784 667967.420635473, 653364.7995960525 667969.3101860404, 653362.484484239 667970.8110126426, 653351.9496782734 667977.6395973158, 653342.1490618597 667983.9933946104, 653338.6128510527 667986.2319868102, 653332.4565672853 667990.1300871096, 653332.2403579953 667990.267179722, 653323.2598628044 667995.9540420943, 653314.3713187869 668001.5818758681, 653307.7098368105 668005.7989516568, 653305.5587497812 668007.1617190047, 653294.515923674 668014.1532791387, 653293.2829272854 668014.9342428055, 653280.5149599384 668023.7294145388, 653275.1986273134 668027.3919363801, 653272.2173031666 668029.4085165175, 653271.993282212 668029.5604879666, 653261.9256547939 668036.3721649416, 653259.1774046153 668038.2318146378, 653256.8463091463 668039.8084774957, 653251.8418738437 668043.1939195113, 653251.391879339 668043.4988221313, 653247.2364868851 668046.3093323205, 653242.8052310155 668049.3826862808, 653229.6932659834 668058.2238942548, 653228.3493686512 668057.1648684088, 653224.3797954441 668054.0373976503, 653221.1212170704 668051.4700040966, 653215.6636307916 668047.1695720531, 653192.2130516568 668028.6599899117, 653185.4177809596 668023.2962137025, 653171.9010721323 668012.627869834, 653169.402296648 668010.6555992975, 653167.242702295 668007.3142670548, 653165.6439654086 668004.3041970221, 653164.8362898815 668001.7281351383, 653163.7689907115 667995.5970493053, 653162.1490696821 667986.9929705603, 653161.8143845822 667986.3247796764, 653161.2903523792 667985.2768913491, 653160.4523311564 667984.1758464886, 653156.8177300951 667982.3024802101, 653154.9927066852 667982.1462658676, 653152.9956887914 667981.9749913334, 653145.5017008826 667981.9171434962, 653142.4258792363 667981.4587048466, 653137.5719590528 667980.281184894, 653135.5221610286 667979.1219172066, 653126.8059166374 667972.9409657713, 653122.711702963 667974.7225367314, 653113.357405651 667978.7912935828, 653106.8510406929 667981.622484424, 653100.2887609836 667984.4767082315, 653092.8155119201 667987.7287385961, 653088.5313651753 667989.5922056243, 653067.6223857895 667998.688643805, 653047.266520288 668007.5440298677, 653045.5065319571 668008.3108507227, 653035.4351037554 668012.6926366199, 653030.5367806477 668014.8230666014, 653027.789672565 668016.0176044673, 653031.6311709527 668019.9940065538, 653030.7743994513 668021.3994858738, 653029.9739057106 668022.7128131134, 653026.7293231505 668028.0344360295, 653023.4947038217 668033.1549427649, 653020.3912801226 668038.0674678067, 653016.9356342694 668043.5389833689, 653005.9025999064 668061.0044760668, 653004.1553373048 668063.7711109266, 652981.2786883976 668087.4951095603, 652976.3331583748 668092.6243716724, 652971.3876283391 668097.7537936487, 652971.3736042011 668097.7688317748, 652971.3446685612 668097.7977879378, 652926.7687110375 668143.7939340093, 652895.6601237515 668175.8928263171, 652883.5397750954 668188.3992088581, 652876.9610825595 668195.0372126824, 652870.3823901265 668201.6744162859, 652854.377593847 668217.8202831997, 652835.2425936719 668237.1226867113, 652820.7459967248 668251.7457058727, 652808.5455391016 668264.6991022422, 652804.4030959462 668269.0983839842, 652800.2595877426 668273.4979855048, 652797.5183145988 668276.4096159589, 652788.5902713544 668285.8884121856, 652778.6620768559 668296.4301130528, 652767.4397571055 668308.3455587549, 652745.8821875323 668331.2356079973, 652732.1046356715 668345.8647864439, 652730.2284572638 668347.5395775402, 652728.1541697396 668349.3913023354, 652726.0390533267 668351.2790217006, 652723.7987874221 668353.277764453, 652721.4456204951 668355.3784121783, 652715.858091832 668360.3647106277, 652705.4080140931 668369.6922833374, 652686.1062227255 668386.9186797244, 652683.319913893 668389.4063095055, 652676.5783461138 668395.4234936312, 652671.3027147653 668400.1317524329, 652666.0332965337 668404.8340919439, 652660.0076550986 668410.2132907491, 652654.5381749759 668415.094483385, 652650.4778257887 668418.7179433351, 652649.0555586378 668419.9867141552, 652643.533178246 668424.9160594783, 652637.761563772 668430.067291446, 652631.0679252766 668436.0414405271, 652627.3164557944 668439.3889411282, 652616.3664888544 668449.1623630684, 652602.314055777 668461.6964912306, 652598.509686358 668465.0889443363, 652589.7503123238 668469.5839869693, 652583.6839318859 668472.6971330117, 652580.0087793099 668474.5827440878, 652573.7924034845 668477.7728308385, 652567.1830221101 668481.1639873359, 652563.1098935631 668483.2545074502, 652556.4815186195 668486.6557413369, 652546.6491006536 668491.7013660122, 652529.1741969776 668500.6682543505, 652509.2572975988 668510.8881098712, 652506.0379887512 668512.5401784922, 652500.542360471 668508.7454973658, 652490.6783077627 668501.9335386899, 652463.876570122 668483.3405308112, 652453.798469941 668476.3935098216, 652443.7203699577 668469.4453692045, 652437.3389102682 668465.0976316737, 652430.0425547146 668459.8057992725, 652422.7461991797 668454.5141270654, 652366.9631203702 668416.6619560741, 652355.8500951291 668409.1207439105, 652344.7370698566 668401.5804921548, 652316.5715125311 668382.3204652475, 652309.7341032545 668377.5947619337, 652302.8956290167 668372.8690586596, 652293.0544655183 668366.1379082032, 652286.1801357658 668361.4331567766, 652279.3047419639 668356.7274434135, 652162.3519680499 668275.9525777455, 652154.8496567038 668271.1888720018, 652149.3691203785 668276.5180472519, 652148.2919365582 668277.5659008565, 652010.6118472652 668407.6041796667, 652009.738812683 668408.4291809968, 651992.7639463255 668424.8287872991, 651989.2086298543 668421.9472859205, 651985.7866022321 668419.1741188704, 651931.6511952698 668376.0132373095, 651920.9681952776 668367.6250049367, 651936.3504266412 668347.4442122047, 652066.0477583235 668207.5660984507, 652070.2292533094 668203.1197913704, 652074.4107484141 668198.6723642446, 652227.5957757821 668031.641371655, 652231.7673363432 668027.1438617276, 652235.9397844973 668022.6453918568, 652352.8332895703 667898.5153113068, 652383.0730347484 667864.7582852041, 652389.982037153 667855.1853281263, 652395.1029406288 667846.94797899, 652400.2238442037 667838.7098296401, 652409.7557394425 667821.5182572459, 652420.9585849327 667796.8559545025, 652428.6089687133 667775.285184244, 652430.1054757057 667770.5523124574, 652431.6010951872 667765.8196005255, 652438.1199777687 667731.5288876449, 652441.1337307912 667699.3146892231, 652441.2161194365 667691.3976984172, 652441.229762406 667690.0757999747, 652441.3257940018 667680.8370707165, 652439.5599239728 667654.1643013476, 652431.7914924091 667615.9098755808, 652419.7118801336 667578.4530129973, 652411.3371471728 667558.359368775, 652409.3836047369 667553.6720093042, 652406.9493513796 667547.8311706668, 652402.9625915611 667538.2655655071, 652376.2725980487 667504.8120547448, 652353.3126788681 667479.5747022078, 652308.6114074885 667442.6060240807, 652256.6078321985 667415.130373294, 652205.6810495697 667399.3244052288, 652147.4973453176 667393.3556654991, 652084.1122480169 667397.9410245401, 652039.3800478996 667410.8705479782, 652011.6469005933 667423.6880114403, 652010.7276654802 667421.8876104873, 652006.5660858506 667413.7365240664, 652000.8210057894 667416.6701952867, 651998.019558149 667418.1005598976, 651995.4993828934 667403.1414558287, 652005.4232495888 667385.3558744182, 652022.686779313 667354.4184103706, 652025.1001938505 667350.0928447625, 652033.8809108238 667334.3571738254, 651998.0493938428 667316.3532041205, 651962.9754379385 667352.2583952354, 651951.0534889511 667362.1787430212, 651940.1031529857 667373.7623794791, 651932.16714549 667383.600262145, 651905.6612272293 667420.2542280381, 651897.2487788125 667433.503989277, 651888.837395003 667446.7526315063, 651885.2495946508 667453.1931944044, 651881.6649892862 667459.6326379, 651879.624554748 667463.5311975661, 651876.4278207878 667469.6377358403, 651797.9044245335 667428.6483789341)))" + } +}, +{ + "model": "common.municipality", + "pk": 14, + "fields": { + "fid": "", + "name_fr": "Anderlecht", + "name_nl": "Anderlecht", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((641192.7032296682 667689.7562988449, 641197.3294474154 667689.2710951122, 641236.0964530093 667685.2045573671, 641239.8720042821 667669.5163883945, 641243.5923245206 667654.0579715474, 641245.8184755279 667644.2537111308, 641248.199572192 667634.5400283418, 641250.4708096825 667624.7224938608, 641252.8018618246 667614.9260864743, 641254.6889388941 667606.9873645569, 641271.9602287496 667498.9150187167, 641273.8765497571 667488.4344119082, 641274.667660534 667484.107517772, 641276.4858396798 667483.6520612827, 641279.7096725178 667482.8842971387, 641281.1439057792 667482.553913129, 641282.5852383006 667482.2292899007, 641284.0263924424 667481.9118665736, 641285.4693206162 667481.6022833707, 641292.5621895485 667480.1161120682, 641343.1258936264 667472.3413134997, 641387.0052041718 667465.6495428889, 641387.3777802951 667465.6047881765, 641393.819879109 667464.5970941373, 641397.6319206445 667464.0212389963, 641401.4668646881 667463.4036271423, 641405.2959557368 667462.7495350551, 641407.4588303737 667462.3584396348, 641406.963808667 667466.5127287897, 641386.9982264409 667634.1186728375, 641421.9702510296 667639.1738228255, 641454.5424980317 667645.5921878286, 641525.6491588808 667657.1933188715, 641535.2286741209 667659.5366705577, 641540.7948909936 667660.793477775, 641546.1992881448 667661.5657919381, 641557.0084072408 667661.9099558517, 641616.4214089329 667675.1005419305, 641699.7850128185 667493.718716198, 641723.3279338639 667430.374410999, 641724.7540769734 667426.5373538461, 641735.6116749427 667428.0869461703, 641751.3582311156 667430.2297526554, 641762.651456375 667431.413961703, 641776.4990976682 667432.3651982427, 641782.2377921952 667432.5610791977, 641787.5591974696 667432.6692316624, 641799.0785226681 667432.6835638357, 641832.9509937012 667431.312258678, 641868.4724558211 667429.7313932725, 641873.6529435199 667429.7046508852, 641881.374499861 667429.7295705238, 641894.3572600158 667430.1010320411, 641896.9238889583 667430.1490171521, 641897.9995251134 667430.2099444355, 641907.6711600987 667431.0669244338, 641912.9548931852 667431.5198674278, 641917.8135098899 667432.0247590072, 641924.0743296768 667432.8437330908, 641947.8656196789 667436.8429894724, 641964.8173642544 667440.6654331582, 641987.5830599718 667446.1491845531, 642000.8390056895 667449.2852772214, 642006.8995436227 667450.6157393778, 642015.613862398 667451.9945167707, 642021.6313499529 667452.6595434961, 642027.5304753778 667453.083439662, 642030.1265595196 667453.2093469208, 642036.5239720956 667453.2464281188, 642040.9967737895 667453.2004013797, 642048.3990647926 667452.7753690757, 642056.3532606182 667451.8580898239, 642064.710793513 667450.4578258315, 642077.218121791 667447.10446812, 642079.9078194816 667442.2421299559, 642129.4868435886 667352.6150788477, 642174.7067600052 667285.4204602715, 642200.7202959744 667234.0861163476, 642205.6981653398 667227.432224284, 642219.2256659078 667199.9740156798, 642237.6385637952 667142.7091191291, 642270.18352588 667052.1325573596, 642274.3046693186 667040.6627945271, 642275.9808258672 667035.9977736211, 642276.7733383757 667031.8035249326, 642286.9896812865 666977.7254744457, 642294.2643884489 666939.1868617238, 642307.1831146292 666866.5293834159, 642311.8161740403 666840.5104462998, 642312.8170753533 666834.8205622812, 642317.610260591 666807.1714262813, 642326.0057771754 666762.1331896622, 642330.1725727676 666748.9172134602, 642330.917645432 666746.553652137, 642334.8479555047 666743.9242166379, 642340.0832487046 666741.159891344, 642345.2976561097 666743.6959675434, 642348.2054785328 666744.7559628158, 642350.4825285737 666745.3589329623, 642351.4646118962 666745.4736029372, 642366.1573035683 666746.0827841202, 642369.3704798411 666745.2334014686, 642373.7375060163 666743.4737629608, 642375.7175441621 666743.0991133483, 642377.4333094169 666743.9917755924, 642379.816535018 666745.7827408687, 642381.5719729456 666747.3833975234, 642384.4559211857 666749.172179777, 642385.4283780877 666749.625083845, 642388.903490197 666750.6406643508, 642389.201500458 666750.7154171593, 642390.8914802158 666750.5556115314, 642396.7568408811 666748.2062317017, 642404.1033556007 666746.4123722533, 642409.1374990547 666745.4535980624, 642410.2668931022 666745.672283314, 642411.0011713266 666745.9307630929, 642414.2065174129 666748.138296019, 642414.6611471111 666752.0131726107, 642416.9855769363 666755.6084257326, 642421.7702493197 666758.2439722045, 642425.6201233696 666758.7444027591, 642429.1353559635 666756.8198696837, 642431.6043823757 666753.7091544757, 642432.5209783614 666752.3003186459, 642433.9437609806 666751.2024159386, 642434.9250035154 666750.936611242, 642436.8852989328 666750.8912347052, 642438.1059012057 666751.3204873666, 642439.9746530188 666752.3510851812, 642441.2548499526 666753.144819461, 642441.8660993123 666753.5877626687, 642445.4353821699 666756.7084442251, 642446.4081688669 666757.3669454916, 642449.5149705232 666758.3972040956, 642451.171247511 666758.0589971693, 642452.651230217 666756.5887862021, 642453.4152972103 666755.6528862952, 642454.6414280724 666754.422643067, 642457.295430254 666752.5730508968, 642460.9356658567 666751.7334765407, 642466.1132177123 666753.7500366066, 642467.6753473543 666754.0092505213, 642468.9004125408 666752.7844470594, 642468.6489561499 666750.8607656152, 642467.8018195864 666747.4602373061, 642466.9939459269 666745.214577173, 642466.3596645944 666744.4228362588, 642465.2365701613 666743.4936017308, 642463.7417811117 666742.6739234172, 642461.1218514555 666741.6886795746, 642460.4144115923 666741.1356469862, 642460.0171664389 666739.7236217867, 642460.7971780743 666739.0336401763, 642476.6386647928 666743.7632138098, 642478.6636847253 666744.225677443, 642482.9462631003 666745.0210329777, 642485.4863913476 666743.9664565199, 642488.0959704107 666743.9590732126, 642489.2425498749 666744.4379167905, 642490.7090782495 666745.5713475252, 642492.1900815173 666747.3566107465, 642497.3449183746 666755.1226875213, 642500.7781835916 666755.7912284097, 642505.3838753159 666756.0435884311, 642513.0390098484 666757.5463575803, 642515.1170577246 666758.3680222959, 642516.1519600772 666759.0978897173, 642516.69124004 666760.2148953701, 642518.1182253225 666766.819765058, 642519.6430284115 666768.9499885857, 642522.2518752745 666769.1242027818, 642528.7877433174 666770.3260491231, 642530.5475052943 666771.406234109, 642532.4232267047 666779.2903371525, 642535.38060955 666787.2764818538, 642537.3904266497 666788.6816508146, 642543.8002942528 666788.2473459532, 642552.1206351017 666785.8734454287, 642561.1048894171 666787.2693278341, 642568.634819359 666790.7802154477, 642582.4746524505 666791.2304234427, 642583.5284387481 666791.396940996, 642584.6906245783 666791.9865050094, 642591.3239273096 666796.182721359, 642599.9573751345 666796.7033762753, 642603.6534265308 666795.20029754, 642609.3027450544 666795.964449617, 642613.1384517086 666797.6501426557, 642616.987117957 666796.4209974674, 642620.3387152342 666795.7961073711, 642622.963835296 666793.5362771949, 642627.5539353552 666792.1118589835, 642630.9238324822 666792.7922329288, 642635.3745665086 666797.4065956254, 642638.1729283965 666799.463045642, 642642.2266034477 666801.14412367, 642643.212448051 666800.9834380386, 642645.7628356295 666800.2189387119, 642647.4054312858 666799.994808667, 642655.0035499913 666803.2684280952, 642657.8421869795 666804.0151407995, 642663.8728066371 666804.4940603568, 642670.4820662598 666805.0066453004, 642679.1554899026 666808.1103096323, 642685.5063610466 666809.6732508703, 642695.181802082 666815.3485142655, 642713.0743850669 666820.6911175931, 642717.9545899591 666820.1217607539, 642727.2645001162 666820.5079350332, 642729.4435452706 666821.3254519143, 642733.2740353077 666823.5784972484, 642734.8685139499 666823.4770801003, 642735.7791327098 666823.0437496279, 642737.1919410811 666820.7748212316, 642739.603286288 666816.1613982843, 642742.3372168276 666812.4166402124, 642743.6970842095 666810.5275405807, 642748.2921190711 666803.5899174906, 642749.347202171 666801.8631809357, 642749.6216764902 666801.3556990416, 642751.106767677 666800.2008436485, 642751.896856154 666799.8409384126, 642753.236699948 666800.5761208836, 642757.3060361773 666803.3711060053, 642762.7157945414 666806.5270388052, 642764.93573249 666806.7872878715, 642766.172693135 666805.5349655467, 642767.2752023374 666803.5295181002, 642768.559420244 666801.9279258571, 642774.2379637733 666797.4007930519, 642775.4665883457 666796.096470438, 642775.5323315664 666794.0833850959, 642775.550182825 666791.8043779006, 642776.0426603584 666791.0071646814, 642776.6627573789 666790.2238857908, 642778.810459758 666790.4335672391, 642779.7186781312 666790.7591061478, 642780.6272295695 666791.2638426761, 642781.9022680743 666792.1500557467, 642783.3929117135 666793.4870072657, 642785.5806097724 666796.1187406215, 642787.7665653459 666798.4850373847, 642789.7743585899 666799.0263784892, 642791.1296764971 666799.5023662515, 642792.061506606 666799.7914283607, 642797.2812668743 666800.6611304455, 642801.9706945854 666801.2691757369, 642804.4760661403 666801.3947390626, 642811.0764194198 666800.7150190577, 642813.1043464041 666800.6317308727, 642817.3760561156 666800.3184610251, 642822.0473222753 666801.3903380536, 642830.3757259247 666802.7498272993, 642833.5605611879 666802.8701880093, 642836.7876705028 666801.2985761673, 642841.5352287705 666799.7311372122, 642842.9309573579 666799.2378625827, 642845.8293107757 666799.3562707112, 642848.120756038 666799.8168452275, 642849.8553172869 666799.4165678957, 642851.3337925691 666798.6572262199, 642853.5761471578 666797.0426228504, 642855.5562818133 666795.8785436281, 642858.0365866993 666794.8751591479, 642859.2863736829 666794.6932237344, 642859.6213127563 666794.6932618332, 642862.1512520157 666795.6735365456, 642862.3968867449 666795.8596420042, 642865.7317349958 666798.6615840198, 642866.9148908944 666799.046033494, 642874.6872343932 666800.0651440963, 642877.102017216 666800.7194901002, 642879.0002577639 666801.5582548941, 642881.3035557288 666802.3434664867, 642885.7254559401 666803.2605973601, 642888.2024878192 666804.3469447377, 642890.107792236 666805.4807464535, 642891.4174173415 666806.5768809066, 642893.0235258902 666808.621996507, 642894.3490807278 666810.0870878908, 642896.613964545 666811.4822070894, 642899.234822788 666812.1367366221, 642903.0637689683 666812.5029274456, 642905.4659005097 666813.5577468047, 642906.6082631204 666815.1383358646, 642906.0976702023 666820.3782083859, 642908.4335800319 666825.9105609878, 642908.6063100308 666827.1640040547, 642908.6815593955 666830.1484531198, 642907.7572915838 666833.3361457222, 642905.471707858 666838.6484552177, 642904.059873909 666843.1127153708, 642906.3631214885 666844.3110417197, 642907.8256604383 666844.6888032283, 642909.9609716147 666846.076227973, 642915.7742846234 666846.9589581599, 642920.1900293081 666841.6044916572, 642924.936652685 666843.3294892532, 642931.6864661681 666846.9529299064, 642935.0809908765 666848.0025825507, 642938.5782030638 666848.2871369817, 642946.9987328765 666846.7090764825, 642950.9542464057 666845.5330622979, 642957.2470453448 666847.3505548658, 642959.5760265798 666847.1263429783, 642961.4440833842 666846.5880027334, 642964.4131449617 666846.1849860633, 642971.821455035 666847.0822977566, 642977.5985461426 666848.0659826257, 642989.8643655409 666840.9324324263, 642991.0904164095 666841.8055205924, 642991.9259846646 666842.5688057434, 642996.1983335563 666848.6480927403, 643017.8961390698 666849.5204718612, 643020.303041457 666849.3007487707, 643031.120977039 666855.4993399823, 643032.0320499141 666855.7071210284, 643040.9462785893 666854.9564658531, 643050.7070456375 666855.25421347, 643058.3326805383 666848.3351713174, 643068.3915145805 666856.9662851273, 643087.4409482954 666860.1032940062, 643092.1516495175 666855.1139746668, 643100.4238715254 666854.3275667535, 643103.6326530405 666853.1265075655, 643109.094043559 666852.0943426907, 643112.4468348494 666851.8662473868, 643118.5431341139 666853.7617972591, 643123.2713645267 666854.9424805439, 643132.4534963891 666855.166403271, 643134.6722666948 666854.8148204219, 643141.0038965307 666852.6075696046, 643145.0815359957 666851.3699697084, 643153.1211938629 666853.8308537836, 643157.5357080307 666857.096274877, 643171.8998094641 666862.4234429249, 643173.0278486446 666862.1106153704, 643174.3270414369 666861.4259720279, 643176.5170630467 666859.5699250094, 643180.2415612527 666859.1303547396, 643181.4240171792 666859.4340055482, 643189.0780085354 666861.5922898467, 643197.1395799697 666863.3882252212, 643201.3293622647 666863.9013359045, 643209.3506137539 666860.103737426, 643211.6988120081 666859.4915325766, 643213.9451479308 666860.1575399888, 643221.0941543251 666863.7935082997, 643224.797400482 666862.9624205389, 643233.5028547515 666859.5967343504, 643235.9168877312 666859.1246952069, 643239.44178455 666859.4511725223, 643247.4692837809 666861.1866249254, 643247.8042137396 666861.2615421299, 643251.3489424344 666860.5251550667, 643255.928109635 666858.5045818996, 643263.3656752033 666853.8382873326, 643267.9151957243 666851.8540302262, 643273.8204784998 666849.6147306439, 643281.0171857423 666843.4356272456, 643291.5223282727 666838.3304869095, 643304.877119278 666832.4648801971, 643320.1333607419 666825.8602987566, 643332.5981302016 666820.3665854816, 643345.7819070937 666813.7241679453, 643364.807370882 666802.3453530655, 643378.47199873 666794.1727681342, 643382.6185384998 666792.676138198, 643386.4802282803 666790.895319326, 643393.6832143778 666781.2458569488, 643395.4479502952 666776.46979619, 643396.6704561769 666774.3883607162, 643398.1948312626 666772.7602340076, 643404.4035382387 666768.4011533502, 643407.584421537 666765.9906645091, 643413.2599155821 666760.2419400483, 643416.0757611888 666757.2062972502, 643419.3042963714 666752.6604797692, 643421.4781848262 666749.1830093152, 643440.6028444929 666721.9551143378, 643443.2474091392 666717.4202699279, 643444.155539873 666715.550155829, 643446.4386784914 666709.8832840602, 643447.3050419986 666708.466279683, 643448.8464176821 666707.1589506402, 643450.6895043147 666706.1468522669, 643452.5787656812 666704.9275616466, 643454.3758730966 666703.9744973213, 643455.8473156949 666702.6572404226, 643456.9015167095 666700.8748217726, 643457.222363499 666698.5320066335, 643457.7251295673 666696.3234505202, 643459.9307277211 666688.9813900162, 643461.9482448816 666685.4639017313, 643463.8118589161 666682.0467147293, 643466.3171306506 666678.6142407296, 643467.0980664016 666676.1583591274, 643466.5558118069 666674.6808754718, 643465.1614139299 666671.5361152884, 643464.7535373999 666669.9604080848, 643464.3369988567 666668.093823423, 643464.5397656935 666666.109710477, 643465.2010026542 666664.1882088084, 643466.6588649836 666662.1546388973, 643467.5043897819 666659.8653225803, 643468.8570594541 666657.3077470546, 643472.2516830274 666653.167222381, 643473.5358734229 666651.7805048745, 643476.369499396 666649.9485285549, 643478.0214496223 666648.6960911322, 643478.7172734363 666646.9821108036, 643478.3461889065 666644.9963327056, 643478.721191855 666642.4949654898, 643479.4264538425 666640.5280292741, 643480.801641506 666638.1694937849, 643482.2900013948 666636.399603993, 643484.0186082545 666635.3329329374, 643486.3515925404 666634.2975300403, 643487.5854671993 666633.5992784096, 643494.275072035 666627.3177128477, 643495.8161472091 666625.5614287792, 643496.4961738038 666623.8463265648, 643496.2132155426 666622.6026294685, 643494.9216996449 666620.0076343025, 643493.2193554484 666616.7556404944, 643492.303517978 666614.860839508, 643491.5903760057 666613.0268607745, 643491.1923615615 666610.6314845462, 643490.4063047841 666607.0349588234, 643489.126001929 666602.729265642, 643488.1437373253 666601.1866129173, 643487.0547409359 666600.0993026046, 643485.5748004161 666599.7780187348, 643483.6768394089 666599.5581262631, 643482.4159264901 666599.2789467166, 643481.746318898 666598.515040047, 643481.374232453 666597.4697305007, 643481.4512203913 666594.9321698894, 643481.6915039053 666592.4170275097, 643482.1380105041 666590.1465455393, 643484.7264436202 666581.5775023587, 643485.3237947462 666579.544474592, 643495.6249599298 666557.1662315151, 643495.9478974418 666555.1454924492, 643495.8512082902 666551.8389613534, 643496.2824412128 666548.1824142612, 643496.6408859676 666543.1815551204, 643497.5222787648 666540.7816838404, 643499.9481009868 666538.2331892113, 643502.3149593754 666535.9677244733, 643506.0601591555 666530.22293763, 643506.6499246159 666527.8054334829, 643506.6025732374 666526.0126496945, 643505.2124275937 666522.932209399, 643505.3625621486 666520.2517786495, 643505.7844687373 666517.387380872, 643506.6880949681 666514.6076765894, 643507.5682106346 666512.4921216472, 643508.6848579248 666510.9797862137, 643512.0997068819 666506.9130215581, 643513.200382538 666505.3742846041, 643513.9641814469 666503.714550981, 643514.9920053554 666501.3538956476, 643516.0420513096 666498.7073262554, 643516.6678455954 666496.3165457766, 643516.2969774881 666494.0109315097, 643515.3074241758 666492.5618769653, 643514.8342455565 666490.8447240649, 643517.9732086917 666478.464587844, 643518.8119104188 666476.8160625002, 643520.2685693183 666474.4591355455, 643520.9083392351 666472.4521919906, 643522.6263433158 666469.5367412713, 643525.10484342 666464.3890833018, 643525.5913392338 666461.6891706957, 643526.5701451027 666456.644381865, 643527.1120188541 666451.0382703493, 643527.5577766538 666449.0858641501, 643530.8857210582 666442.9858334617, 643532.3535885869 666438.9532476937, 643533.1343407352 666436.545364782, 643534.2871782718 666432.2727462475, 643535.2564564099 666428.2165843567, 643535.4555100307 666426.1142320225, 643536.1383305574 666424.7762452026, 643538.7062259899 666423.0695157999, 643543.3449388429 666420.3404725923, 643547.6351195326 666418.460979803, 643556.1349403239 666418.7219370676, 643560.6549348369 666421.6011334418, 643564.9845978287 666424.9207831157, 643568.1587258866 666426.9631974731, 643573.3583877569 666428.1608908079, 643576.7005895481 666428.8834999071, 643580.7709066117 666429.4583530591, 643584.9421909115 666430.2733747765, 643593.986299606 666431.3483041181, 643610.6393646803 666431.2362674702, 643611.7301020707 666431.1423118487, 643616.7730320587 666429.4286620074, 643620.3758181236 666427.0908567597, 643627.8486392078 666422.9349502273, 643633.9072201102 666421.1658956703, 643636.7927676338 666420.0299150925, 643639.2509534241 666418.4516514207, 643646.2248091795 666411.5222808326, 643652.6054953627 666405.812988727, 643656.9955968718 666404.0186257017, 643661.2759168446 666405.8675675294, 643662.7576421942 666406.105812121, 643665.2745813305 666405.9756979689, 643668.7254505329 666404.9199401557, 643671.3631456863 666401.3962793566, 643672.6116063442 666397.5163057775, 643672.6994411943 666394.8142673153, 643672.2109690786 666391.8700866016, 643671.811655117 666388.4806411717, 643672.3540588375 666382.8879678681, 643674.1112236701 666379.0523711552, 643674.7529766124 666378.2518129814, 643684.6942282928 666371.4621347269, 643686.5709462889 666366.8280006984, 643688.4641442742 666360.9575629234, 643691.5910306701 666355.9179746513, 643694.8673416929 666354.037566353, 643696.7152926166 666353.8884966533, 643700.3561035732 666355.598967641, 643705.8404794711 666359.659539388, 643711.7000977799 666362.1651316554, 643713.060108321 666362.0003270857, 643715.7955971613 666359.9998590994, 643716.8716067096 666358.4065590547, 643722.1417946449 666347.4215213632, 643727.4603246565 666346.1517362732, 643735.5870271736 666345.0881856551, 643743.591865265 666344.8540516624, 643748.7986901026 666349.9165007494, 643750.0832548012 666351.288789871, 643753.5658084545 666352.4150899017, 643755.7047602547 666350.1327976332, 643757.9465147416 666346.1306967766, 643763.8317855371 666343.486271376, 643768.9017776652 666344.0119574293, 643776.5356880733 666347.8073352287, 643782.5410483115 666348.7198424572, 643795.3000129198 666348.3443266355, 643801.9204043085 666352.1805498514, 643804.6602017009 666354.1657963501, 643809.1698369862 666356.1162030267, 643815.6498950745 666355.0070270761, 643819.035657502 666352.1968812761, 643821.5025223476 666349.3031126923, 643823.9861094068 666344.6381661604, 643830.2063152872 666342.0238578527, 643838.7160311951 666344.1497627144, 643847.5228858301 666343.3199010901, 643853.2181023716 666341.7975308328, 643864.4365282876 666337.3828660509, 643871.3348241824 666331.5024428547, 643884.0377501526 666326.4101954472, 643897.2832509772 666320.1617030137, 643903.7231416041 666320.9974711232, 643906.3229754396 666322.322713944, 643908.2080354589 666322.0880524818, 643909.5269682547 666321.092854743, 643914.7859386774 666314.8108077213, 643918.4514826051 666311.6584629418, 643924.4992549148 666309.7700566988, 643930.9394440321 666311.0715795454, 643932.9336919617 666313.545860142, 643933.2542601837 666313.4934174407, 643935.075102955 666312.9525120227, 643936.7141314554 666309.9463334321, 643937.5096766665 666305.5715935917, 643940.0522226694 666302.1418431691, 643943.8300313384 666302.6723497729, 643952.0303589022 666307.8236310374, 643957.9339580964 666310.6937149408, 643962.9484429765 666313.7744147273, 643965.7430912748 666315.7484727087, 643967.3131401093 666316.5798432265, 643967.9324327819 666316.5696742623, 643968.9695639256 666316.4971538422, 643972.6281990794 666314.695352935, 643975.9972191341 666310.9175408763, 643977.0668581193 666310.6333465327, 643978.1336568991 666310.3510718383, 643984.768411421 666312.891803285, 643988.6924272636 666318.238913116, 643993.7166433226 666323.0419950762, 643994.6603486644 666323.4785783025, 643996.4803727766 666323.8339828034, 643997.92012588 666323.3003936328, 643998.5541926319 666322.9460701913, 644000.6923190233 666320.1636259926, 644002.6227970666 666315.7138165173, 644002.6982558239 666312.603939708, 644000.8388025131 666308.0588172805, 643999.4096675386 666301.5336061583, 644001.714941901 666294.8152247492, 644002.4897983726 666294.1836404743, 644003.6005146175 666293.2792976795, 644013.6207807488 666293.6354808751, 644018.4906834932 666294.6009876975, 644026.9549065159 666297.1918480429, 644032.8437552539 666301.6764726533, 644037.6458527877 666305.5784193489, 644045.0586047816 666306.4688583002, 644059.4120349728 666309.1239125796, 644075.4080112261 666313.432736903, 644089.5216727209 666318.7587747256, 644102.2701726642 666330.1301902549, 644110.2995549407 666333.9409882964, 644117.9597159894 666339.422125902, 644123.6181941045 666343.7273671435, 644130.233630651 666346.0288999518, 644149.0757909153 666362.0700842913, 644163.7237140168 666377.7266330317, 644176.002035997 666390.4479026361, 644179.4311774353 666398.8086852722, 644182.3496996181 666409.7434615995, 644188.3193088549 666412.7940365032, 644195.9330518215 666413.2198763852, 644197.167351833 666414.8833618065, 644198.0135257934 666416.0268077601, 644197.7281966379 666421.1599991766, 644196.672464798 666426.7849357647, 644202.1320544853 666430.4104909577, 644220.1117200025 666430.7125767716, 644241.8184240414 666436.0164818998, 644256.4367581552 666438.1469576595, 644268.1435887957 666440.2522954424, 644275.6504149692 666441.1968376449, 644278.7069060681 666441.4581500258, 644287.081923394 666442.019597238, 644299.0560305482 666442.9574586792, 644307.9190072966 666443.3845640591, 644317.0058437061 666444.995522849, 644331.8660975168 666445.7486017365, 644343.8992888669 666446.8747881541, 644355.6330654639 666449.2599664506, 644370.3499216845 666452.7709195797, 644371.6884108309 666452.9891528271, 644372.966551698 666453.1976191648, 644390.1368771661 666455.9975051068, 644393.8484131099 666457.1000048275, 644409.206264673 666469.0077442309, 644417.6591752204 666469.8059983783, 644422.3314005046 666471.8140403357, 644437.2825583877 666478.2278708861, 644438.0294584956 666478.3372365636, 644443.7913100201 666479.1810975192, 644449.9824163599 666479.4992539043, 644456.8086696718 666479.3413291281, 644463.7934336212 666480.6122078033, 644485.281601005 666486.2196072685, 644520.2770769367 666490.081555661, 644531.6537593035 666492.1994963547, 644541.4429772752 666495.1110043267, 644542.8412956125 666495.2284457062, 644546.5951714924 666495.5440789408, 644550.2463715605 666495.068348448, 644554.5324450494 666494.9101284832, 644557.8665085015 666492.5292608482, 644559.6767242247 666488.8783091645, 644562.6935119695 666484.2748676137, 644567.6150590129 666480.9417143324, 644573.6480627167 666477.9261269439, 644581.5858595382 666474.4346058667, 644587.6184551821 666473.3241186547, 644592.5395795854 666473.4832491151, 644596.9845955785 666474.1187993167, 644602.3820058338 666473.8019900603, 644617.6542338801 666470.1526030697, 644624.4805786079 666467.7721410976, 644629.8783008063 666466.344143233, 644633.8469302738 666466.8207596759, 644635.6380952756 666468.0524752532, 644636.3867212948 666468.5674369964, 644642.1010039467 666473.8066075586, 644645.5476153049 666474.244123761, 644650.863788793 666474.9189748858, 644653.0859967517 666476.9830521017, 644654.5145324757 666479.681910554, 644654.5590494998 666479.9718327636, 644655.1489449274 666483.8093020652, 644657.5943915612 666495.1245106037, 644658.9572965734 666497.4622451703, 644661.3407926315 666500.0055363248, 644665.5310935212 666500.6816966991, 644666.5077297125 666500.3520536439, 644668.4817702419 666499.685890013, 644670.7046359259 666497.7810479685, 644671.340147817 666494.2886775458, 644672.2929262115 666490.1613104923, 644674.6748642272 666486.5104247371, 644678.6441083385 666481.9070932309, 644681.9781286465 666478.415036485, 644685.034555215 666476.2760536441, 644685.1533117216 666476.192868298, 644685.1911433525 666475.9906347578, 644686.0662436825 666471.3155841641, 644686.7837224631 666468.1320200306, 644689.124408087 666457.7467979728, 644692.9346996744 666455.3659851147, 644697.4455189157 666452.002383662, 644702.3018148971 666446.6358026982, 644702.4609402071 666442.9846583392, 644702.1264290388 666442.3783856329, 644699.3502523083 666437.3486741933, 644699.3506748358 666433.8562297866, 644701.7322816363 666431.4754107666, 644705.5422036326 666430.6821015502, 644712.0510082386 666429.7302275457, 644716.9725981349 666427.5084219896, 644719.9891551704 666424.8100798938, 644722.0531166358 666421.7940303204, 644724.9110715735 666419.8892617032, 644729.8322919221 666419.2547999134, 644730.6516266136 666419.2382552766, 644740.0572236531 666426.9386301, 644752.666175579 666432.8334756133, 644766.0615861305 666437.5660244427, 644770.1997340887 666446.7846523545, 644771.9728497658 666456.3618818526, 644780.0150861344 666469.8945203085, 644783.639079714 666478.6297336487, 644790.7472899881 666486.5365605287, 644809.9958363678 666499.0388331605, 644818.8220215054 666508.6813629004, 644824.2311010465 666514.6120125856, 644828.5411330665 666515.9525004652, 644833.4211926411 666515.1629959729, 644839.1913154473 666512.1936167851, 644848.9909650172 666515.444964088, 644853.8607206488 666517.6653483409, 644855.0604926341 666518.6355582112, 644857.8902504726 666520.9257845181, 644861.1798955292 666524.7762087778, 644863.1092205353 666528.3963971246, 644866.6478763721 666539.7464557048, 644876.5354536235 666558.5976585038, 644873.5657991074 666562.4083150079, 644861.4732621661 666577.9256331112, 644859.9521614771 666587.6954386738, 644865.6317710767 666590.5061518004, 644904.9815652147 666589.1105862327, 644907.7511716363 666591.2308876211, 644900.0592270334 666609.2598933758, 644902.4581793345 666617.5600903787, 644911.1580832623 666617.5910230981, 644928.2291792844 666607.2529530613, 644935.1885775739 666611.323963129, 644955.2890127996 666605.6063598184, 644968.5296847126 666599.7378790969, 644971.04943844 666602.7882224546, 644968.0773597568 666619.3075539658, 644983.6860077529 666628.3694425455, 644991.2561586889 666626.7303401988, 644995.3525877972 666656.560444857, 645006.8024966435 666656.4117001129, 645006.72995054 666678.011640249, 645006.1593080997 666682.2915318644, 645004.2381868201 666691.5609772542, 645002.9173327402 666699.6309843018, 645001.5463044221 666708.6907357811, 645002.7748923937 666719.4307738002, 645004.2238347568 666728.0508582976, 645006.5728317542 666737.0009645299, 645008.3220354451 666743.2311874996, 645010.8011692796 666750.4613258254, 645012.0205537404 666755.111342526, 645014.8589158801 666766.9814774418, 645017.8477076717 666776.6519714853, 645021.0767890915 666785.2122644805, 645024.2359472638 666792.2125646222, 645029.1951097669 666799.2730742333, 645035.9444675174 666803.8437370837, 645044.1346474362 666801.9847107714, 645070.6449308728 666797.5377669288, 645078.6299949839 666796.5479881123, 645078.7849534337 666796.5288063771, 645092.204899874 666795.0604662241, 645095.665027658 666795.2808677565, 645098.8247229393 666796.3711458277, 645101.0846230581 666797.9214744851, 645103.8840952547 666801.1316900484, 645105.7335472782 666806.2120167168, 645105.7830109154 666809.6319894317, 645104.962602909 666814.1217703002, 645103.8720870065 666818.2416032199, 645102.2614850136 666822.4911341796, 645098.7306972734 666829.7408920054, 645097.4698042156 666836.6206782773, 645092.6177418893 666855.4199299401, 645093.177390437 666858.3897265167, 645094.1970969569 666860.0399095842, 645095.9969211782 666861.5901846966, 645098.7866843725 666862.8906578039, 645101.8865857238 666863.7908510023, 645105.0864912054 666864.0912216799, 645110.5364115393 666864.0219386194, 645114.3863729198 666865.3222155417, 645115.7360308972 666867.1225157329, 645115.5558564946 666868.7024794412, 645111.7152908841 666873.371905937, 645109.8549715168 666877.4316495004, 645109.6144679943 666880.3115935915, 645111.3841644455 666882.941854827, 645117.2434667443 666888.1624886367, 645119.7032773414 666889.3226046931, 645121.7131898105 666889.9829930399, 645126.9629943249 666892.0035866348, 645140.7122977204 666897.0750635177, 645144.6819086055 666899.7155017261, 645145.4015727023 666901.9456443298, 645145.33120579 666904.0555358176, 645144.7909956657 666906.2055320424, 645135.3597004673 666918.7241498966, 645135.0393458215 666919.9441007241, 645135.4490273418 666924.2242673615, 645144.7166772506 666942.4750944824, 645148.8261978838 666946.2454584142, 645153.6359294746 666948.7160766943, 645157.765653859 666949.8066287957, 645166.4356544731 666949.0676490201, 645169.995936432 666947.4480805816, 645176.3861207196 666945.4586063476, 645180.9970214074 666937.9193773151, 645186.1468728138 666938.6500520357, 645190.4867240966 666938.5305602541, 645232.4278550663 666926.5755744181, 645247.8475390113 666927.7173649458, 645260.2772981944 666928.5586489199, 645272.30683557 666932.0301012857, 645281.7757716572 666939.9810522919, 645285.2248832323 666947.6413828931, 645285.9940836068 666954.1615711859, 645285.8934416188 666958.5714377783, 645328.364224581 666949.5864049018, 645349.6944007478 666945.0890992316, 645446.5861740923 666924.6106888573, 645594.7909980019 666893.6183614738, 645609.3902580073 666890.5653739952, 645629.9591694706 666887.3122853274, 645687.8657405151 666878.3743272604, 645687.8996433531 666878.3690512679, 645742.9240249582 666870.6496245946, 645747.4504274206 666892.5798786328, 645752.6364055551 666899.878499598, 645756.1266275019 666902.2374463353, 645757.589112293 666903.0849696414, 645831.9852157988 666946.2123210143, 645846.1281638371 666961.9738358478, 645855.8324545863 666975.8831685783, 645892.266758953 667028.1047338773, 645914.076240268 667096.6476512913, 645913.558390759 667104.7185608087, 645898.1008480396 667118.7065501059, 645883.011565997 667130.155724085, 645860.8212009252 667176.095765378, 645869.5925737068 667188.4256454703, 645873.7876219165 667196.8371037212, 645877.6718934617 667218.294332151, 645859.673863202 667228.9410900963, 645806.5959175646 667264.6615953526, 645805.2489498502 667265.5684694562, 645801.5105340144 667268.040969398, 645765.1209779963 667292.1134499973, 645761.5948222326 667294.8181325385, 645761.3897925752 667294.9750671051, 645754.1070669522 667300.5591235494, 645732.3343333091 667317.257381615, 645726.4458094907 667321.7726507233, 645727.3877443993 667331.0037192267, 645732.2861441956 667351.7961093619, 645733.9281737864 667359.2192761751, 645736.8785410533 667372.5524641383, 645725.2336666583 667389.4559071034, 645721.7439148226 667394.9615284714, 645720.1389314007 667407.0748323236, 645720.0245180528 667407.9391312028, 645721.8049433097 667420.2133111367, 645724.516900469 667420.5595465344, 645729.0154650109 667423.1390919122, 645740.5746388813 667429.5034329568, 645759.3703330478 667446.9224463518, 645759.8463306829 667447.3634583652, 645778.0980092617 667464.6414499045, 645779.6479665263 667465.7747420067, 645784.3766408245 667469.2311071116, 645789.1811003102 667472.7426806708, 645803.780067776 667479.4824167716, 645825.3860494287 667486.9898498069, 645843.6780994653 667493.3199440661, 645853.327272298 667499.1911068053, 645859.0624558405 667504.8195717585, 645870.308136416 667515.8569573971, 645878.0280503295 667523.4326789541, 645887.4445409548 667543.8616084142, 645888.4872489346 667546.1237913643, 645911.7625641181 667596.6192108779, 645919.552884882 667613.5199811675, 645923.3446374042 667614.7402567877, 645925.8220448801 667620.1155418381, 645924.9429293228 667630.1273518193, 645924.7266137139 667632.613224867, 645921.60286586 667655.7526570533, 645920.5522642261 667660.0675360457, 645918.0152635054 667676.3421223521, 645916.506200602 667686.3458340962, 645912.3309871772 667712.0010587795, 645913.1863424326 667717.9359738966, 645921.5797149288 667729.6246733101, 645928.3955699918 667739.1165713241, 645945.4592410768 667741.7726320997, 646032.2406432155 667756.2516373513, 646050.712271579 667758.7158623353, 646070.5381674066 667774.7439382502, 646073.9869375512 667785.2242291975, 646077.9144229698 667797.1565606185, 646079.5457598702 667801.9856596916, 646082.2039190435 667809.8030068834, 646083.8011921084 667814.5010634075, 646093.6149641275 667824.3350703431, 646120.696716412 667841.2771113114, 646145.3982234645 667843.4849532889, 646174.3782819539 667850.5242862138, 646257.4070553904 667895.4316493997, 646263.9966152537 667898.9963863789, 646295.2796434358 667915.4514105868, 646297.7222240014 667916.7361642291, 646314.8069787051 667925.7220779083, 646332.3957981498 667934.9739681409, 646420.0449382651 667994.0617308879, 646434.9275103426 668012.9612010017, 646450.0702261372 668038.2974334473, 646453.6436153742 668044.2761904368, 646459.8713145194 668054.6956926696, 646461.2157984519 668066.9867599243, 646493.3865883477 668115.0100377516, 646495.8862054329 668118.7413325263, 646517.059376267 668132.8787167487, 646517.3330886149 668143.0626407887, 646522.3358672084 668153.504079348, 646522.827451734 668157.1530585171, 646523.302011634 668160.6761170166, 646533.8406662907 668179.5251588405, 646543.8478435033 668185.5641941758, 646562.1277179079 668192.9382705048, 646573.6020228256 668198.143567814, 646579.2054742723 668202.5801813334, 646612.0389516641 668229.3858377907, 646616.9250162566 668237.6491787649, 646620.6040348511 668244.9066315008, 646623.1668017021 668247.4857777733, 646628.2353474152 668258.5612613249, 646629.6356820294 668263.9994308089, 646630.4691003383 668269.3184951078, 646632.1543726441 668274.3275841909, 646638.790579322 668297.121009986, 646634.9341194512 668317.0722776705, 646643.0037457013 668335.9880068416, 646646.2059904075 668341.8923026648, 646655.983225897 668355.8492626232, 646659.0830541138 668366.122445843, 646666.1491244562 668380.4901204633, 646667.4491292544 668389.1970331204, 646667.9154659967 668394.6341327413, 646667.9053499344 668396.0800314897, 646672.2284732447 668410.938415779, 646674.8843684567 668419.8275660946, 646666.2445157872 668419.2195143728, 646665.3429733124 668431.094283153, 646665.1077214807 668441.8360263219, 646666.6417509494 668449.875135949, 646672.4250526163 668464.1266613335, 646673.6236967515 668467.0787619773, 646677.5134783486 668476.6630885667, 646690.7533556715 668500.8492776023, 646696.8755230654 668507.647026847, 646699.0801139422 668510.0941334711, 646719.8712761431 668533.1772311585, 646730.1778787771 668543.6864640965, 646738.4156541446 668553.394183761, 646754.1246858856 668576.3797236952, 646759.3615028096 668585.9882103852, 646764.931417191 668594.6948289868, 646775.0367829906 668607.7398442875, 646781.3740391273 668613.410435682, 646796.9843745829 668626.256105233, 646802.1877011004 668631.026735106, 646811.6934182091 668641.6357137578, 646819.297962845 668652.1115095209, 646821.0995765103 668655.0136028226, 646825.0013164789 668665.5219202304, 646828.101618964 668679.1990594104, 646828.93527527 668682.5682318382, 646831.4695537658 668687.3383863016, 646840.2088068094 668693.778372213, 646847.6472218242 668698.3491098871, 646852.4641070993 668698.965671516, 646854.6860015047 668699.2499305857, 646859.7899528542 668699.4505318914, 646872.933300348 668695.7161384778, 646882.2735265426 668693.716311193, 646890.2462029839 668695.4511510216, 646891.98816726 668695.5183962563, 646899.7870694965 668695.8193151243, 646903.7408794537 668697.0647260511, 646907.5257171965 668698.2561977247, 646917.3987999461 668704.962394776, 646921.234459781 668708.5987991927, 646924.9038874946 668713.0020535091, 646931.774532524 668723.010730442, 646938.5530003327 668740.7676111294, 646944.9803101707 668757.6049429819, 646955.7809537639 668817.2174116547, 646971.1523847909 668870.8585046174, 646978.3569209889 668882.2011246565, 646989.5531764665 668895.4133127283, 646995.8470387304 668904.7059329667, 647019.0251145046 668927.1272576512, 647028.4142571421 668941.7892928376, 647062.9203749917 668954.8720886471, 647098.7749242471 668965.2582815771, 647187.5224189047 668989.4875249431, 647222.1910379442 668998.9265523549, 647232.5903373437 669003.2877281986, 647243.6142830544 669011.7988430646, 647269.5554216205 669042.0565597365, 647286.4728645334 669062.2082206116, 647304.4791050853 669083.6571142772, 647321.2255099076 669111.998727962, 647321.5350648514 669123.7106098753, 647320.8423412556 669130.5704368898, 647319.0931191414 669147.8700003303, 647319.0110161783 669148.68101985, 647318.3392620859 669155.3378120038, 647318.5484274277 669157.6688460279, 647319.1131740728 669163.9608299127, 647319.3246356207 669164.9237223063, 647322.8162348894 669180.822006981, 647332.3979011461 669207.7167102944, 647361.9402471776 669244.3888583498, 647370.3288060518 669254.8906774096, 647378.7175423737 669265.3936166493, 647426.8749745603 669324.8084841642, 647425.0729258375 669341.5541300299, 647423.3427920075 669366.8395123648, 647423.0974073616 669370.4323960092, 647423.1614257487 669375.3003397519, 647423.2560489909 669382.4733768895, 647423.3005405089 669385.8962172751, 647427.882578343 669410.0505258836, 647433.2187892031 669416.8249924174, 647434.9036384771 669418.0331771978, 647437.0502991598 669419.5743726074, 647451.6344584889 669430.0420521671, 647460.1071572978 669436.1229815806, 647466.0586742549 669447.9614555228, 647465.8515295284 669458.2243366144, 647462.0868974401 669476.2622121893, 647460.8536281695 669482.1725479257, 647460.6476782308 669489.9044222157, 647460.9534928433 669491.4964378211, 647461.9248612305 669496.5614472872, 647470.1570534791 669502.6093890975, 647478.3903107882 669508.6571711414, 647488.0097186029 669521.5011897276, 647489.0693921044 669522.9163375399, 647497.3585769056 669546.3829392357, 647501.894952187 669559.2244324774, 647511.8460430193 669574.0463856194, 647514.835469709 669578.4996440193, 647527.8335325482 669585.7850990137, 647534.8372536354 669587.46687228, 647535.215284555 669587.8089929074, 647558.1576846241 669608.5304995569, 647566.601619538 669616.1573276438, 647570.3451762421 669619.6638086521, 647589.5148206248 669637.6188225292, 647611.8687824204 669654.206234721, 647625.8166850777 669678.6915825179, 647629.5509068407 669684.5279527634, 647636.5055886804 669695.3987224149, 647638.3641813451 669698.3029067595, 647648.2938262303 669708.4908405289, 647649.2326780889 669709.4539801744, 647656.3078015068 669716.5977726371, 647661.165171615 669721.5011693095, 647671.5861517197 669730.0754229814, 647678.9242019994 669736.79909255, 647684.0162830172 669744.155765898, 647684.4702452825 669744.8127716515, 647686.6746630389 669748.7078647766, 647690.2400271858 669755.0102894846, 647690.9115793486 669766.1043070504, 647690.2729147703 669779.4690189483, 647690.0684489291 669783.7529394473, 647691.7485539347 669790.4200944178, 647691.7439030921 669792.178951249, 647691.6711265186 669819.9957049474, 647691.632147739 669826.7264937608, 647695.5529043027 669837.0917890249, 647722.4616356139 669856.8542722976, 647728.6368017541 669861.3895128521, 647761.7215866469 669885.686119802, 647780.5432953888 669895.1912903093, 647799.1690511074 669904.597399488, 647816.5628635478 669913.3812894154, 647823.6694783762 669916.9700943446, 647831.959759857 669921.1571133825, 647842.8489806129 669928.2412068158, 647843.6877735154 669928.786420297, 647854.3935261397 669938.2426218732, 647871.3305911162 669953.2014198967, 647878.1699487393 669959.2421622201, 647889.2026342576 669968.8633613726, 647893.1344250025 669970.2007751756, 647896.3782588628 669971.3041895818, 647897.8001734846 669971.5373168644, 647905.6080132499 669972.8153554684, 647914.4248331875 669973.463282832, 647931.024551565 669974.684294745, 647937.0775621433 669975.12901381, 647969.2677951525 669980.0347259782, 647970.4916192662 669980.2828693801, 647977.1164215246 669981.6257256658, 647982.5072824938 669982.718357367, 647975.529987792 670010.3162152634, 647967.9524569291 670040.2918910643, 647960.3747485483 670070.2666064856, 647912.4349199198 670259.9045510605, 647911.7135450599 670262.9753057882, 647908.63695859 670276.0638915787, 647905.5314207756 670289.3083118652, 647902.4248180605 670302.5517719669, 647898.7188660648 670318.1780099946, 647897.9330294537 670321.4917938524, 647896.6058979789 670327.0877239255, 647895.0871464526 670333.4935407623, 647893.5802938228 670339.846239682, 647891.57530065 670348.2998122605, 647890.5637467296 670352.5666769443, 647889.0110759599 670359.1113678906, 647887.5012041089 670365.4781462083, 647886.075510014 670371.4898991028, 647884.6277947174 670377.5946081599, 647882.9529414031 670384.6583178156, 647881.8394383433 670389.4562034872, 647880.7268227938 670394.2539292601, 647880.3931286169 670395.678991181, 647879.4980679556 670399.5028353774, 647878.3396392724 670404.4515937219, 647875.5802244556 670416.2370333656, 647874.9497750391 670418.9298836084, 647868.645635387 670445.8619058738, 647866.9787617873 670452.9846555819, 647865.6079326769 670458.8404168984, 647863.3768501305 670468.3709084006, 647862.8316441042 670470.69977346, 647860.4215987369 670479.1374572427, 647860.4045513014 670479.1992144147, 647857.4663474637 670489.9028860528, 647855.8682614638 670497.6886755575, 647853.7870905952 670507.8313371789, 647852.1311027121 670515.9219158224, 647850.8533998245 670522.2738419042, 647849.8307403736 670527.356534688, 647848.2798784398 670535.0653707468, 647846.9210088672 670541.8240018853, 647845.7273147468 670547.7517833814, 647844.3483758105 670554.6084908387, 647842.6945017239 670562.8312279396, 647841.4146682122 670569.1881136054, 647840.3030275578 670574.7147899906, 647836.5928226729 670593.1592303523, 647836.004287412 670597.1000696723, 647835.41575215 670601.0409089883, 647831.6672826123 670626.1331003453, 647830.2306742085 670635.7499257178, 647829.2036863641 670642.8445922956, 647828.2489298935 670650.0264664436, 647827.2929310641 670657.2072204406, 647825.2812619455 670672.3427866077, 647821.3342367753 670672.2043936299, 647820.8099176459 670675.0071750206, 647818.5826720946 670676.376969756, 647806.9541713158 670673.6205660753, 647797.4575475834 670671.3693724441, 647696.3484929934 670659.9535191946, 647696.0612345639 670661.939539294, 647694.9393819317 670669.6892254407, 647691.7186076649 670691.9395939661, 647691.5525062716 670693.0845193671, 647690.95791531 670697.1933553754, 647667.2341484553 670861.0935257145, 647662.3762280989 670860.7137461611, 647652.9947919048 670859.9817469304, 647648.9013518512 670859.6622185793, 647614.4528761958 670856.9710689452, 647604.2043222112 670855.0799403561, 647594.177459831 670853.2256379909, 647586.8699482897 670850.8148699364, 647566.9947807366 670844.257523513, 647560.0071093633 670841.9517526748, 647554.3793794794 670840.0950996566, 647553.2175186812 670839.7119248405, 647553.2123714486 670839.7100042437, 647540.8961272427 670834.9634639904, 647532.1685636698 670831.6005376154, 647517.2295125966 670824.6337107222, 647490.8999422699 670790.5890642892, 647464.4400248707 670765.7711639852, 647456.8831010745 670757.7714790812, 647430.2646469057 670721.5927484321, 647429.3558927549 670719.813622291, 647425.3148558356 670711.9031592665, 647419.4438020103 670696.7647302091, 647418.0462614307 670693.1625290371, 647415.0022282216 670685.3133051699, 647410.3196871679 670673.2389793433, 647406.5928534663 670663.6306563823, 647406.2140128822 670662.6535035633, 647402.3611946264 670652.7211697331, 647400.549081563 670645.6860034706, 647399.2897928656 670640.7940757767, 647398.1442258222 670636.3528758906, 647396.5801381544 670630.2828469161, 647396.3031938995 670629.207787624, 647393.3456919063 670617.7266216528, 647392.2941552259 670613.6455884184, 647390.9458037363 670608.413334509, 647389.2924767184 670601.995299405, 647388.0541226573 670597.1872132119, 647387.382430541 670594.5822064255, 647383.2224203884 670578.3659177994, 647381.6341865741 670572.3477253795, 647379.6061367083 670564.6645418314, 647375.2842680567 670548.2813561226, 647375.2493187641 670548.1343138395, 647373.8499700405 670541.9012669655, 647372.0129702451 670533.7171529628, 647369.6063328676 670522.9880437357, 647353.2494700612 670498.2153629583, 647348.2413803773 670490.6268607599, 647333.9353442477 670460.0135430837, 647332.4546844747 670456.8193270789, 647331.9368762634 670455.7282391784, 647331.3459572133 670454.4843444995, 647324.9526846553 670440.9315945515, 647323.1224593301 670433.9505063165, 647321.526866881 670422.7795810966, 647321.5675714768 670417.9514891796, 647321.7523766874 670395.9218797041, 647321.7624645754 670394.7108567659, 647321.8384730151 670385.6749841555, 647316.5030233568 670390.1283658842, 647303.7897916548 670400.7377419375, 647286.2254199658 670412.1343654776, 647280.3951803169 670414.5056349321, 647275.4499939301 670416.5170152693, 647274.3579153009 670417.3818329012, 647269.2804245335 670421.4002109905, 647254.5471272157 670433.0603701454, 647248.4626794237 670437.1275866432, 647240.1128279499 670443.983454597, 647223.8944602988 670456.5752629664, 647220.0782006211 670458.3058222882, 647195.844256859 670451.2540469087, 647190.737707507 670447.5904424617, 647180.9505685389 670440.5693607649, 647171.5535378696 670433.8283222863, 647160.8745455628 670426.1661822788, 647158.9367828228 670424.7770081583, 647153.0292512852 670420.5393954458, 647146.3368887793 670415.7375360795, 647146.3289020545 670415.731615199, 647140.9394443694 670411.8649398122, 647136.7407219801 670408.8535959768, 647135.1505352536 670411.5493695277, 647129.589941897 670420.9724176672, 647125.2625108476 670428.306841347, 647120.9215874244 670435.6612630882, 647115.5575257889 670444.7524989946, 647115.55148985 670444.7604981642, 647102.0477716039 670467.6484950195, 647096.5716836808 670476.92771515, 647088.91013123 670489.9127038817, 647083.0629936457 670499.8208309693, 647076.7958065878 670510.4408981298, 647076.6978066372 670510.6090441392, 647060.1339160767 670518.2839141337, 647027.8583552269 670533.2377611315, 647022.6159839616 670535.6671795005, 647017.3287030488 670538.1164321953, 647005.0741964992 670543.7945654627, 646994.9647077578 670548.4785693961, 646989.6152986878 670550.955974157, 646984.1460707894 670553.4912837385, 646978.0488173774 670556.3085941409, 646969.5543060775 670560.2344019049, 646969.2614164224 670560.3694049413, 646954.8966856796 670567.0066316308, 646941.6633966832 670573.1209610297, 646935.0925494543 670576.1570914616, 646923.7270091099 670581.4086165, 646913.9775097198 670585.9144254875, 646905.5390912419 670589.8133599816, 646897.0246988281 670593.7482848192, 646890.454384154 670596.7834551308, 646883.5989901333 670599.9505894079, 646875.4976609302 670603.6945262883, 646868.7572926375 670606.8087150229, 646860.8679091618 670610.4537986012, 646853.7744755455 670613.7317826654, 646825.1470717657 670627.0342455469, 646808.8412755376 670634.6101050477, 646786.6930751641 670645.3202603277, 646763.7888621823 670656.396399502, 646757.7775069363 670659.3036380967, 646747.7821311164 670664.1374120169, 646743.5648632015 670666.1768778085, 646656.9752531388 670708.0499066025, 646656.9708154711 670708.0515060498, 646656.964247719 670708.0539052278, 646644.1576552942 670714.2473220443, 646644.1106151532 670714.2713160663, 646618.2892116907 670726.7579629247, 646591.7048727776 670739.6126725735, 646590.8157281338 670740.0434398274, 646590.4056815869 670740.2405078537, 646583.7643451109 670743.4526257685, 646560.2392026283 670754.8296426673, 646558.7071190634 670755.57040838, 646552.1147751375 670758.7586923558, 646534.0697025345 670767.4853637689, 646527.2208575143 670770.7974550948, 646521.6101405355 670773.5106634345, 646514.7095261429 670776.3304351966, 646509.0835950187 670778.6292473192, 646499.7360965034 670783.4040580752, 646499.6549744981 670783.4450077617, 646484.500236666 670791.1859595451, 646468.803205855 670799.2030021511, 646455.4555196654 670806.0203435526, 646442.3718138419 670812.5146809933, 646418.6694789756 670824.2796687841, 646391.7100188371 670837.661202129, 646380.1074536279 670843.4197272321, 646378.2093425262 670844.3625259276, 646376.5982646435 670845.162321119, 646374.4101028387 670846.248122951, 646369.4637955326 670848.7034941253, 646356.9811371245 670854.8998271262, 646350.3607359909 670858.186025387, 646344.3355248729 670861.1762993, 646336.3900139256 670865.1202490025, 646329.1946589473 670868.6913340547, 646320.282075222 670873.1151606871, 646320.2691169407 670873.1221990312, 646316.4549663218 670875.0156339821, 646313.9037972016 670876.281389551, 646313.8567570561 670876.3053835602, 646307.4875320942 670879.4664935551, 646283.0689380832 670893.1163307682, 646273.402652565 670896.4432835504, 646249.6107671506 670904.6311920203, 646248.1197137586 670905.145085773, 646243.2384668267 670906.824476092, 646239.4233228852 670908.1379195518, 646235.7592369383 670909.3985818798, 646232.3731259828 670910.5631189347, 646229.5040907637 670911.5517205455, 646218.5735686909 670915.6093910614, 646209.9412289733 670918.8132694828, 646207.1872089561 670919.8348444224, 646201.2659324373 670922.0341024712, 646194.7345647851 670924.4580840791, 646194.6766975297 670924.4795168284, 646194.4406133029 670924.5671672447, 646188.835476686 670926.648544879, 646188.5473830383 670926.7545887926, 646182.7721948016 670928.8988251043, 646170.1306832877 670933.5913209558, 646166.2685255196 670934.7137212905, 646159.4244582668 670936.7019912807, 646156.4822960469 670937.6696242961, 646153.2921578798 670938.719146376, 646151.9970739855 670939.1450648304, 646121.6330718675 670949.1342371218, 646118.2307190568 670952.4747416908, 646108.4385256664 670962.0865508784, 646107.8376214136 670962.6623106299, 646078.665274388 670990.6025744556, 646073.0046753125 670996.251735131, 646061.4421814494 671008.1591388332, 646056.2527016476 671013.5045203137, 646049.8019331287 671020.1486119715, 646036.6802696898 671033.6628454898, 646035.7802367662 671034.5897643153, 646029.9849412361 671037.1389519196, 646029.147811476 671037.5068461312, 646025.5016068483 671039.141424891, 646022.9515177889 671040.2860622844, 646008.106804116 671047.1241817725, 645987.162707192 671056.7725683615, 645971.2548924382 671064.1015928048, 645934.3011041959 671081.1258926842, 645923.5815280625 671086.0645485041, 645921.9125907704 671086.8332992293, 645919.5384111785 671087.927081514, 645912.8410058983 671091.0121616835, 645900.8427182374 671094.5906845193, 645892.0328232261 671101.1986638885, 645881.9915925678 671103.6875147521, 645875.4337667546 671103.3456196914, 645835.9943356304 671101.2900784158, 645829.214797844 671096.7422962238, 645824.4262766833 671093.530732, 645819.2698311567 671090.071127524, 645813.9832897667 671086.524628809, 645808.7946604256 671083.525974028, 645803.7160692514 671080.617411072, 645798.0074018689 671077.3487782701, 645792.4268796083 671074.1532796868, 645781.8888356736 671067.9250743301, 645756.8447416634 671061.4332277861, 645751.0481932256 671058.3225023705, 645747.1093915232 671056.2891026782, 645745.2555259463 671055.4058546897, 645742.2968657629 671053.4245708142, 645734.9694340143 671048.5188087048, 645733.264195381 671051.4774442085, 645731.84475754 671054.2092368945, 645726.9629305704 671062.082465341, 645716.3764392664 671058.667334668, 645714.0426707588 671057.348035771, 645714.0327315582 671057.3421146732, 645708.6549126543 671054.1504802293, 645705.506315147 671052.281172208, 645705.2342458747 671051.9861040214, 645702.2397084674 671048.7438736958, 645702.2237356674 671048.7269120349, 645702.2106010183 671048.7248305033, 645698.2838679203 671048.1474117315, 645696.5857683995 671047.8977733364, 645696.0208472153 671047.432112734, 645691.316365417 671043.553688076, 645677.087089829 671044.2959856386, 645669.8804183379 671044.6720834045, 645665.0516033671 671042.9225739295, 645654.9912469847 671039.2763091815, 645653.6563889442 671038.3832030743, 645648.3508563603 671034.8327023649, 645645.5240684568 671032.9283931468, 645645.4850219466 671032.9023088728, 645645.3339830731 671032.8003723518, 645639.5595974262 671028.4487072268, 645636.6618443688 671026.265353519, 645634.7282008713 671024.5981071555, 645630.4056779947 671020.8707658602, 645630.1596783185 671020.7606581656, 645624.6209576711 671018.2870745586, 645619.5363710722 671015.0625155636, 645614.2858362506 671011.731858344, 645608.4582980165 671008.0363376874, 645602.5288837042 671004.2745659025, 645596.4063656882 671000.3919728763, 645591.3828341211 670997.2053406838, 645586.428166763 670994.0619160713, 645581.310745552 670990.8173535491, 645575.9851347575 670987.4396481636, 645571.0755485807 670984.3251885273, 645565.9950443405 670981.1036700924, 645560.8884500393 670977.8640688168, 645560.2715127629 670977.4720010201, 645556.3098731928 670974.9595655815, 645554.7158815865 670973.9493103746, 645549.5674008346 670970.6837845057, 645542.8730273414 670966.4350487764, 645539.3223518222 670963.4796284938, 645539.1074228995 670963.3007254743, 645532.211061986 670957.5599868817, 645527.4055991384 670953.5604322256, 645522.6562202392 670949.6069635919, 645517.9036467656 670945.6504546327, 645513.1862143812 670941.723869433, 645508.5007285814 670937.8233676704, 645505.1720813076 670935.0530912065, 645503.4822195669 670934.2129022189, 645503.7593360739 670933.8793398899, 645495.9152278474 670927.3629797172, 645491.510275242 670918.4425824702, 645490.5465197716 670916.4924155375, 645481.1396258499 670909.2434000263, 645467.7308708966 670898.9110725488, 645467.011004506 670898.3559548458, 645460.2912106147 670888.6524136276, 645452.4928013819 670884.3595475124, 645451.3657670519 670883.6903030332, 645448.6820054173 670882.0970466149, 645437.9068474497 670875.6979365627, 645432.6141297636 670874.7573207789, 645428.3111487076 670873.7848230995, 645422.2074674777 670872.617074151, 645416.0395084736 670871.6382748871, 645409.9707692558 670870.6756471886, 645403.9238620307 670869.7159020475, 645398.2899861248 670868.8222850647, 645393.1852236093 670868.0116098018, 645385.8083445916 670866.8417895647, 645385.0813244878 670866.7237848255, 645381.0635422777 670866.0892362682, 645380.4774530588 670865.9962079246, 645379.7985365972 670865.8660490741, 645367.2329980728 670863.399630522, 645365.4679937278 670863.053505644, 645364.2351176596 670862.8112825418, 645362.6637471605 670865.488017755, 645361.1234453085 670868.1129173952, 645359.7651178072 670870.4276030692, 645357.8147080349 670873.7503640509, 645353.3738241598 670881.3158087396, 645343.2118913147 670898.2482803045, 645340.9751827552 670902.9580275202, 645338.559239195 670903.0297393156, 645328.0903278118 670903.3414504367, 645311.8104339413 670903.8985473084, 645310.6054115812 670903.766405942, 645304.9344327187 670904.359643395, 645303.1272952606 670904.5483858975, 645296.9722712894 670905.191645095, 645294.0353399904 670905.4992916058, 645290.8764030215 670905.4400369392, 645285.4664327062 670905.3393952796, 645279.1423462372 670905.2215252027, 645263.1566732442 670904.9217092295, 645255.8606061507 670904.7858038734, 645249.8336517488 670904.6740490334, 645243.9857932604 670904.5643954129, 645237.9448165589 670904.451678841, 645232.2089594995 670904.3439584712, 645228.9039413568 670904.2826064305, 645224.9597149692 670905.9491538026, 645218.1634685722 670908.8203049641, 645214.9498384324 670913.2728195254, 645211.1543108594 670918.1852583472, 645209.9861227011 670920.1871709613, 645208.6116784947 670923.6499982527, 645197.445051232 670945.305322267, 645191.4593158019 670952.0734741961, 645188.1160034493 670954.5220018134, 645184.6466556488 670957.0626731262, 645182.6755320971 670958.5064181968, 645177.8220912206 670962.1477092573, 645168.3224107525 670968.9405629628, 645167.7953705215 670969.3184149141, 645161.9698812154 670973.4906228003, 645153.970553947 670976.3147514135, 645143.6034992866 670978.2843706659, 645140.056863815 670978.5589850438, 645141.0025207503 670968.9373146581, 645141.6935035791 670961.8984571618, 645142.5485223691 670953.199642831, 645144.8644134243 670929.6343341079, 645145.224833155 670925.9653092587, 645145.8226882446 670919.8865869894, 645147.3304783367 670904.547944886, 645148.9354829758 670888.2133285218, 645148.895592739 670887.8323692104, 645146.8611074382 670874.8372725846, 645146.3966040458 670871.8713396424, 645147.1247949937 670853.8548830106, 645146.7478164164 670844.9847246327, 645146.7009152604 670843.8816947815, 645145.992974682 670827.2208881095, 645145.9212949643 670825.5399435521, 645145.5529947361 670819.7039829593, 645145.5439595643 670819.5639838793, 645145.7867380232 670812.6881107448, 645146.0467916903 670805.3431263472, 645146.3166464656 670797.684387586, 645146.5645885344 670790.6774769267, 645146.5845396585 670790.0994075355, 645146.5946919599 670789.8165327762, 645147.5881397349 670776.9597140991, 645141.0952446793 670776.9059831863, 645135.4543424033 670776.8593934281, 645125.1294323888 670776.7730475502, 645108.8686175388 670776.6381568846, 645101.5636669109 670776.5772895637, 645080.1944511728 670776.3525130561, 645080.112979379 670776.3517033856, 645058.9580042757 670776.12919231, 645057.6180663932 670776.1320730222, 645051.2630793117 670776.1473175207, 645038.18018043 670776.1777621984, 645029.0842579206 670776.199720848, 645023.4902079211 670776.2129758112, 645014.0632502622 670776.2358551016, 645008.773255594 670776.2483462179, 645008.1633700237 670776.2492337202, 645003.5373980417 670776.2606837659, 645002.4404558472 670776.263593355, 644981.752446352 670776.3120139244, 644976.2964903031 670776.3254452982, 644960.0287316862 670776.363511361, 644956.4636696675 670776.3720475377, 644954.6176828222 670776.3767880782, 644945.400706216 670776.3987323432, 644937.7450769737 670775.8618301682, 644930.1698541149 670778.2048963371, 644926.0453915192 670780.4794135969, 644915.8092412682 670783.1840783275, 644914.2301614223 670783.6019646694, 644900.8557320227 670787.1363245159, 644887.6475646738 670789.6707183754, 644869.6702494812 670793.1205322128, 644844.1508363502 670798.0174288356, 644841.6049123598 670798.5060792255, 644835.4507095417 670799.6874108055, 644835.062688173 670799.7622436134, 644817.529485005 670803.1261913911, 644812.3025982942 670802.3606210973, 644805.6807749033 670801.3888079658, 644794.6670034355 670799.7745620238, 644782.4443103593 670797.9830549442, 644774.4944657225 670796.8181267518, 644767.7673040477 670790.7614140847, 644765.1785712565 670788.6331369341, 644756.7120045692 670777.6042889212, 644756.1701263129 670777.4002274426, 644755.932111697 670777.3102804435, 644751.961296887 670775.8077500652, 644747.1000461855 670786.6239770253, 644746.2588260748 670788.4958501708, 644745.9937473784 670789.087810165, 644745.2585280882 670790.6836998714, 644741.0474312356 670800.2557019372, 644733.3382880875 670817.6519758559, 644730.7645490207 670823.4615065153, 644727.0106472254 670831.9319786762, 644725.1600622204 670836.1076987373, 644724.6080387069 670837.5645721982, 644723.9567604167 670839.2884700266, 644723.8017535105 670839.6974057297, 644721.9340340507 670844.6380727639, 644720.0886001108 670857.3997501186, 644718.1399783355 670870.6553281023, 644717.534362647 670874.8831953865, 644716.6857192069 670880.8060494357, 644716.5466185834 670881.7698590569, 644715.6427254321 670888.0778210033, 644715.2174274115 670891.0396479032, 644708.3214586595 670939.5880507134, 644707.1954833847 670947.583801981, 644707.1353942355 670948.3489038466, 644704.4319024305 670968.5222126042, 644703.4149069773 670976.553016311, 644701.7089623488 670992.3825060511, 644701.0951088209 671000.0704826056, 644700.0241504534 671012.434817614, 644699.9385958363 671012.4367274148, 644697.3824301432 671012.4908226877, 644685.3304898866 671012.1265148416, 644642.00303793 671010.8162616899, 644623.1992492774 671010.2481139458, 644620.7317818224 670990.6260228321, 644616.7496092055 670958.9690447878, 644604.7828018671 670957.711640082, 644595.0341671682 670956.6864957717, 644590.0253271584 670955.2840004325, 644588.4942762834 670954.8558245619, 644557.3241833455 670949.7441922275, 644556.5092829742 670949.7749748986, 644537.9872052604 670950.4847625393, 644486.625708439 670958.6055790228, 644482.1896262936 670958.8850475738, 644492.317761268 670932.5995907062, 644499.8012033912 670913.1758003848, 644526.2515032928 670844.5279338994, 644534.7630603989 670823.3182111401, 644537.3897799769 670816.821656925, 644538.814269218 670813.4498748028, 644553.8332574156 670779.5361483163, 644557.4293164053 670771.449652156, 644558.2824347686 670769.5328211497, 644559.6788503078 670766.3939923106, 644564.5105341314 670753.5467515201, 644566.9938175591 670750.0562169052, 644569.3363190637 670746.7714225902, 644576.9205497324 670736.1865563035, 644583.580036771 670731.1413403497, 644586.6288063359 670724.9798713466, 644588.8701694846 670721.4041891692, 644587.8715989143 670718.2361160952, 644585.8311639917 670714.6408853518, 644587.1615363912 670711.611167057, 644589.6852138748 670705.8614697064, 644592.450801709 670700.2759587342, 644596.9659311366 670690.8935902918, 644598.1121572198 670688.511680776, 644598.7683608939 670687.1488983622, 644600.4898304853 670683.5720343918, 644604.0136806646 670676.6475927604, 644618.1452393471 670657.9746605913, 644637.1393575516 670656.6514965914, 644661.8409944132 670654.93077649, 644662.5928975093 670638.9901349567, 644663.0520481135 670629.2533694627, 644664.3233791807 670602.7238218213, 644664.6971996282 670595.4799703497, 644664.7964701599 670593.6500884423, 644666.5724862219 670560.9227698052, 644666.9854022256 670553.3078882986, 644663.1162580454 670546.362568463, 644599.5346199459 670536.5207566449, 644591.7003347301 670535.3080034137, 644574.6202116064 670537.8609776251, 644564.0824456918 670536.6387833696, 644556.7164892461 670536.1548752927, 644545.7727215948 670535.4366255624, 644544.1428047755 670535.0063581319, 644539.4827590572 670534.9758850185, 644534.9957314084 670535.2935874676, 644534.4497447822 670535.299922522, 644530.1127372504 670535.3448467212, 644525.0125805404 670536.0562306694, 644520.1627011143 670536.7337248344, 644513.953459717 670538.0358485283, 644508.2023541773 670539.2421880192, 644495.8962694061 670541.5117335622, 644492.2345479847 670539.4663680961, 644483.8052738945 670541.2293414333, 644471.5571948546 670543.7918496268, 644460.923862941 670546.015514495, 644461.0381034098 670545.136500743, 644458.6120135116 670545.446288095, 644446.9047345116 670548.3178160399, 644430.6714305668 670551.08288788, 644430.236546873 670551.1797948248, 644413.9092362399 670554.8317226738, 644407.7271422705 670556.2120884045, 644394.543903227 670558.1234548193, 644394.3589462861 670558.1504724575, 644387.1008969963 670559.19551521, 644379.9067512924 670560.2076060215, 644366.4515795279 670562.1570195613, 644362.4926431018 670562.2095085308, 644356.4558302144 670561.4528023709, 644353.5628245764 670561.0905439554, 644350.6179902066 670560.7212394845, 644335.661311666 670558.7414114499, 644333.2013826784 670558.5411221398, 644332.9503962391 670558.5681319349, 644321.8275399696 670557.7877819776, 644320.0815008738 670557.6656563401, 644303.6160739525 670554.3336686777, 644231.9543894343 670539.8303264362, 644227.823771771 670537.3429918261, 644221.55195558 670536.5481783734, 644215.7370137083 670535.8114983523, 644198.155516158 670533.5854422972, 644197.9334687059 670533.5583763152, 644188.0957549576 670532.3642678284, 644184.2478101825 670531.9847813258, 644172.7849140891 670530.8554511856, 644171.5900082537 670530.737392537, 644157.3510841067 670530.6376413892, 644151.6270627982 670531.0160035298, 644145.4681664871 670531.4223141802, 644135.4639805843 670532.9390370538, 644129.120962783 670533.7923596061, 644128.7159041894 670533.8473512278, 644123.1508712459 670534.6316461663, 644122.4058966732 670534.7365171369, 644117.4859190002 670535.3159308166, 644117.1799061039 670535.3589342482, 644108.5258129487 670536.5649402204, 644105.0037231849 670537.8774674856, 644098.8564856686 670540.1666322404, 644092.8301302311 670542.4110118494, 644075.1200698317 670540.6695967636, 644079.1011029814 670517.0142469817, 644079.4490791684 670514.9471177945, 644079.7384794136 670512.8913415619, 644079.834990408 670511.8494479879, 644079.8644412798 670510.5307505459, 644079.8300233312 670508.9648491805, 644079.7370923273 670506.9034281191, 644079.5496547251 670504.0348076461, 644079.3988319698 670502.1927366073, 644077.5658195843 670479.177334596, 644076.4771538593 670465.4102060809, 644075.7898928416 670456.71761123, 644075.3964630779 670451.7411572104, 644074.1005712745 670435.7660379328, 644072.4522125105 670415.1248652866, 644071.7215560226 670408.5408754619, 644069.9947971759 670397.4973136489, 644069.289755112 670393.3395314518, 644068.9557615854 670391.4310400465, 644068.4401040126 670388.7593384275, 644067.72991415 670386.0380146782, 644067.1452409249 670384.5472477346, 644066.3245108273 670382.925414968, 644065.2960760872 670381.5565141138, 644064.2235917716 670380.4293645592, 644062.9105997887 670379.2788270912, 644060.9124204048 670378.4749240767, 644058.3243701931 670378.0189466955, 644053.7463808752 670377.5841752364, 644051.5881757598 670377.5840817043, 644048.3367493332 670377.6454588483, 644042.7410911486 670377.7724796471, 644021.4042246136 670378.7277191791, 644015.8330428998 670379.0027406978, 644003.6174975608 670379.7590546546, 644003.7462749297 670377.591741382, 644007.7888832405 670309.594007654, 644015.860403402 670173.8260552152, 643998.019272945 670168.4238779736, 643981.4350812047 670161.0122776888, 644010.3132302141 670030.4004543303, 643953.6686021271 670020.787859524, 643961.4853238328 669977.4652493167, 643861.758042342 669963.8276514332, 643870.1037930365 669905.52212157, 643941.8542522677 669913.700032254, 643948.5535036738 669860.9409648482, 643951.0040914055 669821.5204054109, 643948.6737712873 669821.04749876, 643955.2937627892 669718.255581737, 643955.2366089139 669718.2502951021, 643950.7790225755 669718.7881634164, 643946.9579666491 669719.1571490378, 643944.1286515285 669719.0649780846, 643941.6509820127 669718.8013310138, 643935.0594159277 669718.6464790798, 643931.9814047752 669718.7222763897, 643925.6161603519 669718.4773723957, 643919.8615432766 669717.9680641013, 643916.680496555 669717.3014604751, 643911.2277936669 669716.880186324, 643900.9541247935 669717.3242528761, 643893.4596510235 669714.053181765, 643884.1194916754 669715.578941703, 643876.0468815314 669716.8416544856, 643866.9385365348 669718.3568818052, 643858.0909757813 669719.5548645263, 643819.0049100713 669721.0712342068, 643812.2075953514 669721.1576433271, 643797.6136612874 669721.234346935, 643787.884015136 669721.7821684954, 643777.2131854588 669723.0357901065, 643765.4434531059 669724.3676828913, 643751.7905143531 669726.7187015933, 643742.6103264444 669727.8940975415, 643732.5669442106 669729.2261908624, 643727.3884262505 669729.2255893367, 643723.5725623426 669729.2392258828, 643719.2230163693 669728.7977672816, 643716.2606441757 669728.1673526592, 643710.5247198507 669725.7724824063, 643704.5995434986 669723.6926254309, 643701.006735402 669722.7471023157, 643694.7662521813 669721.2342401844, 643688.2108480104 669720.0363767314, 643675.3518249452 669718.3337886449, 643662.6817836248 669717.0723358751, 643659.7192339766 669716.4417612327, 643654.8657238545 669714.929060207, 643650.2948812122 669714.1299812772, 643647.5453056584 669713.695588424, 643643.6744411659 669713.5143415105, 643642.4805642479 669713.6950001139, 643640.5628884409 669714.9966777758, 643639.4774542114 669715.2496678882, 643638.4282873727 669715.0325892832, 643637.1983028546 669714.3815762023, 643635.7875671892 669714.2006150531, 643634.7381562722 669714.5259282592, 643633.6138945587 669714.4149193389, 643632.3933053233 669713.8627858596, 643629.5158740131 669713.891571187, 643625.346293243 669713.5990912487, 643621.0068061131 669713.6350666345, 643614.9737217972 669714.3572349737, 643609.9668142452 669716.0521478789, 643605.4687431114 669718.1682335599, 643600.3591152569 669719.4450608226, 643592.909515064 669719.3569968089, 643587.726689815 669718.2962507323, 643582.3655987304 669719.1485751731, 643576.9518761445 669720.7498422374, 643575.837819592 669721.3698635045, 643569.1228576933 669725.84613618, 643567.6922707574 669726.8782744793, 643564.7231085267 669729.5068500509, 643557.9465919517 669736.5704366652, 643555.1746227279 669739.2623941917, 643553.5878324484 669740.332593776, 643552.2455133179 669740.9487485897, 643549.6344085352 669741.7810727705, 643547.8618398994 669742.3418184357, 643543.2546210503 669744.3444531548, 643539.5320109191 669746.702865274, 643536.3152281368 669750.8595491034, 643533.0642477451 669753.0852180078, 643531.0998374517 669753.3673855813, 643526.6485852589 669754.3681334843, 643523.6436368384 669756.4383133029, 643520.0127617451 669761.9454887267, 643516.6499502289 669765.6741620374, 643511.2681819162 669768.6829316607, 643507.5600117662 669767.6401166096, 643504.8486908627 669767.8978777957, 643501.7499652744 669768.9944613676, 643500.0715270404 669770.5427231223, 643497.1017351337 669773.9618467422, 643495.3585448267 669776.6715235105, 643494.0670722828 669779.5103708047, 643492.9048424009 669782.1556360245, 643491.6134807842 669784.0911369176, 643489.935105921 669785.1232464432, 643487.2237929383 669785.3163686038, 643484.9645368631 669785.7677793874, 643482.7696889777 669786.4127947183, 643480.1228032222 669788.0899020208, 643476.4428557213 669791.3801450562, 643473.344204628 669793.3154359936, 643470.8263285435 669795.2507943955, 643468.3732394004 669797.1861603238, 643458.5261971633 669806.9709492774, 643446.5571607541 669815.3089534773, 643443.7318700576 669817.1402777296, 643439.2504123858 669818.3907783283, 643435.3377242818 669819.1165528863, 643432.6135774095 669821.1946051792, 643430.4390883156 669823.7205545842, 643428.5949153558 669826.3159813182, 643425.6973924073 669830.9592548847, 643423.8603636376 669831.7491096038, 643420.1625545342 669831.6130020871, 643416.1155690822 669831.7768494971, 643413.3084063567 669833.4196986872, 643410.6899281965 669836.4777485136, 643409.3434567138 669837.6625342787, 643407.1867343951 669838.6053095575, 643379.5207125596 669825.9560459413, 643370.0527837779 669821.4775734487, 643358.2216959208 669816.5460332544, 643355.0106329209 669814.596409563, 643354.616289676 669814.1211709031, 643354.3453045313 669813.6731461633, 643353.7764077388 669810.898401813, 643353.0995497514 669805.5764832374, 643352.4255683143 669804.291944266, 643351.0341318288 669803.0578811932, 643350.0705745578 669802.3905792986, 643347.7820144756 669801.5333663395, 643347.0159515139 669801.366719856, 643344.4835903541 669801.2883468604, 643342.9361462824 669801.3888055905, 643341.4772556134 669801.6385523613, 643338.5239320106 669802.4845965615, 643335.2752167855 669803.6086822598, 643333.6437435889 669804.2980823687, 643331.4962939188 669804.8930239609, 643329.1999712574 669805.0953141645, 643321.9614582005 669804.6688797127, 643319.2227538736 669805.3226317337, 643316.1732586785 669807.3483270166, 643314.0274492006 669809.04053229, 643298.9311123153 669811.2286656986, 643294.4895862769 669809.6340937341, 643291.2602652345 669808.9318091621, 643287.1822761397 669808.6188601505, 643280.5612162225 669808.7139295788, 643271.5148563978 669811.3787586145, 643271.2574504812 669810.2035463947, 643255.2979332107 669737.3053491643, 643248.5949492231 669738.1930371692, 643244.6008842185 669726.5885877889, 643239.9869328862 669715.4861388474, 643235.0177728676 669706.1073027172, 643226.5383615196 669694.1770572158, 643218.1050625574 669683.9947907798, 643205.651346745 669668.6562949044, 643193.0658798346 669653.702897943, 643185.633348512 669644.7120898475, 643179.4967794617 669636.7922961791, 643176.4450934745 669630.645794172, 643175.1146748351 669626.8621765586, 643174.765905643 669625.2930396517, 643174.5069766929 669620.6229998394, 643174.5572243728 669619.0513493201, 643175.7860906598 669610.0226679047, 643177.0014523789 669598.2242665906, 643178.2414375573 669591.1569969538, 643179.7830244331 669585.275504511, 643180.7166873916 669579.3261024732, 643180.9487399918 669577.3760787625, 643181.4903142788 669572.8269301159, 643181.6793187348 669568.7487734255, 643181.946132237 669562.6992954295, 643178.936156449 669543.5675536306, 643178.7634079222 669542.4689900922, 643177.5714775276 669536.919975123, 643147.0730193855 669539.4952737667, 643129.366834115 669540.9651949722, 643126.0809496548 669541.2323292936, 643114.1809695574 669542.2725314503, 643090.5892172499 669544.3350402139, 643087.3993577643 669544.6159454838, 643039.8269638789 669548.6601190711, 643019.2028720889 669550.2044203411, 643010.0007369843 669550.8718214688, 643001.4810894553 669551.4906625985, 642992.1438813964 669552.1305284536, 642989.4352344783 669552.2956513595, 642987.4374777839 669552.38869792, 642985.226371159 669552.4554800945, 642984.0639316434 669552.4721448277, 642981.4014528512 669552.4865553618, 642976.1180342542 669552.4800217915, 642970.8317804806 669552.4344484806, 642970.4842391538 669552.428968196, 642960.2750797766 669552.2661848505, 642957.2325907926 669552.1664729656, 642951.2743611454 669551.849785682, 642947.1480836817 669551.6104300155, 642945.0564594258 669551.4665092414, 642938.3706886272 669550.9459005239, 642935.0073056469 669550.657194213, 642930.2457877292 669550.2017680153, 642927.8421361003 669549.9363728234, 642922.1647529695 669549.2622835388, 642919.3060067038 669548.9038768897, 642915.029962237 669548.3253089321, 642909.0252981584 669547.4379048462, 642905.0261562763 669546.8188896602, 642897.0534376644 669545.5371829169, 642894.3656356233 669545.0714377379, 642889.0936981009 669544.0793203319, 642874.7512699955 669541.3496955342, 642847.3564530101 669536.0505933762, 642843.7925452515 669535.367629704, 642816.0279990209 669530.0686446038, 642784.9480535917 669524.0003260002, 642762.9779909427 669519.7707975557, 642712.9432511681 669510.0900909167, 642700.6856807106 669507.6821432458, 642697.8436434892 669507.127421448, 642692.1530012119 669506.0211770394, 642687.8863945188 669505.1963738201, 642659.3577406534 669499.7113823217, 642658.3498443863 669494.4260647561, 642652.1481424029 669461.9072735748, 642589.7849869406 669465.8913292628, 642584.8305500089 669443.6136488912, 642584.1445429482 669440.5283356186, 642580.3985828996 669440.7923764993, 642573.1768108928 669441.357609085, 642566.7802350938 669441.8752582467, 642563.5355296736 669442.1476772294, 642558.4472906982 669442.7006778407, 642554.7873884889 669443.1997651833, 642552.3563211724 669443.5929168669, 642550.5150910228 669443.9066182604, 642547.7600770877 669444.3966908571, 642545.1651563719 669444.9712607749, 642543.8033493283 669445.286937834, 642541.7721720098 669445.7942142542, 642538.8083575116 669446.7912949654, 642534.5947187396 669448.2565434495, 642530.9710797915 669449.76297985, 642529.7018967306 669450.3242639748, 642527.8684207307 669451.0913994482, 642526.6452044565 669451.6958882608, 642523.0775353924 669449.8460616585, 642471.7262456066 669423.2198570775, 642360.9048465863 669446.776549337, 642359.2601028833 669447.1259530239, 642357.8176430564 669447.9984123375, 642252.362446141 669512.8163872914, 642225.8376596748 669489.3306194489, 642139.5240497926 669441.8694671255, 642084.7749208953 669397.8728892566, 642082.8087875778 669396.292844639, 642081.9500409791 669394.9250854609, 642080.6956974193 669393.1748860846, 642077.2088069945 669388.3087942444, 642070.3020996468 669378.6710382458, 642061.5773995129 669366.5380812221, 642050.946866313 669351.755189028, 642050.8070200424 669351.5703760867, 642041.9296198033 669339.8508339887, 642039.0356917479 669335.4299347714, 642037.5374904525 669333.4539558802, 642016.306041167 669304.0852854523, 641993.8902488826 669276.731487358, 641964.9198437321 669243.1013579482, 641964.0760937877 669241.015533358, 641963.3052736823 669239.1097143437, 641962.8680634266 669227.8227900473, 641963.9654153165 669218.7276867585, 641972.3961027702 669193.9848401846, 641974.0692134497 669192.4844110397, 641981.6607868219 669185.6759280168, 641989.0768602806 669178.4911110954, 641995.1315322263 669172.6338012209, 641997.2864149468 669170.8059115885, 641997.8129242453 669170.4166968052, 641996.6245176459 669169.3992975531, 641957.7776776341 669136.1417944644, 641954.9188284405 669133.6943068444, 641953.7421468808 669131.1521308301, 641951.9901732361 669127.3182285412, 641950.2944683725 669123.4707327667, 641948.6605326256 669119.626923883, 641947.4913594207 669116.6506749153, 641946.8435525604 669114.690790136, 641946.3377571715 669112.6298028915, 641945.9757705325 669110.2865960243, 641945.8287430735 669107.8771744479, 641945.8508995046 669105.243935281, 641945.9661822275 669103.1028594945, 641946.2165222486 669101.118436721, 641946.5822215155 669099.2556252447, 641947.1605871117 669097.2077205488, 641947.5842757298 669095.7735494102, 641948.1649172468 669093.9821212536, 641949.0015510833 669091.8732867222, 641950.1172800646 669089.2196917534, 641951.303308198 669086.4878659105, 641952.6334639126 669083.7677362472, 641953.4323930286 669082.2681686253, 641954.2512008088 669080.7783631242, 641955.9367212747 669077.959395231, 641957.6760268126 669075.1165938157, 641960.3711713367 669070.7667227812, 641962.8009035297 669066.8364956136, 641964.9089399466 669063.4585439725, 641967.7709632805 669058.8656556503, 641968.9443032016 669056.8312584732, 641969.6668963614 669055.4251209968, 641970.4919438206 669053.714359613, 641971.5330863984 669051.3610721565, 641972.6882796815 669048.2239691407, 641973.7205222565 669044.6322988691, 641974.4371095746 669041.5476253787, 641977.1039163333 669038.1364579684, 642001.227493393 669007.2806105044, 642027.6217310178 668894.91615791, 642027.705554322 668894.5598527571, 642033.3090111443 668891.7011740685, 642033.2812962023 668890.4635893265, 642030.6390420704 668772.2989654125, 641992.4022530805 668769.360872407, 641989.277248687 668717.1227238971, 641989.0565392465 668713.4334729082, 641988.9336081048 668711.3776490832, 641989.3239990096 668710.8009023455, 642000.0303676905 668694.9828437492, 642022.0854175028 668675.3392660872, 642021.3588840094 668672.7056217664, 642003.1446248867 668606.6939457371, 641973.4844177023 668515.6307480981, 641936.8149712681 668434.5380939394, 641896.3416148045 668371.6231147542, 641894.9880899106 668369.5191480393, 641855.3635456057 668307.9232689538, 641716.07279028 668204.9308950941, 641700.5311112693 668216.8501989221, 641692.139639204 668222.8705782915, 641535.0948248436 668037.1121038743, 641534.4447392058 668036.3521973158, 641532.9649696553 668034.581966317, 641524.1063887133 668024.1311399874, 641508.7887002284 668005.7794943741, 641499.720004174 667994.968645548, 641498.5502786825 667993.5784456232, 641463.590564473 667951.9329356612, 641420.4742922804 667905.4733888734, 641378.6667216648 667860.4240411149, 641304.8242400911 667792.6551497779, 641227.3492846139 667721.5526987425, 641212.0319489728 667707.4951977122, 641198.462072438 667695.0415645163, 641192.7032296682 667689.7562988449)))" + } +}, +{ + "model": "common.municipality", + "pk": 15, + "fields": { + "fid": "", + "name_fr": "Saint-Josse-ten-Noode", + "name_nl": "Sint-Joost-ten-Node", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((649061.8947996686 671751.5820203606, 649428.1832141011 671592.6667956909, 649465.9521901426 671575.65909051, 649488.5746906912 671565.5070434017, 649519.1504704928 671551.6794171431, 649606.9626223587 671511.9767394809, 649631.3485022287 671500.9562269158, 649662.0007918994 671487.048107571, 649693.2644077936 671472.8896644461, 649714.0431125108 671463.5015935696, 649746.7221901709 671448.7557274525, 649763.2374881081 671441.297716322, 649763.6605928872 671441.0978061773, 649764.0679346257 671440.9448608562, 649764.5802264647 671440.8024366961, 649765.1205651942 671440.7081161919, 649765.5781605818 671440.6709312762, 649765.9701824406 671440.6695182531, 649766.3519579378 671440.6951009809, 649766.7311990465 671440.7472240404, 649767.1614147102 671440.839589794, 649767.4657583965 671440.9272267058, 649767.7546453248 671441.0284614469, 649768.0753984492 671441.1627797493, 649768.215063378 671441.2289223524, 649768.4992265593 671441.3788892458, 649768.8027953382 671441.5638422176, 649769.0870227694 671441.7629801991, 649769.4479836838 671442.057978152, 649769.7121096551 671442.3094236684, 649966.0427658616 671354.0655809396, 649973.3642830417 671350.7748128776, 649989.1963013329 671255.0895550629, 649991.6982557236 671239.7144228624, 650008.4823515968 671141.2496175843, 650010.8406359538 671127.2781525266, 650013.8886801739 671108.8682743781, 650018.5393537213 671080.9051742213, 650052.0193502823 670876.3784179729, 650054.0582332376 670864.0757956102, 650066.5522985003 670788.6865866026, 650067.9868422486 670780.030547065, 650059.667923319 670754.997451603, 650072.2114957626 670750.0224328967, 650074.1345853842 670749.1826754913, 650077.6807399514 670747.7221218969, 650311.922815275 670765.404102696, 650316.4520458841 670765.7459527964, 650325.3204796845 670778.1890085149, 650326.4823037888 670779.5781362212, 650329.7188138504 670783.4474919823, 650333.6012262878 670788.0879187807, 650335.3109675879 670790.1311067445, 650335.3289649251 670790.1521087295, 650337.1016966035 670792.2713036187, 650337.1806844587 670792.3673122843, 650337.2796696371 670792.4843231806, 650341.8009852561 670797.8898202657, 650346.2063185024 670803.1563046383, 650346.2873061006 670803.2543135268, 650347.2651582127 670804.4224210606, 650348.6659462566 670806.0965750931, 650350.6266496106 670808.4397906993, 650354.5190605143 670813.0932187376, 650356.2657962904 670815.1804108389, 650358.2165010556 670817.5126253673, 650358.3934743422 670817.7236448351, 650360.0982162122 670819.7628323083, 650360.8878524454 670822.7178984702, 650360.9878062771 670823.092906838, 650362.5510856427 670828.9470377874, 650363.663572939 670833.1120310025, 650456.6776936551 670802.7258157269, 650456.7606964402 670802.6987261563, 650489.1397151281 670896.4659298036, 650493.0988330755 670898.5911925037, 650493.5218029434 670898.8182419394, 650499.1588912777 670901.9273999258, 650507.6667699933 670906.6199930469, 650507.4066798055 670907.3759535386, 650516.0580752477 670911.923965759, 650521.7106803723 670914.8946271611, 650527.3652852647 670917.8672888131, 650533.3608662281 670921.0189902522, 650538.841483208 670923.8996314341, 650544.9790541884 670927.1263494915, 650550.5786628207 670930.0700046448, 650556.6702371535 670933.27171739, 650562.4678319859 670936.3193957574, 650569.5473373386 670940.0402241672, 650574.6659796733 670942.7308231395, 650577.5987747789 670944.2721663477, 650579.8416181494 670945.4504288195, 650579.9636095186 670945.5154430874, 650585.1092500096 670948.2200452689, 650588.3950204081 670949.9474297989, 650593.5216622468 670952.6420297907, 650598.9442833695 670955.492664437, 650606.249780323 670959.2724201027, 650611.191958935 670949.3112253639, 650613.7915508446 670944.3025939718, 650616.5691820739 670938.960887697, 650621.7323834042 670928.8006218565, 650628.4270139666 670931.5094135102, 650636.4265725203 670934.7463595058, 650647.207977513 670939.1096345278, 650653.4276343264 670941.6263701136, 650656.4174694051 670942.8358237222, 650659.0913229253 670943.9091400504, 650666.7039058825 670946.9650406721, 650675.3884301677 670950.4510681534, 650675.6644150199 670950.562100807, 650675.7914081773 670950.6121158378, 650679.0822278701 670951.9335051927, 650683.6399782187 670953.7630444595, 650690.2896140553 670956.4318312537, 650693.6154319788 670957.7662247893, 650696.8802531963 670959.0765111077, 650696.8861542481 670959.0675119149, 650700.7429585065 670953.0681439859, 650712.1414049562 670957.0604985794, 650719.5413163286 670957.4203003781, 650718.374672005 671045.2392784139, 650716.8225582781 671054.4989957397, 650721.0880731122 671055.8135048099, 650725.7401711843 671057.247060024, 650745.7683952235 671062.6494581559, 650755.899861912 671066.5436597493, 650756.025955277 671066.5920747062, 650763.3704687491 671069.4145458145, 650801.5371465322 671094.9189654542, 650808.1975626318 671099.4047365319, 650814.8569787735 671103.8905075338, 650828.0238278132 671112.7320323633, 650837.48705686 671118.6231329888, 650841.7687080498 671121.2886309947, 650847.4312468269 671124.8132896503, 650855.5125885397 671129.8442297062, 650860.3531987257 671132.8207931806, 650867.2617451177 671136.2216057925, 650874.1702915282 671139.6224184425, 650939.1375062891 671167.6601022292, 650946.6790823938 671170.7879956164, 650954.1866604199 671173.9018850109, 651001.1410004459 671193.5534465273, 651019.4719714378 671201.1496188475, 651037.8029425743 671208.7457913666, 651079.1556049265 671226.0236916802, 651069.013686822 671225.8434502304, 651063.4291529411 671230.5248200401, 651056.9325318058 671235.970970547, 651055.5271212155 671239.4297645247, 651052.7010293312 671240.3264093262, 651049.5359261367 671241.3330114951, 651046.519827874 671242.2916323883, 651045.2717873079 671242.6874755273, 651041.6776703008 671243.8290237662, 651033.8544155655 671246.3140404156, 651030.1492949544 671247.4905746891, 651020.8389917102 671250.4484043594, 651007.5715597168 671254.6617365619, 651004.1214473855 671255.7573028536, 651000.2473213193 671256.9868158421, 650994.5921369942 671258.7841048883, 650989.8229816915 671260.2985053316, 650982.143731537 671262.7375399063, 650979.0716314772 671263.7131536768, 650976.5855504771 671264.5028411171, 650973.0444350085 671265.6283959078, 650970.3933486807 671266.4700626028, 650964.6101602048 671268.3073354965, 650960.8640381835 671269.4968644951, 650958.4339590191 671270.2685589617, 650954.5708330788 671271.4960732362, 650942.3464347422 671275.3785361958, 650932.0855012749 671283.581197325, 650926.8050207281 671287.8035082567, 650922.3686170921 671291.3499293261, 650918.122230622 671294.7453751556, 650913.5078107999 671298.4337729432, 650908.7553782038 671302.2341526854, 650904.3059732479 671305.7915719562, 650900.0895894468 671309.1630216147, 650895.5611772848 671312.7834305331, 650890.6387291355 671316.7197879781, 650886.2283277651 671320.2452122588, 650881.89993377 671323.7056472162, 650877.393523791 671327.3070589413, 650872.0230350366 671331.6003578242, 650865.7984665893 671336.592445005, 650859.5738982075 671341.5837321448, 650851.188132719 671348.3056369984, 650847.2597740281 671351.4551239352, 650843.1493987011 671354.7505870787, 650839.3960560508 671357.7590968399, 650835.0566597376 671361.2385300244, 650829.5511570441 671365.6518108662, 650822.3935032942 671371.3908758179, 650818.0251044239 671374.8923051312, 650808.5912427205 671382.4560725861, 650802.1526545885 671387.6181313127, 650801.7964220517 671387.9036847644, 650795.7130664085 671392.7803898617, 650793.2548468285 671394.7100690444, 650787.4993328074 671399.2273178735, 650782.5228883877 671403.1326683576, 650781.7358180694 671403.7505656261, 650777.1634097026 671407.3389688086, 650774.4221648254 671409.4906109913, 650771.823932812 671411.5292718383, 650766.3974480439 671415.7885634672, 650761.1239770255 671419.926875042, 650757.2226284358 671422.9893657132, 650751.5821244805 671427.4166293002, 650750.6890446984 671428.1175126986, 650743.3613900179 671433.8685559508, 650738.2679349477 671437.8658908781, 650727.2619514334 671446.5044536632, 650716.2189381913 671455.3890092868, 650712.569603394 671458.3245319423, 650709.81035023 671460.5441710077, 650699.9404444898 671468.4848798532, 650693.1068174372 671473.9819858341, 650691.4956695002 671475.2787750365, 650678.6074866537 671485.6470887198, 650663.7201201161 671497.6240405585, 650642.111136309 671515.0082123004, 650619.7950870262 671532.9623908569, 650616.0747454276 671535.9549037638, 650603.2325658739 671546.2872222115, 650593.7206922551 671553.9389766047, 650586.3710170862 671559.8520140551, 650585.3469230154 671560.6758799313, 650580.7575022548 671564.3672791753, 650569.8665061116 671573.1298546074, 650567.8393207226 671574.7605894348, 650557.9284141484 671582.7342929299, 650555.1221574496 671584.9919258048, 650551.0927760792 671588.3392976031, 650547.0623946734 671591.6868692635, 650538.3326008943 671598.6697274657, 650526.0714855792 671608.4791236639, 650516.1635839724 671616.4058276505, 650506.1536730953 671624.4135181778, 650498.8150052093 671630.2845580697, 650491.425332753 671636.1955912276, 650486.598892615 671640.063859644, 650486.101047223 671640.4627944938, 650481.7724525715 671643.9312280372, 650465.9028054303 671656.6484510571, 650465.1699740904 671656.942158071, 650465.2589467454 671657.1641667867, 650461.105567962 671660.4926231466, 650457.1362060036 671663.6731035818, 650453.0068293554 671666.9825630394, 650448.8904540759 671670.2800241966, 650444.7960805874 671673.5614881951, 650440.6777049305 671676.8619490312, 650436.8403548709 671679.9374466406, 650432.5989681685 671683.3348913469, 650428.6096041994 671686.532369013, 650425.7483432262 671688.824994376, 650422.5620525358 671691.3785771673, 650417.8736249313 671695.1349632548, 650411.8608763528 671699.9536758823, 650406.2035602051 671704.4874350118, 650394.6265039261 671713.7649187873, 650390.3341121974 671717.2053565726, 650386.3767509587 671720.377838215, 650383.5684946877 671722.6284703687, 650376.9098869421 671727.965598127, 650374.8677006772 671729.6013306137, 650373.2075491355 671730.9321131272, 650361.4334744974 671740.3685706118, 650351.5195694367 671748.3152716486, 650348.1532646213 671750.9928307619, 650339.6804970576 671757.7347209959, 650336.1801800305 671760.5192625037, 650302.0920916076 671787.6417966094, 650287.706788144 671799.0869115652, 650276.3247566062 671808.1434198793, 650271.6485630841 671812.4404011501, 650266.9767700209 671816.7331829799, 650246.9117698312 671825.9326213835, 650244.8496669971 671826.8783581066, 650239.7404123079 671829.2207057821, 650209.4359013324 671843.1148362813, 650204.1266365646 671845.549158286, 650201.3645269847 671846.5828079295, 650176.0465219254 671856.0635964341, 650172.3753777614 671857.439132595, 650165.9251246564 671859.8543176117, 650159.1588590677 671862.3884626677, 650158.4458310964 671862.6553725689, 650156.8547686529 671863.251171531, 650150.9445366752 671865.4644247185, 650142.2371948678 671868.7253244217, 650141.4121624946 671869.0342201678, 650137.2589995553 671870.588695346, 650136.1049541703 671871.0215495074, 650132.1898005538 671872.4870547457, 650131.0707565316 671872.9069133223, 650128.4176523555 671873.9005780342, 650126.5155777435 671874.6123376654, 650120.8803565069 671876.7226254926, 650106.936809204 671881.9428632547, 650106.710800165 671882.028834679, 650090.936180639 671887.9368408538, 650088.1800724542 671888.9684924787, 650071.629422436 671895.1664003723, 650065.3400597278 671897.6511039801, 650065.1721527047 671897.7174827193, 650053.8290468212 671866.1214183194, 649942.1542259494 671903.26534818, 649949.7833610015 671926.5380411703, 649940.6680424445 671929.6229916854, 649926.9445628332 671934.2672610037, 649905.166801582 671941.6375143591, 649887.7671932445 671947.526319693, 649880.7167027348 671951.9264092778, 649871.2643444776 671955.3542145051, 649861.575977198 671958.8679898325, 649860.1889246395 671959.3708145001, 649839.9961590472 671966.6942618084, 649837.9415607817 671971.7339569819, 649829.8602424444 671974.7629342298, 649824.1290166724 671976.9122090096, 649817.8797736712 671979.2544213096, 649811.4625240192 671981.6606124003, 649804.6622595291 671984.2097551816, 649798.1770072748 671986.6409376452, 649791.4527456706 671989.1620899475, 649785.209502845 671991.5023028657, 649778.6002456867 671993.9804695979, 649771.8029812686 671996.5286126115, 649765.125721508 671999.0317707199, 649761.2215696347 672000.4952784551, 649756.6973935734 672002.1917080006, 649752.2032187757 672003.8761413182, 649745.8249706131 672006.2673370438, 649738.8747002461 672008.8724606069, 649735.5015689954 672010.1370352374, 649733.19047904 672011.0037437939, 649732.1034367044 672011.4116067021, 649717.1628553449 672017.0127224938, 649708.2123683585 672021.5125814951, 649684.3726265797 672028.9155908163, 649677.5893717811 672031.382735965, 649675.4862928034 672032.1474709235, 649665.6269224685 672035.7332283368, 649658.2586491685 672038.3842999766, 649647.1252210668 672042.5148958192, 649637.033833048 672046.2586230187, 649626.232417771 672050.2652605934, 649622.9942932135 672051.4668521348, 649610.6438182021 672056.049294182, 649604.8315946836 672058.2055609653, 649597.2343024479 672061.0246025253, 649583.7287830394 672066.034898635, 649576.8695192958 672068.5790332146, 649570.4702731438 672070.9532257958, 649550.7955163199 672078.2527431813, 649535.7069358536 672083.8508390844, 649528.2126475819 672086.630893293, 649514.2101065293 672091.845125836, 649498.6615058223 672097.6341630823, 649431.2289073062 672122.7426522583, 649402.9678356764 672133.266092504, 649394.2735060332 672136.5029972887, 649387.62325385 672138.9791595328, 649340.2864584674 672156.6051956406, 649339.6724351415 672156.8341182682, 649323.5938253276 672162.8200922719, 649319.058653197 672164.5095207818, 649237.1925468985 672194.9922027612, 649227.6921864139 672198.5290051465, 649185.1160701301 672214.3839373665, 649185.0565680568 672214.404529878, 649177.0316692835 672192.2071740939, 649176.8753219026 672191.7747593734, 649153.6070915937 672127.9415640662, 649149.7996310615 672107.0713225941, 649145.7928997879 671997.9420488152, 649145.5354145641 671977.2322481158, 649146.1885914869 671959.2575281205, 649146.2176887363 671958.45454062, 649146.2678548985 671957.0825620332, 649146.3696028134 671950.9136431755, 649146.4794114663 671944.2438308864, 649146.5398528868 671940.6028788262, 649143.9912493505 671929.2326944321, 649142.4919375385 671923.6425737354, 649141.3722218191 671921.3604625145, 649136.8933589405 671912.2320176682, 649136.371456662 671911.4549626437, 649125.2265418804 671894.8747876142, 649124.6456505654 671894.0106263792, 649113.7376069196 671878.479468789, 649102.2295890548 671862.7682402582, 649092.2512614571 671849.5271710368, 649088.3520941506 671842.8767698125, 649081.7837102122 671829.9161137372, 649063.7579249443 671796.164293685, 649055.4902125814 671777.7634920599, 649052.0618766493 671767.5342889217, 649048.6427373055 671757.3328866598, 649061.8947996686 671751.5820203606)))" + } +}, +{ + "model": "common.municipality", + "pk": 16, + "fields": { + "fid": "", + "name_fr": "Koekelberg", + "name_nl": "Koekelberg", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((645647.1726886325 672730.9490875698, 645649.9329981713 672728.2704472598, 645650.3830522057 672719.5516042345, 645650.757930948 672712.2827350701, 645650.8952522594 672709.6247829408, 645651.317962656 672703.7429029882, 645652.8355115444 672682.6383338608, 645653.2151493258 672677.357441667, 645654.0184984549 672666.1866697563, 645654.1687513461 672664.0927124757, 645654.7707627864 672655.7178834525, 645658.0147008381 672656.0802646106, 645658.7669272124 672654.1773765916, 645666.9867405778 672655.3403395045, 645674.2925757878 672656.3651954468, 645675.63254675 672656.543352551, 645681.0674207585 672657.3339889509, 645733.6826961449 672669.1641000136, 645760.044698447 672676.2011483917, 645760.9176654051 672676.434249336, 645774.7311385099 672680.1578461508, 645780.2479281793 672681.6444838726, 645788.4596150176 672683.858433093, 645792.3315898401 672683.8898927529, 645820.5074069446 672684.1162376724, 645829.9905179202 672682.7653805439, 645852.2307780578 672679.5970609132, 645863.1699008876 672678.0803788101, 645873.7840268026 672676.5526582369, 645881.6026551846 672679.2705546352, 645895.048044592 672683.7100988936, 645899.7268285735 672685.2846359061, 645900.3902152897 672690.3316541625, 645901.4002800134 672698.0286818133, 645903.8167394697 672702.3939165208, 645905.7793005059 672705.9391071334, 645908.4420771553 672707.6674027297, 645924.8199328467 672699.8374424807, 645932.4143294079 672696.2073883435, 645944.1429418531 672690.5998491849, 645955.872553505 672684.9933184505, 645962.6119043606 672681.7711734998, 645983.6620000731 672671.7078440901, 645996.6366753466 672665.5054900883, 646011.4854481213 672658.4063737845, 646052.5491991303 672608.818941852, 646236.1138623244 672387.2488134905, 646249.3747139982 672371.2436091, 646262.6345653897 672355.2384044363, 646289.0852585617 672323.3039853949, 646292.7347671211 672318.9054793367, 646308.3861359701 672323.3783212459, 646325.3194535045 672328.2163179143, 646334.5670808409 672330.8584083735, 646346.0346187485 672334.1347606295, 646358.2071282091 672337.613196056, 646373.7876867393 672344.6692025408, 646374.3823593309 672344.8677723312, 646374.8208391303 672345.0141237903, 646391.3313782021 672350.5270615205, 646406.8636633478 672355.705884612, 646424.9758297348 672361.7460106079, 646431.6755248316 672363.9517973758, 646432.6064825152 672364.2579067118, 646438.1822287871 672366.0935615036, 646440.1501391991 672366.7417926098, 646445.1419121571 672368.3843788482, 646447.708795335 672369.2296802999, 646451.2936321989 672370.4101013036, 646454.8494704416 672371.5805189111, 646458.564301489 672372.802955202, 646463.2060902581 672374.3315003506, 646465.1110035843 672374.9587240746, 646470.9937360809 672376.894415001, 646471.6097080287 672377.0974873394, 646477.3694459527 672378.9941638148, 646479.6033443522 672379.7294261917, 646483.4451696337 672380.993877426, 646486.2420424628 672381.9142059314, 646489.3778998245 672382.9465742549, 646493.2407240387 672384.2190279523, 646495.9636003198 672385.114347787, 646500.5193930275 672386.6148828907, 646505.5251654031 672388.26247087, 646507.6880670133 672388.9747249223, 646512.1578637315 672390.4462499514, 646514.0927757123 672391.083477233, 646518.8875577708 672392.6610404546, 646521.8664222287 672393.6423903591, 646524.7262921999 672394.5837263005, 646531.2959934678 672396.7464980285, 646536.1047748771 672398.3290629204, 646538.105683838 672398.9882979637, 646550.903102053 672403.2008013263, 646551.333082564 672403.3418518438, 646557.4187998392 672405.3955661865, 646558.5307482 672405.7706967078, 646563.2825273369 672407.3752544668, 646569.5282370934 672409.4839876003, 646569.9672167079 672409.6320391326, 646575.0899787775 672411.3606404727, 646579.1657893347 672412.7371189066, 646580.9007087848 672413.3223225729, 646586.6954395033 672415.2790028006, 646588.5233545653 672415.896217377, 646592.0991885238 672417.1026371549, 646595.0410518284 672418.0959825017, 646597.8539211848 672419.0453127183, 646600.9257784367 672420.0826733301, 646602.4097094731 672420.5838475307, 646607.7994590867 672422.403480265, 646607.9214534421 672422.4444945869, 646613.7691818567 672424.4181811037, 646615.4781024484 672424.9953817257, 646618.9929391989 672426.1817943683, 646621.3868279932 672426.9900754101, 646628.7334868036 672429.4699379314, 646633.817250766 672431.1855347983, 646634.4492213277 672431.3996089827, 646639.367992881 672433.0601864805, 646645.9026894448 672435.2659537151, 646647.7876019223 672435.9021750238, 646651.4794304703 672437.1486084842, 646655.0272657564 672438.346025047, 646657.0841702842 672439.0402665554, 646661.5299640198 672440.5407885603, 646661.7399542603 672440.6118132167, 646666.9167140445 672442.3594210504, 646668.7526287975 672442.9796366153, 646673.5344070107 672444.5931980949, 646682.3959957849 672447.58523862, 646682.9299710352 672447.7653013266, 646686.045826529 672448.816667206, 646686.9127862534 672449.1097689999, 646689.3866714676 672449.9450594946, 646691.7805603824 672450.7533405973, 646701.0011326978 672453.8654233491, 646707.5068308415 672456.0621873105, 646722.6661277516 672461.178967531, 646752.1568942321 672420.1310263864, 646775.7827211539 672412.1159871352, 646799.4085478935 672404.1009477007, 646908.7907022387 672361.7591126771, 646910.0210523157 672361.28286748, 646920.8925023888 672357.0112360555, 646931.7639524172 672352.7396045836, 647051.6938625025 672306.0326938308, 647057.4594356848 672300.978451457, 647068.949788493 672280.827077293, 647082.2436368656 672256.458970153, 647086.577546314 672248.6735853414, 647084.2833923178 672244.2405581138, 647076.9288427281 672230.0296297347, 647052.5212212908 672186.4581718366, 647059.2659273676 672180.2460611531, 647074.9314055364 672167.1631110708, 647033.7690301161 672081.324162639, 647032.2241758375 672076.9231321967, 646995.9393945963 671973.5499711826, 646952.3688628934 671840.6154161867, 646969.0542601987 671836.5304883455, 646969.0636547484 671836.5746889109, 647023.2373279291 671825.0851931078, 647037.7514825618 671823.0949750268, 647050.4176174528 671821.3585300215, 647116.7601429559 671821.9525498995, 647132.5790290529 671822.1004621143, 647153.154881011 671822.2919493787, 647189.7616032906 671822.7503730106, 647226.5458985456 671826.7047735575, 647262.5347654687 671834.2150327907, 647262.7068601109 671834.250453162, 647297.8131682137 671841.4677097084, 647331.3610665448 671848.8436760083, 647361.1912689793 671853.087932369, 647382.7028776094 671851.9483500547, 647456.2642698536 671861.4951319194, 647463.8821034746 671862.4830414124, 647458.6638377202 671873.1362756699, 647454.0987305419 671882.4546057279, 647451.8111756953 671887.124269999, 647447.3701943781 671895.4026281037, 647499.6722790326 671933.1774842435, 647517.7906250241 671945.865518539, 647532.8732070392 671956.766207976, 647582.476598599 671992.1707713275, 647586.8002010375 671995.2232567826, 647589.6656742515 671991.194654232, 647596.0877350587 671982.1635449614, 647610.092407409 671992.3791133697, 647614.2060173558 671995.380574085, 647617.0975352922 671990.9829792264, 647622.2924663437 671983.0777071173, 647625.4900392508 671978.2131550997, 647629.8356846771 671980.9116476906, 647703.3006916039 672026.5309775945, 647714.1758346793 672033.0352145622, 647753.2145017104 672058.4666360469, 647761.799632373 672065.1805946964, 647812.322049494 672116.7520889668, 647815.7666027069 672120.2684649884, 647815.6906946714 672120.3394549811, 647811.4411448035 672124.3118948635, 647739.6135388566 672191.4524246758, 647724.2328810181 672197.7204867642, 647721.2179807615 672197.0401282376, 647696.6124852728 672202.4020831971, 647679.4810037122 672223.9597645658, 647625.0469032623 672294.2373748058, 647618.9541876399 672300.5075653732, 647617.6293203247 672307.8093226271, 647616.8488096977 672312.1081796427, 647615.4218741786 672319.9839180512, 647614.4493085818 672324.7507465027, 647613.5467831987 672329.1785872575, 647612.8283652002 672332.7014605098, 647611.406537004 672339.6812095746, 647610.4089558385 672344.5790335009, 647613.3538590741 672345.2423833011, 647613.2808189952 672345.580370643, 647613.0807494645 672346.1703397401, 647612.5007768361 672345.970271629, 647611.3303786246 672349.3500916484, 647612.2883334443 672349.6802041566, 647613.1302937424 672349.9703030395, 647610.2391265973 672359.8498412316, 647611.9689870104 672360.9300389622, 647617.3475280558 672372.840557648, 647626.6100157541 672393.3514510386, 647558.4147293419 672424.088829996, 647549.2845892047 672428.2040756368, 647546.6594626432 672429.3873437193, 647546.5014550304 672429.4585237454, 647555.4126223235 672441.811265955, 647565.1699441815 672455.3370977594, 647565.167743499 672455.3428974302, 647565.163943704 672455.3413969852, 647560.0441154785 672462.4986949489, 647548.2452188988 672478.8910780868, 647545.6246623092 672483.6607062222, 647547.7145111017 672484.8209468164, 647544.8668949254 672490.0985416521, 647541.24411109 672496.8120261843, 647534.981755841 672508.4191350644, 647530.9499242111 672515.5505651562, 647525.9238874321 672524.4408546701, 647523.3193501926 672529.0474864801, 647522.1634337358 672528.4063533767, 647521.0025176745 672527.7622197083, 647513.7859744921 672540.9811943198, 647515.062887486 672541.6453418538, 647515.4968578779 672541.8713919907, 647511.2219799329 672549.3997879521, 647512.1269176287 672549.8758924473, 647507.6559687223 672558.0052577807, 647505.6261147491 672556.8850239719, 647501.0860999004 672565.567374627, 647498.6385753294 672570.0600266764, 647495.9840062574 672574.9336492621, 647495.8059663957 672575.2746237919, 647492.5112278654 672581.5921523122, 647489.142472595 672588.0526702033, 647474.268668436 672620.4904970637, 647469.4816391731 672629.3028159665, 647467.2221533128 672633.4624944635, 647464.191501715 672639.0410632325, 647461.1558490557 672644.6286312785, 647458.2251799462 672650.3482105606, 647456.4652859793 672649.5480058482, 647453.0385359933 672655.9655169519, 647449.9088513191 672661.824070442, 647447.9534232536 672665.4867914307, 647435.2267825344 672679.7739836114, 647432.5897003892 672684.7533068744, 647431.0998715041 672687.5663940309, 647428.3952745707 672692.6720076388, 647422.2779241136 672704.2221336099, 647410.7943885128 672725.9064926571, 647401.6503695266 672743.1711858527, 647397.0843613744 672751.7915332593, 647392.5573617094 672760.338886188, 647377.534043718 672788.7057385253, 647370.5364981886 672801.9177380458, 647365.9464842277 672810.5850817254, 647364.0590644352 672814.1728115653, 647311.2494632805 672913.3202629754, 647289.9502332838 672949.5542597268, 647246.163869632 673021.2541151148, 647245.3500170696 673022.5624011558, 647244.53156367 673023.87788655, 647239.7318831005 673021.4373304519, 647228.5305960382 673016.0130296052, 647225.9427607718 673014.7597290812, 647222.5199786284 673013.1023315939, 647216.7533456032 673010.3106619254, 647216.0943883853 673009.9845854834, 647210.4047622749 673007.1319258958, 647204.6071433807 673004.2242538213, 647198.5545412812 673001.1885522043, 647197.8835854615 673000.8514744353, 647186.4093395306 672995.0991443702, 647186.155356306 672994.9711149341, 647179.7477735262 672991.7913718446, 647177.2509361978 672990.5515822959, 647154.190438251 672979.1054081743, 647153.0452344554 672980.8552488001, 647152.775186603 672981.2662112395, 647137.3724480842 673004.7790677428, 647135.9581965079 673006.9388709029, 647133.0636818282 673011.357468046, 647130.2838440886 673010.1341438848, 647124.0671950877 673007.4974178313, 647116.4306310633 673004.2185264191, 647114.1457614157 673003.238259702, 647111.0449383351 673001.9078977461, 647110.054994862 673001.4827821946, 647106.2823897471 673006.6942633515, 647105.8463197381 673007.2972033726, 647103.2439023391 673010.8918454532, 647096.3924211984 673006.8870579237, 647091.2698090744 673003.8934691253, 647088.4550221327 673002.2491455928, 647085.6132373926 673000.5878189725, 647075.1720282384 672994.484618955, 647061.1227273219 672989.3119693598, 647047.0754264672 672984.1393200825, 647033.2433674348 672976.9447203699, 647027.4727600398 672973.943053013, 647009.8559586654 672964.7800157856, 647004.0443542757 672961.755843774, 646995.5259339244 672957.3253587757, 646989.1733662987 672954.0206242553, 646975.6522864796 672946.9880609169, 646963.4984429745 672937.9266874219, 646950.4683611938 672930.8891840569, 646938.0472365594 672924.1807510382, 646935.0144503512 672922.5424011628, 646928.5889038747 672919.0666599598, 646932.4545985442 672913.1081990642, 646935.4641390737 672908.4716187268, 646938.9217605063 672903.1411008977, 646942.1483400597 672898.1695508008, 646948.532487139 672888.3294409737, 646950.3158074357 672885.5816896092, 646954.7215989582 672878.7913038788, 646956.4559106184 672876.1175456792, 646959.2534130657 672871.8069356804, 646964.6323794683 672863.515685562, 646969.7322953602 672855.6573964687, 646975.0042422143 672847.5331313228, 646979.3590245018 672840.8207383151, 646981.636433514 672837.3110557335, 646982.597606093 672835.830189695, 646983.6497951017 672834.2083363442, 646988.051585602 672827.424949811, 646992.3593592973 672820.7855501629, 646996.0339413977 672814.9377643713, 646986.8571412883 672808.7100207383, 646972.4171000447 672798.9103786572, 646907.0259926725 672761.2648663633, 646865.794297229 672785.617572567, 646835.3754252001 672819.2434843604, 646770.6798728287 672868.428046384, 646753.4149805456 672876.64885128, 646736.3674861335 672881.5507212144, 646718.0683059844 672883.8974687227, 646698.7557662459 672889.2800574705, 646664.861293942 672903.0887742629, 646645.4988151067 672907.9653626196, 646619.9026171181 672910.7962169162, 646602.890999962 672916.7120788281, 646533.7014253137 672932.9934734437, 646529.2164473106 672933.0179277358, 646489.5644402769 672943.2129853782, 646472.385854195 672948.8728293979, 646432.2569698641 672958.0658403288, 646412.4225855283 672962.1703794715, 646374.7138545165 672969.9747007221, 646336.9186934415 672973.0500673131, 646289.8922586554 672978.8122846151, 646287.7942318491 672979.1300264504, 646223.9023037789 672981.4342520795, 646215.3954436347 672980.6602298397, 646198.9136034442 672981.7417185102, 646193.8028909779 672982.0770948328, 646172.7413395811 672983.4590246029, 646149.5903175247 672984.6922027799, 646012.1927836621 672962.1978131756, 645984.8812318213 672959.7255315932, 645967.2925344819 672958.0174196709, 645885.2258568121 672950.5837316279, 645825.8688072822 672945.2317512896, 645793.2743284139 672942.30291796, 645787.1664306858 672916.9154962673, 645761.1193966148 672885.2517823689, 645741.2369873928 672864.7026670593, 645716.0683042192 672846.6568938876, 645704.0164722553 672837.5355715328, 645686.834526119 672821.3277241903, 645680.9701191194 672816.6890829856, 645675.1057122101 672812.0504417532, 645664.299249992 672799.8323036733, 645660.8377426221 672795.9189390962, 645661.4775820798 672780.7081957583, 645660.0252679224 672766.8581877864, 645658.5729537046 672753.0091797771, 645656.1778470791 672745.7429815824, 645653.8687041748 672738.7727900723, 645649.2183878568 672733.3393021993, 645647.1726886325 672730.9490875698)))" + } +}, +{ + "model": "common.municipality", + "pk": 17, + "fields": { + "fid": "", + "name_fr": "Ganshoren", + "name_nl": "Ganshoren", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((644559.4388185572 673881.710989397, 644623.626624249 673829.2841422673, 644649.0780819166 673815.6672684257, 644655.5086719851 673811.4280653808, 644663.3090218971 673807.2090242049, 644673.5198695965 673801.000205453, 644689.308598741 673784.7443074267, 644691.291871403 673782.7026452344, 644692.5870565824 673781.4776923651, 644695.1280764699 673779.0738602689, 644696.4404811087 673777.8322696292, 644705.0714987238 673769.66634347, 644739.3283101971 673745.2287540215, 644744.5136956541 673741.5294095981, 644746.1338734317 673740.3734544414, 644762.8673668109 673727.9526956874, 644787.2134316269 673709.8797823254, 644795.6562430222 673703.6128528145, 644804.4689964586 673697.0709302659, 644843.873742465 673664.5039654501, 644895.4330299727 673619.4696234242, 644904.0303549655 673609.5526747513, 644908.5378747117 673604.3532679231, 644928.4575696529 673582.4349173808, 644936.9933797865 673574.7809756912, 644939.6708897859 673570.967416483, 644952.4880350487 673552.7110637261, 644953.9672163242 673550.6042230751, 644958.898116523 673543.5799276474, 644959.7822166266 673542.3211667999, 644972.3339593769 673536.0136052822, 644977.6493231483 673533.343385275, 644982.0174928867 673531.147927681, 644996.4393044382 673523.9017185206, 645003.8437850007 673520.1805580246, 645008.6759659213 673517.7531580997, 645011.9421525171 673516.1114837235, 645021.2349508234 673510.2737712892, 645022.1679603506 673509.6718087017, 645048.6817991401 673494.5831221492, 645074.5981101439 673474.202426515, 645206.244154745 673370.6782598165, 645233.7335926483 673349.1598470724, 645258.6559562762 673329.7510261567, 645282.8961246468 673310.8739586852, 645284.9924181352 673307.7273638342, 645288.9557470924 673305.2257826903, 645304.2317909356 673295.5837055523, 645321.905144899 673284.4289703564, 645322.2573301242 673284.2083746736, 645418.5666685104 673223.872176874, 645420.3622093045 673222.7474429377, 645427.680638351 673218.6881981962, 645462.6624321042 673194.6476681486, 645468.9718114735 673190.3115072377, 645471.0581075829 673189.0890489826, 645488.0537952796 673182.0637867536, 645504.0880802525 673187.2867461157, 645527.0831442063 673194.2063505729, 645531.6677996565 673196.8878224122, 645534.5965049318 673198.6052693179, 645539.8360851536 673201.6757741779, 645548.6767554511 673204.8537446525, 645554.1394655522 673206.351494527, 645566.7640288293 673209.812950162, 645585.9572630721 673215.075003104, 645599.5047916631 673218.789685042, 645599.8168382244 673218.764762328, 645615.0368267047 673217.5685006994, 645680.967212288 673212.3824578609, 645681.7031207872 673212.4075047981, 645692.9330921837 673212.7848315621, 645693.7458575494 673212.8121275839, 645708.5108877688 673213.309551974, 645723.4278567713 673213.8113143168, 645731.6896992045 673214.0894103935, 645732.6607932448 673214.1215251276, 645738.2266118206 673214.3092226846, 645747.2065477534 673215.1502771699, 645753.8344690693 673215.7699354747, 645760.6682869869 673216.4097779457, 645764.1983503891 673216.7402724652, 645766.7942462531 673216.9834572858, 645771.3071113942 673217.4060672104, 645777.8240969799 673218.0157924688, 645796.1444303116 673214.1549703367, 645815.3758897015 673210.1033785715, 645862.6508006782 673200.1420622201, 645870.1499761455 673198.5620102013, 645866.3154198223 673187.1586522729, 645863.2905011253 673178.1644814229, 645865.7027999421 673175.9897534918, 645876.777823452 673166.0062262969, 645877.712990712 673165.1642672749, 645882.4553959995 673160.897840744, 645887.1749012836 673156.6525312683, 645894.8095239042 673152.0024523322, 645900.539795718 673148.5102137194, 645905.5642789509 673145.4478062773, 645911.2185755869 673142.0005181525, 645916.421991013 673138.8291732685, 645922.4073476988 673135.1809668572, 645927.6237214195 673132.0016236427, 645933.1590844749 673128.628240604, 645938.8584688463 673125.1540782492, 645944.0398724307 673121.9966506748, 645949.6203235933 673118.5943133886, 645951.8044336141 673117.2637084151, 645955.7036648936 673114.8870878675, 645960.8270192713 673111.7637469133, 645965.9013801485 673108.671439631, 645971.1997615764 673105.4401211791, 645976.3870200113 673102.2798283827, 645981.7024422722 673099.0405120669, 645991.933174589 673092.8047079436, 646005.3979997456 673084.5974797141, 646017.3069414599 673077.2540523143, 646026.7244950208 673071.4773438721, 646028.2226979117 673070.5573766223, 646034.7940213248 673066.5252620025, 646039.3412431045 673064.6279162659, 646045.7975631673 673061.9336916199, 646052.2618710136 673059.2365879641, 646060.3052897749 673055.8804837043, 646071.3517882904 673051.2719586415, 646083.6564918923 673044.4295328766, 646089.753815221 673041.0392729242, 646095.8692447582 673037.6379752837, 646108.4400370404 673030.6477434208, 646114.5274197252 673027.2624421361, 646135.3986232154 673015.6571118877, 646142.479179546 673011.71897757, 646144.242107623 673011.6551921081, 646144.2491978186 673010.2643295898, 646145.928352025 673010.1534945332, 646146.5247167395 673006.0035364347, 646149.5903875321 672984.6921627863, 646172.7413795887 672983.4589846064, 646193.8028709774 672982.07707483, 646198.9136409475 672981.7416985165, 646215.3954436425 672980.6601898391, 646223.9023462661 672981.4343320824, 646287.7942868392 672979.1301064554, 646289.8921861578 672978.8122846052, 646336.9187609431 672973.0500673216, 646374.7138195168 672969.9747007166, 646412.4226505262 672962.1704194793, 646432.2568848616 672958.0658403189, 646472.3859041831 672948.8729094014, 646489.5644077791 672943.2129853746, 646529.2164298127 672933.0179277329, 646533.7014778106 672932.9935134501, 646602.890954968 672916.7120388232, 646619.9026396142 672910.7962569176, 646645.4988701122 672907.9653226259, 646664.8612264489 672903.0886942593, 646698.7557912485 672889.2800174706, 646718.0683309832 672883.8974687256, 646736.3675186245 672881.5507612186, 646753.414935539 672876.648891273, 646770.6798328288 672868.4280463805, 646835.3754376972 672819.2435243626, 646865.7942897369 672785.6175325641, 646907.0259751688 672761.2649063597, 646972.4170525528 672798.9103386543, 646986.8571937813 672808.7100607426, 646996.0338538929 672814.9378243593, 646992.3592767902 672820.785630153, 646988.0516431103 672827.4248698215, 646983.6497400913 672834.2084163364, 646982.5976860926 672835.8301897021, 646981.6363510222 672837.311015725, 646979.3589719952 672840.8207783102, 646975.0042922149 672847.5331313277, 646969.7323053628 672855.6573564694, 646964.6323469664 672863.5156855577, 646959.2534855623 672871.8069356896, 646956.4559381155 672876.1175856828, 646954.7216289629 672878.7912638794, 646950.3158199365 672885.5816896113, 646948.5325121399 672888.3294409779, 646942.1482875667 672898.1695107965, 646938.9218230088 672903.1410609062, 646935.4642115734 672908.4716187346, 646932.4545135455 672913.108159055, 646928.5889338726 672919.0666599628, 646935.0145128451 672922.5424411714, 646934.3211817303 672924.185377835, 646933.5479533481 672926.017262589, 646932.9968388616 672927.3280603898, 646931.9356247353 672929.8450221873, 646931.5844316913 672930.6769698411, 646929.6558877932 672935.2496423486, 646929.5159785525 672935.5825814753, 646928.6367552521 672937.6474106327, 646927.4343938434 672940.4601918288, 646925.3547643642 672945.314922673, 646923.2362518996 672950.258607747, 646921.8288222497 672953.5444785017, 646921.0825847553 672955.2843675809, 646920.8615365776 672955.7993747313, 646913.8259901507 672952.995511464, 646910.7720884467 672951.7780341227, 646910.4196972717 672955.194111675, 646910.4086792355 672955.3030690737, 646909.760940706 672961.5808375372, 646909.5336545393 672963.7847843478, 646909.1741540335 672967.278780127, 646908.6696466609 672972.1676221029, 646901.3019758485 672969.5608361113, 646895.6101795929 672967.5460871251, 646895.1806567782 672973.2459687864, 646894.630614742 672973.01790452, 646889.2899344789 672970.8123204573, 646879.4823588489 672966.7570945182, 646879.3304275027 672966.6921167924, 646875.652660758 672965.1086478127, 646866.430112537 672961.1315721854, 646861.3045617298 672958.9210544075, 646852.6739419876 672955.2000478592, 646835.9693718423 672994.7284364966, 646829.0964389703 673010.9115721909, 646822.5285359055 673026.3745932579, 646730.9681651709 673241.702858383, 646711.8088001296 673286.9590365689, 646694.4480105162 673327.9674826724, 646613.3023476849 673519.8873466321, 646610.0048969928 673527.6822131763, 646609.5752270508 673528.6976688787, 646626.1128516599 673540.1936301908, 646695.1197088787 673563.6947284937, 646704.5122450738 673566.6828787755, 646713.8387541865 673569.6499012801, 646788.3670230831 673589.1698011799, 646804.9855600792 673593.5228169728, 646801.9366788953 673600.7343612262, 646759.72479135 673700.6450084571, 646754.4563353733 673713.3312577521, 646754.660202688 673713.9891949494, 646746.6568186523 673732.7625594691, 646745.1806748629 673736.2247389638, 646737.143838442 673751.2557429213, 646734.5675384107 673754.8582665089, 646731.4469296878 673759.2629543045, 646726.0891530063 673766.3650976, 646723.2047887157 673769.350790763, 646720.3193718158 673772.233444994, 646710.738602815 673780.2621020566, 646675.6073751324 673807.9515931456, 646601.3880247152 673865.9699196446, 646603.8559344612 673901.1068085865, 646606.6342686253 673948.537685207, 646610.1061164294 674001.8145046425, 646612.3170802441 674034.8193849607, 646613.045214878 674042.104356706, 646615.060290687 674059.3664999318, 646619.3865941089 674082.5847023899, 646623.6965504062 674099.8951254855, 646633.1528022155 674122.719078904, 646646.3062268317 674152.7422879299, 646610.6035998391 674167.210732849, 646526.0533977478 674228.5349515097, 646517.1691656166 674234.9785974362, 646523.1612630343 674276.6499268442, 646526.2526343127 674276.2541488404, 646532.2427112343 674323.047503776, 646534.6030314049 674337.2066818504, 646543.071943294 674371.8363286584, 646545.5477752245 674381.9654909665, 646548.3995515596 674393.6228441063, 646547.7048667137 674399.3716622437, 646546.5077199525 674409.4054965035, 646546.3895671999 674410.4094721143, 646545.144270296 674420.8581763728, 646543.9471224934 674430.9008105319, 646489.7041042744 674424.6603209907, 646427.347353261 674417.4868490165, 646281.9769778419 674400.5321417178, 646280.4483764792 674400.3463576884, 646212.8702451355 674392.1339020097, 646212.1585678989 674397.462722372, 646195.994913354 674396.3417280396, 646189.6859147362 674396.0670436014, 646171.596174625 674395.3107714249, 646154.3842186239 674395.6517150567, 646135.6630539154 674396.9515056242, 646115.8968865671 674398.814043518, 646098.4697417989 674401.2079005525, 646081.2494882215 674404.2316964791, 646066.974119055 674407.2920101457, 646043.2786771918 674413.5479465423, 646015.1967072124 674422.7975597751, 645987.6815449804 674433.1230712561, 645938.8725479996 674452.2739709623, 645905.6812235144 674464.675081403, 645877.5450878862 674475.4766411429, 645815.5719285514 674496.7811804737, 645800.8842139923 674501.3882174052, 645717.5553762205 674527.5265406372, 645716.5327655465 674527.8472180197, 645610.9714363541 674550.8506610973, 645477.2462527759 674583.4368179757, 645439.2446786049 674588.3507347894, 645427.9506625887 674589.1239685765, 645418.2305755018 674591.0680960478, 645034.3026997822 674640.7129336031, 645034.5222016913 674639.7337669284, 645035.6859524779 674634.5427436215, 645036.1971629425 674631.3519484112, 645039.0786333722 674619.4853382866, 645041.5820797945 674607.1887675544, 645046.4956779348 674583.5304882189, 645049.4748435934 674566.4279314677, 645055.6090569616 674537.0199212474, 645060.5897158004 674515.2016366888, 645061.3053844286 674509.111848705, 645065.4207124821 674496.7292285375, 645070.7747315705 674478.8491973146, 645072.0009018832 674469.5593347526, 645074.4786483633 674463.1057562584, 645078.5684250422 674455.2741376264, 645079.0994438493 674446.5413094157, 645078.3709651018 674432.8573330955, 645078.5782825754 674422.0844832213, 645080.419375606 674412.230857559, 645080.6110779534 674406.0397293873, 645082.4532852517 674400.3819104999, 645083.2590622758 674397.9073047254, 645079.5801742488 674379.7319781843, 645062.876664517 674366.8291611797, 645059.8910359627 674346.02897572, 645054.5207332878 674340.501356244, 645054.1671058859 674328.859343051, 645047.1810787971 674321.3915522633, 645044.6975357437 674309.1004948302, 645039.5192467052 674302.420748713, 645034.3134769513 674292.4584696956, 645021.7230569182 674279.207993421, 645006.4818376979 674264.6554571631, 644996.2055300509 674259.993172605, 644992.4834902942 674251.4288140727, 644983.1330574243 674246.4426409965, 644972.052336027 674237.1486239204, 644963.253888742 674230.973566254, 644960.1829127329 674223.9492695539, 644952.8852021549 674220.9593189175, 644947.2658116858 674215.920944261, 644927.8478024863 674207.8496145748, 644920.177281169 674196.3488171493, 644908.8292253334 674188.7374715663, 644898.0259092541 674182.681375706, 644889.7805625382 674176.4023826305, 644884.6906988698 674169.4669665514, 644877.216845476 674167.018109373, 644869.2045211144 674153.4992085305, 644858.6086466627 674152.4320512813, 644857.2457416907 674143.9470071933, 644843.3706015863 674136.5624004821, 644835.0760493494 674132.1153840336, 644827.2731746737 674132.1495052695, 644822.0730016124 674125.9139088803, 644814.7905432831 674121.5290907272, 644806.2383785446 674122.1181586789, 644799.7418659405 674119.4204578863, 644795.0194794365 674115.0447408288, 644793.6946603243 674104.1979612531, 644780.2625178082 674097.1612419646, 644767.7573381971 674089.6039961467, 644758.1646440966 674087.4217656205, 644767.457623231 674070.8420467909, 644765.0575607041 674062.6047168707, 644768.1409688062 674059.0222318509, 644780.6946495859 674061.6886473647, 644778.5205490912 674053.4054246172, 644769.3168856943 674045.6058491081, 644763.9301416263 674041.040934637, 644757.3281643162 674034.0822979871, 644747.6325706153 674029.9711114254, 644741.6153658566 674013.2161496382, 644739.7990789181 674008.1583800004, 644736.193502844 674006.9261257072, 644735.9794472966 674006.8528211124, 644721.2291142115 674009.5380983336, 644714.4415216112 674005.1763762301, 644716.8499970657 674000.7685388424, 644716.8141258003 673996.3903329737, 644716.7430830442 673987.7206405572, 644716.7061617012 673983.2158756711, 644709.923144031 673974.6770703361, 644699.6823925714 673965.0309865456, 644692.8651764756 673959.0041548262, 644683.2710467256 673950.9350930816, 644671.2959679465 673944.1438189961, 644671.2215057997 673931.3559212966, 644650.2665628095 673923.0255199873, 644632.2138126163 673921.5544415915, 644628.6341562325 673909.744040795, 644609.2743070626 673909.7278351338, 644606.4434349854 673907.6743985203, 644598.218302527 673894.0525840728, 644591.2208321032 673888.8127625451, 644585.3290524228 673886.1812092, 644573.5458362867 673879.512834386, 644563.9567917995 673881.006889028, 644559.4388185572 673881.710989397)))" + } +}, +{ + "model": "common.municipality", + "pk": 18, + "fields": { + "fid": "", + "name_fr": "Etterbeek", + "name_nl": "Etterbeek", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((650730.2850588415 669287.5535654081, 650743.8798050006 669280.3833392044, 650777.0886261994 669262.8686736047, 650783.3599699764 669259.5604923754, 650817.1328202093 669241.7439029298, 650817.9508667004 669241.2980099949, 650827.2043925227 669236.2542211947, 650848.5856072429 669224.5970204532, 650853.7138985038 669221.800691989, 650872.4549622001 669211.5841464776, 650879.6963730818 669207.6360950639, 650885.6487109037 669204.3908748064, 650891.2160268606 669201.35560415, 650896.1853087294 669198.6472551895, 650900.8185716341 669196.1208622714, 650905.6158437422 669193.5054908763, 650910.9821480373 669190.580194092, 650916.690471738 669187.4679421898, 650940.9208449376 669174.2581183915, 650943.8590113522 669172.6565036036, 650951.4544415477 669168.5154995251, 650957.7157961614 669165.1013206025, 650955.7052786201 669161.1651310474, 650937.367700961 669125.0894033574, 650916.7376537055 669084.7164537469, 650899.6147772715 669051.1178346329, 650884.5004318515 669021.350405341, 650870.0479548392 668992.6620403439, 650859.5045034899 668971.9220408536, 650856.6542029982 668966.2277717954, 650856.5122378338 668965.9441583967, 650853.803902562 668960.5335026841, 650839.7863046324 668932.8591735769, 650818.4404859201 668890.7331467964, 650814.4274601878 668882.8147653792, 650813.8336042949 668881.6437089232, 650809.893829795 668873.289642746, 650808.0751032413 668869.4331736694, 650803.1153946613 668858.9147124779, 650799.297388829 668850.8183573309, 650795.9722547487 668843.7670479289, 650808.6313854242 668824.9108863227, 650812.2539826958 668819.6184110631, 650814.811404361 668815.8817815576, 650818.4600211297 668810.4223120492, 650821.5185383352 668805.8437568194, 650824.4500340414 668801.4551831753, 650824.621059156 668801.2312075795, 650828.9846972034 668795.5388301271, 650871.9197556789 668732.9070571762, 650883.924430515 668717.8997633976, 650895.92810491 668702.8924701338, 650932.0466571939 668661.8035496644, 650938.0302321487 668656.5043695011, 650944.011806895 668651.206189189, 650954.9496780181 668643.0336660314, 650968.484724626 668633.1814901294, 650973.7661328892 668629.3372020223, 650982.6559388256 668613.4185400429, 650990.4674034687 668600.459700952, 651007.5238399007 668570.9845545066, 651009.8181885807 668567.019598135, 651050.5234264509 668519.640357877, 651092.4346719676 668472.0452775079, 651130.319124604 668431.416598398, 651165.137464582 668391.9385256378, 651164.5957912857 668380.7486270564, 651211.660781057 668334.7401842393, 651285.7686240063 668262.2815245509, 651284.641770636 668261.1294016641, 651288.4212767109 668256.5299433639, 651303.801254671 668238.4991378282, 651345.144541998 668190.0280034739, 651357.2314276485 668172.9187586707, 651371.7616934432 668152.3498703105, 651381.9782856068 668137.8893560899, 651392.4454164631 668123.0716792429, 651443.8317350561 668034.7425981181, 651445.6658232044 668032.1199663614, 651450.3627610247 668025.4035534369, 651448.0630074095 668023.4932931139, 651449.9332784252 668021.013563891, 651461.7194766637 668009.7442069697, 651500.9154316097 668041.00065972, 651519.893474866 668056.1348130722, 651540.5173497164 668072.5821512341, 651707.5971802918 668205.8310192846, 651722.8046218802 668217.9587301585, 651738.0120607266 668230.0874374966, 651906.252761974 668364.2592277392, 651917.0006596947 668372.8304254729, 651920.9682302763 668367.6250049403, 651931.651150276 668376.0131973024, 651985.786684744 668419.174038881, 651989.2086348509 668421.9473059219, 651992.7639713284 668424.8287873033, 652009.7387551937 668408.4291009919, 652010.6118447575 668407.6042196647, 652148.2919515562 668277.5659008572, 652149.3690653759 668276.518047249, 652154.8496442152 668271.1887920004, 652162.3520305448 668275.9526177541, 652279.3047119688 668356.7274034056, 652286.180110768 668361.4331567745, 652293.05451052 668366.1379082088, 652302.8956540279 668372.8689786631, 652309.7340532634 668377.5947219301, 652316.5714525345 668382.3204652397, 652344.7370023534 668401.5805321456, 652355.8500426305 668409.1207439034, 652366.9630828715 668416.6619560692, 652422.7462666915 668454.514047076, 652430.0425972117 668459.8058392782, 652437.338927768 668465.097631673, 652443.7203749623 668469.445329208, 652453.7984924532 668476.393429828, 652463.8766101206 668483.3405308176, 652490.6782502695 668501.9334586855, 652500.5423854651 668508.7455373709, 652506.0379037545 668512.5401384823, 652523.7103266169 668524.985068552, 652561.1580617733 668550.6991719583, 652576.2457274982 668561.2238241012, 652594.789087851 668574.1588557556, 652616.661206237 668588.9682603765, 652623.0656653045 668593.2189660786, 652629.5861145958 668597.5466847026, 652685.7981895539 668636.3888619741, 652691.6006823176 668640.3895003344, 652697.4041750929 668644.3901389284, 652764.2483496384 668690.3405043301, 652770.4957855644 668694.8051909996, 652776.7332225313 668699.2618767257, 652834.4071666923 668739.1892422287, 652843.9063305022 668745.7972912851, 652851.3426760857 668750.9691127598, 652916.341005352 668795.7643069482, 652925.2472250701 668801.9322934387, 652933.4015106447 668807.58019687, 652962.6289610995 668827.7314382617, 652983.7960845922 668842.5877839001, 653005.0732259434 668857.2861467669, 653009.5948336667 668860.3866494574, 653014.6443906017 668863.8922103234, 653019.6949475486 668867.397771399, 653049.6357652729 668887.8016046496, 653061.6903257822 668896.016447518, 653037.8820621595 668891.9916254613, 653033.5120984752 668891.9460978416, 653027.657147097 668891.8853909315, 653026.3051583356 668891.8712276991, 653024.0631769568 668891.8479570064, 653018.1982256453 668891.787248889, 653014.7292545071 668891.7508300608, 653011.7332402826 668892.0564636118, 653001.9171939672 668893.0552629879, 652997.3691724264 668893.5187066833, 652991.7329570773 668895.7169944426, 652983.1045273943 668899.0819040154, 652974.4772977834 668902.4458136017, 652969.8830068947 668905.2302188292, 652968.1178950137 668906.3009902869, 652965.2548137741 668908.0357195936, 652964.7456815061 668908.3445536643, 652960.0563845175 668911.1869464453, 652955.0120648275 668914.2462931582, 652951.0744957498 668919.3847441553, 652947.7340129202 668923.7442783195, 652944.140493522 668928.4337771204, 652940.4539607124 668933.2442628668, 652932.713492151 668946.3551399902, 652930.9330154989 668950.5658646305, 652928.059246112 668957.3624201074, 652913.9761609355 668984.7693244518, 652910.2193356124 668992.0997648088, 652906.9383976761 669000.3702492439, 652902.9352382028 669010.5906181504, 652898.913050354 669021.0559810438, 652898.8510319665 669021.2179712093, 652894.7445381772 669042.9261626703, 652894.6284676458 669043.5401398052, 652892.523181508 669054.7347241708, 652891.0462695447 669062.6714313249, 652889.3331110484 669071.8830914883, 652887.6191526661 669081.093751486, 652887.0323958447 669087.6415863957, 652886.1162152905 669097.8553287251, 652886.0421199421 669098.6803078976, 652885.5945428339 669103.6731819547, 652885.1309448051 669108.8470514556, 652886.2056546975 669119.8840236539, 652887.0806044594 669128.8690010693, 652888.0834013366 669139.1619753059, 652888.7755707457 669146.2679575289, 652889.3628652279 669152.3039423715, 652889.7913510469 669156.7029313934, 652890.2737723525 669161.6539190701, 652891.1617062563 669170.7748962966, 652892.567019638 669185.2048604203, 652893.0600919704 669192.2978187801, 652893.3968271672 669197.1380903997, 652893.5532647714 669199.3867772185, 652894.7252990553 669216.2326785448, 652895.6314018326 669233.3657434881, 652896.2259911457 669244.6094548886, 652895.5848470111 669254.4932353033, 652894.7470753877 669261.1830374772, 652892.570921598 669271.2406283363, 652891.3274199145 669275.6314139292, 652887.5478946867 669288.9797620508, 652885.4562385557 669294.7494243867, 652881.020268257 669303.3607600984, 652876.2503292653 669311.7220584926, 652870.8693788578 669320.2172802483, 652861.2458802237 669333.6789127775, 652851.8635490456 669345.6845951141, 652842.9093566403 669356.4703467882, 652837.0516970388 669362.48954489, 652830.4059601517 669369.2196363258, 652821.5709951478 669378.040429948, 652810.8498527925 669388.4969690933, 652787.481448397 669410.5497937814, 652775.7983149564 669420.9832139576, 652768.7415712149 669427.7932520022, 652750.5963213539 669448.2047348146, 652716.4268306568 669488.81795755, 652712.3522950208 669493.6613874134, 652705.2903668076 669502.0543990585, 652698.074264673 669511.9513692636, 652689.4929536682 669523.7231441429, 652688.9478703683 669524.4710663034, 652672.5973724974 669546.8977305955, 652669.1698608972 669551.4952422794, 652667.769651824 669553.3740427596, 652666.6294809977 669554.9088802125, 652662.2218204926 669560.8432517376, 652661.4076985142 669561.9391356397, 652653.3764950706 669572.7509901299, 652652.1683141032 669574.3768177773, 652651.072149821 669575.8526613851, 652648.9848371817 669578.6613635841, 652648.8788213744 669578.8033484648, 652648.6557878704 669579.1043166318, 652646.6394857912 669581.8180289175, 652640.9626348638 669589.4622186471, 652640.3735467297 669590.2541345664, 652635.6888448993 669596.5594657131, 652635.2257754467 669597.183399578, 652631.3481942186 669602.4048458028, 652630.9371326708 669602.9577870974, 652629.0228459504 669605.5335136985, 652627.0005429772 669608.2552248316, 652626.4264595006 669609.0061431476, 652622.7189198874 669613.8596155227, 652615.205947132 669622.6565616553, 652610.8083776871 669627.8059446331, 652607.856995542 669631.261530471, 652570.663325503 669682.1421958673, 652566.3756716335 669688.0085802795, 652579.0289311607 669702.2379201384, 652586.0920596905 669710.1808684451, 652593.176985275 669718.1484193396, 652614.0231186657 669741.5906303511, 652624.7605347801 669754.589756285, 652634.6590748241 669766.5727948787, 652634.9250350122 669766.8998227178, 652632.9445900312 669770.8385200149, 652646.2776275199 669778.3500488084, 652736.3045313936 669829.0691835211, 652737.5760396727 669829.7855296487, 652734.3149269031 669830.9411108268, 652725.6796281611 669834.0020016903, 652724.0365706917 669834.5897905595, 652722.0895024496 669835.2875403423, 652713.8372098556 669838.2734793298, 652707.9159999329 669840.4157179565, 652705.4569127472 669841.3054017402, 652704.2528700276 669841.7412469027, 652703.400839863 669842.0491373395, 652703.1308302532 669842.1471026137, 652701.0367562292 669842.902833344, 652698.2886589792 669843.8954799494, 652698.0296497614 669843.9894466359, 652691.4444167626 669846.3675997544, 652684.6621766841 669848.8177274448, 652682.8271117415 669849.4804914106, 652677.9940642277 669850.1638855234, 652671.1619971755 669851.1290290124, 652669.6839826653 669851.3378437181, 652662.8369153475 669852.3059852781, 652656.9018570994 669853.1442411664, 652650.1297905559 669854.1013920741, 652641.1770315996 669852.5383117863, 652632.6634087756 669849.7803034679, 652631.8354689392 669849.3102084362, 652623.5130735795 669844.5862533052, 652618.2024594473 669841.5716439268, 652613.2378202408 669838.7530743266, 652610.529017029 669837.2157635625, 652608.2101855298 669835.8994975595, 652602.7045856614 669832.7738660537, 652602.1256276631 669832.4457996445, 652600.9457134501 669831.7756643186, 652597.0020000287 669829.5372120318, 652591.9413678036 669826.6646317134, 652591.6693295878 669827.0085930097, 652585.7975030768 669834.4467572317, 652572.5926444622 669851.1718769342, 652560.3869263144 669866.6311379407, 652546.9320319125 669883.6742198531, 652535.9096466028 669896.1786755128, 652514.2724110887 669907.7588895503, 652395.9631476623 669975.2126065448, 652384.3572848468 669983.0780938859, 652372.9894393851 669990.782612094, 652358.5287862297 669997.0387629751, 652348.7925738265 669999.3435373009, 652339.0543616012 670001.6463113676, 652288.2527122503 670018.1908694324, 652279.7504360949 670020.9597934723, 652272.1101878926 670023.4478265739, 652275.4363381193 670030.3861653599, 652286.0596245403 670052.546247619, 652301.1717658074 670084.0707910876, 652303.9590543096 670089.8850760567, 652305.7792138163 670096.8312299858, 652287.1586241572 670169.7531836139, 652284.8465567668 670178.8128057418, 652282.4984748412 670187.9954221335, 652271.1893084402 670231.8415779975, 652264.5117809641 670244.9716195648, 652260.6769035029 670252.5130690759, 652256.6629851229 670260.4054928499, 652240.5903066618 670292.0091850199, 652203.617414943 670351.5230090928, 652199.5486478531 670358.1414385842, 652195.5199301764 670364.3438772646, 652127.7778382578 670468.6374570624, 652123.696105136 670474.9218962298, 652118.2845875825 670471.1702614278, 652011.0981186881 670396.8517671876, 652006.0295614202 670393.4031772626, 651999.4821334124 670388.9484152575, 651920.4015557735 670335.9888317662, 651920.8041398915 670334.432297207, 651922.027691194 670329.7704953684, 651922.2276286612 670321.9436019291, 651922.3015641749 670314.1396928802, 651922.3623337835 670307.7197676941, 651896.9126112828 670231.7414623345, 651896.8696273911 670231.6094584807, 651890.236785301 670211.478661261, 651885.5808107991 670197.34744209, 651879.7710147211 670179.2919235197, 651876.9530085018 670171.1616654149, 651756.3117325753 669802.9423525108, 651746.1154470937 669771.8216101264, 651740.4834991358 669754.633199576, 651731.0859242956 669725.9494138065, 651728.652147921 669726.74760372, 651726.7697888354 669727.364963895, 651674.1961368079 669744.6102672778, 651630.2437532748 669759.0276712462, 651609.4100966657 669765.8620194672, 651588.6914432201 669772.6583828294, 651526.0434634612 669793.2084131367, 651520.4822874889 669795.032705896, 651514.9201114788 669796.8569985739, 651350.6148954339 669850.7531206214, 651338.128497697 669854.8495352807, 651325.608098838 669858.9559458308, 651196.2049489587 669901.4035597842, 651183.4056292308 669905.6020554312, 651175.202260185 669908.2929271484, 651178.0148166685 669895.2194097935, 651165.4896527622 669897.2798617452, 651148.2984277741 669900.1075369716, 651148.283427585 669900.1099351186, 651128.378166992 669903.3839748699, 651128.3621667846 669903.3865728931, 651109.5410203082 669906.4822466037, 651109.5328201908 669906.4836455892, 651094.481823015 669908.9592852661, 651094.3687215352 669908.9778712864, 651070.1355039141 669912.9640759821, 651070.1234037515 669912.9660744879, 651055.054206249 669915.4442118665, 651055.0362060094 669915.4472096413, 651050.409145289 669916.2086377064, 651044.2160641273 669917.2268722019, 651030.8558889097 669919.424220792, 651015.7366906313 669921.9103519408, 651007.8135866807 669923.2133725677, 651001.9545098398 669924.1766483334, 650995.9213305746 669925.1696025592, 650995.8084291308 669925.1878886076, 650990.1413547144 669926.1201880923, 650983.8202717823 669927.159406736, 650972.5161233189 669929.0190093992, 650964.1381133115 669930.3968737638, 650962.2299882458 669930.7107378958, 650952.2248569251 669932.3555011234, 650853.3535559512 669948.6162787634, 650839.2903702665 669950.9295402296, 650826.4116002198 669953.0476481072, 650820.6441240545 669953.996235107, 650808.9389693947 669955.9217880464, 650816.5310417741 669938.3439031735, 650836.2744263989 669892.6338090458, 650842.4336471402 669856.7150813226, 650835.9831364796 669718.1303068275, 650835.9232899791 669716.8403182434, 650823.982958115 669695.0671753646, 650816.630601595 669681.659471183, 650817.2237673639 669680.2275644114, 650813.8048448374 669679.7791530681, 650813.4585356857 669665.5563166336, 650813.0694379156 669649.5555006817, 650800.7251753296 669627.2433138725, 650784.8625914286 669616.2795319417, 650767.8386571148 669608.3415637501, 650763.7343945039 669602.3841473509, 650760.9209387059 669597.9748665364, 650758.1055492581 669593.0075934668, 650755.5860958026 669588.5613490823, 650752.8366923119 669583.7090823352, 650751.1530575935 669580.7379189642, 650750.1532744837 669578.9738219469, 650747.485853254 669574.2665630609, 650744.5934809023 669569.1622823004, 650741.9890461252 669564.5660294441, 650737.3730480166 669556.4195811894, 650732.8400318691 669548.4211408345, 650732.270566028 669527.1353761468, 650732.8667401414 669525.6334706859, 650736.9479325748 669515.3471180717, 650737.6511380867 669513.5742296425, 650740.7839241741 669506.7647111167, 650741.0129816564 669506.2667463276, 650741.24577639 669499.5648709878, 650743.6754068915 669493.266059087, 650749.7193238313 669477.5972247488, 650754.226056263 669471.1498697787, 650760.9471483479 669461.5348319429, 650759.3259902578 669454.5507336082, 650754.5421474437 669436.6974035901, 650754.3842041213 669436.2303909318, 650752.9687127799 669432.0392775448, 650750.2906752954 669424.1090629682, 650749.9368023804 669423.0620345913, 650748.4374857068 669417.4069318797, 650745.9626129421 669408.078762156, 650745.2019596512 669405.2097100086, 650743.6976797115 669399.2466110033, 650741.0259584704 669388.6574350566, 650739.8755092941 669384.0963592809, 650740.587574251 669375.0895758678, 650740.6592667586 669374.3064958989, 650741.4056304028 669366.1498045331, 650746.3455714956 669357.9115292492, 650746.5034643672 669350.3876566372, 650744.5501539212 669344.7114982372, 650742.8558314155 669339.1203703829, 650740.9445110896 669333.5262158986, 650730.2850588415 669287.5535654081)))" + } +}, +{ + "model": "common.municipality", + "pk": 19, + "fields": { + "fid": "", + "name_fr": "Bruxelles", + "name_nl": "Brussel", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((646189.8386885657 675919.0877706215, 646135.2408186919 675888.6849148875, 646138.5997129348 675886.0173934284, 646159.6509274219 675869.2994783064, 646172.3800376212 675866.8511296279, 646187.5613529225 675863.9459635102, 646188.178355831 675863.8121198287, 646205.3777559719 675860.0782472319, 646227.3810485544 675855.8908829564, 646251.7034533316 675852.5098724924, 646262.1554524753 675851.3631541247, 646282.5165355806 675850.1296408505, 646302.7594383579 675850.1370608468, 646316.0162477816 675850.8177158416, 646328.0481119214 675851.6972623797, 646340.0630634305 675853.0116825211, 646351.9886762594 675854.8160068367, 646367.7882275836 675858.0119923286, 646385.0048428747 675861.9900638226, 646404.4689749872 675867.3243974664, 646419.037455926 675871.5002223625, 646433.5819651962 675875.7568435506, 646438.3577784981 675877.1404554298, 646457.2040052332 675882.7406308237, 646474.3413055122 675888.0247598479, 646474.5324654167 675888.0838227057, 646489.4037234981 675892.5795217243, 646527.5363503173 675904.0010446722, 646565.7548657415 675915.6086565591, 646579.7922665201 675919.8103364521, 646611.2820368096 675929.23314507, 646630.0723455143 675934.8913935717, 646655.2394516839 675942.4513654216, 646669.0968798558 675946.5530244386, 646694.7598963847 675954.3071353552, 646705.2074972031 675957.4443857737, 646723.6968193591 675962.6506021149, 646735.102398151 675965.6639713013, 646753.2498630816 675969.2760815038, 646775.531494842 675972.6187814659, 646808.0530018378 675974.9269076074, 646822.621899794 675975.494608949, 646834.0908650582 675975.2619379491, 646851.4289496546 675974.1122358287, 646870.3269856897 675972.2004925143, 646888.2802808088 675969.5267207933, 646888.3202188464 675969.5206457498, 646900.0124632891 675967.8761759738, 646911.7045301032 675966.2326661558, 646911.9926161467 675966.1917418899, 646952.0660639461 675960.3035946807, 646992.2646679436 675954.2466643552, 647019.8779985295 675950.0971316848, 647031.4032271123 675948.3574422204, 647038.4042795718 675947.2643915585, 647044.060439924 675945.998057637, 647044.517342405 675945.8301153185, 647048.4865723969 675944.3656564341, 647053.2518227645 675941.9142648234, 647056.9616716999 675939.3821045542, 647057.3021420562 675939.1498285807, 647061.891506278 675935.0203517722, 647066.435252451 675929.3550443613, 647069.6058500553 675923.8124473335, 647075.3577743459 675915.5791531368, 647082.5057143577 675907.4852287491, 647086.7780535863 675903.4526720464, 647091.7666114442 675899.3413239325, 647097.8840869764 675895.036196136, 647100.4026189653 675893.5393995902, 647103.6104277845 675891.6329315626, 647100.1763750673 675883.6977078657, 647098.2739111923 675879.3024774598, 647092.0964491428 675865.0298592235, 647085.7672660616 675850.4031458911, 647082.7649543519 675843.4658854736, 647072.4418573314 675819.6128526169, 647067.6410689247 675808.5203723266, 647065.1916979563 675802.8610872948, 647062.7494252415 675797.2168429898, 647060.8918569883 675792.9246571731, 647048.6442929113 675764.6245521428, 647046.2848901239 675759.1742361574, 647044.1884714833 675754.3300264975, 647036.7874785069 675737.2283263763, 647032.8815811892 675728.2039357935, 647032.2486097658 675726.7408325084, 647030.4762187263 675722.6446552895, 647027.2939711851 675715.2902571178, 647024.1108361532 675707.9360188486, 647021.7665171267 675702.5188244651, 647021.5175518746 675701.9437595725, 647018.2045222103 675694.2874283614, 647009.6137134767 675674.4385695392, 647008.1531058336 675671.0624235295, 647003.6793621585 675660.7269362843, 647001.8808857701 675656.5707565057, 647001.8709482254 675656.5497954952, 647001.4480801086 675655.5718332322, 647000.961151822 675654.4457045669, 646996.7922710896 675644.812327851, 646996.4623877322 675644.0502148747, 646997.6193617882 675643.7554795676, 647003.5865290416 675642.2362258043, 647005.674513845 675641.7054469117, 647009.4136453634 675640.7518345291, 647015.2638372806 675639.2616860707, 647021.0039764505 675637.7993638476, 647026.9450505091 675636.2871467316, 647032.8951774989 675634.7707707601, 647035.6314086592 675634.074152902, 647038.7123534897 675633.2895781487, 647044.2584801799 675631.8773116377, 647049.9377352216 675630.4309817723, 647055.070809062 675629.123623606, 647060.6769323159 675627.6953645963, 647065.1101060151 675626.5667989051, 647075.2163220918 675623.9931825511, 647080.7864119372 675622.5738790167, 647092.7468395663 675619.5273744853, 647098.5349047963 675618.0530581869, 647108.4941472822 675615.5152634245, 647110.9272007061 675614.8965276256, 647113.8063228468 675614.1639275943, 647116.8833625196 675613.3803123205, 647119.1994405831 675612.7906818966, 647124.5045161628 675611.4393451862, 647130.3417501269 675609.9520750146, 647136.0458556333 675608.4996681816, 647142.0999999737 675606.9575451139, 647147.9370564113 675605.4702749068, 647153.3463268984 675604.093831189, 647161.6744806755 675601.9729524083, 647167.5236073814 675600.4826436912, 647173.3966970653 675598.9872179535, 647179.2708518093 675597.4911523507, 647184.6630819743 675596.11774649, 647190.7142086391 675594.5765830001, 647191.4173037469 675594.3976309004, 647199.0183992545 675592.4616211818, 647208.2186322678 675590.1187713719, 647209.5387329138 675589.78197641, 647216.571814127 675587.9908556576, 647222.2650916997 675586.541487399, 647225.2240908263 675585.7879373196, 647226.7371387951 675585.4020464735, 647241.1904383872 675581.7208133286, 647244.8054940278 675580.7993452594, 647252.9348424877 675578.7302815043, 647254.507886901 675578.3295181546, 647270.2370883566 675574.3231644514, 647270.3222904961 675574.3014151029, 647258.0918738557 675551.2251004295, 647249.824688086 675535.6263189052, 647268.0993340093 675529.1575807773, 647251.7687001199 675518.2987253192, 647126.5207968582 675435.0171520164, 647108.0283135135 675423.3799604304, 647101.6148630037 675419.1723359323, 647101.301783494 675418.9191800571, 647101.0788720646 675418.6793150934, 647100.8799259407 675418.4141733148, 647100.728906671 675418.1311175819, 647100.6409080249 675417.7751102792, 647100.6180543579 675417.3951111945, 647100.6129507244 675417.0141542879, 647100.6181474602 675416.5861591086, 647100.6361068678 675416.3082440218, 647100.6460640972 675416.1581667071, 647100.6741583438 675415.7292143397, 647100.6163158899 675413.9932242115, 647100.5674257787 675413.1271466821, 647100.4296597455 675411.816262613, 647100.2448558643 675410.5101727333, 647100.06093959 675409.2031229769, 647098.9253708179 675405.4990200637, 647097.538728371 675402.574878783, 647096.526853571 675400.6128540079, 647094.985293436 675397.3486970791, 647096.6134058612 675393.486694229, 647098.2282022106 675389.6562094355, 647108.6049581423 675365.2095591759, 647132.3412680877 675309.2610124366, 647155.6316526633 675253.7125665356, 647158.9364664553 675246.3829627044, 647176.8651790747 675204.3006083362, 647208.331566064 675129.9211856137, 647263.0613316252 675000.1501080864, 647265.1498193161 674995.240491456, 647266.3202275169 674992.3406629051, 647267.0043267373 674990.6466831056, 647268.5564689238 674989.8708806176, 647269.7946747977 674987.0320197708, 647270.5899018298 674985.2222146961, 647271.3922218177 674983.4733698972, 647272.3162864879 674981.896378328, 647273.7194491683 674980.0786477333, 647275.3387862105 674978.4318219332, 647277.129905888 674977.1030940635, 647278.632879515 674976.338325444, 647280.2890165617 674975.7334140181, 647281.3744551588 674975.4636294825, 647281.9820522609 674975.312665312, 647283.4031338986 674975.1118811858, 647285.3381961022 674973.8641301413, 647285.9122297568 674973.8531605011, 647287.9822034689 674974.0434919886, 647289.6520721639 674974.4236926646, 647292.9520117982 674975.514005937, 647320.5706754331 674985.0673733236, 647332.9520840554 674990.1137996805, 647339.5897103773 674992.8195458011, 647355.2919295357 674999.601401808, 647356.5888517643 675000.1615551063, 647368.7792469612 675005.421076212, 647389.9219242659 675015.9005226296, 647405.4790009164 675023.779310606, 647413.2064961423 675027.8242173421, 647416.641253484 675029.626700333, 647419.4349173097 675031.1369476785, 647427.5503557259 675035.7919760203, 647432.7081094861 675039.0375359593, 647435.5608586164 675040.7388287, 647443.1882268094 675027.946926316, 647481.9354641617 675050.6874494215, 647520.2218212215 675072.6948035654, 647527.6659517754 675078.9497344239, 647536.9081122178 675087.0728673675, 647544.6741235338 675094.2236690184, 647554.806071372 675104.0337746539, 647565.3177540058 675114.8330372672, 647574.5085784075 675124.8420658382, 647591.0622186293 675144.376863949, 647603.0962932409 675159.9801067578, 647607.5812838736 675160.497770916, 647643.8596898061 675164.6821725674, 647710.3805437752 675172.3552037234, 647718.8593785155 675173.4362330704, 647721.8698980394 675173.8201188154, 647734.7330753499 675175.4602415641, 647769.5733818786 675179.5993573074, 647770.8161576437 675172.5056580183, 647770.90922802 675171.9757545334, 647771.62075584 675167.913800902, 647772.6663783074 675161.9459865836, 647776.2916886411 675141.2517503772, 647776.8811945322 675137.8867350573, 647777.1502827086 675136.3448628968, 647777.5145713758 675134.271967522, 647781.1279814834 675113.6446091452, 647782.5048571752 675105.7827736399, 647782.9570663192 675103.2009339348, 647787.6271110986 675076.5406430919, 647787.7132547571 675076.0476983988, 647790.0817570757 675062.5311588459, 647791.3224579114 675056.5032089667, 647792.5500159622 675050.5435768211, 647795.308732054 675036.4110910136, 647797.2888336439 675026.2714313159, 647808.2991871352 674969.8762034675, 647808.5183502712 674968.7503211591, 647810.6325424036 674957.923644419, 647812.4187011609 674948.7740313321, 647813.4906365918 674948.6170443139, 647825.8288097724 674946.8156948905, 647836.5139055571 674945.2559003851, 647850.2910884486 674943.2446494467, 647855.3341014968 674942.5083150291, 647858.1831881803 674942.0925084541, 647856.7362962891 674932.0994667906, 647853.5979350776 674910.4232896952, 647842.1349390403 674902.0160445032, 647827.8121398608 674891.5123087412, 647822.3395914587 674887.4987161154, 647831.483479721 674862.5760764724, 647836.9422717084 674847.2519328108, 647837.4884349542 674845.7190544805, 647837.6853449466 674845.1661239272, 647838.1835680576 674843.767158436, 647839.3359084192 674840.5343707344, 647843.6933184911 674828.300942328, 647846.5732823774 674820.2143329888, 647846.7572304484 674819.7004404682, 647847.3935922815 674817.9144556199, 647850.1853105935 674810.0759930647, 647851.2437239306 674807.1041513283, 647852.2479823683 674804.2862214728, 647853.9976225222 674799.3735230843, 647856.7515210913 674791.6428948375, 647859.9704398055 674782.6054561101, 647862.6794163604 674775.002821105, 647866.0350591489 674765.5313232727, 647869.3484434539 674756.1788591081, 647870.5227875274 674752.999033548, 647883.6868377028 674717.3729886601, 647888.6624406078 674703.906687574, 647894.3070678909 674688.6294458127, 647897.5300325982 674679.9070444031, 647898.3222797534 674677.7620420506, 647902.4065599447 674666.7096885062, 647902.64963184 674666.0506846115, 647903.3948272355 674664.0319152807, 647905.2064130758 674659.1301042602, 647905.7951830681 674657.5356316917, 647905.8715314941 674657.3281230355, 647909.0756751136 674648.6586787619, 647910.7191172834 674644.2099627825, 647913.4387048654 674635.3955004787, 647916.1630863113 674626.5687188721, 647917.381461129 674623.291939563, 647918.0387656533 674621.524037044, 647920.833464061 674614.0065315089, 647921.5617901374 674612.0475995195, 647924.0345732772 674607.0311505924, 647925.0566735364 674604.9576957036, 647927.7641650679 674599.4654400856, 647927.9756286185 674586.5415896146, 647928.3342024921 674564.64872296, 647928.5326179932 674552.514943327, 647928.5648000501 674550.4900066371, 647928.8266670823 674534.5250714207, 647928.9344490331 674527.9851471747, 647928.9786038032 674525.2602186482, 647930.9675771587 674403.69458492, 647932.7149658063 674296.9249671205, 647932.72908736 674296.0748974694, 647932.9012414921 674285.5910248508, 647933.736341294 674215.8567941098, 647933.8525738904 674214.5859412258, 647934.1289360782 674211.574965592, 647934.9619348366 674202.4801197952, 647936.4708231568 674186.0054315659, 647936.3395713163 674179.0805256884, 647936.2084968635 674172.1565798256, 647936.817383468 674147.2458668799, 647936.9390165524 674142.2828920754, 647937.4754959187 674120.3102204719, 647938.0412238685 674097.147644495, 647938.6542117976 674072.0748536456, 647938.8942740808 674062.225062857, 647939.0369538334 674056.3891394658, 647939.1002716154 674053.7682137787, 647939.1373687893 674052.2471137332, 647939.4047157549 674041.3053373478, 647939.4057815702 674041.29829755, 647939.4760507323 674038.4293752243, 647939.6358543455 674031.8734612215, 647939.7721159367 674026.2792945909, 647939.7904864737 674025.5234644986, 647866.0391052156 674007.114625021, 647857.3213144969 674004.9387002147, 647857.488754498 674004.438245793, 647860.3434027823 673995.9020419014, 647886.8736179171 673915.573144211, 647906.8120946087 673842.4582454463, 647912.719523109 673820.4433513577, 647918.6271294657 673798.4242972346, 647938.1398788198 673724.6364508083, 647941.6763651962 673711.3269505415, 647945.2270575388 673697.9665725247, 647962.9399167668 673630.784520901, 647968.9661575813 673595.4275682567, 647971.1356542928 673582.6959740854, 647989.0082080422 673477.3842809052, 647989.5645270754 673474.1074250318, 648000.6109339308 673461.32107534, 648013.9578690933 673445.8719958123, 648021.402144088 673437.2553999306, 648021.4708457419 673437.1757291966, 648021.4726204642 673437.1779693906, 648073.4188774223 673400.6575243663, 648097.5687220786 673383.88276352, 648213.6908947607 673216.9435251583, 648237.3218359477 673182.9711223856, 648234.1107460182 673176.6787957489, 648199.167098043 673108.1902832404, 648215.025652112 673085.151524259, 648218.9034705431 673079.2139856888, 648222.1301426423 673072.4645358606, 648226.1634605726 673061.9870657296, 648229.5954347893 673045.3847882166, 648230.1030794156 673039.4668349805, 648229.7514981562 673027.2259648868, 648228.7427739355 673016.8209940648, 648226.979681641 673009.3658184587, 648224.2585917399 673002.2126018552, 648213.0110379849 672982.7674312792, 648210.8619376883 672974.8771731078, 648205.5326255404 672969.656575133, 648189.6267692591 672935.5549926067, 648179.1594899015 672913.2339503025, 648176.6798989534 672909.6936843722, 648157.6747720203 672870.8317746896, 648151.0763909192 672857.7915869745, 648143.3181548131 672842.4303223725, 648125.432452683 672807.0475538857, 648122.0603520662 672800.9192070815, 648119.6439661578 672795.7669669224, 648119.4940177717 672795.4479120104, 648115.2150766455 672786.3415665935, 648112.8011739567 672781.2042066259, 648112.7617781636 672781.1293226136, 648111.1241496559 672778.3611519646, 648109.3594563587 672775.3889679676, 648107.468786005 672772.0226928388, 648106.2311723849 672769.8195251103, 648105.207399435 672767.9965394577, 648103.1238770409 672763.8843288614, 648101.916104885 672761.5002067934, 648097.739122568 672753.2557846552, 648095.660569084 672749.1525346177, 648102.5939268227 672729.2446437237, 648174.6029866389 672693.020594099, 648181.3607625482 672689.7289789602, 648181.2193291311 672689.4515246372, 648182.0490096924 672689.0266710438, 648191.3858714234 672684.0238708758, 648192.1473874972 672683.6401284675, 648194.9641093548 672682.2212894299, 648195.0521278596 672682.3963383172, 648197.288215538 672681.2731048352, 648198.3965851333 672680.7161268533, 648210.2117720044 672674.9952383614, 648210.5507114569 672675.6925523438, 648236.7431447194 672662.4237491561, 648236.1483148736 672661.1996895049, 648246.4328682353 672655.9850074736, 648246.786718215 672656.6772033526, 648269.8500048744 672645.0489579259, 648269.7091052086 672644.7609437667, 648271.7701699107 672643.713207879, 648271.9811658468 672644.1332292011, 648285.1317916415 672637.4488342797, 648284.9317984723 672637.0468941086, 648286.9889559133 672636.0181975097, 648287.1298539158 672636.3201315171, 648308.2661058989 672625.5837994684, 648308.1200595861 672625.2889047482, 648310.1761546596 672624.2381282242, 648310.3672720457 672624.6471072341, 648312.0770449005 672623.7796462618, 648319.1089117711 672620.2120670369, 648325.3160688189 672617.063102134, 648325.1190935095 672616.6579623575, 648325.9338624246 672616.247346693, 648327.1680864561 672615.625264739, 648327.3171503467 672615.9193598405, 648348.4562407371 672605.1940673825, 648348.3093076734 672604.8929326171, 648350.3513786279 672603.8562341605, 648361.0748978176 672598.5456061049, 648366.2787664565 672595.9379521776, 648366.0393775788 672595.460327971, 648375.4267899413 672590.7584894411, 648375.6477216347 672591.213551627, 648380.8620681547 672588.5601394186, 648397.7838698484 672580.0952648536, 648398.6872954296 672581.945435717, 648400.2177835398 672581.1686716797, 648408.1627211369 672577.461205432, 648407.7822533979 672576.7096468635, 648411.37948071 672574.9301069259, 648411.5232200801 672575.2205214294, 648420.5234946255 672570.6234340222, 648420.3738984859 672570.3272588642, 648423.9501853503 672568.5096367331, 648431.8703122418 672564.4720908464, 648431.4926826964 672563.7355724563, 648436.1329606975 672561.3912063551, 648436.2594875321 672561.6409791881, 648447.0781663057 672556.1562839921, 648446.9514619529 672555.9066711351, 648451.0798039465 672553.8071996142, 648451.2065082997 672554.0568124705, 648465.5952618666 672546.7506541935, 648465.468557513 672546.5010413337, 648469.6038227865 672544.3945306814, 648469.7296395886 672544.6446234275, 648480.5642894689 672539.1909695877, 648480.4382951674 672538.940876819, 648485.8006850994 672536.2065232127, 648485.9273894347 672536.4562960727, 648496.7428734531 672530.9687201156, 648496.6161691173 672530.7189472507, 648500.7514322012 672528.63051631, 648500.8781365564 672528.8801291741, 648503.5952763876 672527.5015968591, 648508.7822851815 672524.8696205779, 648514.1443184374 672522.1490266705, 648515.2619190393 672521.5819696678, 648515.1352146837 672521.3323567988, 648519.2784655625 672519.2408868141, 648519.4055249352 672519.4903397248, 648530.2281116951 672513.9787646247, 648530.1010523221 672513.7293117077, 648534.7202094247 672511.3718228202, 648534.8690964349 672511.660797989, 648542.7576357888 672507.5608482326, 648546.3378245276 672505.768345986, 648546.4673673175 672506.0309190652, 648555.5524716541 672501.5528798747, 648555.3709327497 672501.1978214043, 648558.939942798 672499.3739580887, 648566.6763435998 672495.4579875929, 648571.3057939163 672493.1145796896, 648566.0416273975 672481.9247735702, 648561.1848948031 672471.6009282529, 648560.6929929008 672470.5554390973, 648523.2617798839 672390.9886612236, 648484.27088307 672308.4738883204, 648484.1809137554 672308.284759332, 648479.646863063 672301.0263619864, 648475.3577140808 672294.158870481, 648433.7737860208 672227.5794553002, 648429.2787778326 672220.3829826065, 648424.5876709552 672212.8725693419, 648424.5356738333 672212.7885638834, 648375.9334264062 672134.4674619386, 648373.4888508276 672130.5272054775, 648383.886678804 672123.5584715714, 648385.1017558165 672122.743588874, 648383.8515095379 672120.7936970051, 648382.8151094886 672119.1774276921, 648379.7037890682 672114.3260648183, 648381.356970435 672113.2182800511, 648401.253371603 672099.881830145, 648424.1261380995 672084.5508870765, 648429.4840221092 672080.9594243203, 648429.4625486764 672080.9271020795, 648419.6794906971 672065.1192135309, 648407.8658897902 672046.0299960392, 648392.6463048514 672020.759956189, 648367.3831745666 671977.5615434581, 648340.6524019149 671932.122020308, 648321.4155103831 671899.3562227763, 648299.6288598328 671863.5763913487, 648284.1773324885 671837.9200930236, 648288.1621124134 671835.5657272099, 648296.6212812036 671830.304261855, 648297.7344565332 671829.6447255028, 648271.4603532178 671785.9718035928, 648196.362989293 671661.190229456, 648191.3280100134 671652.8226769217, 648187.2002213622 671645.9633767947, 648186.2932081406 671644.4560844823, 648060.0155990379 671434.6313019843, 648053.4858523726 671423.7816881665, 648048.6732365951 671415.7848863589, 648046.9562832427 671412.9320744826, 648039.5976076724 671400.7041379365, 648036.6534985785 671395.8120032966, 647900.1490623086 671168.9953271128, 647886.1179618752 671145.6800082679, 647883.4100561615 671141.1808583542, 647882.8647128134 671140.2747240026, 647877.5977928211 671131.5231590839, 647820.1074657779 671012.7161292933, 647817.3847880581 671007.0897924097, 647812.1477639412 670996.2668984866, 647809.160469129 670972.9357945919, 647785.9525950861 670902.3754946869, 647775.2118641457 670906.0736738178, 647763.0125635894 670910.2742307833, 647661.0727999741 670945.3725473763, 647655.3427173899 670947.3457523894, 647657.2614060257 670933.4330415907, 647658.9589136668 670921.1145237072, 647667.2341483553 670861.0943681318, 647690.95791521 670697.1941977935, 647691.5525061716 670693.0853617851, 647691.7186075649 670691.9404363828, 647694.9393818317 670669.6900678574, 647696.0612344639 670661.9403817098, 647696.3484928935 670659.954361612, 647797.4575474834 670671.3702148615, 647806.9541712159 670673.6214084934, 647818.5826719946 670676.3778121747, 647820.8099175459 670675.008017438, 647821.3342366753 670672.2052360465, 647825.2812618456 670672.3436290257, 647827.2929309643 670657.2080628601, 647828.2489297935 670650.0273088594, 647829.2036862641 670642.8454347136, 647830.2306741085 670635.7507681374, 647831.6672825124 670626.133942764, 647835.4157520501 670601.0417514057, 647836.0042873122 670597.1009120911, 647836.5928225729 670593.1600727681, 647840.3030274579 670574.7156324086, 647841.4146681122 670569.1889560213, 647842.6945016239 670562.8320703569, 647844.3483757105 670554.609333256, 647845.7273146469 670547.7526257974, 647846.9210087672 670541.8248443033, 647848.2798783398 670535.0662131663, 647849.8307402737 670527.3573771082, 647850.8533997245 670522.2746843244, 647852.1311026122 670515.9227582405, 647853.7870904952 670507.8321795949, 647855.8682613638 670497.6895179756, 647857.4663473637 670489.9037284688, 647860.4045512014 670479.2000568327, 647860.421598637 670479.1382996637, 647862.8316440042 670470.700615876, 647863.3768500306 670468.3717508209, 647865.607932577 670458.8412593143, 647866.9787616874 670452.9854980006, 647868.6456352873 670445.8627482939, 647874.9497749393 670418.9307260264, 647875.5802243558 670416.2378757851, 647878.3396391725 670404.4524361413, 647879.4980678558 670399.503677794, 647880.3931285171 670395.6798336004, 647880.7268226941 670394.2547716816, 647881.8394382434 670389.4570459074, 647882.9529413034 670384.6591602344, 647884.6277946177 670377.59545058, 647886.0755099142 670371.4907415188, 647887.5012040092 670365.4789886279, 647889.0110758601 670359.1122103109, 647890.56374663 670352.5675193601, 647891.5753005503 670348.3006546786, 647893.580293723 670339.8470821029, 647895.087146353 670333.4943831825, 647896.6058978791 670327.0885663435, 647897.933029354 670321.492636274, 647898.718865965 670318.1788524154, 647902.4248179607 670302.5526143885, 647905.5314206758 670289.3091542854, 647908.6369584902 670276.0647340004, 647911.7135449601 670262.9761482091, 647912.4349198202 670259.9053934786, 647960.3747484474 670070.2674489078, 647967.9524568284 670040.2927334837, 647975.5299876912 670010.3170576829, 647982.5072823929 669982.7191997893, 647977.1164214249 669981.626568086, 647979.9760281587 669969.1581070593, 647995.590534868 669972.5988131914, 647998.824503445 669958.5363367706, 648004.780262428 669932.6375344933, 648009.8690116245 669910.514420883, 648011.0185709896 669905.5165410977, 648013.3727439427 669895.2780711266, 648021.3679529823 669860.5133860279, 648021.6039734362 669859.4854272229, 648029.3491312708 669825.8086981511, 648031.651261001 669815.7982196253, 648038.6109278484 669785.5343131348, 648043.4474776413 669764.5052362337, 648046.413269811 669751.6089531662, 648048.1938107065 669743.8660634839, 648048.2108595256 669743.7923064529, 648052.870349586 669723.5327983944, 648055.64771647 669711.4532823478, 648065.5408517665 669668.4340471352, 648066.7534516569 669663.1612992316, 648068.1303243407 669657.1745000602, 648075.4710997194 669625.255784105, 648079.3079805346 669608.5684553348, 648080.7287180763 669602.3938238329, 648085.0880749373 669583.4365064773, 648099.1793080922 669522.163931804, 648108.2141350964 669482.8755926901, 648112.7283518062 669463.2453426255, 648117.8179671742 669441.1132732892, 648121.9631470637 669423.0879187087, 648132.5297054773 669377.142887977, 648134.4606655751 669368.745625951, 648136.9020255631 669358.1295732583, 648141.4483856842 669338.3614090133, 648141.4924286065 669338.1694167296, 648145.0873029808 669322.0990114472, 648146.6430334933 669315.1452061316, 648149.3525962632 669303.0437644264, 648172.9711024263 669197.5018551013, 648174.6438346542 669191.7642086557, 648176.4853313373 669187.7964802352, 648177.20243554 669186.2516259908, 648180.3781153054 669181.4330288577, 648184.0585215098 669177.2735241747, 648189.4390436728 669172.8363076036, 648194.1262032585 669170.3007436444, 648200.03447478 669168.4174784323, 648261.5743468138 669149.2281669162, 648284.6459921025 669141.7940775877, 648304.3716325971 669135.6374895035, 648313.1309799666 669132.991537214, 648314.741985867 669132.7608540308, 648316.536883549 669132.5041133113, 648318.4363297496 669132.2325053615, 648325.623020457 669131.2052233448, 648327.4299518821 669131.2473602968, 648339.4948580789 669131.5268889194, 648351.6193566853 669135.1302226803, 648358.9190758097 669137.3531531662, 648371.7314894246 669141.5344823712, 648372.1684738562 669141.6775731576, 648393.374961571 669143.0557881072, 648411.4168502727 669144.2283447566, 648420.9376269365 669144.8474029336, 648466.3870484096 669147.8019566834, 648473.2109314085 669148.2456525131, 648481.0318167098 669148.753627548, 648512.7452561533 669150.8153390358, 648526.3530853287 669151.7000059044, 648542.5057084566 669154.0910399893, 648544.4695186386 669154.3821526953, 648546.8225765119 669154.7304315281, 648561.5419357861 669158.2761583979, 648576.2063034673 669163.0178634243, 648580.0601410051 669164.6003872327, 648590.3964411294 669168.8444575552, 648592.7554338933 669170.0208865694, 648597.9730482924 669172.6213215918, 648602.7465875648 669175.0020259481, 648603.1675842239 669175.2519934499, 648616.3026075478 669183.0633952639, 648663.5115155989 669212.9509376258, 648675.1737071258 669220.3340073908, 648684.8908394827 669226.4851786814, 648697.367865288 669234.3844998732, 648716.4281690548 669246.4516002894, 648731.4750021817 669255.9774499313, 648747.0496762516 669265.8371187776, 648769.1597075382 669279.8346413185, 648769.1866851742 669279.8525643353, 648779.1978775526 669286.46568469, 648784.9004554711 669290.2330428495, 648789.8819137659 669293.5240002997, 648842.5964177079 669328.3468588964, 648860.3258806237 669340.0589225907, 648889.5423205037 669359.3601114068, 648903.6299785203 669368.6666474706, 648957.006326023 669403.9266983572, 648962.5848428579 669407.6122823291, 648967.8325285754 669411.0787892943, 648971.4342225493 669413.458232141, 649026.6823137366 669450.0455299206, 649042.7799825459 669460.706209741, 649062.4653737006 669430.6558428764, 649064.9762497192 669432.2982038726, 649133.1632150577 669328.191742938, 649179.8745587096 669256.8784396095, 649185.325481688 669248.5561625046, 649177.7519452685 669211.9376414658, 649176.5305785425 669206.034690293, 649173.8013523647 669192.8385313201, 649168.6803058314 669168.0782329675, 649167.2660866283 669161.441027986, 649166.6505144337 669158.5529909857, 649171.9326220639 669156.2346165413, 649174.6497379718 669155.0429588938, 649175.3507173473 669154.7352072223, 649184.9782222703 669150.511300311, 649196.1127706775 669145.6256633194, 649204.3929893194 669141.9927866843, 649213.6534074397 669137.9299535756, 649214.6834867178 669137.4780833744, 649214.8850704527 669115.8454256954, 649215.0442203707 669098.7266248926, 649215.0927805181 669093.4546984991, 649216.7281888784 669091.0438863587, 649218.4603513662 669088.4890877191, 649248.1670691096 669064.8609677573, 649258.4310259719 669056.6973484292, 649260.0531642275 669055.4076403085, 649260.9402101489 669054.7016761774, 649265.901595998 669050.7552842521, 649265.1648424502 669048.9222591348, 649265.110899112 669048.7873743763, 649265.0429376214 669048.6172883841, 649264.9068368718 669048.2793563418, 649262.7365021376 669042.8740445896, 649260.6252563631 669037.6189780101, 649258.3189984041 669031.875734227, 649252.2718359999 669016.8201598146, 649250.9331876934 669013.4880415, 649250.4374051549 669012.2540776753, 649245.4779832626 668999.9065993489, 649245.0941688878 668998.9505654302, 649242.982200343 668989.3843541563, 649241.7497900678 668983.8013975577, 649240.0557808295 668976.1232123197, 649238.239895944 668967.8981394263, 649237.0426112798 668962.4740650009, 649235.8722919538 668957.169992278, 649266.3125393535 668910.5582573146, 649283.730858948 668883.8654983924, 649287.4455326177 668878.1239396421, 649287.4473079321 668878.1212198889, 649292.0011739729 668871.0828189048, 649349.3193093628 668783.3769318874, 649354.5122692282 668775.5155787318, 649359.0409080718 668768.6592526315, 649421.9021300141 668672.2191097259, 649431.6377960435 668657.1204776586, 649471.7886009371 668596.0081850254, 649473.68321356 668593.1242906146, 649478.202092448 668586.2410841641, 649478.5271520567 668585.7459297373, 649482.5897757799 668579.5575393332, 649541.6061959208 668488.9559041273, 649535.4980273849 668483.7741148598, 649527.4998692432 668476.9882784935, 649523.4893428031 668473.5847990558, 649518.0169094669 668468.9422794079, 649514.188300468 668465.6938598813, 649508.5928732925 668460.9462067445, 649503.3355464354 668456.4856306427, 649505.30676336 668454.2239775083, 649508.2690919375 668450.8253385588, 649513.2047676464 668445.1629667709, 649524.5502708929 668432.1485028042, 649525.2964006051 668431.2926038228, 649527.3246023752 668428.9658384277, 649530.4669393762 668425.3603438864, 649534.7626328844 668427.8968289518, 649534.9557376557 668428.0107707623, 649549.5134900942 668436.6384941735, 649561.506570616 668443.7467280126, 649599.9538724727 668413.9418289288, 649608.0438242515 668405.756830266, 649613.0342728588 668400.7075773275, 649618.4110055852 668395.2663760507, 649619.006864533 668387.924462999, 649620.9796670954 668363.6370154786, 649621.4423816979 668357.9392250776, 649623.4704679868 668332.9727530441, 649628.4475746085 668331.1173772226, 649630.1437013004 668330.0575954705, 649639.7053825569 668324.0828258459, 649640.8734112008 668323.3528961485, 649648.8349998022 668318.3769606635, 649649.0180149325 668318.2600242476, 649658.8907158662 668311.9342567316, 649661.7887816862 668310.0755902913, 649665.4270734512 668307.7450192303, 649677.631930614 668299.9256323609, 649680.7762058408 668297.9100376624, 649692.984080614 668290.0885712384, 649694.9670703436 668288.8179468518, 649704.2287736672 668282.8841007107, 649715.7672826613 668261.9368039728, 649718.5418366507 668256.9002439338, 649718.7468926448 668256.5252335315, 649729.4751204615 668237.0517400821, 649732.3696897816 668231.7961573899, 649727.3604968525 668225.9365893055, 649708.4583068145 668203.8246175562, 649688.6152969452 668188.2763035741, 649689.6184408038 668187.4484330565, 649726.5847562461 668156.9892045733, 649745.9257877739 668138.6048465483, 649750.3420262755 668136.5113208301, 649751.2851298836 668136.064399199, 649755.2743921602 668133.2589892864, 649772.873619406 668120.8802797516, 649777.5271373818 668117.6077548328, 649752.3499559464 668094.5289543597, 649736.6458976988 668080.1342473589, 649737.9168313281 668079.7694444587, 649748.5564156629 668078.58866038, 649760.5370196475 668077.2590796496, 649881.1027067313 668063.8780552464, 649891.745652827 668062.6963363033, 649889.7440832411 668059.0670079525, 649885.3430794359 668051.0846297648, 649871.6563661791 668024.3361840597, 649867.4420002999 668011.187801922, 649859.11555338 667965.2642129532, 649857.7865584608 667958.3991604013, 649856.4582735575 667951.5341079005, 649854.3967864142 667939.8640547885, 649850.3255212505 667917.5297042927, 649838.4132455259 667845.0329848281, 649837.2116136182 667834.4039863107, 649847.2692785293 667827.8521747012, 649909.9650138032 667774.990541239, 649916.1324574657 667769.8263966225, 649922.3011435574 667764.6620922678, 650039.8028739936 667665.614770026, 650044.6952202539 667661.490451762, 650051.8516078866 667655.4579506273, 650057.5264453626 667650.6740552571, 650057.803036532 667650.2654531259, 650063.5874842047 667641.7228851543, 650065.5066855702 667632.053289334, 650066.2220786245 667628.4532501532, 650066.7053426116 667626.0154118306, 650067.240644663 667623.3174222856, 650068.5453320303 667616.7446827529, 650070.3824792737 667607.4920332321, 650070.579620717 667606.5010265349, 650072.3966985503 667597.3493337011, 650073.6985441771 667590.7906738354, 650075.1263122753 667583.5937953307, 650075.4035531757 667582.2029621701, 650079.7699755322 667560.2016209447, 650079.849187047 667559.8056343216, 650081.412999884 667551.9289389843, 650082.1525477065 667548.2020241842, 650083.627380258 667540.7742738493, 650085.3822965034 667531.9315310294, 650087.0903245119 667523.3287802998, 650092.4985770662 667496.0837766943, 650093.6823146306 667490.1230173337, 650096.0917053287 667477.9834258694, 650097.0742167488 667473.0325525001, 650098.1009514857 667467.861686628, 650098.9243284455 667463.714946289, 650100.844594539 667454.0402320863, 650101.9631510971 667448.4063418867, 650108.3420835708 667415.6904701003, 650108.9536084252 667410.8656297787, 650110.3498814689 667399.8539030378, 650111.1547064141 667393.5039805991, 650111.1946813487 667393.1870284408, 650112.1906741213 667385.3312234357, 650112.8032640513 667380.5052633329, 650112.9413106288 667379.4123304036, 650113.6580126128 667373.7644707126, 650115.0121795251 667363.0788159958, 650115.9010394652 667356.0719500887, 650118.5375951225 667335.2764667579, 650120.3181588796 667321.2288158494, 650122.1038731338 667307.1550859197, 650125.2927902234 667282.0038715058, 650125.7342885758 667278.5239581221, 650126.8782765826 667269.499062663, 650128.7249307004 667254.9375051515, 650130.186406602 667243.4097921442, 650132.2363104378 667227.240114808, 650134.2680921706 667211.2145940405, 650135.2360299868 667203.4407055505, 650132.1142850732 667202.6553636597, 650126.08041422 667201.1386254688, 650123.941624832 667200.6014037941, 650113.4049369597 667197.9521146141, 650110.4060170522 667197.1986276667, 650103.6703394754 667195.504843467, 650102.9523806941 667195.3247556061, 650091.7108686916 667192.4984199373, 650082.3301921334 667190.1403118785, 650073.6385430314 667187.9552480641, 650070.817648517 667187.2469027753, 650065.8758916738 667186.0042178805, 650062.892946314 667185.2539327423, 650059.7021566516 667184.452582142, 650048.5126503862 667181.6392123214, 650038.1509676538 667179.061943476, 650031.9362299667 667177.5160623906, 650025.5415144734 667175.9253592249, 650020.1286899559 667174.5786162782, 650014.0328726587 667173.0618696494, 649998.3795933889 667169.1679522056, 649982.4371783029 667165.2019990843, 649977.1514388032 667163.8874314595, 649971.4287117851 667162.4636902604, 649965.7908262899 667161.0620394247, 649960.8201368424 667159.8254303149, 649955.7212975333 667158.5568054827, 649957.9925981122 667147.882142081, 649959.2462625754 667141.9873100155, 649962.0737999716 667128.6978590648, 649963.534543222 667121.8329814373, 649969.0505587314 667095.9039772925, 649972.2509152737 667059.7226146305, 649935.5874199022 667050.8401915095, 649937.225118437 667044.0625351605, 649942.0133699409 667024.2541862059, 649945.0488427767 667011.7357948702, 649947.993347652 666999.9602658361, 649975.076895635 666891.62460485, 649975.3530372037 666890.520489151, 649981.4163903107 666862.1616584009, 649985.0094677695 666848.9387052668, 649985.6492749047 666846.5842462262, 649992.1210407919 666822.7633478564, 649994.0818437871 666815.4935779776, 649996.1748729697 666806.8209571376, 650000.1048523977 666790.534554195, 650012.6707239347 666739.4365842957, 650021.0145988221 666706.3809660379, 650022.0051518459 666702.458085391, 650022.4283402828 666700.6962342376, 650023.3986730911 666696.6512721552, 650025.6788835474 666687.1506832745, 650027.5678710759 666679.2789908027, 650035.9112594652 666648.8003116116, 650043.3640671241 666624.8713829094, 650048.5577607497 666610.660323633, 650051.2315778837 666603.3456856761, 650051.307567162 666603.1773767492, 650051.954718875 666601.7437110543, 650052.6769722393 666600.1428563177, 650054.126317394 666596.5560309191, 650070.5580862629 666555.9005453627, 650072.4620024146 666556.7857372997, 650095.6731075904 666544.5786924183, 650102.6736873044 666539.6496993698, 650107.2350606151 666536.4413433948, 650109.0551010504 666535.1615844194, 650118.3248283578 666528.640732189, 650119.204937614 666528.0219687679, 650136.4566195047 666512.2041281175, 650148.0266529439 666501.7917624783, 650151.9771189324 666498.2362933207, 650156.9834565229 666493.7320061157, 650161.8779583533 666489.327703986, 650165.1073497141 666486.421098106, 650164.8585601662 666484.4070052418, 650164.0912683243 666478.1839256658, 650200.6613638755 666465.8329244913, 650237.9282705644 666453.55887926, 650255.6556511325 666447.7201364065, 650259.5488586293 666446.4377785708, 650276.1675439029 666430.5370679123, 650277.6894597627 666422.6483830623, 650281.2666462506 666404.101030182, 650286.3305821844 666377.8438671104, 650299.094405014 666345.254977594, 650295.639961703 666324.2526467411, 650283.6756991046 666251.5157736879, 650268.7770675648 666250.2119038075, 650266.2772185144 666249.9934966186, 650253.9244073961 666248.9128397942, 650211.523532476 666245.2023293233, 650212.5356166635 666242.2794499857, 650220.7706077315 666218.4978780653, 650224.6717303357 666207.3783368674, 650227.4629608579 666197.7488755583, 650234.2760045137 666171.0999456609, 650242.7932201695 666133.8555025284, 650243.3454673006 666131.9245537888, 650246.9437076439 666096.0594493017, 650247.4051004299 666092.4331447202, 650248.1548413585 666086.5416197889, 650248.4058189429 666086.5736526923, 650248.4528550432 666086.5797388611, 650248.865181776 666083.5977417101, 650249.4073462352 666073.922865257, 650250.4166739869 666062.8411840517, 650251.3298488641 666052.8234800906, 650252.3162707277 666040.3237294757, 650252.9523163867 666032.2637303259, 650254.7053336637 666013.9351769113, 650255.7277480946 666003.255414055, 650256.4958972579 665993.3165284198, 650257.1868889766 665993.36477988, 650262.7607596634 665959.7355678465, 650263.8682763604 665955.8258095824, 650265.7120068088 665949.3159987859, 650266.7629334065 665940.6662510524, 650270.3608297502 665924.062630915, 650274.4156601272 665908.4843470537, 650279.961452097 665884.1011734667, 650280.7108833443 665880.8072050797, 650281.7654152575 665876.1684821295, 650283.3891432701 665870.3595988647, 650284.5235716861 665865.5308071335, 650286.3603592967 665857.7045195459, 650287.0469120311 665854.7841413293, 650294.6334733602 665856.5672179731, 650308.8520409869 665859.911966045, 650325.134313661 665863.7411455757, 650325.9586481965 665860.4542266254, 650327.4933103146 665854.3306119606, 650330.7318698819 665841.4099673068, 650334.9998712146 665824.3837133956, 650336.9887874192 665816.4508850132, 650339.4807934326 665806.5072502927, 650335.5979884388 665805.4198612524, 650324.9456559739 665802.4363848391, 650315.6479387907 665799.8332461407, 650318.5626629152 665793.548534266, 650329.7864017005 665769.3491448989, 650332.3338735421 665766.1315729069, 650333.9099330433 665764.1417081882, 650337.2244868896 665759.9551208854, 650343.1262779619 665752.5020067486, 650348.6989391886 665745.4656474503, 650352.9186997898 665740.0451036609, 650357.3761659665 665734.3189921937, 650365.6015045702 665723.0029974112, 650367.8777504335 665719.8703487157, 650370.7778841064 665718.3608165222, 650376.6484869961 665713.4614479434, 650379.5866802153 665711.3220028887, 650384.2380504792 665707.9335080528, 650388.7903685896 665704.6171598221, 650391.8285016682 665702.4046080534, 650392.7416280824 665701.7386507476, 650396.289933275 665699.154167474, 650401.1502790955 665695.254821637, 650401.5523440134 665694.9769555965, 650405.0915807055 665692.528310532, 650409.3307476623 665689.5959193322, 650409.6527570643 665689.352922606, 650413.9672217167 665686.0955823663, 650414.6412403473 665685.5865529326, 650420.7086028506 665681.4054467345, 650427.3718405999 665678.578334341, 650439.3826376208 665670.5100239762, 650440.0736966298 665669.9960367387, 650444.0590467142 665667.0276116821, 650446.4732268116 665665.2309756953, 650449.176333749 665663.7622166552, 650450.0432876522 665663.2912924188, 650457.6738157445 665658.0844747234, 650462.6141505195 665654.713136494, 650465.9835576343 665651.9695892374, 650473.0750937988 665646.1945419525, 650480.2267106969 665639.6943846992, 650489.4765756854 665631.2866695388, 650497.0785114601 665614.3077243764, 650525.8543622136 665613.1945545662, 650532.1752549856 665612.9504271022, 650548.5711302782 665612.3154666581, 650549.843095271 665612.1657144958, 650566.29006849 665610.2229246284, 650568.0340060527 665610.0170747256, 650570.7618839968 665610.6974316471, 650575.5676559561 665611.9580602859, 650602.9325532345 665618.4946023707, 650641.2427999822 665628.2836170748, 650645.3744418297 665623.5542541064, 650648.2316547617 665620.2845592495, 650648.868534539 665605.2707646694, 650649.5052363828 665590.2608100025, 650651.4386575185 665544.7011901987, 650651.7827283015 665536.5983777929, 650646.4633413317 665531.8376921233, 650641.2239972134 665527.1478969066, 650626.2259989507 665519.6339480641, 650616.318857339 665514.6707410641, 650601.6777848873 665507.334759601, 650590.5581689766 665505.2473856591, 650577.8636526801 665502.864765946, 650577.7126057234 665502.8359462022, 650565.0672550004 665500.4630930851, 650551.7726329128 665497.9672752689, 650546.9057596677 665497.0536791394, 650541.6849642269 665496.0739567718, 650526.604579318 665493.2430259158, 650510.5231305528 665490.2249244986, 650507.7872449661 665489.7104870243, 650497.2815786831 665487.7391943847, 650471.6976570422 665482.9368121473, 650464.2248231678 665481.5339160345, 650467.3175092591 665475.8393005005, 650472.6054682367 665474.7499179154, 650476.0885879333 665474.0324572758, 650524.4461398575 665464.0727533628, 650534.829113522 665461.9341220518, 650535.9172001433 665461.7102654754, 650539.3381942684 665461.0057564013, 650543.864315266 665460.0733929767, 650686.1718547634 665430.7641383618, 650709.9830266694 665425.8602326331, 650729.7262099346 665421.7938709438, 650942.4585428761 665377.9797594816, 650968.4238814418 665372.6321297217, 650971.371478643 665372.0251568046, 651000.2611621965 665366.0743901307, 651001.2012131213 665365.8754735541, 651081.2210979994 665348.9370161619, 651106.3225536981 665343.623349187, 651467.3447716794 665267.2026300203, 651480.8088394237 665264.3523903862, 651485.1219632941 665263.4391942611, 651509.1703048297 665258.3480978329, 651555.6137339051 665248.5172069495, 651571.4999962298 665245.154162351, 651587.3871461557 665241.791277575, 651851.0953353412 665185.8456785141, 651885.0516788374 665178.6150622905, 651901.8483548288 665175.0385300382, 651939.4534340847 665167.0311068704, 651975.5060882225 665159.3534186408, 652024.8177513954 665149.8532778103, 652064.7533679398 665142.1598787945, 652102.6690038615 665134.8553635499, 652246.5317777186 665102.2861507443, 652259.982984788 665099.2408482891, 652259.6395316512 665103.5892899975, 652258.4566451724 665118.5635581373, 652250.8366654819 665119.7637843571, 652265.832280426 665146.1255564779, 652266.7107735722 665158.8955854207, 652268.4200946338 665180.8239559942, 652270.6864299244 665209.8931950724, 652272.7605085027 665225.9684855965, 652273.8535850599 665234.4383775985, 652269.0063520333 665244.0938136171, 652255.4671386607 665271.8063191178, 652238.7835832651 665302.0813380583, 652218.6234726728 665337.3220331032, 652200.3559907372 665368.473829041, 652199.7548704337 665369.384799106, 652191.5744142449 665381.786887462, 652182.2670152076 665394.2258040573, 652169.3449413306 665411.8443004505, 652157.7377546377 665431.1109521284, 652145.7721320638 665452.3916433346, 652133.8666723028 665473.7481819555, 652125.4981288355 665487.1072103042, 652116.4986700992 665499.7042446132, 652107.9412214227 665511.9811706705, 652100.5519944963 665523.4003130795, 652074.2457659515 665560.5153391287, 652032.5779975802 665619.0124256238, 652015.0806704821 665638.4943938748, 652009.6249729897 665645.1979209463, 652001.3627831411 665655.3497625701, 651989.5244955141 665683.3775192975, 651981.840332469 665701.5695947589, 651972.7762636734 665695.0984513455, 651952.3366048415 665684.7370160698, 651942.5053848022 665679.7529838199, 651936.258727492 665676.586118936, 651849.7853007911 665632.7467028064, 651842.074926962 665628.8397659023, 651829.834740073 665622.6320719091, 651812.8590321301 665614.0258341867, 651810.2605408998 665611.9605314176, 651798.9425113003 665605.2710396695, 651800.0397308336 665602.9142231051, 651791.9722132556 665599.1581204465, 651783.7191270192 665596.0943137859, 651775.4211358427 665593.0140213484, 651765.266971304 665587.6736442798, 651756.1566031354 665582.882364909, 651754.2604133985 665585.6861162955, 651752.0049045483 665589.0208605379, 651745.8480153609 665598.1080529889, 651740.4234089997 665603.3843006208, 651732.7137304245 665609.8764077023, 651699.346394829 665632.8489800676, 651699.1193568242 665633.0038301988, 651695.3491732221 665635.6054141731, 651665.5230450837 665656.0285281615, 651660.6677318926 665659.3558887396, 651647.5959125364 665668.314166797, 651642.351492784 665671.9083557749, 651633.4599080518 665678.0012639491, 651633.3148805351 665678.1012448344, 651628.8145919157 665681.1856116223, 651622.7362202092 665685.3508902731, 651616.1977369717 665689.831948106, 651609.6182484902 665694.3411603728, 651603.8618839803 665698.2863210638, 651599.0324880932 665701.5957639304, 651595.0292337046 665704.340195721, 651588.6148318796 665708.7353492469, 651581.184350234 665713.8282884436, 651575.9189845448 665717.4367132947, 651558.3967598783 665729.4423992166, 651552.8393868251 665733.2515850443, 651544.117682452 665739.2275526004, 651542.9326112632 665740.0403559869, 651540.3384487488 665741.8179331453, 651527.2135555778 665750.8133181634, 651526.3835087898 665751.3821684159, 651508.1360001666 665782.785667312, 651496.2607310027 665803.2240900038, 651493.6152926844 665807.7757384515, 651487.6281263458 665818.0800625657, 651484.4215021376 665823.5985161649, 651474.3574332412 665840.966297249, 651471.0899151725 665846.5877823407, 651465.1247642798 665856.8490681275, 651460.5669277692 665864.6873410606, 651450.9589513196 665881.2211007131, 651446.9381557966 665888.1384846491, 651446.1490162878 665889.4945394237, 651437.3614264322 665904.614327244, 651436.39812585 665906.2701987063, 651425.4151483937 665925.1647325692, 651422.7326060884 665929.7813342978, 651413.6027313689 665945.488114443, 651411.1772590609 665949.6597902421, 651405.7132885096 665959.060979691, 651403.7408835053 665962.4538359083, 651396.2824493812 665975.6558778407, 651393.9048971091 665979.8645195692, 651389.2350096994 665988.1298142759, 651385.2912413959 665995.1113660447, 651377.7307534422 666008.4952167306, 651375.4982626304 666012.4478264542, 651374.1340910511 666014.8616924699, 651369.5212233509 666023.0289689996, 651368.2289531047 666025.3146864048, 651366.8246585211 666027.8015062312, 651366.4855721045 666028.3704135042, 651364.5582828702 666031.6051538235, 651360.9905963754 666037.5925991969, 651357.3579331298 666043.6880346457, 651354.3663384977 666048.7075698522, 651351.5698316756 666053.5711331319, 651350.0655909239 666056.185938281, 651349.1303446905 666057.8126722659, 651348.1832142555 666059.3696456379, 651346.5339466346 666062.0823503163, 651344.9466373757 666064.6941045997, 651339.8557440885 666073.066276807, 651339.4736962048 666073.695337689, 651336.4691161038 666078.9527886143, 651336.2931790471 666079.2706810175, 651336.220211994 666079.4044295418, 651335.7740662621 666080.2157194879, 651334.6728024 666082.3345849959, 651333.7526266527 666084.1043592191, 651328.1572303413 666095.9893778402, 651327.1429057387 666098.4032909817, 651321.8095790184 666111.0923339876, 651316.0674910232 666129.7313231876, 651314.8128257393 666135.2642027342, 651314.7728685134 666135.4381151097, 651313.3130878994 666141.8768754014, 651313.160006434 666142.5507660887, 651310.677846472 666153.4994106445, 651310.3324071977 666156.791320675, 651309.6425960646 666163.3541811864, 651308.59330755 666173.3439881341, 651307.5383329062 666183.3865936953, 651306.8675339958 666189.7774591635, 651306.7415482235 666190.9774263949, 651305.5280689063 666202.5350308067, 651302.3515912768 666232.7982050632, 651301.7250389411 666238.7620423438, 651300.957042627 666246.0618430692, 651300.4065439225 666251.2997801828, 651300.0500892656 666254.6806478143, 651300.0420930063 666254.7566457383, 651299.8178414606 666256.8956274758, 651299.2351806788 666262.4333963734, 651298.9039566681 666265.5913903466, 651298.6075615184 666268.4122734457, 651294.5721077551 666306.831226856, 651292.8882713685 666322.8467906966, 651292.8182583072 666323.5237324168, 651292.1995241242 666329.4146121204, 651290.0801545731 666349.597103203, 651289.3844784336 666356.2198031184, 651288.612570807 666363.570643308, 651288.1731310764 666367.7526096143, 651287.9899270076 666369.500562158, 651287.8317783573 666371.0064412362, 651287.4904264364 666374.2533929577, 651286.3705906719 666380.3651641712, 651285.4070521044 666385.6259273313, 651284.1772747613 666392.3416761371, 651284.074275018 666392.779736762, 651284.031299476 666392.9606887072, 651283.589999314 666394.8335657672, 651279.9603270773 666410.2418437602, 651279.7221846493 666411.2537990158, 651278.5986268342 666415.7975910103, 651278.2514646744 666417.1196078392, 651276.8578843089 666422.0803189261, 651275.8194527698 666425.7250941633, 651272.7994429551 666435.4076092328, 651270.7576335173 666441.9542944491, 651266.0161966922 666455.1474177012, 651265.3560461671 666456.9833068156, 651263.3043904541 666462.726961766, 651262.305107347 666465.5227937549, 651260.5345802531 666469.9825026977, 651260.1583641043 666470.8275622189, 651258.712088378 666474.0822096732, 651255.8584196798 666480.4987110064, 651254.6431292383 666483.23163571, 651253.2946804303 666486.2653390251, 651249.3798290783 666495.0717501341, 651246.5410734066 666501.4583342577, 651246.4109335335 666501.7503133706, 651243.3312203786 666508.9338155872, 651241.6397394363 666512.8794622549, 651239.2000921584 666518.4621095444, 651239.1042179556 666518.6801741294, 651237.2536652955 666522.9147563124, 651231.229204693 666536.698907198, 651229.3946310282 666540.8965322038, 651225.1106446848 666550.6998435621, 651220.5294481873 666561.1781475955, 651217.8098123061 666567.3965909265, 651214.750887359 666574.3932198974, 651211.7691943863 666581.2137014163, 651209.1855576904 666587.1222469541, 651206.2617442117 666593.8107380316, 651204.5793266947 666597.6565483073, 651202.2888166643 666602.8961811258, 651199.6190701775 666609.0027932671, 651197.4766328408 666613.90245002, 651196.9844758807 666615.0433309763, 651195.3150113167 666618.9251427423, 651195.2170057148 666619.1550469695, 651192.9133292814 666624.6626749485, 651190.5137665049 666630.4912463501, 651189.303421108 666633.4300903936, 651188.6021132298 666635.0720576555, 651185.8597332649 666641.4909769092, 651183.074742171 666648.0090894593, 651181.1272367913 666652.5678166377, 651176.2359423311 666664.6259033617, 651171.7084335841 666677.4041885418, 651164.9405799627 666702.0481094054, 651162.5154651135 666712.3545409669, 651161.9828116302 666718.2743952918, 651161.3859493075 666725.3233467204, 651161.6280875345 666732.638163754, 651161.4318129037 666744.4471058113, 651161.3196971279 666753.0098607172, 651160.9141964401 666758.3786987672, 651160.0869480914 666768.4085419395, 651159.4631435982 666775.1363741754, 651159.2459732189 666777.427356438, 651157.8051378599 666784.2301217552, 651156.1603191843 666791.3606964655, 651154.1763893893 666799.2633773421, 651151.3621012672 666810.4019090324, 651149.7985095907 666816.5865867715, 651141.7654519565 666835.137307512, 651139.4678589455 666840.2150247501, 651139.0406888133 666841.1589573198, 651131.1778103857 666858.5375565754, 651129.6724172191 666861.8643591485, 651128.5292131626 666864.3912188642, 651125.6004519061 666870.8637171121, 651123.505968566 666875.4923469777, 651121.4013652216 666880.1430553301, 651120.6542612073 666881.7939776198, 651116.054102566 666891.9633329733, 651115.3918646026 666893.4251487092, 651112.1090805301 666900.681591843, 651108.973302921 666907.6102183235, 651107.8620564333 666910.0670034563, 651106.7247109535 666912.5818644926, 651105.4744479421 666915.3435878254, 651103.9030081033 666918.8163006536, 651096.2462550654 666935.7402168624, 651093.7815923315 666941.1867095886, 651092.7743863259 666943.4135913425, 651091.3059218401 666946.6573606933, 651088.8412588498 666952.1049736073, 651086.0305630269 666958.3165323099, 651084.7403522972 666961.1673697917, 651083.6120612797 666963.6622326832, 651080.6303742019 666970.3916830394, 651078.8788949114 666974.3443672361, 651077.889630098 666976.4512530649, 651076.4841709015 666979.7479902385, 651074.0235776779 666985.3005629645, 651071.5260547521 666990.9392099517, 651062.3876463878 667011.558892963, 651058.4779381867 667035.4630496885, 651054.7125616136 667056.1162263561, 651050.6399106843 667078.4975024856, 651049.0937967119 667088.4941387507, 651045.171891091 667131.6310164405, 651044.5566463935 667133.460034632, 651043.7894107313 667135.7389477054, 651042.6359824155 667139.1665969002, 651041.4266347052 667142.6585581419, 651035.6366455549 667159.3705670676, 651027.471359051 667179.2791924323, 651017.9838960278 667202.160732783, 651007.1635536203 667222.4150964385, 651005.9334001383 667224.7189104946, 651004.5921331954 667227.0206303679, 650995.662366387 667242.3444185762, 650989.8226281801 667250.3845324712, 650986.443053724 667255.0381229696, 650975.6636244741 667268.4704996669, 650972.8767958699 667266.3941685293, 650957.4506519561 667252.5973227331, 650938.1098488392 667235.3000164191, 650926.4652573741 667224.8856907184, 650923.1247428796 667221.8984187646, 650922.4858176587 667221.3263437981, 650919.4081500008 667218.5739826048, 650915.9955789936 667215.5225420293, 650911.8750434532 667211.8369382634, 650907.9713875744 667208.3455998531, 650903.5759367247 667204.4150035583, 650899.3494462994 667200.6345069992, 650893.0801699212 667195.0278501991, 650887.789869165 667190.2961882616, 650883.4602634375 667186.4246391123, 650877.499091724 667181.0919379911, 650872.0116180406 667176.133372979, 650860.2330809729 667165.1939112998, 650850.5603264819 667156.210855908, 650841.5012929209 667147.797791965, 650808.4143396355 667117.0699011805, 650779.3808211273 667090.1055210121, 650760.9491557128 667072.9883067186, 650747.3278661229 667060.3377383614, 650731.892748198 667046.0027942706, 650714.2101669594 667078.1622367826, 650706.7646634342 667091.7020875219, 650701.8996448715 667100.5503231069, 650695.2672554661 667112.611416065, 650673.0788051116 667152.9660386625, 650667.2826088839 667163.5072584035, 650662.386520213 667172.4105321087, 650657.6846555157 667180.9627949075, 650653.9658079939 667187.7252837031, 650651.764360819 667191.7299174789, 650643.4027675362 667206.9357190616, 650642.1465221924 667209.22145307, 650641.0202333097 667211.2702463445, 650633.7918519647 667224.4162167537, 650628.8487148068 667233.4055657325, 650619.2848454273 667250.7980723098, 650614.8830150956 667258.8044621771, 650608.3714892992 667270.931817863, 650607.5613877794 667272.4406579303, 650606.8632494775 667274.2704688542, 650601.7987279798 667287.5464778298, 650597.3068298718 667294.6919066071, 650596.7848876432 667295.5227109463, 650596.3247263926 667296.2546442485, 650596.1207429323 667296.5786146893, 650590.768705262 667305.0919191581, 650588.9793666542 667307.9386999471, 650588.093307601 667309.3484916018, 650588.0063171576 667309.4875189929, 650580.0939216841 667322.0743733328, 650568.7928138173 667339.9917789468, 650566.0813760681 667344.3123468057, 650558.3140201538 667356.6881839614, 650555.9936839575 667360.385928662, 650552.8909666712 667365.3294804227, 650548.6383780683 667372.1047062546, 650543.6074912328 667380.1210999514, 650536.0961345668 667392.0880161272, 650535.1488265267 667393.5977994794, 650530.2489574774 667401.4050928769, 650520.5431330585 667416.870573915, 650511.3917390425 667431.4513358986, 650498.2874273374 667452.3315302946, 650497.1551312783 667454.1353674657, 650495.6629787368 667456.5130729057, 650488.012612299 667468.702053281, 650484.06679615 667474.9894863953, 650475.5813204824 667488.5103479336, 650471.232507478 667495.4386838198, 650467.9469307461 667500.6742124337, 650447.968414697 667532.5043089116, 650445.4799542514 667536.4700325624, 650442.2904216307 667541.5514959466, 650425.6734627647 667568.029118963, 650423.7321545337 667571.1218014885, 650416.0878202855 667583.3007093037, 650412.4171760757 667589.1502650902, 650404.0756742988 667602.4410744478, 650377.8109298042 667644.2883709308, 650373.8622700967 667650.5798088409, 650369.7694545187 667657.1011464229, 650367.097957709 667661.3567663797, 650349.0397820229 667690.1301676581, 650341.0083028518 667703.9190761159, 650339.9558344885 667706.0981184089, 650336.9001609004 667712.4242206205, 650329.5275555742 667727.6891965277, 650299.4404114106 667789.9827777024, 650296.6307902854 667795.9642763038, 650293.8030596084 667801.9849722843, 650293.4959098657 667802.6388462365, 650290.7372434208 667808.5125126786, 650287.4894454062 667815.4270659259, 650284.3236720874 667822.1685115739, 650280.1957919721 667830.9579732618, 650277.8550621395 667835.9405027548, 650273.8452480064 667844.4789424472, 650269.6671227921 667853.3752771599, 650265.5871789495 667862.0617067865, 650256.700951501 667881.063417188, 650218.6587077152 667962.4106963724, 650208.8412608638 667983.403313308, 650207.654920577 667985.8930571146, 650204.8133496926 667991.8566351471, 650204.2942147761 667992.9466380583, 650201.812523088 667998.1531496545, 650195.134964122 668012.1681586845, 650190.4758880587 668021.946507593, 650186.4359033803 668030.4249485561, 650183.0480331907 668037.5334063848, 650181.9007518092 668039.9431563307, 650179.346089454 668045.3038977893, 650176.1323875928 668052.0463038393, 650172.3004785104 668060.0887850003, 650168.3675474259 668068.3432515346, 650164.705546841 668076.028637676, 650162.1840820288 668081.3204301451, 650150.9951617942 668106.1731848994, 650144.6425818616 668120.2837712117, 650139.1661892015 668132.4469665627, 650136.2302957318 668138.9684552685, 650133.275582218 668145.533141264, 650132.5483555023 668147.1490344516, 650130.5738712368 668151.5346644882, 650127.4281188865 668158.5212425851, 650126.2557854807 668161.1250704647, 650124.7154000278 668164.5469243145, 650122.6159235585 668169.2104561159, 650121.520644869 668171.643415344, 650120.1661497366 668174.6512165428, 650116.2841811503 668175.3027328713, 650100.7330542182 668177.9107153883, 650085.3388388982 668180.4927975832, 650060.0485322681 668184.7347672253, 650051.424420271 668186.1816930181, 650046.5801493801 668189.0119481067, 650028.5541004186 668199.5446631282, 650013.2201805387 668208.5047658767, 650010.7799387545 668209.9313312527, 650003.487615456 668214.1913511436, 649989.279646406 668222.4935197673, 649982.8040607709 668226.1874044668, 649999.408182026 668256.9896757044, 649993.0251408537 668260.10853164, 649978.1652110966 668268.4536390741, 649977.2261809035 668268.9815169523, 649949.2356269533 668284.7018774363, 649949.1674629933 668284.7397885795, 649945.0353752375 668287.0603713796, 649942.8761383699 668288.273050668, 649919.2507424335 668301.5410196376, 649919.086722808 668301.632998319, 649914.7275990115 668304.0813917526, 649896.9804603477 668314.0491653329, 649892.6211589314 668316.4975588543, 649892.3401598947 668316.6554423418, 649890.128024844 668317.897234897, 649884.4088063797 668321.1884907223, 649907.0241705319 668349.8358484, 649910.5563700825 668347.0473578413, 649914.7606687817 668352.3737234145, 649910.3711828906 668356.3792129818, 649904.7836284785 668361.4775014505, 649893.7065092121 668371.5859317313, 649892.214471029 668372.9466911025, 649881.1682398077 668383.025205818, 649880.1611804082 668383.9450704058, 649869.7561435966 668393.4396715534, 649867.6019634324 668395.4043419866, 649865.5656549775 668397.2631082458, 649868.615449417 668400.0864850662, 649871.7339282082 668402.9729094575, 649872.645813926 668403.8170101598, 649868.6682511704 668409.7183658585, 649864.2765104824 668416.2347028481, 649857.9590402714 668420.2009172569, 649861.5425990907 668424.2813030126, 649861.2438317025 668424.5927823555, 649855.8859010078 668430.1786132492, 649733.6475338992 668557.6200819729, 649723.0279490262 668571.0635875296, 649722.9739823429 668571.1226202564, 649715.1459581793 668579.8076440742, 649717.7663357282 668584.5218187061, 649768.99093934 668676.6767151402, 649771.1374984008 668680.5377236356, 649771.6363391281 668681.4350520879, 649769.8840747398 668684.3688828255, 649746.8365595815 668722.9635244436, 649752.0351140982 668726.1271497204, 649758.1005085865 668729.817752586, 649758.4725185309 668730.0438744726, 649761.6821689322 668731.9971958154, 649803.4699228159 668757.422860131, 649808.3523756382 668760.3934897515, 649822.179395534 668768.8059262278, 649828.3908590599 668772.585671533, 649831.0946741482 668774.2309403409, 649835.5763622186 668776.957532191, 649769.169530019 668887.4991177145, 649766.5547991164 668886.0087421415, 649764.6608407991 668884.9294880392, 649754.7634844413 668879.2883692002, 649753.5887014567 668878.6183164002, 649748.8819373535 668875.9358643104, 649743.654427966 668872.9562333492, 649737.7057910275 668869.5654808593, 649732.1533040135 668866.3998931537, 649726.468766935 668863.1602104894, 649719.9022995892 668859.4175481149, 649714.2907095688 668856.2188336898, 649709.0019675668 668853.2033557625, 649703.1074641037 668849.84364933, 649700.1036800815 668848.1553094088, 649698.5148239755 668847.2260100776, 649697.5208996389 668846.6599776932, 649696.21193688 668845.9137497011, 649695.799103421 668845.677703034, 649693.8811844059 668844.5856461731, 649685.5483903461 668851.4855129437, 649675.8589598717 668848.1754287165, 649673.3345003927 668847.3127358148, 649672.088156109 668846.8869912069, 649671.5086944068 668852.1951725807, 649670.0690140688 668865.3804282169, 649669.8406774733 668867.465333681, 649669.8358850677 668867.4648531113, 649664.8588876671 668866.9268205991, 649655.3239616547 668865.896725458, 649650.8320617698 668865.4110706988, 649648.7351200733 668865.1837810618, 649648.4981623831 668865.1587928415, 649647.9962026979 668865.1048130813, 649641.4173007959 668864.3939498536, 649636.445450699 668863.8573579392, 649634.553517287 668863.6521727126, 649627.920656519 668862.9353830534, 649621.9237651081 668862.2876291038, 649614.3718282768 668861.4716900277, 649595.8624446613 668874.7624084733, 649590.9020012369 668878.2617656976, 649585.768659438 668881.8830205287, 649524.9787514966 668926.0541675991, 649520.4943864137 668929.3275052186, 649516.4671202956 668932.2690621797, 649467.0471564148 668968.2965652798, 649460.9197435015 668972.5668123328, 649454.7932180543 668976.8370595084, 649413.5128486407 669006.6326645939, 649409.9268759323 669009.3080383269, 649409.3906042844 669009.7081285974, 649423.7145788705 669025.8366851337, 649429.7506745006 669032.5742849301, 649426.6874481984 669035.1428429845, 649426.3663203411 669035.4149608108, 649421.40792297 669039.6051897779, 649421.2728326326 669039.7202120282, 649417.0404787112 669043.2966163403, 649415.0853094698 669044.9483596382, 649402.5710896177 669055.5247965986, 649398.1076087576 669059.2971705897, 649385.553472738 669069.697445096, 649379.7259649558 669074.5248413036, 649378.9929990121 669075.1386651499, 649366.5916901017 669085.5309582546, 649364.3675845038 669087.3967464621, 649362.4133043616 669089.0344100951, 649377.0741869636 669106.509069732, 649379.2077720396 669109.0323340375, 649375.4212004001 669114.8638030455, 649309.4394461013 669216.4665521147, 649304.150331829 669225.3757208498, 649298.8622824994 669234.2848897331, 649277.3298913494 669238.9182387264, 649272.4189020408 669240.0237134787, 649267.5066702357 669241.1291880851, 649265.5827123757 669241.5409512472, 649254.1825654883 669243.9795479141, 649225.4270371733 669250.130888234, 649196.5114007553 669256.3162889399, 649190.5664090215 669265.3845769891, 649144.5103415013 669335.6341327237, 649123.2328540956 669368.0909960133, 649076.1891746011 669439.8474559557, 649078.6970323357 669441.4966964889, 649078.163906379 669442.3038220088, 649059.0015352446 669471.4529832692, 649075.230196989 669482.1258385045, 649098.7080404699 669497.5715043606, 649105.9227329846 669486.6054728391, 649107.3644642974 669484.4138343738, 649107.5666736816 669484.095782777, 649114.7320723452 669488.8977030349, 649119.5585822274 669492.1312020739, 649119.9185204259 669492.3731622655, 649124.9821492527 669495.7668477881, 649124.9940407012 669495.7748491153, 649125.5697999765 669496.1606734155, 649126.2760102645 669496.6338722907, 649131.7884971566 669500.3276079412, 649131.8935679579 669500.3975396823, 649134.4832422098 669502.1338288896, 649134.7712859676 669502.4376996353, 649143.6904210043 669508.3047771247, 649143.8794414395 669508.4348781902, 649225.5240315537 669564.5778966179, 649233.0424313257 669569.7476943581, 649240.5606536365 669574.9174920629, 649289.3384049793 669608.4610065436, 649294.2680107283 669611.9551543843, 649294.4618227608 669612.092615924, 649299.0094812578 669615.3380810214, 649350.4566090797 669652.0658745292, 649439.3334036756 669715.5136627393, 649466.2608402498 669734.7366520353, 649476.1958324704 669741.8287948463, 649496.5050888686 669756.3270489633, 649501.3228484164 669749.6917138994, 649505.3623663211 669752.6251215854, 649520.496892563 669763.6138388602, 649543.9843117433 669780.6671616693, 649579.9482695005 669806.7794666775, 649580.726355944 669807.3445128822, 649595.5951889949 669807.8312538161, 649610.4640219187 669808.3191147309, 649713.4217580127 669811.6933664915, 649722.178801164 669811.9803353604, 649729.2055933746 669812.2112569796, 649798.3678811163 669814.4774608223, 649801.9109193849 669814.5940451811, 649808.1022449539 669819.0876909146, 649813.2777956788 669823.1621799982, 649878.092078685 669874.1893513891, 649880.5464769348 669876.1217832112, 649882.8645690246 669877.9469999267, 649887.9343585181 669871.4595383743, 649911.5759338865 669840.5858443899, 649915.0724981434 669836.0192875555, 649915.7556205702 669835.1270619559, 649916.6917184007 669833.9046313144, 649918.9414701831 669835.7568794332, 649933.7648784189 669847.9604340701, 649968.3822353567 669876.4592900997, 649970.6943626697 669883.614515806, 649973.0075550133 669890.7697416148, 649992.0783014082 669948.4954031266, 649994.6063411627 669956.4316212506, 650001.7795220912 669954.1424349096, 650077.5280234746 669929.1829195935, 650084.7033499301 669926.7638147563, 650091.8786763663 669924.3447099007, 650153.400619984 669904.0123797482, 650159.0058940657 669902.216117873, 650164.6099255286 669900.4208158228, 650231.5381543392 669878.2770831465, 650237.1654431483 669876.4248338913, 650239.9573742164 669884.8821802494, 650241.7427198322 669890.2892049726, 650249.564711915 669913.9830084607, 650283.3621060083 670016.3519252659, 650286.1123388916 670024.597130135, 650286.1771024496 670024.7912159042, 650291.7552269055 670022.9427960041, 650357.4334860108 670001.2291182027, 650364.9805989722 669999.4279776847, 650366.2387469392 669999.1279742974, 650375.4618521084 669996.9273623911, 650400.2196788639 669988.6565500214, 650410.3000670121 669985.2886952958, 650424.0555393344 669980.6925018369, 650425.1424081598 669980.3326382205, 650434.4158550863 669977.2248422729, 650438.923968974 669975.767327568, 650443.0050078658 669974.4158396566, 650443.2160624386 669974.3457861444, 650451.6856448023 669969.5428706947, 650461.427271225 669964.0161181404, 650470.631926507 669958.7963367713, 650474.7920742697 669956.4368694617, 650477.0141569928 669955.1761939946, 650485.122681859 669950.5772322405, 650500.8086185779 669941.6802006749, 650507.4730913754 669937.9000939766, 650509.6923338269 669936.6413381192, 650506.2861475815 669926.239916371, 650475.18909124 669831.2765498467, 650455.284532165 669770.4928838336, 650454.9287767474 669769.4069354428, 650444.6494877189 669738.01715467, 650447.3115406872 669737.1453318134, 650457.6288818151 669733.7667985454, 650458.3989191757 669733.3667381256, 650459.83910741 669732.1869303533, 650459.8660907918 669732.157014063, 650481.3408298028 669707.919973356, 650524.7774245423 669658.8960446119, 650525.6975542963 669657.6060951159, 650525.8875852504 669656.706130919, 650525.837823773 669655.7360583835, 650525.6778343625 669654.7762122247, 650518.3309071487 669621.187782594, 650519.7250113355 669620.94283654, 650520.5529062574 669620.6000625947, 650521.073051803 669619.9770949179, 650521.3270888558 669619.6720102513, 650521.4931740371 669618.6331650648, 650517.0724148018 669591.6400408304, 650514.7246911627 669589.7167403531, 650511.1601552715 669586.7963448409, 650496.5679088795 669521.9705009569, 650495.5160487714 669520.5063924763, 650493.8801901886 669519.2751692098, 650490.5513430198 669518.0947778337, 650466.3650674159 669514.6907421444, 650436.4258119188 669510.3872937713, 650417.6861885614 669507.5959163341, 650410.1724476721 669506.477091147, 650410.783871576 669502.5132206173, 650411.6493146635 669496.9076438193, 650411.757672615 669496.2063867471, 650418.6507733925 669495.2282049073, 650468.2420833822 669488.1913394715, 650473.2382234602 669487.4820416073, 650486.3634045693 669485.6507156201, 650496.565488675 669484.2270257856, 650501.0884079302 669483.5954289119, 650503.9154577734 669483.3578587096, 650519.0285570028 669482.0877287412, 650534.2483574093 669482.0995139726, 650546.9180888049 669483.1412122073, 650554.2559192424 669484.3358547852, 650566.2347175718 669486.2864154988, 650566.6566228679 669486.3624661393, 650564.4564246825 669496.1561390662, 650563.1157760903 669502.1229310727, 650566.0346551491 669502.8512784883, 650569.0264840384 669503.5975545835, 650574.8562548691 669505.0532884083, 650577.5640943549 669505.7284906869, 650580.4870557884 669506.4579585507, 650580.85109474 669506.5490418711, 650586.5658500703 669507.9746819703, 650592.367577041 669509.423372374, 650595.1425090334 669510.1157025846, 650598.1673518048 669510.8699825343, 650603.9051812161 669512.3026652758, 650604.0752199442 669512.3447655111, 650606.7690374492 669513.0170860409, 650609.4639199529 669513.6894066953, 650609.5709484398 669513.7164594256, 650615.4246812374 669515.176995914, 650621.1635757315 669516.6096786966, 650616.8816507979 669532.5590088653, 650617.4215796785 669532.7449923625, 650634.1109215221 669538.4810351398, 650648.779272458 669542.6507723156, 650650.934211272 669543.2630275119, 650654.7400096429 669544.3444381983, 650660.1897452556 669545.8940435156, 650660.2367813246 669545.9031691488, 650662.0427914065 669546.2323456169, 650662.2726472099 669546.2744531629, 650682.5711356426 669549.9748059767, 650682.035805322 669552.9108586148, 650689.1125063518 669555.8505631622, 650690.5488822123 669552.3917483285, 650690.9240443973 669551.4884471425, 650692.0420738688 669548.7964624179, 650692.07563077 669548.7153476828, 650692.0791805536 669548.7169480952, 650692.0832627952 669548.7188685695, 650700.0567708836 669552.4479516526, 650715.0833275703 669562.4845279441, 650719.9020347297 669565.7031517923, 650720.8569018914 669566.3413395482, 650723.6924471519 669569.2986043222, 650725.6963365806 669571.3887396835, 650729.2547442572 669575.1020820504, 650734.2201874754 669580.2816555651, 650735.922899102 669582.0568785039, 650740.1443599671 669586.461331967, 650744.5278572583 669591.0338027821, 650748.1132417821 669594.7741878154, 650749.0346687431 669600.48717852, 650750.5516108806 669600.16640873, 650754.4051288225 669604.4727383378, 650756.5191948155 669603.9990837917, 650763.7343544068 669602.3849497661, 650767.8386595042 669608.342486171, 650784.8625063206 669616.2804543477, 650800.7251527307 669627.2441562919, 650813.0694928084 669649.556383109, 650813.4586230799 669665.5571590643, 650813.8048322308 669679.7800354876, 650817.2237597574 669680.2284468259, 650816.6305664905 669681.6603535982, 650823.9829255227 669695.0679377819, 650835.9233448902 669716.841080672, 650835.9831863793 669718.1311492561, 650842.4337320292 669856.7160037492, 650836.27434381 669892.6345714574, 650816.5309591672 669938.3447855845, 650808.9389868045 669955.9225504721, 650820.6442014443 669953.9971175357, 650826.4116076109 669953.0485505309, 650839.2902901741 669950.9303026462, 650853.3536233421 669948.6171611921, 650952.2249093328 669932.356263552, 650962.2300056419 669930.7115803174, 650964.1381557031 669930.3977361902, 650972.5160882118 669929.0198918165, 650983.8203241727 669927.1603291669, 650990.1413596021 669926.1211105146, 650995.8084765297 669925.1886910356, 650995.9213679703 669925.1704649858, 651001.954494741 669924.1774507529, 651007.8136690842 669923.214175002, 651015.7366630347 669921.9111543602, 651030.8559563104 669919.4250232227, 651044.2160240336 669917.2276346234, 651050.409080185 669916.2095201195, 651055.0362109106 669915.4480120622, 651055.0541386371 669915.4451342782, 651070.1233761405 669912.9669969039, 651070.1354463098 669912.9649183995, 651094.3687739196 669908.9787937151, 651094.4818429141 669908.9600876933, 651109.532797587 669906.4845080087, 651109.5409627054 669906.4830690189, 651128.3622441773 669903.3874753225, 651128.3782193974 669903.3847572978, 651148.2835074868 669900.1107375522, 651148.2984176754 669900.1083393925, 651165.4896951583 669897.2807041716, 651178.0147915768 669895.2201722143, 651175.20234508 669908.2937695807, 651183.4055991233 669905.6029578473, 651196.2050113567 669901.4044022135, 651325.6080812338 669858.956788246, 651338.1284425971 669854.8503776959, 651350.6149003231 669850.7540430409, 651514.9201513852 669796.8578009976, 651520.4822898849 669795.0335483133, 651526.0435408549 669793.2092955626, 651588.6914831257 669772.6591852545, 651609.4101140742 669765.8627818916, 651630.2437681815 669759.028473667, 651674.196104196 669744.6111896888, 651726.769706227 669727.3658863031, 651728.6521528257 669726.7483661402, 651731.0859316936 669725.9502762224, 651740.4834390413 669754.6340019876, 651746.115414495 669771.8224125451, 651756.3117374814 669802.9431549294, 651876.9529909054 670171.162427838, 651879.7710521169 670179.2927659483, 651885.5807306918 670197.3483245004, 651890.2367827008 670211.4794636833, 651896.8696297901 670231.6103009016, 651896.9125686771 670231.7423047498, 651922.3622936787 670307.7206101094, 651922.3015290732 670314.1405352962, 651922.2276410582 670321.9444443515, 651922.0277260869 670329.7713777949, 651920.8041997908 670334.433099637, 651920.4016231699 670335.9896741962, 651919.3995323791 670339.864071306, 651918.0318019137 670345.1516889756, 651917.2145992683 670348.0936384212, 651905.1023508629 670391.6797033979, 651902.850341345 670399.756303877, 651900.5985092972 670407.8329043668, 651892.9693580616 670433.5877031811, 651881.2094919314 670466.3160257105, 651877.9644509178 670475.191548655, 651875.5287170591 670481.8531105458, 651875.1856759773 670482.7911401106, 651874.5245057348 670484.1529669814, 651864.9710943067 670503.8175413155, 651854.8566467718 670524.637236304, 651804.7326988152 670610.20329164, 651799.3196357086 670619.4935851236, 651793.4334005986 670629.9217302689, 651755.5364722236 670688.9306368765, 651748.2253970837 670698.3525365723, 651738.686896668 670710.6432574594, 651729.5539568807 670723.1348258604, 651709.818320847 670750.1273638722, 651707.0189258435 670753.5610698838, 651699.7622026536 670759.7321256873, 651683.4576083597 670773.5959240079, 651610.5546889666 670835.5875530331, 651604.3611055547 670840.9366656194, 651598.1673446144 670846.2857781333, 651586.3421583747 670856.8253532676, 651575.4099849474 670867.230958763, 651562.1875310298 670880.6711355022, 651557.0937151508 670886.1471010587, 651556.8338229697 670886.426426647, 651551.4801148776 670892.1817177528, 651519.0962660876 670926.4334340745, 651502.0992640532 670943.0801576648, 651494.0293846618 670950.9851756984, 651481.8589720047 670962.9054939628, 651470.8628536317 670973.6760465966, 651460.3976974193 670983.9257089655, 651347.8243543097 671090.8038727195, 651342.0526683412 671096.2842336302, 651336.2800947531 671101.765394382, 651273.08118143 671162.4111528029, 651264.3000917525 671171.5378294005, 651255.9141643525 671180.2538382355, 651235.6316617262 671201.226191335, 651213.6835851257 671220.1002794043, 651205.8898201414 671225.6661503765, 651198.097297469 671231.2331414231, 651172.3916018558 671246.8898388847, 651159.7877650731 671252.6827976272, 651151.7975601682 671256.3553824574, 651129.6897682061 671247.0846019095, 651101.7063285682 671235.5233636285, 651094.60975181 671232.5194828861, 651087.5140625687 671229.5156022874, 651079.1555274242 671226.0237316696, 651037.803027563 671208.7458713751, 651019.4720339384 671201.1496188538, 651001.1410404585 671193.5533665365, 650954.1866079182 671173.9018850031, 650946.6791348875 671170.7880356222, 650939.1375837907 671167.6601022377, 650874.1703340407 671139.622338451, 650867.2617776059 671136.2216857946, 650860.3532212148 671132.8208731806, 650855.5126535277 671129.8443097105, 650847.4312043273 671124.8132896431, 650841.7687055519 671121.288630994, 650837.4870443551 671118.6231729825, 650828.0238703197 671112.7319923689, 650814.8570062772 671103.8904675387, 650808.1976126345 671099.4047365375, 650801.5371540292 671094.9190054571, 650763.3705212479 671069.4145858167, 650756.0259577732 671066.5921147035, 650755.8999394135 671066.5436197586, 650745.7684177274 671062.6494581538, 650725.7400936878 671057.2470600147, 650721.0880206107 671055.8135447999, 650716.8225257843 671054.4989557355, 650718.374634512 671045.2392384075, 650719.5413363313 670957.4202603844, 650712.1414499554 670957.0604785879, 650700.7429535148 670953.0680839859, 650696.8862392437 670959.0675519262, 650696.8802031936 670959.0765110978, 650693.6154469782 670957.7662047907, 650690.2896340499 670956.431891258, 650683.6399607125 670953.7631044531, 650679.0821878766 670951.9334451864, 650675.7913406802 670950.6120958321, 650675.6644350095 670950.5621608077, 650675.3884376622 670950.4510881541, 650666.7038383757 670946.9651006636, 650659.0912804212 670943.9091600433, 650656.4173893983 670942.8358837123, 650653.4275668239 670941.626410103, 650647.2079550093 670939.1096745279, 650636.4264900247 670934.7463194979, 650628.4270064705 670931.509373508, 650621.7324309078 670928.8005818621, 650616.5691745837 670938.960807699, 650613.7915558392 670944.3026539697, 650611.1920089342 670949.3112453681, 650606.2497628193 670959.2724600985, 650598.9443658647 670955.4927244477, 650593.5215872535 670952.6420097814, 650588.3950354006 670949.9474897989, 650585.1092075065 670948.2200652596, 650579.9636645128 670945.5154630918, 650579.8415531404 670945.4504888124, 650577.5988197799 670944.2721463506, 650574.6660146676 670942.7308431474, 650569.5472723462 670940.040164158, 650562.4678244912 670936.3193357581, 650556.6701921445 670933.2717773851, 650550.5786403271 670930.0699446419, 650544.9790841809 670927.1264094993, 650538.841563207 670923.899611444, 650533.3609237332 670921.0189702585, 650527.3652152744 670917.8672288039, 650521.7106378828 670914.8945671568, 650516.0580127457 670911.9239857504, 650507.4067348106 670907.3759335442, 650507.6666849944 670906.6199730383, 650499.1588437707 670901.9274599167, 650493.5218554364 670898.8183219429, 650493.0989030732 670898.5912325137, 650489.1396926254 670896.4659698, 650456.7607189275 670802.6988061549, 650456.6776461549 670802.7258357212, 650363.6636379356 670833.1120710104, 650362.5510481453 670828.9470177853, 650360.9878162727 670823.092966838, 650360.8879299394 670822.7179184788, 650360.0982412176 670819.7628123104, 650358.3934343508 670817.7235848301, 650358.2164935513 670817.5126853645, 650356.2657087836 670815.180470829, 650354.5190180131 670813.0932387299, 650350.6266771042 670808.4398507043, 650348.6659537582 670806.0965950931, 650347.265158217 670804.4223610598, 650346.287281105 670803.254253526, 650346.2063534958 670803.1563246376, 650341.8009377635 670797.88976026, 650337.2796321397 670792.4843031756, 650337.1806019568 670792.3673322758, 650337.1016266044 670792.2713236109, 650335.3290249251 670790.1520887372, 650335.3109225828 670790.1311267375, 650333.6011462887 670788.0878987708, 650329.7187438523 670783.447471973, 650326.4823437891 670779.5781562255, 650325.3204271805 670778.1890685107, 650316.4519633943 670765.7458727871, 650311.9227652797 670765.404042691, 650077.680704958 670747.7220618941, 650074.1346328795 670749.1827354956, 650072.2115082582 670750.0224928968, 650059.667923319 670754.997451603, 650067.9868422486 670780.030547065, 650066.5522985003 670788.6865866026, 650054.0582332376 670864.0757956102, 650052.0193502823 670876.3784179729, 650018.5393537213 671080.9051742213, 650013.8886801739 671108.8682743781, 650010.8406359538 671127.2781525266, 650008.4823515968 671141.2496175843, 649991.6982557236 671239.7144228624, 649989.1963013329 671255.0895550629, 649973.3642830417 671350.7748128776, 649966.0427658616 671354.0655809396, 649769.7121096551 671442.3094236684, 649769.4479836838 671442.057978152, 649769.0870227694 671441.7629801991, 649768.8027953382 671441.5638422176, 649768.4992265593 671441.3788892458, 649768.215063378 671441.2289223524, 649768.0753984492 671441.1627797493, 649767.7546453248 671441.0284614469, 649767.4657583965 671440.9272267058, 649767.1614147102 671440.839589794, 649766.7311990465 671440.7472240404, 649766.3519579378 671440.6951009809, 649765.9701824406 671440.6695182531, 649765.5781605818 671440.6709312762, 649765.1205651942 671440.7081161919, 649764.5802264647 671440.8024366961, 649764.0679346257 671440.9448608562, 649763.6605928872 671441.0978061773, 649763.2374881081 671441.297716322, 649746.7221901709 671448.7557274525, 649714.0431125108 671463.5015935696, 649693.2644077936 671472.8896644461, 649662.0007918994 671487.048107571, 649631.3485022287 671500.9562269158, 649606.9626223587 671511.9767394809, 649519.1504704928 671551.6794171431, 649488.5746906912 671565.5070434017, 649465.9521901426 671575.65909051, 649428.1832141011 671592.6667956909, 649061.8947996686 671751.5820203606, 649048.6428197923 671757.332966669, 649052.061806661 671767.5342289147, 649055.4901975845 671777.7634920585, 649063.7579899438 671796.16429369, 649081.7836927016 671829.9161937301, 649088.3520616594 671842.8766898132, 649092.2512589571 671849.5271710368, 649102.2295240582 671862.7682402525, 649113.7375294084 671878.4795487777, 649124.6456105666 671894.0106263729, 649125.2264593747 671894.8748476029, 649136.3713741708 671911.4549026323, 649136.8933039346 671912.2320776605, 649141.3722668085 671921.3605225194, 649142.4920075405 671923.6425537481, 649143.9911943437 671929.2327544251, 649146.5399403917 671940.6028588404, 649146.4793264722 671944.2437708771, 649146.3695328217 671950.913603167, 649146.2677873882 671957.0826420198, 649146.2177437486 671958.4544606255, 649146.1885364942 671959.2574881156, 649145.5354670668 671977.2322481222, 649145.7928122767 671997.9421288018, 649149.799563564 672107.0713225842, 649153.6070615833 672127.9416440648, 649176.875399406 672191.7747193805, 649177.0317242744 672192.2072141004, 649185.056638064 672214.40444989, 649185.1161026221 672214.3839773736, 649214.5354762225 672305.0772158507, 649233.2377270396 672369.8188488485, 649254.499377745 672438.6607555894, 649263.8854633749 672470.3516648737, 649280.9181204214 672533.9495893994, 649295.057415521 672586.7442297329, 649295.5712231417 672588.6642733542, 649314.1634058352 672653.585898722, 649334.784624958 672725.5576241986, 649352.4330607342 672787.3892489213, 649355.8589545537 672799.3913089812, 649358.5045344641 672808.6598607384, 649403.789695971 672967.430872324, 649404.4151178967 672969.6238872468, 649406.7161816516 672977.3290529725, 649410.0306915554 672988.6173474616, 649410.0526924641 672988.6923894171, 649417.3326918146 673013.4810364809, 649421.2761798804 673026.9103470885, 649426.1468824046 673037.6969181288, 649428.1568741829 673045.1830105081, 649428.8715222143 673047.8441918052, 649428.9265220538 673048.05219649, 649430.4738021748 673053.8163291954, 649431.2104506595 673056.5563924222, 649443.1289915378 673093.5584488722, 649443.464796395 673095.2394334113, 649444.7139967504 673101.499524637, 649445.9413776296 673107.6516142627, 649447.2895637897 673114.4027527898, 649448.4637268654 673120.2877985616, 649451.3130109158 673134.5640067803, 649452.024711281 673138.1280588156, 649452.7601818198 673141.8151525549, 649453.070974361 673143.3702152981, 649455.257695671 673154.3303751348, 649463.8516734395 673163.1393446537, 649477.0876395988 673179.0756945357, 649479.3092516299 673181.7509812826, 649493.681958228 673200.352564957, 649501.9507105637 673211.0515562657, 649529.528904391 673249.6374480495, 649536.4117472165 673258.8351642732, 649539.9611587734 673263.5845142272, 649543.2915717534 673268.0399201487, 649567.9694638356 673301.9155837392, 649589.5705982816 673332.748019599, 649603.048080108 673353.6694733766, 649616.1900927704 673377.5377372486, 649638.4141728515 673417.9421580761, 649641.5282484924 673423.6045661712, 649643.0980635103 673426.4586515768, 649645.9404473668 673431.6259910886, 649670.6279020084 673418.065165874, 649684.2607108261 673410.5758788396, 649695.1122845144 673404.6151940782, 649698.6316083041 673402.6817265557, 649700.406204384 673405.9779129621, 649705.7850052655 673415.9674380259, 649751.7056829755 673391.1914170629, 649739.8302272266 673369.4360861663, 649728.00002391 673347.7639603914, 649728.3792216596 673345.7709065338, 649734.9602159513 673311.1470945951, 649738.3943115643 673293.074733595, 649742.7240949553 673270.2974086105, 649745.5958756679 673255.1889493176, 649749.7574622568 673233.2946347956, 649751.3494287261 673233.4719489994, 649770.631055432 673235.6192612599, 649781.7151819053 673236.8537334953, 649787.3047254599 673237.4763354113, 649796.3905958788 673238.4883238852, 649803.4164415366 673239.2701410557, 649838.2118074851 673243.145433134, 649866.1591885518 673246.2579768237, 649868.0761517758 673246.4770916133, 649868.089109089 673246.4780532037, 649868.9591722846 673246.7882377355, 649869.7690132662 673247.5184107746, 649869.8699963796 673247.6344221341, 649872.2286348176 673250.3486074352, 649877.2479075118 673256.6792466877, 649883.5289214046 673265.3328995435, 649884.8766276207 673267.1900082197, 649908.7025228607 673299.752639801, 649919.070890407 673313.6638678547, 649922.2802893486 673317.8040648607, 649925.5798661123 673321.8745137044, 649927.579645022 673324.2248183832, 649935.618399023 673333.1556465823, 649939.6480126048 673337.3661844337, 649947.8368248426 673345.6970371072, 649956.7858427841 673354.2180620738, 649964.8448604593 673361.4189897343, 649971.9242289308 673367.3998080045, 649982.6730882715 673375.9111359823, 649988.9025819886 673380.4817827704, 649995.24196548 673384.902524631, 650001.6413567559 673389.2133550051, 650010.5007129805 673394.9643159701, 650014.9802772616 673397.764843367, 650015.4483082386 673398.049858544, 650020.0998649088 673400.8854469047, 650025.2595733126 673403.9559759079, 650030.4691696449 673406.9367519618, 650036.4286684099 673410.1774583352, 650042.4384101422 673413.3280918108, 650043.5892406661 673413.9423083368, 650055.1774163253 673420.1195232054, 650068.676604441 673427.3011250836, 650081.20571038 673433.4126972331, 650089.6332722324 673437.1657043742, 650092.1651614874 673438.2940069489, 650096.8047561459 673440.2246427219, 650101.474531609 673442.1052027113, 650107.4343140336 673444.4558378903, 650113.4239270124 673446.7165576302, 650119.473732422 673448.8072064474, 650124.8235364385 673450.437933016, 650130.2132944112 673451.9286658637, 650135.6129947361 673453.3992401422, 650146.3726356045 673456.3405438232, 650154.8463819074 673458.312613813, 650157.9772005453 673459.0409943754, 650158.7122036468 673459.2121237218, 650166.0810618879 673460.819900454, 650168.4818514403 673461.3432326203, 650175.151777329 673462.4570075372, 650177.1716767331 673462.7943748009, 650189.0314191653 673464.6257512087, 650202.9911706508 673466.3173899071, 650211.9510618238 673467.0586562821, 650216.4410444744 673467.3590513848, 650224.0308661928 673467.7100711917, 650227.8309322785 673467.8204623527, 650233.0508163769 673467.9111901191, 650238.2907733601 673467.871841602, 650243.5508008277 673467.7225766061, 650258.5177793177 673467.1094021272, 650260.8307819689 673467.0146504274, 650262.2906957655 673467.15483052, 650262.3757132856 673467.1932407254, 650263.0207104828 673467.4848381443, 650263.9755756605 673468.159030493, 650264.6205068846 673469.0050227204, 650265.1603307418 673470.0852397018, 650265.5253187994 673471.1582350208, 650277.9558485502 673507.7184781774, 650287.0796617533 673534.5532816943, 650312.1587931649 673608.3166721071, 650318.9792965935 673628.3773335443, 650324.217416221 673643.7848816573, 650332.1476115473 673667.1077311536, 650343.8435539451 673701.5087065301, 650346.5966677846 673709.6056138796, 650349.4484365971 673717.9932508357, 650387.4522479008 673829.7710355502, 650394.5987839781 673850.788696141, 650407.5851955903 673888.9840496519, 650411.260857541 673891.9924804128, 650415.8707872314 673895.7658206812, 650435.2655396846 673911.6412139116, 650476.696519278 673945.5540711969, 650489.2791409532 673955.8545866804, 650500.3911431081 673964.9499698526, 650557.8734650441 674012.062632026, 650570.2152442276 674022.1779999313, 650597.7165162968 674044.7183468405, 650611.5771922729 674056.0778934936, 650697.721721983 674126.6830473582, 650699.7964932867 674128.3832910408, 650724.8358611739 674150.9042945994, 650726.1259148071 674149.4534282553, 650742.7023391664 674163.8624084638, 650741.5191445805 674165.274248484, 650748.6803327985 674171.6400455224, 650756.4263351948 674162.9281281601, 650767.3451797523 674172.4202863873, 650768.6002613751 674171.0095753092, 650783.5507782438 674184.0052457211, 650782.1735572807 674185.5890600508, 650792.3664799929 674194.6492918236, 650789.6691873249 674197.8649271823, 650852.246620277 674253.1292093997, 650850.9634794572 674254.665035892, 650852.284295427 674255.8333504711, 650893.3809858066 674292.1671211751, 650896.1483830417 674289.01645395, 650896.9383579303 674288.1165603644, 650898.7131753081 674289.6508484639, 650917.8231661405 674306.178049277, 650918.4931597426 674306.7571678496, 650922.9216829198 674310.5976870978, 650933.3165919113 674319.7558647398, 650939.4279757002 674325.2396198099, 650944.5325093156 674329.8202170974, 650947.4790503662 674332.4645618853, 650955.5933085643 674339.7453514349, 650956.4771595071 674340.5814544943, 650958.4399122057 674342.4367233755, 650959.3008681413 674343.2518637645, 650963.7183571684 674347.4282192705, 650964.0576999388 674347.7496988713, 650964.227371333 674347.9103586734, 650977.4069007202 674360.3739770422, 651007.7575696413 674380.502553568, 651011.7982307683 674373.5471173895, 651021.9363152565 674381.2022375887, 651025.7149487595 674384.1337242764, 651033.7624482306 674388.5926108607, 651038.203216161 674390.9702649645, 651046.9136608971 674395.5633132238, 651052.3330519631 674398.9119612579, 651052.3378441237 674398.9148418326, 651053.269118844 674399.4911131869, 651063.5635102652 674404.6013148661, 651076.2818091722 674411.5158038718, 651084.467346266 674414.9007970513, 651092.6539484693 674418.2857903921, 651106.6163678889 674423.3665305001, 651204.339999757 674458.9296316723, 651380.7223365903 674523.1178412656, 651381.0443074766 674523.2350005028, 651305.1030211746 674626.704646245, 651761.8889531981 674821.2602990693, 651868.3421674164 674866.2399676227, 652097.5146673027 674963.0717587618, 652162.8638545908 674990.6836744417, 652166.37678556 674992.174179939, 652338.754548827 675065.320015632, 652415.8232244395 674989.9201233459, 652406.6458933625 674965.4892560848, 652425.7168506975 674956.6417785833, 652427.7965047631 674961.0420022802, 652431.4860621747 674964.0923569623, 652458.6932667344 674949.106100177, 652481.7994195403 674936.3788351907, 652477.390392067 674927.3484384682, 652537.5271409334 674867.3664730889, 652565.6701609893 674842.2501716285, 652567.4398540686 674843.8002994466, 652587.663197686 674814.8531230595, 652590.1095498581 674811.271375556, 652613.9614816903 674776.3536571175, 652644.6825464221 674731.3778315337, 652650.7936320066 674722.4314617958, 652656.802721977 674716.1304985064, 652664.1050531938 674708.4732266567, 652690.1076703048 674685.7867178608, 652714.6019478639 674647.7900618031, 652728.4248323466 674621.5919861221, 652773.4950582077 674529.1083948786, 652808.294308442 674445.3232972929, 652812.6672864668 674418.5040511371, 652819.9285978879 674360.2014107936, 652823.2070644913 674329.236143613, 652823.8728948688 674321.0236520987, 652824.0883210608 674318.3662601432, 652861.2649275145 674259.6214292323, 652871.1377409478 674235.2428509308, 652897.0123273925 674194.1263944955, 652902.7841831422 674180.7462586171, 652908.5336677151 674167.4179595292, 652913.8868684215 674155.0086438481, 652930.715342165 674115.1172935793, 652943.0456513752 674085.8859391613, 652950.2265535125 674068.8640873596, 652955.5281720504 674071.8156023495, 652966.8525500192 674078.121438696, 652977.7229095421 674084.1742606745, 652979.486802656 674085.1565519391, 653002.3945177908 674097.9112955299, 653015.3967077342 674105.1507726699, 653019.9624543965 674107.6933994606, 653028.5769203196 674112.4894711409, 653056.8954139577 674128.2578226253, 653059.6321288546 674129.7821503793, 653074.3263307611 674137.9639102196, 653077.2291763782 674139.5792978851, 653147.1731165822 674178.5246349116, 653164.7611156777 674188.3187414368, 653208.1816972512 674212.4949820769, 653217.5321094 674199.3602484319, 653229.1060619699 674183.1018159088, 653242.2660507857 674164.6144781621, 653266.9740688466 674129.908864241, 653278.8059662331 674113.288546538, 653281.3382278851 674109.7317694549, 653321.3827173267 674053.4802321588, 653339.6295885592 674027.8487829943, 653400.2211603947 673942.735867342, 653405.4259272506 673935.424572061, 653416.9776757759 673919.1971761314, 653428.0094438085 673903.701709763, 653440.0563162826 673886.7793408212, 653440.7985608493 673885.7364013168, 653444.5605447504 673885.4109511555, 653493.1502028926 673881.2118995222, 653507.929399518 673879.9348652965, 653543.7451579823 673855.2764699484, 653549.8527045491 673851.0711819784, 653631.5991314278 673794.7909025713, 653643.6726123644 673781.4285124501, 653656.8004184501 673766.9001860174, 653662.9650724214 673760.0779695037, 653674.2224936567 673747.6535530607, 653687.2672201259 673733.1823360552, 653697.4284396807 673721.9366524921, 653714.6997872 673702.8219899103, 653730.7711296174 673690.9080780026, 653761.933749299 673667.8081271618, 653893.2878926307 673606.6308104766, 653898.5132439241 673604.1975574282, 653916.912295282 673595.6279059162, 653921.3006260097 673592.5553548611, 653972.5626631794 673556.667121465, 654005.2515543594 673533.7814404573, 654009.041837719 673531.1302519792, 654138.6024134302 673850.2420983777, 654225.788316461 673994.2813084323, 654114.9290449534 674070.1968699729, 654116.5654484766 674096.1486989887, 654122.33296731 674125.5753947565, 654192.2034548144 674142.9349793092, 654238.2720984963 674151.871141878, 654291.6741573233 674164.121435405, 654566.8790860687 674225.8940353368, 654803.993273128 674271.0722840708, 654806.8669416144 674271.6198234811, 654785.3571406696 674309.7650982066, 654762.2134235584 674354.9341256812, 654698.9570014575 674454.6235652901, 654665.4681936648 674512.3400830416, 654594.2683619352 674634.4119231702, 654547.3008325926 674722.7493170343, 654476.2459700046 674858.0481143727, 654454.3966628892 674894.1002680459, 654433.311534117 674939.8333996478, 654411.4361815915 675000.5688831536, 654387.306010162 675038.0135529705, 654375.5552202198 675060.6217360904, 654345.5283990611 675102.1466295321, 654337.8180837235 675116.0347220051, 654320.0810326231 675149.1942690873, 654309.9929760888 675175.9530197262, 654302.3894081423 675192.5118247623, 654279.4600050534 675226.6153834448, 654217.8933286562 675311.042578721, 654191.0091350392 675345.74469967, 654132.6398524815 675437.4701421758, 654120.8728901745 675468.3488206316, 654112.3457511138 675503.4054549083, 654099.8992723927 675617.88629856, 654103.3884764429 675623.8661907548, 654106.6026199374 675629.2319746929, 654107.8354991438 675631.2797867008, 654111.073616755 675636.5385343236, 654113.375231536 675640.217181857, 654115.7378981307 675643.975356096, 654117.5147390777 675646.7557076451, 654118.8832202946 675648.8846552684, 654120.6064670271 675651.562121183, 654122.4115271929 675654.3749558645, 654124.8950742072 675658.1118647322, 654126.109873723 675659.9230363728, 654128.4720365888 675663.4132125864, 654129.7730902905 675665.3295137531, 654133.8120253963 675671.2110724932, 654136.620895885 675675.2819377662, 654140.2857217136 675680.5805761617, 654145.9112756534 675688.6976678326, 654152.1957833321 675697.8172307711, 654156.2974151516 675703.3387997581, 654161.5210198313 675710.6168096989, 654167.500330426 675718.7635435011, 654235.7344875018 675807.0201605485, 654247.2569399194 675822.1623848178, 654255.9372739356 675833.7404559627, 654260.0420684797 675839.5914629085, 654311.2979734741 675862.4158240346, 654332.0743498782 675871.6673604056, 654377.4770943823 675891.8828810251, 654380.0228699752 675893.0036572162, 654382.5310068494 675894.2052282903, 654385.0180165141 675895.4504765056, 654387.4826564979 675896.7390817209, 654389.9245718335 675898.0707238859, 654392.3425201006 675899.4446028676, 654394.7363237988 675900.8607186419, 654397.1049179912 675902.3184310838, 654399.4475927792 675903.8174200542, 654401.7638156661 675905.3570455514, 654404.0526991567 675906.9371474689, 654406.31353335 675908.5567657306, 654408.5457857266 675910.2160602739, 654410.7485689014 675911.9139110029, 654412.9215279237 675913.6498378731, 654415.0634202879 675915.4238407359, 654417.1742461126 675917.2347996068, 654419.25294041 675919.0825543527, 654421.2991482632 675920.9663049404, 654423.3119822188 675922.8855712655, 654425.2910873429 675924.8397132943, 654427.2355761823 675926.8282509175, 654429.1449163018 675928.8505440822, 654431.0187527846 675930.9057927484, 654432.8366817506 675932.9115951002, 654446.1313663148 675947.556755098, 654443.924619977 675949.7619143935, 654437.5016051541 675956.1799773725, 654466.1370891691 675996.660857593, 654483.8675811535 676020.3843988939, 654501.4749644881 676041.9871398527, 654504.2139499262 676045.3476019123, 654475.5604442769 676067.6164798655, 654422.1127963471 676093.9351538592, 654394.3274448721 676103.8603871707, 654404.246848979 676149.2008994353, 654395.4467456482 676158.176893679, 654338.4894628049 676219.3295757985, 654331.7499784685 676226.66326835, 654324.9881258153 676234.0251178257, 654311.317107139 676248.6417594381, 654297.1706258329 676262.7170676248, 654282.4993955339 676277.2851081329, 654269.7725727388 676289.7572384828, 654259.2358454942 676299.8990903558, 654256.6036021766 676302.1699556455, 654251.0007526561 676307.1148422335, 654243.8522755887 676313.1715339291, 654215.1403603648 676335.5811799195, 654207.6518381126 676339.6860060097, 654197.2380018743 676344.6607131247, 654184.7176169341 676349.3606090241, 654181.8489209623 676350.2719365748, 654159.5257624248 676355.9394453969, 654149.3992172005 676358.5422060404, 654122.6916162954 676365.7021344893, 654117.8484981185 676367.3122585634, 654097.6764643785 676376.1481583475, 654079.5827586815 676388.3383593622, 654074.1116402445 676394.6120483237, 654071.3883555599 676398.2876897004, 654062.8627187428 676411.4823925202, 654057.7051905515 676419.773541798, 654049.9433374678 676431.4424288577, 654042.2316709672 676441.9202806801, 654037.8469747037 676447.104651825, 654024.6240317066 676460.9680108273, 654019.9326898835 676465.4853093874, 654023.010678925 676467.5483944982, 654036.4695595551 676476.5699910797, 654043.9380309406 676491.1007165029, 654050.8661649849 676507.2914985206, 654062.4740847264 676526.7725730406, 654084.1605389249 676556.7750951779, 654095.5386028345 676576.1863845877, 654108.9481034906 676579.0978978091, 654112.8785108033 676577.4983853125, 654125.6028973318 676587.8078343507, 654151.7250457777 676608.9728905616, 654154.4043799863 676615.5230796704, 654153.5630024378 676626.5628866574, 654144.6322707904 676632.3515205011, 654123.341180162 676640.4490425346, 654107.9998258777 676652.447089799, 654098.8345253817 676655.9277137823, 654089.6789878713 676659.4046588644, 654078.9176642444 676670.063429429, 654068.5170929938 676675.4621275429, 654062.216931821 676674.9914504276, 654056.6369727942 676675.3606932238, 654058.8058841968 676684.4008788167, 654053.4326745993 676711.4300808707, 654050.7723933571 676715.1897274879, 654034.1220674658 676716.8775820229, 654030.2617529983 676720.1370738616, 654028.8609449663 676726.0568492628, 654031.4899206652 676735.2970981163, 654032.8590630826 676742.3773684929, 654030.658721336 676745.7970655576, 654024.18819123 676749.5861478607, 653991.4772451356 676757.0020124309, 653987.426836085 676760.1814810915, 653987.5262040652 676765.111371493, 653987.0951990486 676774.6813163594, 653977.9138403058 676784.9300058719, 653954.4831350866 676789.7171274968, 653951.6127940402 676792.5466659308, 653947.0609208125 676808.3160444531, 653938.4189266446 676825.2948231574, 653919.8085286085 676827.79248399, 653917.2582348213 676830.5119830858, 653913.8362113422 676838.0628522425, 653902.4444985826 676863.199937984, 653842.8165530686 676927.9919525395, 653812.0125726456 676961.1179082404, 653793.5766091478 676925.3359197536, 653789.0672862025 676919.8653251502, 653783.767294653 676918.1747593728, 653779.3174305891 676917.3842917465, 653772.3768247067 676921.6333101639, 653752.5694479668 676983.7402237409, 653726.9587180082 676989.6870598551, 653695.1489998013 676985.7932087305, 653687.7088403417 676985.9521174474, 653681.7485662401 676987.1513611774, 653679.1183800589 676990.0110071383, 653671.9368892881 677002.1200829372, 653662.9154963533 677013.5687741536, 653653.3044297466 677022.0474973934, 653648.003759973 677027.806943747, 653637.9435060988 677029.4955899443, 653624.2632564325 677031.4938603652, 653605.6126946791 677035.9114875383, 653583.9415710389 677043.6287076946, 653572.9258186821 677046.9466599011, 653570.5010680194 677047.676910015, 653558.8204450475 677052.1154091354, 653546.8901341958 677054.5138948571, 653532.7698758565 677054.8161248647, 653505.4725640675 677055.7407001134, 653487.0332445182 677058.4722881336, 653472.9481521676 677066.3346158932, 653468.6286675376 677070.9089149116, 653453.0842586965 677099.5847962445, 653446.7452864255 677107.7439307063, 653441.205488337 677113.8001437286, 653430.3472658714 677125.6537196818, 653427.1313187713 677131.6692638752, 653427.1195550978 677139.8351101965, 653433.5030633452 677159.6928539054, 653435.9776564444 677165.4651127434, 653439.9803173695 677183.9764903733, 653454.4436541013 677227.3038390188, 653456.6474626955 677233.9056567582, 653434.1588907395 677243.6518738926, 653420.7630090676 677250.1990980619, 653412.1080834336 677254.5402155367, 653393.9510069722 677263.3252233308, 653305.7605476093 677310.0819194231, 653301.5205108376 677312.1960893254, 653298.9524605955 677313.4167166764, 653282.1968512433 677321.3820661787, 653270.970009095 677326.6095727765, 653251.2665153944 677335.633743221, 653232.2026756486 677343.6370427783, 653225.4599678194 677346.4593725834, 653212.5567977146 677351.7418683472, 653206.3905150042 677353.8783564772, 653194.1439337451 677357.5095892266, 653183.9586610644 677359.7078131026, 653178.041746093 677360.4833846352, 653162.1464857238 677362.2503367626, 653155.242063877 677363.614899237, 653142.4159036909 677366.0239901874, 653127.599908277 677368.8379480958, 653106.2862711878 677375.0240212962, 653099.1176480869 677380.1779564683, 653095.923441308 677383.9036025883, 653092.9922774297 677389.2990266327, 653091.5388774849 677395.6655071313, 653090.458693476 677405.7318410735, 653074.0836991814 677411.6906948783, 653075.4257609237 677418.1026452987, 653076.7183268782 677424.2785916534, 653077.508097351 677431.4314261812, 653077.6150868828 677436.4576342889, 653077.4717201288 677440.6294187136, 653077.4390034812 677444.2181822647, 653077.5463608552 677452.2290035172, 653077.517449946 677458.2321457822, 653077.3070340782 677469.7411356638, 653077.1415333581 677473.4446015301, 653076.4046606905 677483.3018602871, 653074.90919694 677497.3749859175, 653074.2229154042 677505.655305216, 653073.5269304912 677514.9676139884, 653073.4113773914 677516.5141455544, 653072.2297142795 677524.5819246371, 653070.6426966304 677533.6896434984, 653070.0924501142 677538.2708131825, 653068.6334267339 677548.7882953625, 653068.1855526775 677556.8651263502, 653068.0675532958 677562.7186587594, 653068.2681054883 677567.5746401157, 653069.7918853092 677577.4598620815, 653070.9449147164 677583.8153093877, 653072.2116528583 677589.2291794524, 653074.9302792894 677598.3659180697, 653075.9154281757 677601.3138550641, 653081.063807377 677614.0846257521, 653084.0352379561 677619.7550273908, 653085.9396449723 677622.8840380937, 653087.8144895507 677625.3128913998, 653092.1234680653 677628.7708407984, 653098.6194236911 677632.364743343, 653104.2453871081 677634.9220661005, 653106.7480161231 677636.1290891955, 653109.2055838717 677637.1263485281, 653110.9869033721 677637.8492854913, 653120.93893508 677641.8831377216, 653123.9841650364 677643.2675073134, 653127.9172526088 677646.2311740962, 653132.7602459408 677650.442783825, 653135.5534333036 677653.2955886193, 653141.9099951364 677660.4688016712, 653145.9793900871 677665.0991906407, 653149.1230058793 677669.1427487419, 653152.8922250647 677673.9578984249, 653158.4164381431 677681.5278035973, 653162.2850176289 677688.108387403, 653163.7065726332 677691.577013051, 653165.10380677 677696.5841811366, 653166.1209427842 677700.658830325, 653166.6100142527 677704.7196533865, 653167.2119726457 677710.3246360465, 653167.3501901935 677713.8233403864, 653167.1911788634 677719.6151057949, 653166.3236685614 677732.23167791, 653165.367760079 677743.3588129784, 653164.3144251506 677752.7896318147, 653163.6821429863 677760.54019934, 653163.609973571 677761.4249819366, 653163.5286868004 677774.9520441071, 653164.1842206885 677786.8566540602, 653167.2834599009 677796.85918867, 653170.3736870389 677805.0055796729, 653171.8225848412 677808.4090894214, 653174.5483242539 677814.1943369692, 653178.3131456492 677821.5777798784, 653183.7616655397 677831.1378936098, 653188.7728284313 677839.1352472143, 653193.1454920928 677847.3649984963, 653202.3281166975 677865.8950965127, 653209.9622446618 677882.7085364125, 653202.2499616451 677893.5612259686, 653200.3933474295 677902.2146713331, 653199.0059482343 677908.6819561978, 653197.3967174031 677913.2557910584, 653190.9375908973 677913.5485773904, 653180.2274570378 677917.123519083, 653178.5858613428 677917.6714678195, 653176.2125325429 677918.8413988214, 653165.704916659 677924.0208307656, 653122.9670747353 677942.0240950605, 653106.4071185078 677947.8947604287, 653086.7750207473 677954.8543097205, 653078.3341310952 677957.8468216063, 653071.5272082519 677960.2600241178, 653058.1428812457 677965.0048587015, 652972.2548062237 677995.4529856133, 652966.9817353497 677997.3223857059, 652909.9524783674 678017.5398320687, 652887.3353961747 678025.5577155491, 652733.3285500644 678063.7055308045, 652704.1362055128 678070.9365153976, 652641.7558480767 678086.3882129578, 652635.4522718303 678089.2037142867, 652526.163656963 678138.0163993113, 652439.9680977486 678176.5148016227, 652430.2430764813 678145.4549996213, 652410.0880853103 678081.0838093433, 652326.0591067487 678113.521102677, 652260.8137254575 678115.3903318079, 652130.6092594949 678101.8465994814, 652120.3934340192 677980.1565797352, 652077.1653743747 677975.5481594452, 652033.8040130597 677970.9258026476, 651999.3201074419 677948.8236102659, 651957.3213928408 677903.5667808106, 651941.560062896 677886.4750496307, 651863.7935737951 677794.8774033645, 651836.7578246557 677763.0330482031, 651817.0387193051 677783.4084607677, 651777.0665046506 677762.827335943, 651774.6899213282 677761.6038502584, 651730.6638634509 677747.8912708175, 651697.4805381742 677737.5556961208, 651599.4088495597 677746.824605924, 651588.5674083956 677746.0603758163, 651562.667690579 677744.2344304734, 651553.9546361296 677743.6200654166, 651492.5125859417 677739.2885738714, 651493.3907532456 677743.1908860423, 651472.2395948677 677758.8535259586, 651440.9262016786 677782.0410607386, 651422.558037636 677742.0594676746, 651394.8542135757 677767.3811938866, 651389.0821766524 677772.6569001515, 651335.7026352013 677735.7795328076, 651316.9685775775 677638.1185984544, 651305.1190383562 677576.3472940709, 651303.824790468 677569.4620765634, 651302.5436690502 677562.6510999961, 651301.1034274874 677554.9940314342, 651299.6823446744 677547.4348843469, 651296.8281158668 677532.2577091902, 651295.9475433064 677527.586602704, 651284.8862418962 677435.3580818936, 651224.8953457671 677270.0540330181, 651181.1998411316 677186.5123781482, 651161.9419932246 677150.9093370676, 651113.4850109582 677052.3861023006, 651112.5805052948 677050.5471260528, 651110.1210501093 677046.0558195963, 651109.5458936051 677045.0053572835, 651101.759719163 677030.7848337078, 651086.3441042978 677002.3351661068, 651073.0239514968 676977.8647639503, 651071.2413664411 676974.3727329628, 651056.8133226323 676947.8119711461, 651041.5028833912 676917.0114579235, 650999.2502022841 676832.7803821926, 650998.8897826786 676832.0617836638, 650997.66229744 676829.6149324498, 650995.8907589011 676828.8427174219, 650901.4111917624 676787.6550887916, 650884.2441470315 676779.5698102867, 650806.418936999 676747.0936276786, 650735.2011268479 676718.0452812319, 650735.2027375449 676704.110850031, 650732.2177756237 676691.3453133282, 650728.6302702047 676681.0543981717, 650723.640651945 676671.1130638486, 650720.9889584138 676666.6889319503, 650712.5902268806 676654.0959942861, 650699.281532806 676634.9975404672, 650682.8737386739 676612.4014498695, 650664.2045603829 676587.03678082, 650647.2491472501 676563.1889526665, 650643.9794235226 676553.9515074049, 650647.3884557235 676537.3516081247, 650648.4545917738 676527.5286324037, 650650.9261668336 676517.7557526171, 650650.4968488239 676509.6119337473, 650653.9025058959 676503.7756149543, 650658.0981629787 676498.391871159, 650662.914087322 676484.6218825002, 650670.4231057051 676467.2236253485, 650669.560455368 676464.1581053169, 650666.8798531472 676462.6032231699, 650656.0995523602 676461.2210827306, 650672.6557997335 676389.6199808045, 650672.2249759516 676365.3332705371, 650665.2694596108 676326.989870559, 650661.4465292103 676320.3016121732, 650653.7583819398 676311.8888843292, 650624.3002701182 676303.0547067925, 650615.8644413211 676298.1610525729, 650611.1526545063 676295.531365127, 650606.7060857174 676294.1498594932, 650604.524602624 676295.7598107624, 650602.4848798343 676297.9068948978, 650598.9059527905 676304.0055894253, 650597.135904302 676307.2381374476, 650594.2155019817 676313.3370746754, 650591.7071185947 676317.5426410529, 650590.207324274 676319.9171509094, 650585.8815442554 676325.43735702, 650582.7920464635 676329.4658920801, 650575.3921091672 676335.2933894122, 650568.5331879036 676339.6195284877, 650552.6856170498 676347.4617867364, 650554.4672810353 676351.2248558681, 650543.7903423243 676360.0933539391, 650536.0121061231 676364.7859343154, 650530.1957536489 676368.1458132498, 650528.4567085203 676368.8201487277, 650525.1052123532 676369.8526784598, 650512.8155545429 676372.1935139362, 650504.9896327505 676374.81762728, 650499.6609552385 676377.463813969, 650493.6501484282 676380.0836752615, 650491.569196684 676382.651070448, 650490.1362890361 676385.289586351, 650480.095686372 676397.2302962757, 650475.5978536082 676401.6893707067, 650474.5331283198 676402.3849106552, 650471.2641365682 676403.7020881664, 650329.8923294885 676452.8248507251, 650327.5091189921 676453.6530239811, 650326.9143853784 676451.5281687257, 650271.6787595897 676469.4228315358, 650254.4285951069 676475.6137296839, 650239.4383704705 676480.1841263311, 650231.8901044204 676482.4042785884, 650219.4090259804 676485.3855647241, 650205.765471788 676488.6118227231, 650196.7485461913 676491.0450687194, 650188.5461689114 676493.4697770556, 650174.6249486437 676497.2043165717, 650169.2243342103 676498.8578680248, 650164.3303827718 676499.8270088914, 650158.0775153447 676500.6214218807, 650152.6288186491 676500.5714628355, 650148.099788648 676500.1617817503, 650139.4851657723 676503.5592365749, 650131.2927343622 676507.6452178222, 650124.6250460744 676511.538750985, 650118.3673167347 676514.4617840963, 650103.0245172391 676521.4164477406, 650090.8616466753 676528.1539099623, 650082.5289179952 676532.8661081978, 650066.7574784348 676542.6976525506, 650036.2245471979 676559.5100133178, 649664.0384942945 676757.1503039945, 649640.1345977974 676753.4648183339, 649634.5958402319 676752.6111804133, 649634.3743251453 676752.5794732784, 649620.7227476726 676750.6282010177, 649615.6302062833 676749.9152570635, 649611.6166387373 676749.5701233863, 649609.8414888463 676749.417585039, 649596.8250463668 676748.6220611108, 649576.5373832458 676747.6005192536, 649573.7764022858 676747.4616187717, 649558.4983126702 676747.8080448253, 649545.1769268553 676748.5452696088, 649542.5960747744 676748.6879889306, 649523.0161876894 676749.9764342974, 649437.6947831011 676755.5912648756, 649436.6613761479 676755.6581763803, 649415.1145813379 676760.3730681102, 649408.4482599435 676761.8317502241, 649401.613590506 676762.439058967, 649400.4433275533 676762.5429132106, 649193.9833524097 676781.458315738, 649146.0939918747 676779.5082456946, 649088.468155019 676777.0836948348, 649033.7878907156 676774.8559868596, 648997.8919116341 676773.3173176044, 648932.259998586 676770.7400970497, 648910.1476068577 676770.0498463038, 648894.6150277792 676770.0146442996, 648856.7314743303 676771.0365071979, 648846.0692301969 676771.5246235243, 648824.4413329206 676773.0494540028, 648808.7161782825 676774.4604559839, 648784.9644295557 676776.644218139, 648777.9781614923 676777.3790273595, 648760.8924652123 676778.8938602001, 648732.8308128903 676781.5182055196, 648721.8610815222 676782.5205196119, 648687.4171897335 676785.8869490328, 648675.7021381947 676786.9059707976, 648659.6425519509 676788.5254099414, 648649.9778036167 676789.4386063397, 648633.502166996 676790.9952745608, 648594.5977222541 676794.3917120714, 648582.913189597 676795.5070569143, 648571.6484457736 676796.5904539791, 648556.727147365 676798.3271531604, 648545.941061796 676799.9024453942, 648470.597684129 676823.2420492201, 648402.3886376577 676850.2461870115, 648315.1400694557 676882.7279259063, 648293.2652621106 676887.448541189, 648283.2901466343 676889.2330526253, 648262.9289182673 676892.0170711332, 648244.018102849 676894.2031939959, 648228.3421096843 676895.6865227842, 648209.6197141628 676897.059395664, 648188.9386697385 676897.9761900099, 648174.4392701844 676898.3247936417, 648164.4380762774 676898.4435555637, 648138.0260582586 676898.3706890821, 648120.4033197353 676897.7549142091, 648103.817578703 676896.7168710221, 648091.3425384094 676895.6633364266, 648079.7587341389 676894.4908730232, 648068.4600065163 676893.2024458082, 648047.4421208875 676890.3579480883, 648027.3424797484 676887.1194871555, 648004.8949871897 676882.935943517, 647972.1630680632 676876.7143441734, 647939.4126859775 676870.5188227125, 647886.5897166046 676860.4382902998, 647829.321717514 676849.6287350599, 647754.3388102276 676835.3086991135, 647655.1668068602 676816.4669918801, 647530.440603145 676792.7151318886, 647500.266837627 676786.1007385228, 647464.2147612527 676776.9741208553, 647439.992951638 676769.9255881751, 647421.810939711 676764.1685111355, 647397.2335632587 676755.8839461516, 647381.7209563385 676750.1860791276, 647337.3621385061 676731.3550041663, 647312.3274791386 676720.363848775, 647304.7511874525 676716.5007071894, 647281.6329047792 676704.6857967832, 647264.6135507653 676695.5036167337, 647240.802813168 676682.4031935321, 647205.314327547 676660.6217275097, 647127.8829717962 676610.6687828584, 647059.38382299 676570.7846966963, 647007.1143509699 676539.9412553515, 646929.1968784026 676494.1943171042, 646913.8564588275 676485.2897901522, 646893.0615047126 676473.1283020946, 646860.2882364305 676451.2641507129, 646835.2151242401 676433.5236303278, 646824.5719726651 676425.2558380122, 646810.0913532526 676413.31032836, 646791.7952792818 676396.1095195145, 646786.9937761975 676391.2032152067, 646779.70546332 676383.5916706716, 646767.7866775221 676370.2159314688, 646762.0226246896 676363.5096862373, 646749.4828282804 676348.0678110274, 646736.8800268826 676331.014424036, 646724.8093164655 676314.4134186738, 646709.0406569222 676292.5491297207, 646695.961566715 676274.4553759005, 646676.6309549706 676247.0444642762, 646659.6574740194 676223.2504485373, 646630.1101657681 676182.0337781447, 646623.2392939281 676171.6495151997, 646609.9476171797 676154.5989288, 646593.0209319319 676134.4572068006, 646584.8071231467 676125.4234865998, 646557.5388394243 676098.7184807351, 646542.2177328527 676085.4661704309, 646525.5872307884 676072.0677813756, 646504.0790374887 676057.2156887125, 646487.126568079 676046.7709920745, 646467.0402525305 676035.6174384542, 646449.2350643113 676026.827661657, 646437.9122767843 676021.5770450691, 646402.1881536355 676006.8736097575, 646209.3876961845 675927.2201452423, 646196.7984590112 675921.9522678537, 646189.8386885657 675919.0877706215)))" + } +}, +{ + "model": "common.municipality", + "pk": 20, + "fields": { + "fid": "", + "name_fr": "Berchem-Sainte-Agathe", + "name_nl": "Sint-Agatha-Berchem", + "niscode": null, + "geom": "SRID=3812;MULTIPOLYGON (((643694.7095082543 672712.1561730297, 643695.8455342454 672704.7571824691, 643696.9318104535 672700.6138312833, 643698.2645400679 672698.640885017, 643700.8267314419 672695.8280885101, 643701.6532856876 672692.0144640801, 643701.2208808733 672689.3018037134, 643700.4345433379 672686.5887828275, 643700.3954173734 672684.3478424099, 643701.6941269404 672679.0405280846, 643702.8327773582 672673.3720793455, 643702.9506155964 672669.2601370186, 643701.9715080559 672666.5971735223, 643703.3431836842 672662.6029734964, 643705.0220750369 672661.7425349809, 643706.4971575957 672659.7772850082, 643707.0983172304 672657.1565820122, 643706.6069713401 672654.5357539183, 643707.153557323 672652.5702376342, 643707.8091217626 672652.0787979874, 643709.3386158851 672652.1882519613, 643710.4858114221 672652.0245450938, 643711.032077289 672651.2055764528, 643711.1963976292 672647.2745975369, 643711.3606917835 672644.9813610093, 643713.0542064866 672642.1425454811, 643713.7129480595 672640.3671200186, 643715.7162846231 672635.4062828134, 643717.3903601308 672627.595678576, 643718.070803853 672622.5847704265, 643720.5360764307 672614.092184198, 643723.7660139356 672605.5995256106, 643727.6760906328 672593.1151479839, 643733.7539115147 672572.4562269159, 643733.9476617556 672570.2317930845, 643734.3353666294 672564.1386231661, 643735.2550478355 672560.8018045186, 643738.1108253148 672552.2427040522, 643743.6152841479 672537.6158126635, 643745.5435399143 672531.2097027865, 643746.6166177182 672526.1404404745, 643747.8216016216 672519.4456108332, 643752.0379746484 672504.6270541344, 643754.0375968718 672498.1223936167, 643760.1581392369 672482.1746276609, 643762.7385720562 672473.9609323149, 643764.0514182855 672467.7208302962, 643764.6734707785 672462.655516368, 643766.9691770164 672457.4470359687, 643766.9639461194 672453.8320344336, 643766.4884484294 672447.9432435213, 643767.7980711351 672446.2184523926, 643770.4168285377 672443.8394185237, 643772.3808891907 672442.114542487, 643774.4642952721 672438.4269813703, 643776.9397967918 672433.3046007738, 643778.6005602992 672425.4736760205, 643780.2620616267 672413.1303201979, 643779.8640219694 672409.5466332929, 643778.8685753862 672406.9583870613, 643776.0141343682 672401.9144340454, 643774.0890895244 672398.197773315, 643773.6067863816 672395.6539054677, 643772.9465344166 672386.730246173, 643772.9483652974 672371.9896057474, 643773.9420559356 672358.7034634689, 643774.2076455855 672349.7138311589, 643774.1091502276 672348.1523967325, 643775.1049302379 672348.0545921196, 643802.9432823865 672345.322137951, 643806.1319083689 672345.0090674508, 643796.3747720969 672250.8964867797, 643796.4484980479 672250.3809808365, 643797.4893660847 672243.1050192775, 643797.3914612188 672238.2162609937, 643797.301847905 672233.737419221, 643793.7752138451 672217.3090318536, 643786.6280707241 672194.2903795011, 643783.07920145 672186.8303722263, 643774.5019928124 672172.0655457266, 643912.448426242 672093.8690610892, 643914.459628683 672092.7291525066, 643919.7522806154 672108.510356843, 643925.260776173 672119.9607657804, 643930.8828163976 672131.8713694535, 643933.5576653904 672138.0679352175, 643934.4213019618 672140.0860943731, 643936.9071069619 672146.3463172482, 643938.4899745064 672145.450192607, 644023.6934083985 672097.205898269, 644021.3182556598 672093.2493540951, 644011.3186213032 672076.5932102303, 644002.757478561 672063.8963806331, 643993.5171853027 672048.6639861292, 644021.9598914373 672031.8857914895, 644046.4088596978 672016.6615074221, 644049.3342642449 672014.8455554032, 644032.9845849238 671994.1180026735, 644046.2673508408 671988.3733992123, 644052.7300716637 671985.5785154813, 644059.4377538887 671982.6774219424, 644068.5943400353 671978.717270152, 644074.8495535929 671976.0121208826, 644159.5847793056 671937.4227490308, 644177.1576329059 671929.2599623714, 644241.4079681044 671900.4107046888, 644245.8088679948 671914.7977507774, 644280.5320990481 671838.6376534862, 644340.3385250542 671912.0431255521, 644409.3745892068 671805.6713687992, 644443.2256234548 671721.9483901482, 644433.1894864317 671716.1999141708, 644430.8676980337 671714.1695856241, 644429.9093080774 671713.3315630301, 644453.2555997707 671704.3059868164, 644472.526398808 671664.6558660752, 644480.1461688875 671666.7066651308, 644487.5678567429 671668.7056013176, 644495.0585895831 671670.7213454964, 644501.9422965442 671672.5752596301, 644509.4569909071 671674.5980465702, 644525.5224057261 671678.922939982, 644542.8765966254 671683.594862089, 644587.8067911214 671695.7150845734, 644600.7442479309 671699.2044168727, 644607.7630107824 671701.2274646867, 644607.8570815707 671701.2564354893, 644617.7636174797 671704.3526123648, 644623.7122602906 671706.2121750322, 644634.343850983 671709.5345151334, 644635.7128464036 671709.962672241, 644642.563503484 671712.1034584387, 644648.9700756315 671714.106113667, 644655.1508780741 671716.0378629926, 644661.8996547477 671718.1475974956, 644669.8672688305 671720.6374718928, 644673.5770282772 671721.7969376387, 644677.9708417213 671723.1704018877, 644689.2414037539 671726.6928153506, 644691.4943092049 671727.3970739042, 644693.0408065066 671722.0472514216, 644697.9349401543 671705.1181463439, 644703.7973881135 671684.8380025018, 644706.4105758704 671675.7984376115, 644708.4891146785 671672.6955276281, 644718.3005848057 671670.8797605775, 644741.4292691295 671667.528639981, 644764.5780028385 671668.5584216814, 644771.2378553415 671669.2062456453, 644782.3106300097 671670.2835490729, 644785.2906215187 671670.5739798567, 644792.338481738 671671.2597694937, 644799.3344480047 671671.0276455405, 644807.1185121379 671670.7695357661, 644810.5474479216 671670.6560255327, 644814.4524373489 671670.5273721832, 644817.8103733761 671670.4157734727, 644824.9153242684 671670.1806225356, 644831.8163282922 671669.9515272361, 644838.821279686 671670.2653168695, 644841.7673505864 671669.2647213112, 644852.7001369572 671665.5499137698, 644852.506586461 671667.6130624503, 644852.2341239864 671670.5078703358, 644851.4862285142 671678.4627122708, 644851.3881211058 671679.5017263506, 644850.9224633772 671684.4576029868, 644856.5610646391 671687.3631944426, 644860.8188128917 671689.5577512542, 644870.0121343756 671694.2957407995, 644871.7520446833 671695.1919356553, 644872.3769782833 671695.5139256429, 644874.4187948281 671696.566154305, 644876.6457307944 671697.7144036926, 644878.0384753855 671698.4326396593, 644883.338253149 671701.1631531894, 644884.1960503748 671701.605329253, 644889.9817888332 671704.5878971897, 644893.2514642425 671706.2722633722, 644896.6352640227 671708.0166423206, 644902.1929313503 671710.880304742, 644903.3547647242 671711.4794748514, 644908.3544112046 671714.0560347761, 644910.064326165 671714.9371862706, 644914.6470544727 671717.2986595114, 644916.7827619571 671718.4000186839, 644922.5264363317 671721.3595419497, 644924.3593500841 671722.3048672118, 644924.842278275 671722.678839584, 644933.1874765494 671729.1387846217, 644938.2579565335 671733.0642945133, 644942.9784421478 671736.7198464364, 644947.7718730447 671740.4302862982, 644952.4183489891 671744.0267902253, 644959.6216289828 671749.6035714354, 644966.7619029998 671755.1313858251, 644971.7605030971 671759.0008879501, 644976.6298963875 671762.7705360607, 644981.3855238268 671766.4520118384, 644986.0259654026 671770.0454751122, 644997.1278132529 671778.6396391977, 644997.2758331011 671778.7546952456, 645004.7151645527 671784.5135821116, 645009.6919783786 671788.0109267649, 645014.5651418684 671791.435620074, 645029.4299157815 671800.431186728, 645033.8535668319 671803.1087967597, 645039.8602121149 671806.7433821638, 645045.4387621435 671810.1190401467, 645057.6066427955 671817.4823080954, 645061.1364765037 671819.6186991266, 645063.5582677727 671821.0829674295, 645075.6872791192 671828.4243110735, 645087.3782593155 671835.4747265612, 645105.5307884258 671846.3746587334, 645111.5223513225 671849.9722828977, 645117.517818858 671853.5729474868, 645120.4846701669 671855.3543963628, 645123.8674854055 671856.4517441327, 645137.3659102261 671860.8323314199, 645144.490429026 671863.1440280926, 645152.1580726113 671865.6319470296, 645157.7659244506 671867.4516298518, 645171.6711606286 671871.9642638019, 645208.3354545522 671883.8623866746, 645225.6716693564 671890.4234011862, 645236.6303477306 671892.6797151954, 645251.5286734784 671897.33134559, 645252.4145362509 671897.607447302, 645282.777195539 671907.2078915997, 645290.3088888247 671909.588755923, 645299.0624150987 671912.531838058, 645315.1676400757 671917.944721875, 645340.0324264764 671926.3024485518, 645348.8819760975 671929.2765817137, 645352.4507982061 671930.4749103144, 645381.686491369 671940.3022574446, 645384.8593298192 671941.3687006973, 645385.869256638 671941.7076963305, 645398.7325998037 671946.031249044, 645401.914490377 671947.1005733409, 645405.3212626649 671948.2459233775, 645409.9230854509 671949.7925302422, 645415.3317847559 671951.6101094906, 645420.314694296 671953.1836413671, 645431.5470959645 671956.7319704784, 645432.8621338671 671957.1481213915, 645446.572501082 671961.4786549062, 645441.6350653919 671981.3217958241, 645440.2433201547 671986.9175536332, 645438.3465196379 671994.538343629, 645438.2923556261 671994.7572141874, 645447.4177803707 672008.9685067318, 645452.5534384516 672016.966129052, 645453.0734286846 672017.6297019101, 645454.1932545621 672019.193813898, 645455.3130183441 672021.2639989632, 645441.7414940932 672051.3229320645, 645439.0477027963 672057.6665245863, 645436.4609738353 672063.7611732594, 645432.2237193246 672073.8974901416, 645425.9449830474 672088.919577136, 645423.2842043806 672095.2791733581, 645421.0594388903 672101.0197899628, 645420.9075071026 672100.9607326764, 645419.2120731486 672105.0115154113, 645417.1313508947 672110.0051193725, 645411.7098335777 672122.9763012421, 645410.503702839 672129.031606469, 645409.2654250852 672135.2489859712, 645407.6019830966 672138.992743905, 645400.2959055286 672155.4366407789, 645398.3254838809 672159.8713940803, 645396.1349033228 672164.8019553539, 645388.4972921101 672161.9761190942, 645382.9934763993 672159.9415676118, 645377.1469260717 672157.7778568192, 645371.9801657539 672155.8671839957, 645366.4293151355 672153.8145470901, 645359.6227166043 672151.2958461016, 645353.2790143125 672148.949038206, 645347.4913909996 672146.8083740966, 645341.5986931137 672144.6288179193, 645337.3287945765 672143.0492879493, 645330.6809507278 672158.9212704984, 645325.9756863475 672170.1535387554, 645323.6570547908 672175.6903178804, 645322.2535184356 672179.0409915546, 645321.0722753778 672181.8598579231, 645318.343501961 672188.3775367682, 645318.010417453 672189.1714078211, 645317.1862281705 672191.1392866687, 645315.5877412631 672194.9560117032, 645314.0352401586 672198.6609035244, 645311.7515858138 672204.1144878535, 645315.2484597072 672205.8358440209, 645364.0211740441 672229.8373690491, 645366.5688535734 672232.4467617241, 645374.2718797885 672240.333506165, 645377.1047393605 672240.5228011506, 645389.1575276 672241.326306404, 645394.5484807496 672241.6859838831, 645412.9647318035 672247.2881102037, 645424.9388055914 672221.0148043218, 645447.4097892117 672228.5073111869, 645455.2394813207 672231.1182925026, 645462.6470982328 672233.5881052129, 645471.0587858204 672236.3930734816, 645479.2402667402 672239.1210552312, 645487.3159627478 672241.81302479, 645495.3068488387 672233.9860682563, 645495.7879280535 672233.515091075, 645496.3020259484 672233.0121182023, 645511.0716909695 672218.5450870408, 645519.644664594 672210.1481267187, 645522.5367627494 672209.2536015814, 645526.1535293031 672219.0887960449, 645521.1188828339 672224.033258333, 645510.6767975837 672234.2878942925, 645509.7575986659 672235.1906542225, 645507.8064755012 672237.1075193557, 645507.6573588252 672237.2524598954, 645504.2699207403 672240.5800174196, 645502.344715551 672242.4708059548, 645492.4817001392 672252.1575177307, 645488.1042327281 672256.4548660362, 645483.5707248945 672260.907233961, 645478.9990502287 672265.3967169275, 645474.5425859176 672269.7740549456, 645470.5842429985 672273.6615380388, 645464.8975766308 672279.1459166463, 645460.8641512112 672283.0362708085, 645461.8768980394 672285.0083681173, 645455.8504278744 672296.9204706817, 645426.4216603087 672312.8137416583, 645408.442579889 672323.3385185619, 645419.1680375715 672327.0227915063, 645473.6243845898 672345.7789703152, 645471.9736303769 672351.7419036654, 645469.0904930336 672362.1553579839, 645467.6137643543 672367.4871194996, 645468.186707614 672367.6791854026, 645473.5345335364 672369.4678005605, 645479.4242417786 672371.4374380412, 645486.9159903984 672373.9434197814, 645492.657670797 672375.8620002301, 645499.1593703679 672378.0387480372, 645505.9700833843 672380.3164913994, 645511.7607514036 672382.2531574264, 645517.8295489989 672384.2829754282, 645523.8001937333 672386.2796621339, 645530.3759077858 672388.4792984092, 645536.9715025973 672390.6720571138, 645542.581304938 672392.5627821091, 645548.9500639391 672394.6915145615, 645554.6587309688 672396.6011310702, 645560.9744198008 672398.7138573751, 645566.3052070276 672400.4965504106, 645572.279934288 672402.4951574816, 645578.3446499025 672404.52385489, 645583.9283629196 672406.3915369741, 645592.2259249442 672409.1664111213, 645601.0156715878 672412.1064618317, 645604.7004003635 672413.3388855223, 645606.2170277826 672417.1009413493, 645608.6272712538 672423.0792371997, 645610.1019811525 672432.9271359082, 645609.535586854 672435.8151943204, 645608.4729543809 672441.2279637966, 645607.1421323641 672448.0107651517, 645604.5787058152 672461.0762254939, 645603.7031772832 672464.8591165568, 645606.7042640524 672472.7213002964, 645607.862927892 672483.3773118188, 645575.6403755028 672646.6435481554, 645578.2485719644 672647.8931232989, 645584.503806331 672650.889671127, 645591.1467608322 672650.4455057515, 645593.1297959217 672650.3127829615, 645616.046879172 672648.780484313, 645625.669663271 672649.9955333686, 645632.9864154181 672650.9203918293, 645634.7173593224 672651.1386749267, 645654.9160825782 672653.6909647855, 645658.7668647176 672654.1773365845, 645658.014748334 672656.0802646127, 645654.7708202754 672655.7179634526, 645654.1687938536 672664.0926724799, 645654.0185534508 672666.1867097641, 645653.2150818378 672677.3573616606, 645652.8354815426 672682.6383738565, 645651.3179701599 672703.7429029903, 645650.8951647521 672709.6248629267, 645650.7579909437 672712.2827350765, 645650.3831197122 672719.5515642429, 645649.9329906744 672728.2704472591, 645647.1727336284 672730.9491275734, 645649.2184753582 672733.3392622056, 645653.8686466614 672738.7728700638, 645656.1777745804 672745.742981576, 645658.5729536989 672753.0092597743, 645660.0253479172 672766.8582277929, 645661.4775645809 672780.7081557561, 645660.837790122 672795.9189391024, 645664.2992574997 672799.8322636761, 645675.1056946975 672812.0505217505, 645680.9700566217 672816.6890829757, 645686.834596109 672821.327804196, 645704.0164947552 672837.5355715321, 645716.0682867161 672846.6568938853, 645741.2370673874 672864.7027070699, 645761.1194466272 672885.2517023765, 645787.1664456849 672916.9155362672, 645793.2743134258 672942.3028379614, 645825.8688272804 672945.2317912924, 645885.2258293123 672950.5837316265, 645967.292519479 672958.0174196716, 645984.8812243147 672959.7256115897, 646012.1927011617 672962.197853165, 646149.5903875321 672984.6921627863, 646146.5247167395 673006.0035364347, 646145.928352025 673010.1534945332, 646144.2491978186 673010.2643295898, 646144.242107623 673011.6551921081, 646142.479179546 673011.71897757, 646135.3986232154 673015.6571118877, 646114.5274197252 673027.2624421361, 646108.4400370404 673030.6477434208, 646095.8692447582 673037.6379752837, 646089.753815221 673041.0392729242, 646083.6564918923 673044.4295328766, 646071.3517882904 673051.2719586415, 646060.3052897749 673055.8804837043, 646052.2618710136 673059.2365879641, 646045.7975631673 673061.9336916199, 646039.3412431045 673064.6279162659, 646034.7940213248 673066.5252620025, 646028.2226979117 673070.5573766223, 646026.7244950208 673071.4773438721, 646017.3069414599 673077.2540523143, 646005.3979997456 673084.5974797141, 645991.933174589 673092.8047079436, 645981.7024422722 673099.0405120669, 645976.3870200113 673102.2798283827, 645971.1997615764 673105.4401211791, 645965.9013801485 673108.671439631, 645960.8270192713 673111.7637469133, 645955.7036648936 673114.8870878675, 645951.8044336141 673117.2637084151, 645949.6203235933 673118.5943133886, 645944.0398724307 673121.9966506748, 645938.8584688463 673125.1540782492, 645933.1590844749 673128.628240604, 645927.6237214195 673132.0016236427, 645922.4073476988 673135.1809668572, 645916.421991013 673138.8291732685, 645911.2185755869 673142.0005181525, 645905.5642789509 673145.4478062773, 645900.539795718 673148.5102137194, 645894.8095239042 673152.0024523322, 645887.1749012836 673156.6525312683, 645882.4553959995 673160.897840744, 645877.712990712 673165.1642672749, 645876.777823452 673166.0062262969, 645865.7027999421 673175.9897534918, 645863.2905011253 673178.1644814229, 645866.3154198223 673187.1586522729, 645870.1499761455 673198.5620102013, 645862.6508006782 673200.1420622201, 645815.3758897015 673210.1033785715, 645796.1444303116 673214.1549703367, 645777.8240969799 673218.0157924688, 645771.3071113942 673217.4060672104, 645766.7942462531 673216.9834572858, 645764.1983503891 673216.7402724652, 645760.6682869869 673216.4097779457, 645753.8344690693 673215.7699354747, 645747.2065477534 673215.1502771699, 645738.2266118206 673214.3092226846, 645732.6607932448 673214.1215251276, 645731.6896992045 673214.0894103935, 645723.4278567713 673213.8113143168, 645708.5108877688 673213.309551974, 645693.7458575494 673212.8121275839, 645692.9330921837 673212.7848315621, 645681.7031207872 673212.4075047981, 645680.967212288 673212.3824578609, 645615.0368267047 673217.5685006994, 645599.8168382244 673218.764762328, 645599.5047916631 673218.789685042, 645585.9572630721 673215.075003104, 645566.7640288293 673209.812950162, 645554.1394655522 673206.351494527, 645548.6767554511 673204.8537446525, 645539.8360851536 673201.6757741779, 645534.5965049318 673198.6052693179, 645531.6677996565 673196.8878224122, 645527.0831442063 673194.2063505729, 645504.0880802525 673187.2867461157, 645488.0537952796 673182.0637867536, 645471.0581075829 673189.0890489826, 645468.9718114735 673190.3115072377, 645462.6624321042 673194.6476681486, 645427.680638351 673218.6881981962, 645420.3622093045 673222.7474429377, 645418.5666685104 673223.872176874, 645322.2573301242 673284.2083746736, 645321.905144899 673284.4289703564, 645304.2317909356 673295.5837055523, 645288.9557470924 673305.2257826903, 645284.9924181352 673307.7273638342, 645282.8961246468 673310.8739586852, 645258.6559562762 673329.7510261567, 645233.7335926483 673349.1598470724, 645206.244154745 673370.6782598165, 645074.5981101439 673474.202426515, 645048.6817991401 673494.5831221492, 645022.1679603506 673509.6718087017, 645021.2349508234 673510.2737712892, 645011.9421525171 673516.1114837235, 645008.6759659213 673517.7531580997, 645003.8437850007 673520.1805580246, 644996.4393044382 673523.9017185206, 644982.0174928867 673531.147927681, 644977.6493231483 673533.343385275, 644972.3339593769 673536.0136052822, 644959.7822166266 673542.3211667999, 644958.898116523 673543.5799276474, 644953.9672163242 673550.6042230751, 644952.4880350487 673552.7110637261, 644939.6708897859 673570.967416483, 644936.9933797865 673574.7809756912, 644928.4575696529 673582.4349173808, 644908.5378747117 673604.3532679231, 644904.0303549655 673609.5526747513, 644895.4330299727 673619.4696234242, 644843.873742465 673664.5039654501, 644804.4689964586 673697.0709302659, 644795.6562430222 673703.6128528145, 644787.2134316269 673709.8797823254, 644762.8673668109 673727.9526956874, 644746.1338734317 673740.3734544414, 644744.5136956541 673741.5294095981, 644739.3283101971 673745.2287540215, 644705.0714987238 673769.66634347, 644696.4404811087 673777.8322696292, 644695.1280764699 673779.0738602689, 644692.5870565824 673781.4776923651, 644691.291871403 673782.7026452344, 644689.308598741 673784.7443074267, 644673.5198695965 673801.000205453, 644663.3090218971 673807.2090242049, 644655.5086719851 673811.4280653808, 644649.0780819166 673815.6672684257, 644623.626624249 673829.2841422673, 644559.4388185572 673881.710989397, 644554.8645770104 673882.423723058, 644557.0783841693 673877.6057854969, 644559.0744105284 673875.706197422, 644564.798618433 673873.9268887372, 644571.9712243597 673869.10665711, 644576.215414611 673866.0620644754, 644578.2176586192 673864.1184776085, 644583.0114577857 673858.9661279804, 644582.9055168789 673858.7440373597, 644581.9644596715 673856.8639422326, 644581.4677678414 673855.7718928728, 644581.020043137 673853.3669005068, 644581.0681116914 673852.1478765996, 644581.3251756222 673851.766950227, 644581.8082156533 673851.2209319675, 644582.5633748339 673850.5672667854, 644585.5173869893 673849.9865008113, 644597.2356856784 673847.2207092072, 644598.3067463639 673846.9680379422, 644599.0057795924 673846.6210835205, 644599.7319177736 673845.2135013943, 644599.9078598358 673844.8722451117, 644600.510768165 673837.827416742, 644596.4639208235 673827.6319857854, 644589.4169910084 673827.8810706892, 644586.2729282852 673828.0277377999, 644583.6691303693 673825.5293713714, 644580.3252423287 673808.7871990592, 644580.7259325192 673802.1362632269, 644579.4302927639 673799.7382104957, 644575.7310093717 673795.3928501353, 644572.4558502774 673794.8983870451, 644569.467797301 673798.1589659097, 644568.4303578077 673799.2909935873, 644565.636123545 673801.1356074666, 644562.8823093101 673800.3920881669, 644560.3335898339 673797.4390119085, 644560.2370059782 673794.6339043478, 644561.9352351567 673792.338124707, 644567.5317100753 673788.5418625296, 644567.4330707619 673785.2409282707, 644566.6362761507 673783.7968504042, 644565.18636482 673783.1406863641, 644556.8359966776 673776.6496923353, 644550.7957524387 673773.2918959919, 644549.0404857037 673773.0968106082, 644545.5955483991 673773.3464803508, 644543.0976991428 673772.5433140228, 644541.142850669 673771.4928547132, 644535.2111626716 673764.396073479, 644533.4550650122 673762.2950895496, 644530.4491936063 673761.6478613523, 644527.2021374882 673762.1455516772, 644526.8245803909 673762.276225543, 644524.6039694989 673763.0450742041, 644522.1058570575 673762.9398199306, 644513.6594240902 673759.6478181495, 644511.563709664 673758.0405484443, 644509.2201439547 673755.4953400211, 644508.3251384395 673753.0455018515, 644507.9603225347 673752.0470699838, 644507.2686328198 673750.3982068088, 644502.76687199 673747.6986648368, 644500.2651359611 673746.7484596643, 644492.3742527894 673745.0909050285, 644490.9723994344 673744.7963425398, 644488.3214477577 673744.7920290233, 644487.0797363195 673742.747905232, 644486.6527413023 673742.1920210003, 644485.0808814389 673740.1456181804, 644483.0341629628 673737.0454110807, 644479.5267652193 673734.250147751, 644476.9306794383 673734.0948024163, 644473.7467753895 673734.8998567103, 644473.1835525687 673735.0423484807, 644466.3326916575 673734.3925854318, 644463.0400682982 673731.596227493, 644460.293488281 673728.3488992233, 644459.7151888226 673726.9598864795, 644459.2928651624 673725.9457679596, 644457.9399834166 673725.7996895711, 644454.5941110955 673722.9972453758, 644450.397343352 673721.3438075589, 644444.201842058 673717.5469574509, 644440.9031346955 673715.1507542231, 644438.2566262139 673712.2144742275, 644436.2067206099 673710.4961710827, 644429.4100955378 673707.6045196034, 644424.4448000086 673707.0362585956, 644421.4672911533 673706.695590187, 644419.7584567151 673705.6483598144, 644417.9156051371 673704.6431931152, 644410.4322047738 673699.1151700056, 644407.7665850791 673697.1460768023, 644405.6687701709 673695.3028093529, 644404.0258494981 673695.1986161529, 644403.6067664002 673695.257605907, 644398.7275811621 673695.9448609309, 644396.8755408442 673696.0476806646, 644395.4759600762 673694.5955314127, 644393.0803266712 673691.1044072415, 644389.1829161025 673687.2529494356, 644389.0317153541 673687.0201341648, 644386.7812734005 673683.5536668753, 644386.4306228652 673681.3957296441, 644386.6947482956 673679.9007376985, 644385.9368089306 673678.5997026451, 644383.8798421894 673679.4524096892, 644379.54202041 673681.6589116608, 644378.2884419069 673682.2966761797, 644376.5825020135 673682.249434871, 644371.0953130627 673683.4948516659, 644367.5504416066 673682.3214854359, 644359.5556168549 673677.1965970704, 644351.560792232 673672.0717086596, 644334.6819945929 673663.7086853514, 644331.6966657133 673657.6864796411, 644318.9448862649 673648.0309989632, 644316.1574739843 673643.4118408796, 644314.9696212081 673634.9897146774, 644309.3956841383 673625.7531585265, 644294.917747455 673610.1546597711, 644289.9131098435 673604.7626078115, 644287.8675051165 673599.8374608192, 644285.0685175698 673596.9691616357, 644282.1508574218 673594.9376390378, 644279.4737813957 673592.3170314943, 644276.6854831928 673587.6893932291, 644274.0648004394 673581.166275988, 644271.2205900572 673576.5386310756, 644266.4245715119 673570.6843688289, 644262.0187090319 673566.1678977845, 644257.5010964723 673562.4878841426, 644255.3819489711 673559.4769469305, 644254.2111880311 673554.9611787949, 644252.5939809666 673552.1733384957, 644247.6397037889 673548.6279916099, 644244.9943490799 673546.4241830623, 644243.6171084124 673540.8596021596, 644242.460308226 673535.4054461159, 644240.6968581754 673532.4854299475, 644238.0514424418 673530.7772958265, 644232.870552753 673529.6747747668, 644229.7289655134 673528.4624163617, 644226.8631976675 673525.9278653616, 644221.9579770797 673523.2829941266, 644218.596126068 673520.5280668299, 644214.9999924618 673516.2011293224, 644211.8445379992 673512.2919993132, 644207.0428037388 673505.2967887741, 644202.7900408831 673498.5760401456, 644198.8801475876 673490.8951827849, 644195.6564324156 673484.9289475413, 644192.1580921788 673479.2367586686, 644188.1411631996 673474.4649847401, 644182.6749205834 673469.9563993311, 644177.2767024182 673465.1062255971, 644170.7174951528 673458.5482558104, 644166.2110955039 673453.1943513043, 644164.1695222711 673450.132707925, 644163.4891822074 673447.1392220875, 644164.7145505428 673444.9622687958, 644167.4376237069 673443.057646663, 644170.3646518476 673441.0850490162, 644170.8413788016 673438.4998532127, 644170.637607333 673435.6424212974, 644169.0046697614 673430.8798843463, 644168.8007459827 673427.8184546735, 644168.3891386267 673424.8339599888, 644166.3281111328 673421.2006406675, 644163.2905231469 673417.2419313628, 644160.6324989429 673414.1507765454, 644159.7106917279 673411.7105364453, 644159.2229469364 673407.2097297319, 644158.1922590806 673405.3661103265, 644155.75131996 673403.6848850608, 644153.5271810374 673401.4070516403, 644152.7137656327 673399.4549787043, 644151.4001628645 673397.4591680628, 644148.806500317 673396.3787183394, 644147.2394598398 673395.5684650403, 644145.9971813237 673392.3816359097, 644144.646508268 673390.16710337, 644143.2417221176 673389.1946307373, 644141.4584076169 673389.0325050717, 644139.4591032962 673388.6540366777, 644138.1625716412 673387.8438148352, 644137.7301527886 673386.6556177161, 644138.1629984347 673384.3872132488, 644139.3519398805 673381.0926681423, 644141.8924094326 673375.961980668, 644143.351748782 673372.127473031, 644143.9798178795 673367.1912809155, 644144.3574124762 673361.0046735412, 644143.8196817534 673355.9477071187, 644142.8513144219 673353.9570565391, 644141.0214239535 673352.9347749563, 644138.9223849409 673352.3966166787, 644138.4917139836 673351.428097315, 644138.7073906325 673349.8679797256, 644139.6765664481 673346.7479671366, 644139.8384826046 673343.5739412113, 644139.4158882652 673337.5705986685, 644137.7551862466 673329.1040993702, 644139.3646454052 673318.4412050343, 644138.6145295294 673315.6011492517, 644137.3281621753 673312.9754286713, 644133.6976034652 673307.1485107554, 644131.8296836618 673303.6132925561, 644128.7608575978 673298.2104352772, 644126.2921982903 673295.3421797301, 644123.8901963399 673293.1408045342, 644120.0868466855 673290.8727069435, 644116.8172206033 673288.7379499971, 644113.8815281506 673286.1363570828, 644108.6621291162 673280.6794899333, 644101.8551076347 673274.3525275533, 644096.9627106515 673268.5043427153, 644091.8576959278 673261.0079913207, 644088.3517928453 673254.7735721027, 644085.5447654823 673249.4782638403, 644084.1675421697 673248.1013187103, 644081.4657416358 673247.3597323172, 644078.022415092 673246.0884654368, 644076.6453169261 673245.1353556237, 644075.6920401703 673243.1229468676, 644074.3677443353 673241.4815308062, 644071.7722258017 673239.6278090639, 644069.6005286197 673236.7154283607, 644067.763979231 673233.5654893041, 644065.1046863951 673226.3905388862, 644062.9099872415 673220.0177136909, 644061.2638285626 673215.9658867271, 644058.1454610187 673209.2932772291, 644053.9429247282 673201.2179969129, 644049.1494706849 673191.895301481, 644045.7348116331 673185.5927332791, 644040.546564008 673180.8654610682, 644037.2728525327 673177.3275187396, 644035.9650406241 673171.5730298365, 644036.2273451546 673167.5843043681, 644035.0499695836 673166.3419409054, 644033.1526814203 673166.3417198813, 644030.5357713675 673166.6682914228, 644029.0311597969 673166.2758004676, 644028.7697525461 673164.4449101902, 644029.0973849017 673160.3907530338, 644027.9200174793 673159.0827902793, 644025.7611037095 673159.0172594864, 644020.5273888075 673158.8204919199, 644016.9950277687 673156.989060563, 644011.8927198275 673153.4576250004, 644007.5752726438 673150.2529972596, 644002.5679423249 673145.1622298693, 643997.2254359281 673139.1045540249, 643992.9257045668 673131.8745325247, 643989.2772741324 673125.6216961358, 643985.7593138685 673120.7363798865, 643982.056005909 673117.597582827, 643974.4629216432 673112.0826785568, 643969.2710382395 673108.0598776797, 643965.7667922952 673104.2319913137, 643963.4956648079 673100.1447314827, 643960.0563989262 673094.759909731, 643956.6817925066 673091.8403484517, 643953.1773620021 673089.5044456283, 643948.3099480737 673086.9090267804, 643944.545900729 673084.0542193576, 643942.7602199694 673081.5095591121, 643941.4831148827 673078.6360017409, 643939.694836141 673075.5708668816, 643938.22552678 673073.9105538062, 643934.3289568691 673071.9307612963, 643929.6020462541 673069.9507119933, 643927.0468924285 673067.5240407144, 643924.2366610556 673063.7565543705, 643921.9131259753 673061.4721485169, 643917.3959512891 673058.6095732609, 643912.5603038551 673056.0651973847, 643908.2338934975 673053.5842401594, 643904.7981814677 673051.0398674492, 643900.5352960009 673047.2870914993, 643898.6905871691 673044.1069111876, 643897.8960835527 673041.3857282902, 643895.9691754897 673038.34425687, 643892.8757827949 673036.4175172636, 643889.3768486991 673034.1360142416, 643886.3849273989 673031.2464169592, 643883.3426263048 673026.6838720534, 643880.3005012425 673020.7019826346, 643877.0554256552 673015.2778234371, 643874.9257887608 673012.4388860847, 643870.6879566861 673008.4806750985, 643869.4758321147 673006.9154308233, 643864.4253503726 672999.1392466612, 643861.748317628 672996.2104864015, 643858.6673032925 672993.2312795668, 643857.0005285579 672992.2717356444, 643850.6361052702 672989.5446231306, 643846.898302807 672988.3323604238, 643843.8050186742 672986.9652945601, 643841.9433994626 672985.6071722342, 643840.1324924073 672983.2433079085, 643839.5289277894 672980.9298241662, 643839.1268217369 672977.2083787401, 643838.6744647538 672973.6378059322, 643837.8193840685 672971.3242934583, 643836.0585696496 672968.6586813785, 643833.7948007415 672966.7976431033, 643830.8765833129 672966.4453136809, 643826.8009600517 672966.998122247, 643822.8762261616 672968.8584538443, 643818.9516697546 672970.7186254796, 643810.7799741017 672976.4088305839, 643804.1625241656 672981.5787388878, 643799.470127151 672984.7047292142, 643796.3821552714 672986.3077192622, 643794.2166225301 672986.6682681, 643791.4494776544 672986.1468378023, 643789.4845055786 672985.1845438697, 643773.7695082583 672969.022124331, 643755.4995833212 672941.4998551081, 643737.0695482971 672899.7679596952, 643720.339106692 672862.0195741355, 643699.5005701253 672817.6402287697, 643699.4640103753 672789.0651714135, 643705.8672174817 672786.7475286918, 643710.259692876 672784.6213339509, 643710.4589187688 672784.0416829698, 643711.8632153575 672780.3626831438, 643713.4020820111 672776.4753809642, 643714.3347852944 672772.6244890601, 643715.3914367096 672769.7673606565, 643716.9454323294 672766.9725284863, 643719.1209134273 672763.6808127676, 643721.3893267012 672761.0100610174, 643725.4119729715 672756.8212863113, 643722.5180667071 672752.2494844196, 643713.7997111619 672737.6888034701, 643712.2145127142 672735.9057612452, 643709.0932572616 672733.974064842, 643706.7151897524 672732.0422934684, 643705.4769535754 672729.2193021, 643703.5945246157 672726.7925527464, 643699.3833841047 672723.771143268, 643696.1134944498 672722.3842239394, 643695.103449544 672718.7166277103, 643694.7095082543 672712.1561730297)))" + } +} +] diff --git a/streetup/common/forms.py b/streetup/common/forms.py new file mode 100644 index 0000000..508d596 --- /dev/null +++ b/streetup/common/forms.py @@ -0,0 +1,42 @@ +from django import forms +from django.contrib.auth import get_user_model + +User = get_user_model() + +class UserConfigDuplicateForm(forms.Form): + user = forms.ModelChoiceField( + queryset=User.objects.order_by('username'), + label="Nouvel utilisateur", + widget=forms.Select(attrs={"class": "vTextField admin-autocomplete"}) + ) + + +class UserConfigAssignCompanyForm(forms.Form): + company = forms.ModelChoiceField( + queryset=None, # set in __init__ + label="Société à assigner", + required=False, + empty_label="— Aucune (retirer la société) —", + help_text="La société sélectionnée sera appliquée à tous les utilisateurs choisis.", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + from contracts.models import Company + self.fields['company'].queryset = Company.objects.order_by('name') + + +class UserConfigCopyFromForm(forms.Form): + source = forms.ModelChoiceField( + queryset=None, # set in __init__ + label="Copier la configuration depuis", + help_text="Toute la configuration de cet utilisateur sera recopiée sur les utilisateurs sélectionnés.", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Import here to avoid circular imports at module level + from common.models import UserConfig + self.fields['source'].queryset = ( + UserConfig.objects.select_related('user').order_by('user__username') + ) diff --git a/streetup/common/help_docs/fr/approuver_interventions.md b/streetup/common/help_docs/fr/approuver_interventions.md new file mode 100644 index 0000000..790866f --- /dev/null +++ b/streetup/common/help_docs/fr/approuver_interventions.md @@ -0,0 +1,18 @@ +# Approuver des interventions + +
+ Note : Ce guide s'adresse uniquement aux utilisateurs disposant des permissions d'approbation d'interventions sur un ou plusieurs contrats. +
+ +Lorsqu'un prestataire ou un inspecteur signale un besoin de travaux, l'intervention est créée au statut **"À approuver"**. + +## 1. Localiser les interventions à approuver +- Sur votre **Page d'accueil**, un widget "Interventions à approuver" affiche le décompte des chantiers en attente d'approbation. Cliquez dessus pour ouvrir la liste pré-filtrée. +- Vous pouvez également aller dans le menu **Interventions** et filtrer le tableau par statut en sélectionnant **"À approuver"**. + +## 2. Procédure d'approbation +1. Cliquez sur le code de l'intervention pour ouvrir sa page de détails. +2. Examinez les informations saisies : thématique, urgence, description, photos et géolocalisation. **Vérifiez également que le prestataire désigné et le contrat associé sont corrects. Si nécessaire, vous avez la possibilité de les modifier via le bouton avant d'approuver.** +3. Deux actions principales s'offrent à vous dans le bloc de statut : + - **Approuver** : Fait passer l'intervention au statut **"À planifier"** ou **"À traiter"** afin que le prestataire puisse programmer les travaux. + - **Rejeter / Annuler** : Si les travaux ne sont pas nécessaires ou relèvent d'une autre juridiction, vous pouvez annuler l'intervention. Renseignez le motif d'annulation pour en informer le demandeur. diff --git a/streetup/common/help_docs/fr/assets.md b/streetup/common/help_docs/fr/assets.md new file mode 100644 index 0000000..54a9e79 --- /dev/null +++ b/streetup/common/help_docs/fr/assets.md @@ -0,0 +1,18 @@ +# Rechercher et filtrer des assets + +L'inventaire de vos équipements de voirie (ou "assets") est accessible depuis le menu **Assets** à gauche de votre écran. + +## 1. La barre de recherche textuelle +En haut du tableau des assets se trouve un champ de recherche. Vous pouvez y saisir : +- Un code unique d'asset (ex : `IT-02941`). +- Un nom ou une description. +Le tableau et la carte se filtrent automatiquement pour ne garder que les assets correspondant à votre saisie. + +## 2. Les filtres rapides (Filtres actifs) +Sous le titre de la page, vous pouvez appliquer des filtres plus larges : +- **Thématique** : Filtre les assets par thématique métier (ex : Espaces verts, Marquage, Signalisation). +- **Catégorie d'asset** : Filtre les types d'équipements (ex : Banc public, Corbeille de propreté). +- **Contrat** : Si vos accès sont restreints à certains contrats de gestion, ce filtre vous permet de n'afficher que les assets sous la juridiction de ces contrats. + +## 3. Personnalisation du tableau +Vous pouvez adapter le tableau des assets à votre convenance en cliquant sur le bouton **Masquer/Afficher les colonnes** (l'icône de tableau avec un engrenage). Cochez ou décochez les colonnes pour masquer les informations qui ne vous sont pas utiles. Vos préférences sont sauvegardées automatiquement pour vos prochaines visites. diff --git a/streetup/common/help_docs/fr/creer_controle.md b/streetup/common/help_docs/fr/creer_controle.md new file mode 100644 index 0000000..c899361 --- /dev/null +++ b/streetup/common/help_docs/fr/creer_controle.md @@ -0,0 +1,40 @@ +# Ajouter un contrôle + +> [!NOTE] +> La création et la gestion des contrôles sont réservées aux utilisateurs disposant des permissions d'administration ou de contrôle (Admin, Manager, Controller, External Manager, Top Manager). + +Les contrôles permettent d'inspecter la conformité des travaux effectués lors des interventions ou dans le cadre d'un projet global. + +## 1. Ajouter un contrôle depuis le menu principal +1. Allez dans le menu **Contrôles** dans la barre latérale de gauche. +2. Cliquez sur le bouton **Créer un contrôle** en haut à droite. +3. Remplissez le formulaire : + - **Projet** et **Phase** : Sélectionnez-les si ce contrôle s'inscrit dans un chantier global. + - **Interventions** : Recherchez (par code ou description) et associez la ou les interventions concernées par le contrôle. + - **Type de contrôle** : Choisissez la nature de la vérification (ex : Réception de chantier, Contrôle de sécurité, etc.). +4. Cliquez sur **Enregistrer** pour créer le contrôle. + +## 2. Raccourci depuis une intervention (Création rapide) +Si vous examinez les détails d'une intervention individuelle et que vous souhaitez lancer son contrôle : +1. Sur la page de détails de l'intervention, repérez le bloc d'actions. +2. Cliquez sur le bouton de création rapide de contrôle. +3. Le formulaire s'ouvre avec l'intervention déjà pré-sélectionnée et associée. + +## 3. Saisir des remarques et ajouter des photos +Une fois le contrôle créé, vous pouvez y ajouter des remarques spécifiques pour documenter vos constats : +1. Sur la page de détails du contrôle, cliquez sur **Ajouter une remarque**. +2. Choisissez le statut de conformité : + - **Info** (bleu) : Pour des notes ou remarques informatives simples. + - **OK** (vert) : Pour confirmer que l'élément est conforme. + - **NOK** (rouge) : Pour signaler une non-conformité à corriger. +3. Rédigez une description détaillée dans le commentaire de suivi. +4. Ajoutez des photos ou des fichiers : + - Utilisez le bouton **Prendre des photos (mode continu)** pour utiliser directement l'appareil photo de votre mobile/tablette. + - Ou cliquez sur **Photos, vidéos, fichiers** pour choisir des fichiers existants depuis la galerie de votre appareil. +5. Cliquez sur **Enregistrer** pour lier ces remarques au rapport de contrôle. + +## 4. Réaliser des contrôles sur le terrain (Application Hors-ligne) +Pour faciliter les contrôles directement sur site, StreetUp dispose d'une application mobile dédiée (PWA) : +- **Installation** : Lors de votre connexion sur mobile ou tablette, l'application vous propose d'ajouter le raccourci "Contrôles" sur votre écran d'accueil. +- **Fonctionnement hors-ligne** : Vous pouvez réaliser l'ensemble de votre contrôle, noter vos remarques de conformité et prendre des photos, même dans les zones sans couverture réseau (4G/Wi-Fi). +- **Synchronisation automatique** : Dès que votre appareil retrouve une connexion Internet, vos rapports de contrôles et photos sont synchronisés de manière transparente avec le serveur central. diff --git a/streetup/common/help_docs/fr/gerer_projets.md b/streetup/common/help_docs/fr/gerer_projets.md new file mode 100644 index 0000000..c58679e --- /dev/null +++ b/streetup/common/help_docs/fr/gerer_projets.md @@ -0,0 +1,21 @@ +# Gérer les projets + +
+ Note : La gestion des projets est réservée aux utilisateurs disposant des rôles d'administration ou de gestion (Admin, Manager, Controller, External Manager, Top Manager). +
+ +Les projets permettent de regrouper plusieurs interventions afin de coordonner des chantiers d'envergure. + +## 1. Créer un nouveau projet +1. Allez dans le menu **Projets** via le menu de gauche. +2. Cliquez sur le bouton **Créer un projet** en haut à droite. +3. Renseignez le nom du projet, une description, les dates prévues de début et de fin, et les thématiques concernées. +4. Enregistrez pour valider la création. + +## 2. Associer des interventions à un projet +Une fois le projet créé : +- Dans la page de détail du projet, vous pouvez ajouter des interventions existantes en cliquant sur **Associer des interventions**. +- Alternativement, depuis la page de détails d'une intervention individuelle, vous pouvez utiliser le champ **Projet** pour l'affecter directement à un projet existant. + +## 3. Suivi et progression +La page d'un projet synthétise l'avancement global en calculant le pourcentage d'interventions terminées par rapport au total des interventions associées. Vous y trouverez également une carte regroupant toutes les interventions du projet pour une visualisation géographique globale. diff --git a/streetup/common/help_docs/fr/prise_en_main.md b/streetup/common/help_docs/fr/prise_en_main.md new file mode 100644 index 0000000..5bc658b --- /dev/null +++ b/streetup/common/help_docs/fr/prise_en_main.md @@ -0,0 +1,27 @@ +# Prise en main de StreetUp + +Bienvenue dans StreetUp ! Ce guide rapide va vous présenter les bases de la navigation et de l'utilisation de l'application. + +## 1. La barre de navigation principale (Header) +En haut de votre écran, vous trouverez la barre d'outils contenant : +- L'accès à vos informations de compte. +- Un sélecteur de vue (si vous avez accès à plusieurs portails). +- Le centre de notifications () pour suivre les discussions et alertes. +- Le sélecteur de langue () pour basculer entre le français et le néerlandais. +- Le bouton d'aide () qui ouvre ce panneau. + +## 2. Le menu latéral (Sidebar) +À gauche de l'écran (ou via le menu repliable sur mobile), vous pouvez naviguer vers : +- **Accueil** : Votre tableau de bord personnalisé avec vos compteurs d'interventions et de signalements. +- **Assets** : L'inventaire de vos équipements de voirie sur carte et en tableau. +- **Observations** : Vos constats et inspections sur le terrain. +- **Interventions** : Le suivi des chantiers et travaux de maintenance corrective ou préventive. +- **Projets** : La planification des travaux d'envergure. + +## 3. Dispositions d'affichage +Sur la plupart des pages (Assets, Interventions), vous pouvez choisir d'afficher : +- La **carte** seule (). +- Le **tableau** des données seul (). +- Les **deux** côte à côte (, par défaut). + +Ces boutons (situés en haut à droite des pages concernées) vous permettent de masquer la carte ou le tableau pour maximiser votre espace de travail. Vos choix de disposition sont automatiquement sauvegardés pour vos prochaines visites. diff --git a/streetup/common/help_docs/fr/statuts_intervention.md b/streetup/common/help_docs/fr/statuts_intervention.md new file mode 100644 index 0000000..e21e922 --- /dev/null +++ b/streetup/common/help_docs/fr/statuts_intervention.md @@ -0,0 +1,129 @@ +# Statuts des interventions + +Le cycle de vie d'une intervention dans StreetUp est jalonné par 15 statuts successifs. Chaque statut correspond à une étape précise de planification, de réalisation ou de validation administrative. + +Voici l'explication détaillée de chaque étape : + +
+ + +
+
1
+
+ En préparation : L'intervention est en cours d'édition ou de rédaction par son créateur. Elle n'est pas encore visible par les prestataires de voirie. +
+
+ + +
+
2
+
+ À approuver : L'intervention a été soumise et attend l'analyse d'un gestionnaire pour valider le bien-fondé des travaux à réaliser. +
+
+ + +
+
3
+
+ À planifier : L'intervention est validée sur le fond. Il convient désormais d'y associer une date de début prévue et de l'attribuer à une équipe de terrain. +
+
+ + +
+
4
+
+ À traiter : L'intervention est prête à être exécutée par le prestataire, qui peut l'affecter à ses ouvriers sur le terrain. +
+
+ + +
+
5
+
+ Pris en charge : L'intervention a été affectée à une équipe ou à un ouvrier specific qui se chargera de sa réalisation. +
+
+ + +
+
6
+
+ Démarré : Les travaux de réparation ou de maintenance sont actuellement en cours d'exécution sur le terrain. +
+
+ + +
+
7
+
+ En pause : Le chantier est temporairement arrêté (en attente de pièces détachées, conditions météo défavorables, etc.). +
+
+ + +
+
8
+
+ Terminé : Les ouvriers ont terminé les tâches sur le terrain. L'intervention est complétée. +
+
+ + +
+
9
+
+ Traité : Le prestataire a renseigné les rapports de fin de travaux et soumis l'intervention pour validation de conformité. +
+
+ + +
+
10
+
+ À corriger : Après inspection, le gestionnaire a refusé les travaux pour non-conformité et demande au prestataire d'effectuer des corrections. +
+
+ + +
+
11
+
+ Corrigé : Les corrections demandées ont été effectuées sur le terrain et l'intervention est de nouveau prête à être inspectée. +
+
+ + +
+
12
+
+ Validé : Les travaux sont validés et déclarés pleinement conformes par le gestionnaire. +
+
+ + +
+
13
+
+ Comptabilisé : L'intervention validée est passée en comptabilité administrative pour facturation. +
+
+ + +
+
14
+
+ Finalisé : L'intervention est définitivement clôturée et archivée (fin du cycle de vie). +
+
+ + +
+
15
+
+ Annulé : L'intervention a été annulée suite à un doublon, une erreur de saisie ou un refus lors de la phase d'approbation. +
+
+ +
diff --git a/streetup/common/help_docs/nl/approuver_interventions.md b/streetup/common/help_docs/nl/approuver_interventions.md new file mode 100644 index 0000000..84a1fa1 --- /dev/null +++ b/streetup/common/help_docs/nl/approuver_interventions.md @@ -0,0 +1,18 @@ +# Interventies goedkeuren + +
+ Let op: Deze handleiding is alleen bedoeld voor gebruikers die over de nodige machtigingen beschikken om interventies op een of meer contracten goed te keuren. +
+ +Wanneer een dienstverlener of inspecteur meldt dat er werken nodig zijn, wordt de interventie aangemaakt met de status **"Goed te keuren"**. + +## 1. Interventies lokaliseren die goedkeuring behoeven +- Op uw **Startpagina** toont een widget "Goed te keuren interventies" het aantal werven dat wacht op goedkeuring. Klik erop om de vooraf gefilterde lijst te openen. +- U kunt ook naar het menu **Interventies** gaan en de tabel filteren op status door **"Goed te keuren"** te selecteren. + +## 2. Goedkeuringsprocedure +1. Klik op de code van de interventie om de detailpagina te openen. +2. Controleer de ingevoerde gegevens: thematiek, urgentie, omschrijving, foto's en geolocatie. **Controleer ook of de aangewezen aannemer en het bijbehorende contract correct zijn. Indien nodig heeft u de mogelijkheid om deze te wijzigen via de knop voordat u goedkeurt.** +3. Er zijn twee hoofdacties beschikbaar in het statusblok: + - **Goedkeuren**: Wijzigt de status van de interventie naar **"In te plannen"** of **"Te behandelen"** zodat de dienstverlener de werken kan inplannen. + - **Weigeren / Annuleren**: Als de werken niet nodig zijn of onder een andere bevoegdheid vallen, kunt u de interventie annuleren. Vul de reden voor annulering in om de aanvrager te informeren. diff --git a/streetup/common/help_docs/nl/assets.md b/streetup/common/help_docs/nl/assets.md new file mode 100644 index 0000000..f43730c --- /dev/null +++ b/streetup/common/help_docs/nl/assets.md @@ -0,0 +1,18 @@ +# Assets zoeken en filteren + +De inventaris van uw weginfrastructuur (of "assets") is toegankelijk via het menu **Assets** aan de linkerkant van uw scherm. + +## 1. De tekstzoekbalk +Bovenaan de assetstabel bevindt zich een zoekveld. U kunt hier invoeren: +- Een unieke code van een asset (bijv: `IT-02941`). +- Een naam of omschrijving. +De tabel en de kaart worden automatisch gefilterd om alleen de assets te tonen die aan uw invoer voldoen. + +## 2. Snelfilters (Actieve filters) +Onder de paginatitel kunt u bredere filters toepassen: +- **Thematiek**: Filtreert assets per vakgebied (bijv: Groenvoorziening, Markeringen, Signalisation). +- **Assetcategorie**: Filtreert de types uitrustingen (bijv: Bank, Vuilnisbak). +- **Contract**: Als uw toegang beperkt is tot bepaalde beheercontracten, kunt u met dit filter alleen de assets tonen die onder de jurisdictie van deze contracten vallen. + +## 3. De tabel aanpassen +U kunt de tabel met assets aanpassen door te klikken op de knop **Kolommen verbergen/tonen** (het tabelicoon met een tandwiel). Vink de kolommen aan of uit om informatie te verbergen die u niet nodig heeft. Uw voorkeuren worden automatisch opgeslagen voor uw volgende bezoeken. diff --git a/streetup/common/help_docs/nl/creer_controle.md b/streetup/common/help_docs/nl/creer_controle.md new file mode 100644 index 0000000..307e325 --- /dev/null +++ b/streetup/common/help_docs/nl/creer_controle.md @@ -0,0 +1,40 @@ +# Een controle toevoegen + +> [!NOTE] +> Het aanmaken en beheren van controles is voorbehouden aan gebruikers met beheerders- of controlebevoegdheden (Admin, Manager, Controller, External Manager, Top Manager). + +Met controles kunt u de conformiteit inspecteren van uitgevoerde werken tijdens interventies of in het kader van een globaal project. + +## 1. Een controle toevoegen via het hoofdmenu +1. Ga naar het menu **Controles** in het linkermenu. +2. Klik bovenaan rechts op de knop **Controle aanmaken**. +3. Vul het formulier in: + - **Project** en **Fase**: Selecteer deze als de controle deel uitmaakt van een grotere werf. + - **Interventies**: Zoek (op code of omschrijving) en koppel de betreffende interventie(s) aan de controle. + - **Type controle**: Kies de aard van de controle (bijv: Oplevering van de werf, Veiligheidscontrole, enz.). +4. Klik op **Opslaan** om de controle aan te maken. + +## 2. Snelkoppeling vanaf een interventie (Snel aanmaken) +Als u de details van een individuele interventie bekijkt en u wilt de controle starten: +1. Zoek op de detailpagina van de interventie het actieblok. +2. Klik op de knop om snel een controle aan te maken. +3. Het formulier opent met de interventie al vooraf geselecteerd en gekoppeld. + +## 3. Opmerkingen invoeren en foto's toevoegen +Nadat de controle is aangemaakt, kunt u specifieke opmerkingen toevoegen om uw vaststellingen te documenteren: +1. Klik op de detailpagina van de controle op **Opmerking toevoegen**. +2. Kies de status van de conformiteit: + - **Info** (blauw): Voor eenvoudige informatieve opmerkingen of notities. + - **OK** (groen): Om te bevestigen dat het element conform is. + - **NOK** (rood): Om een non-conformiteit te signaleren die moet worden gecorrigeerd. +3. Schrijf een gedetailleerde omschrijving in het opmerkingenveld. +4. Voeg foto's of bestanden toe: + - Gebruik de knop **Foto's maken (continu-modus)** om direct de camera van uw mobiele apparaat/tablet te gebruiken. + - Of klik op **Foto's, video's, bestanden** om bestaande bestanden te selecteren uit de galerij van uw apparaat. +5. Klik op **Opslaan** om deze opmerkingen aan het controlerapport te koppelen. + +## 4. Controles uitvoeren op het terrein (Offline applicatie) +Om controles rechtstreeks op het terrein te vergemakkelijken, heeft StreetUp een speciale mobiele applicatie (PWA): +- **Installatie**: Wanneer u inlogt op een mobiel apparaat of tablet, stelt de applicatie voor om de snelkoppeling "Controles" aan uw startscherm toe te voegen. +- **Offline werking**: U kunt uw volledige controle uitvoeren, uw opmerkingen over de conformiteit noteren en foto's maken, zelfs in zones zonder netwerkbereik (4G/Wi-Fi). +- **Automatische synchronisatie**: Zodra uw apparaat weer verbinding heeft met internet, worden uw controlerapporten en foto's naadloos gesynchroniseerd met de centrale server. diff --git a/streetup/common/help_docs/nl/gerer_projets.md b/streetup/common/help_docs/nl/gerer_projets.md new file mode 100644 index 0000000..ec8c1ca --- /dev/null +++ b/streetup/common/help_docs/nl/gerer_projets.md @@ -0,0 +1,21 @@ +# Projecten beheren + +
+ Let op: Projectbeheer is voorbehouden aan gebruikers met administratieve of beheerdersrollen (Admin, Manager, Controller, External Manager, Top Manager). +
+ +Projecten maken het mogelijk om meerdere interventies te groeperen om grootschalige werven te coördineren. + +## 1. Een nieuw project aanmaken +1. Ga naar het menu **Projecten** via het linkermenu. +2. Klik bovenaan rechts op de knop **Project aanmaken**. +3. Vul de projectnaam, een omschrijving, de geplande start- en einddatum en de betrokken thematieken in. +4. Sla op om de aanmaak te bevestigen. + +## 2. Interventies aan een project koppelen +Zodra het project is aangemaakt: +- Op de detailpagina van het project kunt u bestaande interventies toevoegen door te klikken op **Interventies koppelen**. +- Alternatief kunt u op de detailpagina van een individuele interventie het veld **Project** gebruiken om deze rechtstreeks aan een bestaand project toe te wijzen. + +## 3. Opvolging en voortgang +De pagina van een project toont de algemene voortgang door het percentage voltooide interventies te berekenen ten opzichte van het totaal aantal gekoppelde interventies. U vindt er ook een kaart met alle interventies van het project voor een globale geografische visualisatie. diff --git a/streetup/common/help_docs/nl/prise_en_main.md b/streetup/common/help_docs/nl/prise_en_main.md new file mode 100644 index 0000000..beb8d6d --- /dev/null +++ b/streetup/common/help_docs/nl/prise_en_main.md @@ -0,0 +1,27 @@ +# Aan de slag met StreetUp + +Welkom bij StreetUp! Deze snelle handleiding toont u de basisprincipes van navigatie en gebruik van de applicatie. + +## 1. De hoofdnavigatiebalk (Header) +Bovenaan uw scherm vindt u de knoppenbalk met: +- Toegang tot uw accountgegevens. +- Een weergaveschakelaar (als u toegang heeft tot meerdere portalen). +- Het notificatiecentrum () om discussies en waarschuwingen te volgen. +- De taalkeuze () om te schakelen tussen Frans en Nederlands. +- De helpknop () die dit paneel opent. + +## 2. Het zijmenu (Sidebar) +Aan de linkerkant van het scherm (of via het inklapbare menu op mobiel) kunt u navigeren naar: +- **Home**: Uw gepersonaliseerde dashboard met tellers voor interventies en meldingen. +- **Assets**: De inventaris van uw weginfrastructuur op kaart en in tabelvorm. +- **Observaties**: Uw vaststellingen en inspecties op het terrein. +- **Interventies**: De opvolging van werven en onderhoudswerken (correctief of preventief). +- **Projecten**: De planning van grotere werken. + +## 3. Weergave-indelingen +Op de meeste pagina's (Assets, Interventies) kunt u kiezen om weer te geven: +- Alleen de **kaart** (). +- Alleen de **tabel** met gegevens (). +- **Beide** naast elkaar (, standaard). + +Met deze knoppen (bovenaan rechts op de betreffende pagina's) kunt u de kaart of de tabel verbergen om uw werkruimte te maximaliseren. Uw voorkeuren voor de indeling worden automatisch opgeslagen voor uw volgende bezoeken. diff --git a/streetup/common/help_docs/nl/statuts_intervention.md b/streetup/common/help_docs/nl/statuts_intervention.md new file mode 100644 index 0000000..8bc1deb --- /dev/null +++ b/streetup/common/help_docs/nl/statuts_intervention.md @@ -0,0 +1,129 @@ +# Statussen van interventies + +De levenscyclus van een interventie in StreetUp wordt gekenmerkt door 15 opeenvolgende statussen. Elke status komt overeen met een specifieke fase van planning, uitvoering of administratieve validatie. + +Hier is de gedetailleerde uitleg van elke stap: + +
+ + +
+
1
+
+ In voorbereiding : De interventie wordt opgesteld of bewerkt door de maker. Ze is nog niet zichtbaar voor de wegaannemers. +
+
+ + +
+
2
+
+ Goed te keuren : De interventie is ingediend en wacht op beoordeling door een beheerder om de noodzaak van de werken te bevestigen. +
+
+ + +
+
3
+
+ In te plannen : De interventie is inhoudelijk goedgekeurd. Nu moet er een geplande startdatum aan worden gekoppeld en moet ze aan een team op het terrein worden toegewezen. +
+
+ + +
+
4
+
+ Te behandelen : De interventie is klaar om te worden uitgevoerd door de aannemer, die ze kan toewijzen aan zijn arbeiders op het terrein. +
+
+ + +
+
5
+
+ Toegewezen : De interventie is toegewezen aan een specifiek team of een specifieke arbeider die verantwoordelijk is voor de uitvoering ervan. +
+
+ + +
+
6
+
+ Gestart : De reparatie- of onderhoudswerken zijn momenteel aan de gang op het terrein. +
+
+ + +
+
7
+
+ In pauze : De werf is tijdelijk stilgelegd (in afwachting van onderdelen, slechte weersomstandigheden, enz.). +
+
+ + +
+
8
+
+ Voltooid : De arbeiders hebben de taken op het terrein afgerond. De interventie is klaar op het terrein. +
+
+ + +
+
9
+
+ Behandeld : De aannemer heeft de rapporten na afloop van de werken ingevuld en de interventie ingediend voor conformiteitscontrole. +
+
+ + +
+
10
+
+ Te corrigeren : Na inspectie heeft de beheerder de werken geweigerd wegens niet-conformiteit en de aannemer verzocht correcties uit te voeren. +
+
+ + +
+
11
+
+ Gecorrigeerd : De gevraagde correcties zijn uitgevoerd op het terrein en de interventie is opnieuw klaar voor inspectie. +
+
+ + +
+
12
+
+ Gevalideerd : De werken zijn goedgekeurd en door de beheerder volledig conform verklaard. +
+
+ + +
+
13
+
+ Gefactureerd : De gevalideerde interventie is administratief verwerkt voor facturatie. +
+
+ + +
+
14
+
+ Afgewerkt : De interventie is definitief afgesloten en gearchiveerd (einde van de levenscyclus). +
+
+ + +
+
15
+
+ Geannuleerd : De interventie is geannuleerd wegens een doublure, een invoerfout of een afwijzing tijdens de goedkeuringsfase. +
+
+ +
diff --git a/streetup/common/locale/en/LC_MESSAGES/django.mo b/streetup/common/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..d3c313b Binary files /dev/null and b/streetup/common/locale/en/LC_MESSAGES/django.mo differ diff --git a/streetup/common/locale/en/LC_MESSAGES/django.po b/streetup/common/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..56017f9 --- /dev/null +++ b/streetup/common/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,259 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-06 10:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: common/middleware.py:154 +msgid "" +"Votre session a expiré après le délai configuré. Veuillez vous reconnecter." +msgstr "" +"Your session has expired after the configured timeout. Please log in again." + +#: common/private_files/views.py:37 +msgid "Lien expiré ou invalide" +msgstr "Expired or invalid link" + +#: common/private_files/views.py:43 +msgid "Modèle inconnu" +msgstr "Unknown model" + +#: common/private_files/views.py:47 +msgid "Objet introuvable" +msgstr "Object not found" + +#: common/private_files/views.py:53 +msgid "Fichier non exposé" +msgstr "File not exposed" + +#: common/private_files/views.py:58 +msgid "Accès refusé" +msgstr "Access denied" + +#: common/private_files/views.py:63 +msgid "Fichier introuvable" +msgstr "File not found" + +#: common/templates/admin/common/status_permission_matrix.html:19 +msgid "Permission" +msgstr "Permission" + +#: common/templates/admin/common/status_permission_matrix.html:36 +msgid "Voir" +msgstr "View" + +#: common/templates/admin/common/status_permission_matrix.html:53 +msgid "Modifier" +msgstr "Edit" + +#: common/templates/admin/common/status_permission_matrix.html:70 +msgid "Changer → vers" +msgstr "Change → to" + +#: common/templates/admin/common/status_permission_matrix.html:102 +msgid "Enregistrez d'abord pour configurer les permissions par statut." +msgstr "Save first to configure permissions by status." + +#: common/templates/common/account_info.html:8 +msgid "Déconnexion" +msgstr "Logout" + +#: common/templates/common/account_info.html:26 +msgid "Vous êtes connecté en tant que :" +msgstr "You are logged in as:" + +#: common/templates/common/account_info.html:32 +msgid "Changer de langue" +msgstr "Change language" + +#: common/templates/common/account_info.html:40 +msgid "Choisir la langue" +msgstr "Choose language" + +#: common/templates/common/account_info.html:42 +#: common/templates/common/account_info.html:88 +#: common/templates/common/components/document_viewer.html:201 +msgid "Fermer" +msgstr "Close" + +#: common/templates/common/account_info.html:73 +#: common/templates/common/account_info.html:86 +msgid "Modifier mon mot de passe" +msgstr "Change my password" + +#: common/templates/common/account_info.html:158 +msgid "Annuler" +msgstr "Cancel" + +#: common/templates/common/account_info.html:161 +msgid "Enregistrer" +msgstr "Save" + +#: common/templates/common/account_info.html:174 +msgid "Se déconnecter" +msgstr "Sign out" + +#: common/templates/common/account_info.html:181 +msgid "Retour" +msgstr "Back" + +#: common/templates/common/components/document_viewer.html:40 +#: common/templates/common/components/document_viewer.html:50 +#: common/templates/common/components/document_viewer.html:61 +msgid "Supprimer" +msgstr "Delete" + +#: common/templates/common/components/document_viewer.html:75 +msgid "Copier vers une localisation ou un asset" +msgstr "Copy to a location or an asset" + +#: common/templates/common/components/document_viewer.html:99 +#: common/templates/common/components/document_viewer.html:154 +msgid "Historique des versions" +msgstr "Version history" + +#: common/templates/common/components/document_viewer.html:156 +msgid "Historique" +msgstr "History" + +#: common/templates/common/components/document_viewer.html:180 +msgid "Visionneuse de documents" +msgstr "Document viewer" + +#: common/templates/common/components/document_viewer.html:188 +msgid "Ouvrir en pleine page" +msgstr "Open in full screen" + +#: common/templates/common/components/document_viewer.html:195 +#: common/templates/common/components/document_viewer.html:196 +#: common/templates/common/components/document_viewer.html:261 +msgid "Télécharger" +msgstr "Download" + +#: common/templates/common/components/document_viewer.html:229 +msgid "File truncated to the first 50 000 characters." +msgstr "File truncated to the first 50,000 characters." + +#: common/templates/common/components/document_viewer.html:245 +msgid "Only the first 200 rows are shown." +msgstr "Only the first 200 rows are shown." + +#: common/templates/common/components/document_viewer.html:301 +msgid "Précédent" +msgstr "Previous" + +#: common/templates/common/components/document_viewer.html:309 +msgid "Suivant" +msgstr "Next" + +#: common/templates/common/components/document_viewer.html:432 +msgid "Aucun document associé" +msgstr "No associated document" + +#: common/tests.py:46 common/tests.py:78 common/views.py:265 +#: common/views.py:269 common/views.py:280 +msgid "Vous ne pouvez pas accéder à ce compte." +msgstr "You cannot access this account." + +#: common/views.py:104 +msgid "Coordonnées invalides" +msgstr "Invalid coordinates" + +#: common/views.py:120 +msgid "Aucune rue trouvée" +msgstr "No street found" + +#: common/views.py:205 +msgid "Mot de passe mis à jour avec succès." +msgstr "Password updated successfully." + +#: common/views.py:235 +msgid "Aucun compte lié n'est disponible." +msgstr "No linked account is available." + +#: common/views.py:239 +msgid "Aucun compte cible n'a été sélectionné." +msgstr "No target account was selected." + +#: common/views.py:243 +msgid "Ce compte est déjà actif." +msgstr "This account is already active." + +#: common/views.py:286 +msgid "Changement de compte effectué." +msgstr "Account switch completed." + +#: common/views.py:304 common/views.py:329 common/views.py:356 +#: common/views.py:397 common/views.py:426 common/views.py:487 +#: common/views.py:514 +msgid "Données invalides" +msgstr "Invalid data" + +#: common/views.py:308 common/views.py:333 +msgid "Layout invalide" +msgstr "Invalid layout" + +#: common/views.py:316 common/views.py:341 common/views.py:383 +#: common/views.py:414 common/views.py:474 common/views.py:500 +#: common/views.py:531 +msgid "Configuration utilisateur non trouvée" +msgstr "User configuration not found" + +#: common/views.py:363 common/views.py:403 common/views.py:520 +msgid "Format de filtres invalide" +msgstr "Invalid filter format" + +#: common/views.py:367 common/views.py:406 common/views.py:523 +msgid "Format de tri invalide" +msgstr "Invalid sort format" + +#: common/views.py:375 +msgid "Format de colonnes invalide" +msgstr "Invalid column format" + +#: common/views.py:439 +msgid "Format de thématiques invalide" +msgstr "Invalid thematic format" + +#: common/views.py:441 +msgid "Format de catégories invalide" +msgstr "Invalid category format" + +#: common/views.py:443 +msgid "Format de modèles invalide" +msgstr "Invalid model format" + +#: common/views.py:445 +msgid "Format de filtres thématiques invalide" +msgstr "Invalid thematic filter format" + +#: common/views.py:447 +msgid "Format de colonnes visibles invalide" +msgstr "Invalid visible columns format" + +#: common/views.py:449 +msgid "Format de filtre type invalide" +msgstr "Invalid type filter format" + +#: common/views.py:451 +msgid "Format de filtres statut invalide" +msgstr "Invalid status filter format" + +#: common/views.py:493 +#, fuzzy +#| msgid "Format de filtres invalide" +msgid "Format de préférences invalide" +msgstr "Invalid filter format" diff --git a/streetup/common/locale/en/LC_MESSAGES/djangojs.mo b/streetup/common/locale/en/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..aed1603 Binary files /dev/null and b/streetup/common/locale/en/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/common/locale/en/LC_MESSAGES/djangojs.po b/streetup/common/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..2e0501c --- /dev/null +++ b/streetup/common/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,135 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#: common/static/common/jquery-ui.min.js:6 +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: common/static/common/custom-draw-toolbar.js:315 +msgid "Draw Mode" +msgstr "Draw Mode" + +#: common/static/common/custom-draw-toolbar.js:316 +msgid "Add Polygon" +msgstr "Add Polygon" + +#: common/static/common/custom-draw-toolbar.js:317 +msgid "Delete" +msgstr "Delete" + +#: common/static/common/custom-draw-toolbar.js:318 +msgid "Snap" +msgstr "Snap" + +#: common/static/common/custom-draw-toolbar.js:319 +msgid "Snap enabled" +msgstr "Snap enabled" + +#: common/static/common/custom-draw-toolbar.js:320 +msgid "Snap disabled" +msgstr "Snap disabled" + +#: common/static/common/custom-draw-toolbar.js:321 +msgid "Split Polygon" +msgstr "Split Polygon" + +#: common/static/common/custom-draw-toolbar.js:322 +msgid "Split cancelled" +msgstr "Split cancelled" + +#: common/static/common/custom-draw-toolbar.js:323 +msgid "Split successful" +msgstr "Split successful" + +#: common/static/common/custom-draw-toolbar.js:324 +msgid "Merge Polygons" +msgstr "Merge Polygons" + +#: common/static/common/custom-draw-toolbar.js:325 +msgid "Merge successful" +msgstr "Merge successful" + +#: common/static/common/custom-draw-toolbar.js:326 +msgid "Error merging polygons" +msgstr "Error merging polygons" + +#: common/static/common/custom-draw-toolbar.js:327 +msgid "Polygon removed from edit mode" +msgstr "Polygon removed from edit mode" + +#: common/static/common/custom-draw-toolbar.js:328 +msgid "Polygon added to edit mode" +msgstr "Polygon added to edit mode" + +#: common/static/common/custom-draw-toolbar.js:329 +msgid "Draw mode enabled - Click on a geometry to edit" +msgstr "Draw mode enabled - Click on a geometry to edit" + +#: common/static/common/custom-draw-toolbar.js:330 +msgid "Draw mode disabled" +msgstr "Draw mode disabled" + +#: common/static/common/custom-draw-toolbar.js:331 +msgid "Save geometries" +msgstr "Save geometries" + +#: common/static/common/custom-draw-toolbar.js:332 +msgid "Undo" +msgstr "Undo" + +#: common/static/common/custom-draw-toolbar.js:333 +msgid "No features to delete" +msgstr "No features to delete" + +#: common/static/common/custom-draw-toolbar.js:334 +msgid "Geometry(ies) deleted" +msgstr "Geometry(ies) deleted" + +#: common/static/common/custom-draw-toolbar.js:335 +msgid "Click to add points. Double-click or click the first point to finish." +msgstr "Click to add points. Double-click or click the first point to finish." + +#: common/static/common/custom-draw-toolbar.js:336 +msgid "Draw a line across the polygon to split it." +msgstr "Draw a line across the polygon to split it." + +#: common/static/common/echarts.min.js:45 +msgid "category" +msgstr "category" + +#: common/static/common/jquery-ui.min.js:6 +msgid "d" +msgstr "d" + +#: common/static/common/jquery-ui.min.js:6 +msgid "o" +msgstr "o" + +#: common/static/common/jquery-ui.min.js:6 +msgid "m" +msgstr "m" + +#: common/static/common/jquery-ui.min.js:6 +msgid "y" +msgstr "y" + +#: common/static/common/jquery-ui.min.js:6 +msgid "@" +msgstr "@" + +#: common/static/common/jquery-ui.min.js:6 +msgid "!" +msgstr "!" diff --git a/streetup/common/locale/fr/LC_MESSAGES/django.mo b/streetup/common/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..3b64e88 Binary files /dev/null and b/streetup/common/locale/fr/LC_MESSAGES/django.mo differ diff --git a/streetup/common/locale/fr/LC_MESSAGES/django.po b/streetup/common/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..6c0eb77 --- /dev/null +++ b/streetup/common/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,264 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: streetup/common/middleware.py:154 +msgid "" +"Votre session a expiré après le délai configuré. Veuillez vous reconnecter." +msgstr "" +"Votre session a expiré après le délai configuré. Veuillez vous reconnecter." + +#: streetup/common/private_files/views.py:37 +msgid "Lien expiré ou invalide" +msgstr "Lien expiré ou invalide" + +#: streetup/common/private_files/views.py:43 +msgid "Modèle inconnu" +msgstr "Modèle inconnu" + +#: streetup/common/private_files/views.py:47 +msgid "Objet introuvable" +msgstr "Objet introuvable" + +#: streetup/common/private_files/views.py:53 +msgid "Fichier non exposé" +msgstr "Fichier non exposé" + +#: streetup/common/private_files/views.py:58 +msgid "Accès refusé" +msgstr "Accès refusé" + +#: streetup/common/private_files/views.py:63 +msgid "Fichier introuvable" +msgstr "Fichier introuvable" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:19 +msgid "Permission" +msgstr "Permission" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:36 +msgid "Voir" +msgstr "Voir" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:53 +msgid "Modifier" +msgstr "Modifier" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:70 +msgid "Changer → vers" +msgstr "Changer → vers" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:102 +msgid "Enregistrez d'abord pour configurer les permissions par statut." +msgstr "Enregistrez d'abord pour configurer les permissions par statut." + +#: streetup/common/templates/common/account_info.html:8 +msgid "Déconnexion" +msgstr "Déconnexion" + +#: streetup/common/templates/common/account_info.html:26 +msgid "Vous êtes connecté en tant que :" +msgstr "Vous êtes connecté en tant que :" + +#: streetup/common/templates/common/account_info.html:32 +msgid "Changer de langue" +msgstr "Changer de langue" + +#: streetup/common/templates/common/account_info.html:40 +msgid "Choisir la langue" +msgstr "Choisir la langue" + +#: streetup/common/templates/common/account_info.html:42 +#: streetup/common/templates/common/account_info.html:88 +#: streetup/common/templates/common/components/document_viewer.html:201 +msgid "Fermer" +msgstr "Fermer" + +#: streetup/common/templates/common/account_info.html:73 +#: streetup/common/templates/common/account_info.html:86 +msgid "Modifier mon mot de passe" +msgstr "Modifier mon mot de passe" + +#: streetup/common/templates/common/account_info.html:158 +msgid "Annuler" +msgstr "Annuler" + +#: streetup/common/templates/common/account_info.html:161 +msgid "Enregistrer" +msgstr "Enregistrer" + +#: streetup/common/templates/common/account_info.html:174 +msgid "Se déconnecter" +msgstr "Se déconnecter" + +#: streetup/common/templates/common/account_info.html:181 +msgid "Retour" +msgstr "Retour" + +#: streetup/common/templates/common/components/document_viewer.html:40 +#: streetup/common/templates/common/components/document_viewer.html:50 +#: streetup/common/templates/common/components/document_viewer.html:61 +msgid "Supprimer" +msgstr "Supprimer" + +#: streetup/common/templates/common/components/document_viewer.html:75 +msgid "Copier vers une localisation ou un asset" +msgstr "Copier vers une localisation ou un asset" + +#: streetup/common/templates/common/components/document_viewer.html:99 +#: streetup/common/templates/common/components/document_viewer.html:154 +msgid "Historique des versions" +msgstr "Historique des versions" + +#: streetup/common/templates/common/components/document_viewer.html:156 +msgid "Historique" +msgstr "Historique" + +#: streetup/common/templates/common/components/document_viewer.html:180 +msgid "Visionneuse de documents" +msgstr "Visionneuse de documents" + +#: streetup/common/templates/common/components/document_viewer.html:188 +msgid "Ouvrir en pleine page" +msgstr "Ouvrir en pleine page" + +#: streetup/common/templates/common/components/document_viewer.html:195 +#: streetup/common/templates/common/components/document_viewer.html:196 +#: streetup/common/templates/common/components/document_viewer.html:261 +msgid "Télécharger" +msgstr "Télécharger" + +#: streetup/common/templates/common/components/document_viewer.html:229 +msgid "File truncated to the first 50 000 characters." +msgstr "File truncated to the first 50 000 characters." + +#: streetup/common/templates/common/components/document_viewer.html:245 +msgid "Only the first 200 rows are shown." +msgstr "Only the first 200 rows are shown." + +#: streetup/common/templates/common/components/document_viewer.html:301 +msgid "Précédent" +msgstr "Précédent" + +#: streetup/common/templates/common/components/document_viewer.html:309 +msgid "Suivant" +msgstr "Suivant" + +#: streetup/common/templates/common/components/document_viewer.html:432 +msgid "Aucun document associé" +msgstr "Aucun document associé" + +#: streetup/common/tests.py:46 streetup/common/tests.py:78 +#: streetup/common/views.py:265 streetup/common/views.py:269 +#: streetup/common/views.py:280 +msgid "Vous ne pouvez pas accéder à ce compte." +msgstr "Vous ne pouvez pas accéder à ce compte." + +#: streetup/common/views.py:104 +msgid "Coordonnées invalides" +msgstr "Coordonnées invalides" + +#: streetup/common/views.py:120 +msgid "Aucune rue trouvée" +msgstr "Aucune rue trouvée" + +#: streetup/common/views.py:205 +msgid "Mot de passe mis à jour avec succès." +msgstr "Mot de passe mis à jour avec succès." + +#: streetup/common/views.py:235 +msgid "Aucun compte lié n'est disponible." +msgstr "Aucun compte lié n'est disponible." + +#: streetup/common/views.py:239 +msgid "Aucun compte cible n'a été sélectionné." +msgstr "Aucun compte cible n'a été sélectionné." + +#: streetup/common/views.py:243 +msgid "Ce compte est déjà actif." +msgstr "Ce compte est déjà actif." + +#: streetup/common/views.py:286 +msgid "Changement de compte effectué." +msgstr "Changement de compte effectué." + +#: streetup/common/views.py:304 streetup/common/views.py:329 +#: streetup/common/views.py:356 streetup/common/views.py:397 +#: streetup/common/views.py:426 streetup/common/views.py:487 +#: streetup/common/views.py:514 +msgid "Données invalides" +msgstr "Données invalides" + +#: streetup/common/views.py:308 streetup/common/views.py:333 +msgid "Layout invalide" +msgstr "Layout invalide" + +#: streetup/common/views.py:316 streetup/common/views.py:341 +#: streetup/common/views.py:383 streetup/common/views.py:414 +#: streetup/common/views.py:474 streetup/common/views.py:500 +#: streetup/common/views.py:531 +msgid "Configuration utilisateur non trouvée" +msgstr "Configuration utilisateur non trouvée" + +#: streetup/common/views.py:363 streetup/common/views.py:403 +#: streetup/common/views.py:520 +msgid "Format de filtres invalide" +msgstr "Format de filtres invalide" + +#: streetup/common/views.py:367 streetup/common/views.py:406 +#: streetup/common/views.py:523 +msgid "Format de tri invalide" +msgstr "Format de tri invalide" + +#: streetup/common/views.py:375 +msgid "Format de colonnes invalide" +msgstr "Format de colonnes invalide" + +#: streetup/common/views.py:439 +msgid "Format de thématiques invalide" +msgstr "Format de thématiques invalide" + +#: streetup/common/views.py:441 +msgid "Format de catégories invalide" +msgstr "Format de catégories invalide" + +#: streetup/common/views.py:443 +msgid "Format de modèles invalide" +msgstr "Format de modèles invalide" + +#: streetup/common/views.py:445 +msgid "Format de filtres thématiques invalide" +msgstr "Format de filtres thématiques invalide" + +#: streetup/common/views.py:447 +msgid "Format de colonnes visibles invalide" +msgstr "Format de colonnes visibles invalide" + +#: streetup/common/views.py:449 +msgid "Format de filtre type invalide" +msgstr "Format de filtre type invalide" + +#: streetup/common/views.py:451 +msgid "Format de filtres statut invalide" +msgstr "Format de filtres statut invalide" + +#: streetup/common/views.py:493 +#, fuzzy +#| msgid "Format de filtres invalide" +msgid "Format de préférences invalide" +msgstr "Format de filtres invalide" diff --git a/streetup/common/locale/fr/LC_MESSAGES/djangojs.mo b/streetup/common/locale/fr/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..7eaeef1 Binary files /dev/null and b/streetup/common/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/common/locale/fr/LC_MESSAGES/djangojs.po b/streetup/common/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..1633b3b --- /dev/null +++ b/streetup/common/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,135 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#: common/static/common/jquery-ui.min.js:6 +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: common/static/common/custom-draw-toolbar.js:315 +msgid "Draw Mode" +msgstr "Draw Mode" + +#: common/static/common/custom-draw-toolbar.js:316 +msgid "Add Polygon" +msgstr "Add Polygon" + +#: common/static/common/custom-draw-toolbar.js:317 +msgid "Delete" +msgstr "Delete" + +#: common/static/common/custom-draw-toolbar.js:318 +msgid "Snap" +msgstr "Snap" + +#: common/static/common/custom-draw-toolbar.js:319 +msgid "Snap enabled" +msgstr "Snap enabled" + +#: common/static/common/custom-draw-toolbar.js:320 +msgid "Snap disabled" +msgstr "Snap disabled" + +#: common/static/common/custom-draw-toolbar.js:321 +msgid "Split Polygon" +msgstr "Split Polygon" + +#: common/static/common/custom-draw-toolbar.js:322 +msgid "Split cancelled" +msgstr "Split cancelled" + +#: common/static/common/custom-draw-toolbar.js:323 +msgid "Split successful" +msgstr "Split successful" + +#: common/static/common/custom-draw-toolbar.js:324 +msgid "Merge Polygons" +msgstr "Merge Polygons" + +#: common/static/common/custom-draw-toolbar.js:325 +msgid "Merge successful" +msgstr "Merge successful" + +#: common/static/common/custom-draw-toolbar.js:326 +msgid "Error merging polygons" +msgstr "Error merging polygons" + +#: common/static/common/custom-draw-toolbar.js:327 +msgid "Polygon removed from edit mode" +msgstr "Polygon removed from edit mode" + +#: common/static/common/custom-draw-toolbar.js:328 +msgid "Polygon added to edit mode" +msgstr "Polygon added to edit mode" + +#: common/static/common/custom-draw-toolbar.js:329 +msgid "Draw mode enabled - Click on a geometry to edit" +msgstr "Draw mode enabled - Click on a geometry to edit" + +#: common/static/common/custom-draw-toolbar.js:330 +msgid "Draw mode disabled" +msgstr "Draw mode disabled" + +#: common/static/common/custom-draw-toolbar.js:331 +msgid "Save geometries" +msgstr "Save geometries" + +#: common/static/common/custom-draw-toolbar.js:332 +msgid "Undo" +msgstr "Undo" + +#: common/static/common/custom-draw-toolbar.js:333 +msgid "No features to delete" +msgstr "No features to delete" + +#: common/static/common/custom-draw-toolbar.js:334 +msgid "Geometry(ies) deleted" +msgstr "Geometry(ies) deleted" + +#: common/static/common/custom-draw-toolbar.js:335 +msgid "Click to add points. Double-click or click the first point to finish." +msgstr "Click to add points. Double-click or click the first point to finish." + +#: common/static/common/custom-draw-toolbar.js:336 +msgid "Draw a line across the polygon to split it." +msgstr "Draw a line across the polygon to split it." + +#: common/static/common/echarts.min.js:45 +msgid "category" +msgstr "category" + +#: common/static/common/jquery-ui.min.js:6 +msgid "d" +msgstr "d" + +#: common/static/common/jquery-ui.min.js:6 +msgid "o" +msgstr "o" + +#: common/static/common/jquery-ui.min.js:6 +msgid "m" +msgstr "m" + +#: common/static/common/jquery-ui.min.js:6 +msgid "y" +msgstr "y" + +#: common/static/common/jquery-ui.min.js:6 +msgid "@" +msgstr "@" + +#: common/static/common/jquery-ui.min.js:6 +msgid "!" +msgstr "!" diff --git a/streetup/common/locale/nl/LC_MESSAGES/django.mo b/streetup/common/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..a26e863 Binary files /dev/null and b/streetup/common/locale/nl/LC_MESSAGES/django.mo differ diff --git a/streetup/common/locale/nl/LC_MESSAGES/django.po b/streetup/common/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..eb2f238 --- /dev/null +++ b/streetup/common/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,265 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: streetup/common/middleware.py:154 +msgid "" +"Votre session a expiré après le délai configuré. Veuillez vous reconnecter." +msgstr "" +"Uw sessie is verlopen na de ingestelde time-out. Gelieve opnieuw in te " +"loggen." + +#: streetup/common/private_files/views.py:37 +msgid "Lien expiré ou invalide" +msgstr "Verlopen of ongeldige link" + +#: streetup/common/private_files/views.py:43 +msgid "Modèle inconnu" +msgstr "Onbekend model" + +#: streetup/common/private_files/views.py:47 +msgid "Objet introuvable" +msgstr "Object niet gevonden" + +#: streetup/common/private_files/views.py:53 +msgid "Fichier non exposé" +msgstr "Bestand niet toegankelijk" + +#: streetup/common/private_files/views.py:58 +msgid "Accès refusé" +msgstr "Toegang geweigerd" + +#: streetup/common/private_files/views.py:63 +msgid "Fichier introuvable" +msgstr "Bestand niet gevonden" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:19 +msgid "Permission" +msgstr "Machtiging" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:36 +msgid "Voir" +msgstr "Bekijken" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:53 +msgid "Modifier" +msgstr "Bewerken" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:70 +msgid "Changer → vers" +msgstr "Wijzigen → naar" + +#: streetup/common/templates/admin/common/status_permission_matrix.html:102 +msgid "Enregistrez d'abord pour configurer les permissions par statut." +msgstr "Sla eerst op om machtigingen per status te configureren." + +#: streetup/common/templates/common/account_info.html:8 +msgid "Déconnexion" +msgstr "Afmelden" + +#: streetup/common/templates/common/account_info.html:26 +msgid "Vous êtes connecté en tant que :" +msgstr "U bent ingelogd als:" + +#: streetup/common/templates/common/account_info.html:32 +msgid "Changer de langue" +msgstr "Taal wijzigen" + +#: streetup/common/templates/common/account_info.html:40 +msgid "Choisir la langue" +msgstr "Kies de taal" + +#: streetup/common/templates/common/account_info.html:42 +#: streetup/common/templates/common/account_info.html:88 +#: streetup/common/templates/common/components/document_viewer.html:201 +msgid "Fermer" +msgstr "Sluiten" + +#: streetup/common/templates/common/account_info.html:73 +#: streetup/common/templates/common/account_info.html:86 +msgid "Modifier mon mot de passe" +msgstr "Mijn wachtwoord wijzigen" + +#: streetup/common/templates/common/account_info.html:158 +msgid "Annuler" +msgstr "Annuleren" + +#: streetup/common/templates/common/account_info.html:161 +msgid "Enregistrer" +msgstr "Opslaan" + +#: streetup/common/templates/common/account_info.html:174 +msgid "Se déconnecter" +msgstr "Afmelden" + +#: streetup/common/templates/common/account_info.html:181 +msgid "Retour" +msgstr "Terug" + +#: streetup/common/templates/common/components/document_viewer.html:40 +#: streetup/common/templates/common/components/document_viewer.html:50 +#: streetup/common/templates/common/components/document_viewer.html:61 +msgid "Supprimer" +msgstr "Verwijderen" + +#: streetup/common/templates/common/components/document_viewer.html:75 +msgid "Copier vers une localisation ou un asset" +msgstr "Kopiëren naar een locatie of asset" + +#: streetup/common/templates/common/components/document_viewer.html:99 +#: streetup/common/templates/common/components/document_viewer.html:154 +msgid "Historique des versions" +msgstr "Versiegeschiedenis" + +#: streetup/common/templates/common/components/document_viewer.html:156 +msgid "Historique" +msgstr "Geschiedenis" + +#: streetup/common/templates/common/components/document_viewer.html:180 +msgid "Visionneuse de documents" +msgstr "Documentviewer" + +#: streetup/common/templates/common/components/document_viewer.html:188 +msgid "Ouvrir en pleine page" +msgstr "Volledig scherm openen" + +#: streetup/common/templates/common/components/document_viewer.html:195 +#: streetup/common/templates/common/components/document_viewer.html:196 +#: streetup/common/templates/common/components/document_viewer.html:261 +msgid "Télécharger" +msgstr "Downloaden" + +#: streetup/common/templates/common/components/document_viewer.html:229 +msgid "File truncated to the first 50 000 characters." +msgstr "Bestand ingekort tot de eerste 50.000 tekens." + +#: streetup/common/templates/common/components/document_viewer.html:245 +msgid "Only the first 200 rows are shown." +msgstr "Alleen de eerste 200 rijen worden weergegeven." + +#: streetup/common/templates/common/components/document_viewer.html:301 +msgid "Précédent" +msgstr "Vorige" + +#: streetup/common/templates/common/components/document_viewer.html:309 +msgid "Suivant" +msgstr "Volgende" + +#: streetup/common/templates/common/components/document_viewer.html:432 +msgid "Aucun document associé" +msgstr "Geen gekoppeld document" + +#: streetup/common/tests.py:46 streetup/common/tests.py:78 +#: streetup/common/views.py:265 streetup/common/views.py:269 +#: streetup/common/views.py:280 +msgid "Vous ne pouvez pas accéder à ce compte." +msgstr "U hebt geen toegang tot dit account." + +#: streetup/common/views.py:104 +msgid "Coordonnées invalides" +msgstr "Ongeldige coördinaten" + +#: streetup/common/views.py:120 +msgid "Aucune rue trouvée" +msgstr "Geen straat gevonden" + +#: streetup/common/views.py:205 +msgid "Mot de passe mis à jour avec succès." +msgstr "Wachtwoord succesvol bijgewerkt." + +#: streetup/common/views.py:235 +msgid "Aucun compte lié n'est disponible." +msgstr "Geen gekoppeld account beschikbaar." + +#: streetup/common/views.py:239 +msgid "Aucun compte cible n'a été sélectionné." +msgstr "Geen doelaccount geselecteerd." + +#: streetup/common/views.py:243 +msgid "Ce compte est déjà actif." +msgstr "Dit account is al actief." + +#: streetup/common/views.py:286 +msgid "Changement de compte effectué." +msgstr "Accountwissel uitgevoerd." + +#: streetup/common/views.py:304 streetup/common/views.py:329 +#: streetup/common/views.py:356 streetup/common/views.py:397 +#: streetup/common/views.py:426 streetup/common/views.py:487 +#: streetup/common/views.py:514 +msgid "Données invalides" +msgstr "Ongeldige gegevens" + +#: streetup/common/views.py:308 streetup/common/views.py:333 +msgid "Layout invalide" +msgstr "Ongeldige lay-out" + +#: streetup/common/views.py:316 streetup/common/views.py:341 +#: streetup/common/views.py:383 streetup/common/views.py:414 +#: streetup/common/views.py:474 streetup/common/views.py:500 +#: streetup/common/views.py:531 +msgid "Configuration utilisateur non trouvée" +msgstr "Gebruikersconfiguratie niet gevonden" + +#: streetup/common/views.py:363 streetup/common/views.py:403 +#: streetup/common/views.py:520 +msgid "Format de filtres invalide" +msgstr "Ongeldig filterformaat" + +#: streetup/common/views.py:367 streetup/common/views.py:406 +#: streetup/common/views.py:523 +msgid "Format de tri invalide" +msgstr "Ongeldig sorteervoorbeeld" + +#: streetup/common/views.py:375 +msgid "Format de colonnes invalide" +msgstr "Ongeldig kolomformaat" + +#: streetup/common/views.py:439 +msgid "Format de thématiques invalide" +msgstr "Ongeldig thematiekformaat" + +#: streetup/common/views.py:441 +msgid "Format de catégories invalide" +msgstr "Ongeldig categorieformaat" + +#: streetup/common/views.py:443 +msgid "Format de modèles invalide" +msgstr "Ongeldig modelformaat" + +#: streetup/common/views.py:445 +msgid "Format de filtres thématiques invalide" +msgstr "Ongeldig thematiekfilterformaat" + +#: streetup/common/views.py:447 +msgid "Format de colonnes visibles invalide" +msgstr "Ongeldig zichtbare kolommenformaat" + +#: streetup/common/views.py:449 +msgid "Format de filtre type invalide" +msgstr "Ongeldig type-filterformaat" + +#: streetup/common/views.py:451 +msgid "Format de filtres statut invalide" +msgstr "Ongeldig statusfilterformaat" + +#: streetup/common/views.py:493 +#, fuzzy +#| msgid "Format de filtres invalide" +msgid "Format de préférences invalide" +msgstr "Ongeldig filterformaat" diff --git a/streetup/common/locale/nl/LC_MESSAGES/djangojs.mo b/streetup/common/locale/nl/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..b2d66ae Binary files /dev/null and b/streetup/common/locale/nl/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/common/locale/nl/LC_MESSAGES/djangojs.po b/streetup/common/locale/nl/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..d5778d7 --- /dev/null +++ b/streetup/common/locale/nl/LC_MESSAGES/djangojs.po @@ -0,0 +1,137 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#: common/static/common/jquery-ui.min.js:6 +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: common/static/common/custom-draw-toolbar.js:315 +msgid "Draw Mode" +msgstr "Tekenmodus" + +#: common/static/common/custom-draw-toolbar.js:316 +msgid "Add Polygon" +msgstr "Voeg veelhoek toe" + +#: common/static/common/custom-draw-toolbar.js:317 +msgid "Delete" +msgstr "Verwijderen" + +#: common/static/common/custom-draw-toolbar.js:318 +msgid "Snap" +msgstr "Vastklikken" + +#: common/static/common/custom-draw-toolbar.js:319 +msgid "Snap enabled" +msgstr "Vastklikken ingeschakeld" + +#: common/static/common/custom-draw-toolbar.js:320 +msgid "Snap disabled" +msgstr "Vastklikken uitgeschakeld" + +#: common/static/common/custom-draw-toolbar.js:321 +msgid "Split Polygon" +msgstr "Veelhoek splitsen" + +#: common/static/common/custom-draw-toolbar.js:322 +msgid "Split cancelled" +msgstr "Splitsen geannuleerd" + +#: common/static/common/custom-draw-toolbar.js:323 +msgid "Split successful" +msgstr "Splitsen geslaagd" + +#: common/static/common/custom-draw-toolbar.js:324 +msgid "Merge Polygons" +msgstr "Veelhoeken samenvoegen" + +#: common/static/common/custom-draw-toolbar.js:325 +msgid "Merge successful" +msgstr "Samenvoegen geslaagd" + +#: common/static/common/custom-draw-toolbar.js:326 +msgid "Error merging polygons" +msgstr "Fout bij samenvoegen van veelhoeken" + +#: common/static/common/custom-draw-toolbar.js:327 +msgid "Polygon removed from edit mode" +msgstr "Veelhoek verwijderd uit bewerkingsmodus" + +#: common/static/common/custom-draw-toolbar.js:328 +msgid "Polygon added to edit mode" +msgstr "Veelhoek toegevoegd aan bewerkingsmodus" + +#: common/static/common/custom-draw-toolbar.js:329 +msgid "Draw mode enabled - Click on a geometry to edit" +msgstr "Tekenmodus ingeschakeld - Klik op een geometrie om te bewerken" + +#: common/static/common/custom-draw-toolbar.js:330 +msgid "Draw mode disabled" +msgstr "Tekenmodus uitgeschakeld" + +#: common/static/common/custom-draw-toolbar.js:331 +msgid "Save geometries" +msgstr "Geometrieën opslaan" + +#: common/static/common/custom-draw-toolbar.js:332 +msgid "Undo" +msgstr "Ongedaan maken" + +#: common/static/common/custom-draw-toolbar.js:333 +msgid "No features to delete" +msgstr "Geen objecten om te verwijderen" + +#: common/static/common/custom-draw-toolbar.js:334 +msgid "Geometry(ies) deleted" +msgstr "Geometrie(ën) verwijderd" + +#: common/static/common/custom-draw-toolbar.js:335 +msgid "Click to add points. Double-click or click the first point to finish." +msgstr "" +"Klik om punten toe te voegen. Dubbelklik of klik op het eerste punt om te " +"voltooien." + +#: common/static/common/custom-draw-toolbar.js:336 +msgid "Draw a line across the polygon to split it." +msgstr "Teken een lijn over de veelhoek om deze te splitsen." + +#: common/static/common/echarts.min.js:45 +msgid "category" +msgstr "categorie" + +#: common/static/common/jquery-ui.min.js:6 +msgid "d" +msgstr "d" + +#: common/static/common/jquery-ui.min.js:6 +msgid "o" +msgstr "o" + +#: common/static/common/jquery-ui.min.js:6 +msgid "m" +msgstr "m" + +#: common/static/common/jquery-ui.min.js:6 +msgid "y" +msgstr "y" + +#: common/static/common/jquery-ui.min.js:6 +msgid "@" +msgstr "@" + +#: common/static/common/jquery-ui.min.js:6 +msgid "!" +msgstr "!" diff --git a/streetup/common/management/commands/backup_database.py b/streetup/common/management/commands/backup_database.py new file mode 100644 index 0000000..eae92c6 --- /dev/null +++ b/streetup/common/management/commands/backup_database.py @@ -0,0 +1,377 @@ +import os +import sys +import subprocess +import gzip +import shutil +from datetime import datetime, timedelta +from pathlib import Path + +from django.core.management.base import BaseCommand, CommandError +from django.conf import settings +from django.utils import timezone + +try: + import boto3 + from botocore.config import Config + from botocore.exceptions import ClientError, NoCredentialsError + BOTO3_AVAILABLE = True +except ImportError: + BOTO3_AVAILABLE = False + + +class Command(BaseCommand): + help = "Effectue une sauvegarde complète de la base de données PostgreSQL, puis effectue les transferts optionnels (Rsync, MinIO S3)." + + def log_info(self, message: str): + self.stdout.write(f"[{timezone.now().strftime('%Y-%m-%d %H:%M:%S')}] [INFO] {message}") + + def log_warning(self, message: str): + self.stdout.write(self.style.WARNING(f"[{timezone.now().strftime('%Y-%m-%d %H:%M:%S')}] [WARNING] {message}")) + + def log_error(self, message: str): + self.stderr.write(self.style.ERROR(f"[{timezone.now().strftime('%Y-%m-%d %H:%M:%S')}] [ERROR] {message}")) + + def get_backup_dir(self) -> Path: + """Retourne le répertoire de sauvegarde, le crée si nécessaire.""" + backup_dir = os.getenv("BACKUP_DIR") + if backup_dir: + backup_path = Path(backup_dir) + else: + # Répertoire 'backups' à la racine Django + backup_path = settings.BASE_DIR / "backups" + + backup_path.mkdir(parents=True, exist_ok=True) + return backup_path + + def get_db_config(self) -> dict: + """Récupère la configuration de la base de données depuis l'environnement ou les settings Django.""" + db_settings = settings.DATABASES.get("default", {}) + + # Priorité aux variables d'environnement, sinon repli sur la conf Django + config = { + "host": os.getenv("DB_HOST") or db_settings.get("HOST") or "localhost", + "port": os.getenv("DB_PORT") or db_settings.get("PORT") or "5432", + "name": os.getenv("DB_NAME") or db_settings.get("NAME"), + "user": os.getenv("DB_USER") or db_settings.get("USER"), + "password": os.getenv("DB_PASSWORD") or db_settings.get("PASSWORD"), + } + + # Vérification des paramètres requis + missing = [key for key, value in config.items() if not value and key != "port"] + if missing: + raise CommandError(f"Paramètres de connexion à la base de données manquants: {', '.join(missing)}") + + return config + + def get_schemas(self) -> list: + """Récupère les schémas PostgreSQL à sauvegarder.""" + schemas_str = os.getenv("BACKUP_SCHEMAS") + if not schemas_str: + # Récupération automatique du schéma configuré + db_schema = os.getenv("DB_SCHEMA") + if not db_schema: + options = settings.DATABASES.get("default", {}).get("OPTIONS", {}).get("options", "") + if "search_path=" in options: + db_schema = options.split("search_path=")[1].split(",")[0].strip() + else: + db_schema = "django" + schemas_str = f"{db_schema},public" + + return [s.strip() for s in schemas_str.split(",") if s.strip()] + + def run_pg_dump(self, db_config: dict, output_file: Path, timeout_seconds: int, schemas: list) -> bool: + """Exécute pg_dump pour créer la sauvegarde brute.""" + pg_dump_path = os.getenv("PG_DUMP_PATH", "pg_dump") + + cmd = [ + pg_dump_path, + "-h", db_config["host"], + "-p", str(db_config["port"]), + "-U", db_config["user"], + "-d", db_config["name"], + "-F", "p", # Plain SQL format + "--no-password", + ] + + for schema in schemas: + cmd.extend(["-n", schema]) + + env = os.environ.copy() + env["PGPASSWORD"] = db_config["password"] + + try: + self.log_info(f"Démarrage de pg_dump vers {output_file}") + self.log_info(f"Schémas : {', '.join(schemas)}") + + with open(output_file, "w") as f: + result = subprocess.run( + cmd, + stdout=f, + stderr=subprocess.PIPE, + env=env, + timeout=timeout_seconds, + ) + + if result.returncode != 0: + err_msg = result.stderr.decode('utf-8', errors='replace') + self.log_error(f"Erreur pg_dump : {err_msg}") + if output_file.exists(): + output_file.unlink() + raise CommandError(f"pg_dump a échoué (code {result.returncode}) : {err_msg}") + + self.log_info("pg_dump terminé avec succès") + + except subprocess.TimeoutExpired: + self.log_error(f"Timeout: pg_dump a dépassé la limite de {timeout_seconds}s") + if output_file.exists(): + output_file.unlink() + raise CommandError(f"pg_dump a expiré après {timeout_seconds} secondes.") + except FileNotFoundError: + self.log_error(f"pg_dump introuvable à '{pg_dump_path}'. Assurez-vous que postgresql-client est installé.") + if output_file.exists(): + output_file.unlink() + raise CommandError(f"pg_dump introuvable à '{pg_dump_path}'. Veuillez installer postgresql-client ou configurer PG_DUMP_PATH.") + except Exception as e: + self.log_error(f"Erreur inattendue durant pg_dump : {str(e)}") + if output_file.exists(): + output_file.unlink() + raise CommandError(f"Erreur inattendue durant pg_dump : {str(e)}") + + def compress_file(self, input_file: Path, output_file: Path): + """Compresse la sauvegarde brute avec gzip.""" + try: + self.log_info(f"Compression du dump vers {output_file}") + with open(input_file, "rb") as f_in: + with gzip.open(output_file, "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + # Suppression du fichier non compressé + input_file.unlink() + + size_mb = output_file.stat().st_size / (1024 * 1024) + self.log_info(f"Compression terminée. Taille : {size_mb:.2f} Mo") + except Exception as e: + self.log_error(f"Erreur lors de la compression : {str(e)}") + raise CommandError(f"Erreur lors de la compression gzip : {str(e)}") + + def upload_to_minio(self, backup_file: Path) -> bool: + """Téléverse la sauvegarde sur le serveur MinIO configuré via l'API S3.""" + if not BOTO3_AVAILABLE: + self.log_error("La librairie 'boto3' est requise pour le téléversement MinIO.") + return False + + endpoint_url = os.getenv("BACKUP_MINIO_ENDPOINT_URL", "https://apps.mobility.brussels") + bucket = os.getenv("BACKUP_MINIO_BUCKET", "datatransfer") + region = os.getenv("BACKUP_MINIO_REGION", "us-east-1") + access_key = os.getenv("BACKUP_MINIO_ACCESS_KEY") + secret_key = os.getenv("BACKUP_MINIO_SECRET_KEY") + + if not access_key or not secret_key: + self.log_error("BACKUP_MINIO_ACCESS_KEY ou BACKUP_MINIO_SECRET_KEY non configuré dans l'environnement. Téléversement impossible.") + return False + + object_key = f"{datetime.now().date().isoformat()}_{backup_file.name}" + self.log_info(f"Téléversement vers MinIO ({endpoint_url}/{bucket}/{object_key})...") + + s3 = boto3.client( + "s3", + endpoint_url=endpoint_url, + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + region_name=region, + config=Config(signature_version="s3v4", s3={"addressing_style": "path"}), + ) + + try: + from boto3.s3.transfer import TransferConfig + + # Configure le multipart upload avec des blocs de 5 Mo (minimum S3 requis) + # afin de contourner la limite de taille d'entité imposée par le proxy Nginx/Apache. + transfer_config = TransferConfig( + multipart_threshold=5 * 1024 * 1024, + multipart_chunksize=5 * 1024 * 1024, + ) + + s3.upload_file( + Filename=str(backup_file), + Bucket=bucket, + Key=object_key, + Config=transfer_config + ) + self.log_info("Téléversement MinIO réussi.") + return True + except NoCredentialsError: + self.log_error("Identifiants non valides pour MinIO/S3.") + return False + except ClientError as e: + self.log_error(f"Échec MinIO ClientError : {e}") + return False + except Exception as e: + self.log_error(f"Erreur inattendue durant l'upload MinIO : {e}") + return False + + def sync_to_remote(self, backup_file: Path) -> bool: + """Effectue le transfert distant historique (rsync) s'il est activé.""" + remote_enabled = os.getenv("BACKUP_REMOTE_ENABLED", "false").lower() == "true" + if not remote_enabled: + return True + + remote_host = os.getenv("BACKUP_REMOTE_HOST") + remote_dir = os.getenv("BACKUP_REMOTE_DIR") + ssh_key = os.getenv("BACKUP_SSH_KEY") + # Si le chemin configuré n'existe pas dans le conteneur mais qu'un montage volume + # a mis la clé dans /app/backup_key, on se replie sur ce montage. + if ssh_key and not os.path.exists(ssh_key) and os.path.exists("/app/backup_key"): + self.log_info("Repli automatique sur la clé SSH montée dans le conteneur (/app/backup_key)") + ssh_key = "/app/backup_key" + + timeout = int(os.getenv("BACKUP_REMOTE_TIMEOUT", "3600")) + + if not remote_host or not remote_dir: + self.log_warning("BACKUP_REMOTE_HOST ou BACKUP_REMOTE_DIR non configuré. Transfert Rsync ignoré.") + return False + + self.log_info(f"Transfert Rsync vers {remote_host}:{remote_dir}") + + # Préparer une clé SSH temporaire avec des permissions 600 pour contourner + # les restrictions de permissions d'SSH sur les clés à droits trop larges (ex: 644) + tmp_ssh_key = None + if ssh_key and os.path.exists(ssh_key): + try: + import tempfile + fd, tmp_ssh_path = tempfile.mkstemp() + with os.fdopen(fd, 'wb') as tmp_file: + with open(ssh_key, 'rb') as src_file: + tmp_file.write(src_file.read()) + + os.chmod(tmp_ssh_path, 0o600) + tmp_ssh_key = tmp_ssh_path + self.log_info(f"Clé SSH copiée temporairement avec les permissions 600 : {tmp_ssh_key}") + except Exception as e: + self.log_warning(f"Impossible de sécuriser la clé SSH pour le conteneur: {e}. Utilisation directe.") + tmp_ssh_key = ssh_key + else: + tmp_ssh_key = ssh_key + + ssh_cmd = "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + if tmp_ssh_key: + ssh_cmd = f"ssh -i {tmp_ssh_key} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + cmd = [ + "rsync", + "-avz", + "-e", ssh_cmd, + str(backup_file), + f"{remote_host}:{remote_dir}/", + ] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + ) + + if result.returncode != 0: + self.log_error(f"Erreur Rsync : {result.stderr}") + return False + + self.log_info("Transfert Rsync réussi.") + return True + except subprocess.TimeoutExpired: + self.log_error(f"Timeout: Rsync a dépassé {timeout}s.") + return False + except FileNotFoundError: + self.log_error("rsync non trouvé sur le système.") + return False + except Exception as e: + self.log_error(f"Erreur Rsync inattendue : {str(e)}") + return False + finally: + if tmp_ssh_key and tmp_ssh_key != ssh_key and os.path.exists(tmp_ssh_key): + try: + os.unlink(tmp_ssh_key) + except OSError: + pass + + def cleanup_old_backups(self, backup_dir: Path, retention_days: int): + """Supprime les sauvegardes expirées locales.""" + cutoff_date = datetime.now() - timedelta(days=retention_days) + deleted_count = 0 + + self.log_info(f"Nettoyage des sauvegardes locales de plus de {retention_days} jours") + + for backup_file in backup_dir.glob("streetup_backup_*.sql.gz"): + try: + date_str = backup_file.name.replace("streetup_backup_", "").replace(".sql.gz", "") + # Format attendu : YYYYMMDD_HHMMSS + file_date = datetime.strptime(date_str, "%Y%m%d_%H%M%S") + + if file_date < cutoff_date: + backup_file.unlink() + self.log_info(f"Supprimé : {backup_file.name}") + deleted_count += 1 + except (ValueError, OSError) as e: + self.log_warning(f"Impossible de traiter/supprimer {backup_file.name} : {str(e)}") + + if deleted_count > 0: + self.log_info(f"{deleted_count} sauvegarde(s) ancienne(s) supprimée(s).") + else: + self.log_info("Aucune sauvegarde expirée à supprimer.") + + def handle(self, *args, **options): + # Vérification globale de l'activation des sauvegardes (évite les lancements indésirables en dev/test après import de dump) + backup_enabled = os.getenv("BACKUP_ENABLED", "false").lower() == "true" + if not backup_enabled: + self.log_warning("La sauvegarde de la base de données est désactivée via la variable d'environnement BACKUP_ENABLED.") + return + + self.log_info("=" * 60) + self.log_info("SAUVEGARDE DE LA BASE DE DONNÉES STREETUP") + self.log_info("=" * 60) + + db_config = self.get_db_config() + backup_dir = self.get_backup_dir() + retention_days = int(os.getenv("BACKUP_RETENTION_DAYS", "30")) + timeout_seconds = int(os.getenv("BACKUP_TIMEOUT_SECONDS", "3600")) + + schemas = self.get_schemas() + + # Configuration des transferts + minio_enabled = os.getenv("BACKUP_MINIO_ENABLED", "false").lower() == "true" + remote_enabled = os.getenv("BACKUP_REMOTE_ENABLED", "false").lower() == "true" + + self.log_info(f"Base de données : {db_config['name']} @ {db_config['host']}") + self.log_info(f"Dossier local : {backup_dir}") + self.log_info(f"Rétention locale : {retention_days} jours") + self.log_info(f"Schémas : {', '.join(schemas)}") + self.log_info(f"Upload MinIO : {'ACTIVÉ' if minio_enabled else 'DÉSACTIVÉ'}") + self.log_info(f"Transfert Rsync : {'ACTIVÉ' if remote_enabled else 'DÉSACTIVÉ'}") + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + sql_file = backup_dir / f"streetup_backup_{timestamp}.sql" + gz_file = backup_dir / f"streetup_backup_{timestamp}.sql.gz" + + # 1. pg_dump + self.run_pg_dump(db_config, sql_file, timeout_seconds, schemas) + + # 2. gzip + self.compress_file(sql_file, gz_file) + + # 3. MinIO + if minio_enabled: + if not self.upload_to_minio(gz_file): + self.log_warning("Sauvegarde MinIO en échec (le backup local est conservé).") + + # 4. Rsync + if remote_enabled: + if not self.sync_to_remote(gz_file): + self.log_warning("Sauvegarde Rsync en échec (le backup local est conservé).") + + # 5. Clean local + self.cleanup_old_backups(backup_dir, retention_days) + + self.log_info("-" * 60) + self.log_info(f"✓ Sauvegarde exécutée avec succès : {gz_file.name}") + self.log_info("=" * 60) diff --git a/streetup/common/management/commands/copy_contract_access.py b/streetup/common/management/commands/copy_contract_access.py new file mode 100644 index 0000000..3f3fd27 --- /dev/null +++ b/streetup/common/management/commands/copy_contract_access.py @@ -0,0 +1,207 @@ +""" +Copie les droits d'accès (UserContractAccess + UserContractStatusPermission) +d'un contrat source vers un contrat cible, pour tous les utilisateurs du +contrat source (ou un sous-ensemble). + +Comportement par défaut : + - Si l'utilisateur a déjà un accès au contrat cible → son accès est ignoré + (utiliser --update pour le remplacer). + - L'accès au contrat source est conservé tel quel ; il n'est pas supprimé. + +Options : + --source NUMERO Numéro du contrat source (obligatoire) + --target NUMERO Numéro du contrat cible (obligatoire) + --update Met à jour l'accès cible s'il existe déjà + --dry-run Affiche sans écrire + --user EMAIL Limite à un utilisateur précis (répétable) + +Exemples : + python manage.py copy_contract_access --source CSC-2022-01 --target CSC-2025-01 + python manage.py copy_contract_access --source CSC-2022-01 --target CSC-2025-01 --update + python manage.py copy_contract_access --source CSC-2022-01 --target CSC-2025-01 --dry-run + python manage.py copy_contract_access --source CSC-2022-01 --target CSC-2025-01 --user alice@example.com +""" + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from common.models import UserContractAccess, UserContractStatusPermission +from contracts.models import Contract + + +class Command(BaseCommand): + help = "Copie les droits d'accès d'un contrat vers un autre pour tous les utilisateurs concernés." + + def add_arguments(self, parser): + parser.add_argument( + '--source', + metavar='NUMERO', + required=True, + help='Numéro du contrat source (contract_number).', + ) + parser.add_argument( + '--target', + metavar='NUMERO', + required=True, + help='Numéro du contrat cible (contract_number).', + ) + parser.add_argument( + '--update', + action='store_true', + default=False, + help='Met à jour les accès existants au contrat cible au lieu de les ignorer.', + ) + parser.add_argument( + '--dry-run', + action='store_true', + default=False, + dest='dry_run', + help='Affiche ce qui serait fait sans rien écrire.', + ) + parser.add_argument( + '--user', + metavar='USERNAME_OR_EMAIL', + action='append', + default=[], + dest='users', + help='Limite la copie à ces utilisateurs (répétable). Accepte username ou email.', + ) + + # ------------------------------------------------------------------ + + def handle(self, *args, **options): + dry_run = options['dry_run'] + do_update = options['update'] + identifiers = options['users'] + + if dry_run: + self.stdout.write(self.style.WARNING('Mode DRY-RUN : aucune modification.\n')) + + # Résoudre les deux contrats + try: + source_contract = Contract.objects.get(contract_number=options['source']) + except Contract.DoesNotExist: + raise CommandError(f"Contrat source introuvable : {options['source']}") + + try: + target_contract = Contract.objects.get(contract_number=options['target']) + except Contract.DoesNotExist: + raise CommandError(f"Contrat cible introuvable : {options['target']}") + + if source_contract.pk == target_contract.pk: + raise CommandError("Source et cible sont le même contrat.") + + # Récupérer les accès source + qs = UserContractAccess.objects.filter( + contract=source_contract, + ).select_related('user_config__user').prefetch_related('status_permissions') + + if identifiers: + qs = qs.filter( + user_config__user__username__in=identifiers + ) | qs.filter( + user_config__user__email__in=identifiers + ) + qs = qs.distinct() + + source_accesses = list(qs) + if not source_accesses: + self.stdout.write(self.style.WARNING( + f'Aucun accès trouvé pour le contrat source « {options["source"]} ».' + )) + return + + self.stdout.write( + f'{len(source_accesses)} utilisateur(s) à traiter : ' + f'{options["source"]} → {options["target"]}\n' + ) + + created_count = updated_count = skipped_count = 0 + + with transaction.atomic(): + for src_uca in source_accesses: + username = src_uca.user_config.user.username + result = self._copy_access( + src_uca, target_contract, do_update, dry_run + ) + if result == 'created': + self.stdout.write(f' [CRÉE] {username}') + created_count += 1 + elif result == 'updated': + self.stdout.write(f' [MAJ] {username}') + updated_count += 1 + else: + self.stdout.write(f' [IGNORÉ] {username} (accès cible déjà présent)') + skipped_count += 1 + + prefix = '[DRY-RUN] ' if dry_run else '' + self.stdout.write('') + self.stdout.write(self.style.SUCCESS( + f'{prefix}Résultat — créés : {created_count} | ' + f'mis à jour : {updated_count} | ignorés : {skipped_count}' + )) + + # ------------------------------------------------------------------ + + def _copy_access(self, src_uca, target_contract, do_update, dry_run): + """ + Crée ou met à jour le UserContractAccess + UserContractStatusPermission + pour le contrat cible. + Retourne 'created', 'updated' ou 'skipped'. + """ + cfg = src_uca.user_config + + try: + existing = UserContractAccess.objects.get( + user_config=cfg, + contract=target_contract, + ) + except UserContractAccess.DoesNotExist: + existing = None + + if existing and not do_update: + return 'skipped' + + if dry_run: + return 'updated' if existing else 'created' + + # Champs à copier depuis la source + access_fields = { + 'can_view_posts': src_uca.can_view_posts, + 'can_view_prices': src_uca.can_view_prices, + 'can_view_interventions': src_uca.can_view_interventions, + 'can_view_assets': src_uca.can_view_assets, + 'can_create_interventions': src_uca.can_create_interventions, + 'can_check_interventions': src_uca.can_check_interventions, + 'can_approve': src_uca.can_approve, + } + + if existing: + for field, value in access_fields.items(): + setattr(existing, field, value) + existing.save() + target_uca = existing + # Supprimer les anciennes permissions par statut avant de les recréer + target_uca.status_permissions.all().delete() + label = 'updated' + else: + target_uca = UserContractAccess.objects.create( + user_config=cfg, + contract=target_contract, + **access_fields, + ) + label = 'created' + + # Copier les permissions par statut + UserContractStatusPermission.objects.bulk_create([ + UserContractStatusPermission( + user_contract=target_uca, + status=sp.status, + can_view=sp.can_view, + can_edit=sp.can_edit, + can_change_status_to=sp.can_change_status_to, + ) + for sp in src_uca.status_permissions.all() + ]) + + return label diff --git a/streetup/common/management/commands/export_user_configs.py b/streetup/common/management/commands/export_user_configs.py new file mode 100644 index 0000000..1519168 --- /dev/null +++ b/streetup/common/management/commands/export_user_configs.py @@ -0,0 +1,192 @@ +""" +Exporte les configurations utilisateur (UserConfig + toutes les relations) +dans un fichier JSON portable entre environnements. + +Usage : + # Exporter tous les utilisateurs + python manage.py export_user_configs --output exports/user_configs.json + + # Exporter un ou plusieurs utilisateurs précis + python manage.py export_user_configs --user alice@example.com --user bob@example.com --output users.json + + # Afficher dans la console (stdout) + python manage.py export_user_configs +""" + +import json +import sys +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + +from common.models import UserConfig, UserThematics, UserContractAccess, UserWarehouseAccess, UserWarehouseLocationAccess +from common.models import UserThematicStatusPermission, UserContractStatusPermission +from contracts.models import UserCompanyTeamAccess + +User = get_user_model() + +EXPORT_VERSION = 2 + + +class Command(BaseCommand): + help = 'Exporte les configurations utilisateur en JSON.' + + def add_arguments(self, parser): + parser.add_argument( + '--output', '-o', + metavar='FICHIER', + default=None, + help='Chemin du fichier de sortie. Si absent, affiche sur stdout.', + ) + parser.add_argument( + '--user', + metavar='USERNAME_OR_EMAIL', + action='append', + default=[], + dest='users', + help='Limite l\'export à ces utilisateurs (répétable). Accepte username ou email.', + ) + + def handle(self, *args, **options): + qs = UserConfig.objects.select_related( + 'user', 'default_thematic', 'company' + ).prefetch_related( + 'roles', + 'accessible_views', + 'linked_configs__user', + 'userthematics__thematic', + 'userthematics__status_permissions', + 'usercontractaccess_set__contract', + 'usercontractaccess_set__status_permissions', + 'userwarehouses__warehouse', + 'team_accesses__team__company', + 'userlocations__warehouse_location', + ) + + identifiers = options['users'] + if identifiers: + qs = qs.filter( + user__username__in=identifiers + ) | qs.filter( + user__email__in=identifiers + ) + qs = qs.distinct() + if not qs.exists(): + self.stderr.write(self.style.ERROR( + f'Aucun utilisateur trouvé pour : {", ".join(identifiers)}' + )) + return + + payload = { + 'version': EXPORT_VERSION, + 'exported_at': __import__('datetime').datetime.utcnow().isoformat() + 'Z', + 'count': qs.count(), + 'users': [self._serialize_config(cfg) for cfg in qs], + } + + output_json = json.dumps(payload, indent=2, ensure_ascii=False) + + output_path = options['output'] + if output_path: + with open(output_path, 'w', encoding='utf-8') as fh: + fh.write(output_json) + self.stdout.write(self.style.SUCCESS( + f'{payload["count"]} configuration(s) exportée(s) vers {output_path}' + )) + else: + sys.stdout.write(output_json + '\n') + + # ------------------------------------------------------------------ + + def _serialize_config(self, cfg): + return { + 'username': cfg.user.username, + 'email': cfg.user.email, + 'first_name': cfg.user.first_name, + 'last_name': cfg.user.last_name, + 'is_intern': cfg.is_intern, + 'generic_email': cfg.generic_email, + 'default_thematic': cfg.default_thematic.code if cfg.default_thematic else None, + 'company': cfg.company.name if cfg.company else None, + 'limit_assets_to_contracts': cfg.limit_assets_to_contracts, + 'limit_interventions_to_contracts': cfg.limit_interventions_to_contracts, + 'limit_shortcuts_to_contracts': cfg.limit_shortcuts_to_contracts, + 'hide_intervention_map_by_default': cfg.hide_intervention_map_by_default, + 'auto_logout_hours': cfg.auto_logout_hours, + 'can_add_unique_tag_documents': cfg.can_add_unique_tag_documents, + 'roles': list(cfg.roles.values_list('name', flat=True)), + 'accessible_views': list(cfg.accessible_views.values_list('code', flat=True)), + 'linked_configs': list(cfg.linked_configs.values_list('user__username', flat=True)), + 'thematics': [self._serialize_thematic(ut) for ut in cfg.userthematics.all()], + 'contracts': [self._serialize_contract(uc) for uc in cfg.usercontractaccess_set.all()], + 'warehouses': [self._serialize_warehouse(wa) for wa in cfg.userwarehouses.all()], + 'locations': [self._serialize_location(la) for la in cfg.userlocations.all()], + 'team_accesses': [self._serialize_team(ta) for ta in cfg.team_accesses.all()], + } + + def _serialize_thematic(self, ut): + return { + 'thematic': ut.thematic.code, + 'can_view_assets': ut.can_view_assets, + 'can_edit_assets': ut.can_edit_assets, + 'can_view_interventions': ut.can_view_interventions, + 'can_edit_interventions': ut.can_edit_interventions, + 'can_edit_locations': ut.can_edit_locations, + 'can_process_observations': ut.can_process_observations, + 'status_permissions': [ + { + 'status': sp.status, + 'can_view': sp.can_view, + 'can_edit': sp.can_edit, + 'can_change_status_to': sp.can_change_status_to, + } + for sp in ut.status_permissions.all() + ], + } + + def _serialize_contract(self, uc): + return { + 'contract': uc.contract.contract_number, + 'can_view_posts': uc.can_view_posts, + 'can_view_prices': uc.can_view_prices, + 'can_view_interventions': uc.can_view_interventions, + 'can_view_assets': uc.can_view_assets, + 'can_create_interventions': uc.can_create_interventions, + 'can_check_interventions': uc.can_check_interventions, + 'can_approve': uc.can_approve, + 'status_permissions': [ + { + 'status': sp.status, + 'can_view': sp.can_view, + 'can_edit': sp.can_edit, + 'can_change_status_to': sp.can_change_status_to, + } + for sp in uc.status_permissions.all() + ], + } + + def _serialize_warehouse(self, wa): + return { + 'warehouse': wa.warehouse.code, + 'can_view': wa.can_view, + 'can_manage': wa.can_manage, + 'can_administer': wa.can_administer, + 'can_manage_preparation_orders': wa.can_manage_preparation_orders, + 'can_manage_purchase_orders': wa.can_manage_purchase_orders, + 'can_validate_inventory': wa.can_validate_inventory, + 'is_default': wa.is_default, + } + + def _serialize_location(self, la): + return { + 'location_code': la.warehouse_location.code, + 'can_perform_inventory': la.can_perform_inventory, + 'can_validate_inventory': la.can_validate_inventory, + } + + def _serialize_team(self, ta): + return { + 'team': ta.team.name, + 'company': ta.team.company.name, + 'can_view': ta.can_view, + 'can_edit': ta.can_edit, + } diff --git a/streetup/common/management/commands/fill_status_permissions.py b/streetup/common/management/commands/fill_status_permissions.py new file mode 100644 index 0000000..788a833 --- /dev/null +++ b/streetup/common/management/commands/fill_status_permissions.py @@ -0,0 +1,491 @@ +""" +Commande Django pour pré-remplir les permissions par statut +(UserThematicStatusPermission et UserContractStatusPermission) +à partir des rôles et flags déjà présents dans la base. + +Même logique que la migration 0055, mais utilisable à tout moment sur +n'importe quel serveur, avec des options de contrôle. + +Usage : + # Crée les lignes manquantes (ne touche pas aux lignes existantes) + python manage.py fill_status_permissions + + # Met également à jour les lignes existantes selon les rôles + python manage.py fill_status_permissions --update + + # Efface tout et recrée depuis zéro (demande confirmation) + python manage.py fill_status_permissions --reset + + # Aperçu sans rien écrire + python manage.py fill_status_permissions --dry-run + + # Limité à un seul utilisateur + python manage.py fill_status_permissions --user kevin@example.com + + # Combinaisons + python manage.py fill_status_permissions --update --user kevin@example.com --dry-run +""" + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from common.models import UserThematics, UserContractAccess +from common.models import UserThematicStatusPermission, UserContractStatusPermission + +# --------------------------------------------------------------------------- +# Référentiels rôles → statuts (sans 'archived', supprimé en migration 0057) +# --------------------------------------------------------------------------- + +VIEW_ROLE_STATUS_PERMISSIONS = { + 'admin': [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', + ], + 'manager': [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', + ], + 'controller': [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', + ], + 'operator': [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', + ], + 'editor': [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', + ], + 'external_manager': [ + 'in_preparation', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', + ], + 'technician': [ + 'in_preparation', 'to_be_processed', 'assigned', 'in_progress', + 'on_pause', 'finished', + ], + 'observer': [ + 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', + 'on_pause', 'finished', 'processed', 'to_be_corrected', 'corrected', + 'validated', 'invoiced', 'closed', 'canceled', + ], + 'viewer': [ + 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', + 'on_pause', 'finished', 'processed', 'to_be_corrected', 'corrected', + 'validated', 'invoiced', 'closed', 'canceled', + ], +} + +ROLE_TRANSITIONS = { + 'manager': [ + ('in_preparation', 'to_be_approved'), + ('in_preparation', 'to_be_planned'), + ('in_preparation', 'to_be_processed'), + ('in_preparation', 'canceled'), + ('to_be_approved', 'to_be_planned'), + ('to_be_approved', 'to_be_processed'), + ('to_be_approved', 'canceled'), + ('to_be_planned', 'to_be_processed'), + ('to_be_planned', 'canceled'), + ('to_be_processed','canceled'), + ('processed', 'to_be_corrected'), + ('processed', 'validated'), + ('corrected', 'validated'), + ('invoiced', 'closed'), + ], + 'technician': [ + ('in_preparation', 'to_be_approved'), + ('in_preparation', 'to_be_planned'), + ('in_preparation', 'to_be_processed'), + ('in_preparation', 'canceled'), + ('to_be_processed','assigned'), + ('assigned', 'in_progress'), + ('assigned', 'to_be_processed'), + ('to_be_processed','in_progress'), + ('in_progress', 'on_pause'), + ('in_progress', 'finished'), + ('on_pause', 'in_progress'), + ('on_pause', 'finished'), + ], + 'external_manager': [ + ('in_preparation', 'to_be_approved'), + ('in_preparation', 'to_be_planned'), + ('in_preparation', 'to_be_processed'), + ('in_preparation', 'canceled'), + ('to_be_approved', 'canceled'), + ('to_be_planned', 'to_be_processed'), + ('to_be_planned', 'canceled'), + ('to_be_processed','assigned'), + ('to_be_processed','in_progress'), + ('to_be_processed','to_be_planned'), + ('to_be_processed','canceled'), + ('assigned', 'in_progress'), + ('assigned', 'to_be_processed'), + ('assigned', 'canceled'), + ('in_progress', 'on_pause'), + ('in_progress', 'finished'), + ('in_progress', 'canceled'), + ('on_pause', 'in_progress'), + ('on_pause', 'finished'), + ('on_pause', 'to_be_planned'), + ('on_pause', 'to_be_processed'), + ('on_pause', 'canceled'), + ('finished', 'processed'), + ('finished', 'to_be_processed'), + ('finished', 'on_pause'), + ('finished', 'canceled'), + ('to_be_corrected','corrected'), + ], + 'controller': [ + ('in_preparation', 'to_be_approved'), + ('in_preparation', 'to_be_planned'), + ('in_preparation', 'to_be_processed'), + ('to_be_approved', 'to_be_planned'), + ('to_be_approved', 'to_be_processed'), + ('to_be_approved', 'canceled'), + ('to_be_planned', 'to_be_processed'), + ('to_be_planned', 'canceled'), + ('to_be_processed','canceled'), + ('processed', 'validated'), + ('processed', 'to_be_corrected'), + ('corrected', 'to_be_corrected'), + ('corrected', 'validated'), + ], + 'operator': [ + ('in_preparation', 'to_be_approved'), + ('in_preparation', 'to_be_planned'), + ('in_preparation', 'to_be_processed'), + ('in_preparation', 'canceled'), + ('to_be_approved', 'to_be_planned'), + ('to_be_approved', 'to_be_processed'), + ('to_be_approved', 'canceled'), + ('to_be_planned', 'to_be_processed'), + ('to_be_planned', 'canceled'), + ('to_be_processed','canceled'), + ], +} + +ROLE_EDIT_STATUSES = { + 'admin': list(VIEW_ROLE_STATUS_PERMISSIONS['admin']), + 'manager': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'processed', 'to_be_corrected', 'corrected', 'validated', 'invoiced'], + 'controller': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'processed', 'to_be_corrected', 'corrected', 'validated'], + 'external_manager': ['in_preparation', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'to_be_corrected', 'corrected'], + 'technician': ['in_preparation', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished'], + 'operator': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed'], +} + +ALL_STATUSES = [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', +] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _build_view_statuses(roles): + result = set() + for role in roles: + result.update(VIEW_ROLE_STATUS_PERMISSIONS.get(role, [])) + return result + + +def _build_transition_targets(roles): + result = set() + for role in roles: + if role == 'admin': + return set(ALL_STATUSES) + for (_src, dst) in ROLE_TRANSITIONS.get(role, []): + result.add(dst) + return result + + +def _build_edit_statuses(roles, can_edit_flag): + if not can_edit_flag: + return set() + result = set() + for role in roles: + result.update(ROLE_EDIT_STATUSES.get(role, [])) + return result + + +def _compute_thematic_rows(ut): + """Retourne la liste de dicts {status, can_view, can_edit, can_change_status_to} pour un UserThematics.""" + roles = list(ut.user_config.roles.values_list('name', flat=True)) + view_statuses = _build_view_statuses(roles) + trans_targets = _build_transition_targets(roles) if ut.can_edit_interventions else set() + edit_statuses = _build_edit_statuses(roles, ut.can_edit_interventions) + all_involved = view_statuses | trans_targets | edit_statuses + return [ + { + 'status': status, + 'can_view': status in view_statuses, + 'can_edit': status in edit_statuses, + 'can_change_status_to': status in trans_targets, + } + for status in all_involved + ] + + +def _compute_contract_rows(uca): + """Retourne la liste de dicts {status, can_view, can_edit, can_change_status_to} pour un UserContractAccess.""" + roles = list(uca.user_config.roles.values_list('name', flat=True)) + view_statuses = _build_view_statuses(roles) + trans_targets = _build_transition_targets(roles) + edit_roles = {'admin', 'manager', 'controller', 'external_manager', 'technician'} + can_edit_flag = bool(set(roles) & edit_roles) + edit_statuses = _build_edit_statuses(roles, can_edit_flag) + + if uca.can_approve: + trans_targets.update(['to_be_planned', 'to_be_processed', 'canceled']) + if uca.can_create_interventions: + view_statuses.add('in_preparation') + edit_statuses.add('in_preparation') + + all_involved = view_statuses | trans_targets | edit_statuses + return [ + { + 'status': status, + 'can_view': status in view_statuses, + 'can_edit': status in edit_statuses, + 'can_change_status_to': status in trans_targets, + } + for status in all_involved + ] + + +# --------------------------------------------------------------------------- +# Commande +# --------------------------------------------------------------------------- + +class Command(BaseCommand): + help = ( + 'Pré-remplit UserThematicStatusPermission et UserContractStatusPermission ' + 'à partir des rôles et flags existants.' + ) + + def add_arguments(self, parser): + parser.add_argument( + '--update', + action='store_true', + default=False, + help='Met à jour les lignes existantes en plus de créer les manquantes.', + ) + parser.add_argument( + '--reset', + action='store_true', + default=False, + help='Supprime toutes les lignes existantes puis les recrée. ' + 'Demande une confirmation interractive sauf si --no-input est fourni.', + ) + parser.add_argument( + '--dry-run', + action='store_true', + default=False, + dest='dry_run', + help='Affiche ce qui serait fait sans écrire en base.', + ) + parser.add_argument( + '--user', + metavar='EMAIL', + default=None, + help='Limite le traitement à un seul utilisateur (email).', + ) + parser.add_argument( + '--no-input', '--noinput', + action='store_true', + default=False, + dest='no_input', + help='Ne pose pas de question de confirmation (utile pour les scripts automatisés).', + ) + + # ------------------------------------------------------------------ + + def handle(self, *args, **options): + dry_run = options['dry_run'] + do_reset = options['reset'] + do_update = options['update'] + user_email = options['user'] + no_input = options['no_input'] + + if dry_run: + self.stdout.write(self.style.WARNING('Mode DRY-RUN : aucune modification ne sera effectuée.\n')) + + if do_reset and not dry_run: + if not no_input: + confirm = input( + 'ATTENTION : --reset va supprimer toutes les lignes de permissions par statut ' + 'et les recréer depuis zéro.\nContinuer ? [o/N] ' + ) + if confirm.strip().lower() not in ('o', 'oui', 'y', 'yes'): + self.stdout.write(self.style.WARNING('Opération annulée.')) + return + + # Filtrage optionnel par utilisateur + ut_qs = UserThematics.objects.select_related('user_config').prefetch_related('user_config__roles') + uca_qs = UserContractAccess.objects.select_related('user_config').prefetch_related('user_config__roles') + + if user_email: + ut_qs = ut_qs.filter(user_config__user__email=user_email) + uca_qs = uca_qs.filter(user_config__user__email=user_email) + if not ut_qs.exists() and not uca_qs.exists(): + raise CommandError(f"Aucun utilisateur trouvé avec l'email « {user_email} ».") + + with transaction.atomic(): + t_created, t_updated, t_skipped = self._process_thematics(ut_qs, do_reset, do_update, dry_run) + c_created, c_updated, c_skipped = self._process_contracts(uca_qs, do_reset, do_update, dry_run) + + # Résumé + prefix = '[DRY-RUN] ' if dry_run else '' + self.stdout.write('') + self.stdout.write(self.style.SUCCESS( + f'{prefix}Thématiques — créées: {t_created} | mises à jour: {t_updated} | ignorées: {t_skipped}' + )) + self.stdout.write(self.style.SUCCESS( + f'{prefix}Contrats — créées: {c_created} | mises à jour: {c_updated} | ignorées: {c_skipped}' + )) + + # ------------------------------------------------------------------ + + def _process_thematics(self, qs, do_reset, do_update, dry_run): + created = updated = skipped = 0 + + if do_reset and not dry_run: + deleted, _ = UserThematicStatusPermission.objects.filter( + user_thematic__in=qs.values_list('pk', flat=True) + ).delete() + self.stdout.write(f' Suppression de {deleted} lignes thématiques existantes.') + + for ut in qs.filter(can_view_interventions=True).iterator(chunk_size=500): + rows = _compute_thematic_rows(ut) + if not rows: + continue + + existing_map = {} + if not do_reset: + existing_map = { + p.status: p + for p in UserThematicStatusPermission.objects.filter(user_thematic=ut) + } + + for row in rows: + status = row['status'] + if status in existing_map: + perm = existing_map[status] + needs_update = ( + perm.can_view != row['can_view'] or + perm.can_edit != row['can_edit'] or + perm.can_change_status_to != row['can_change_status_to'] + ) + if needs_update and do_update: + if dry_run: + self.stdout.write( + f' [MAJ thém.] ut={ut.pk} statut={status} ' + f'view={row["can_view"]} edit={row["can_edit"]} ' + f'transition={row["can_change_status_to"]}' + ) + else: + perm.can_view = row['can_view'] + perm.can_edit = row['can_edit'] + perm.can_change_status_to = row['can_change_status_to'] + perm.save(update_fields=['can_view', 'can_edit', 'can_change_status_to']) + updated += 1 + else: + skipped += 1 + else: + if dry_run: + self.stdout.write( + f' [CRÉE thém.] ut={ut.pk} statut={status} ' + f'view={row["can_view"]} edit={row["can_edit"]} ' + f'transition={row["can_change_status_to"]}' + ) + else: + UserThematicStatusPermission.objects.get_or_create( + user_thematic=ut, + status=status, + defaults={ + 'can_view': row['can_view'], + 'can_edit': row['can_edit'], + 'can_change_status_to': row['can_change_status_to'], + }, + ) + created += 1 + + return created, updated, skipped + + # ------------------------------------------------------------------ + + def _process_contracts(self, qs, do_reset, do_update, dry_run): + created = updated = skipped = 0 + + if do_reset and not dry_run: + deleted, _ = UserContractStatusPermission.objects.filter( + user_contract__in=qs.values_list('pk', flat=True) + ).delete() + self.stdout.write(f' Suppression de {deleted} lignes contrat existantes.') + + for uca in qs.filter(can_view_interventions=True).iterator(chunk_size=500): + rows = _compute_contract_rows(uca) + if not rows: + continue + + existing_map = {} + if not do_reset: + existing_map = { + p.status: p + for p in UserContractStatusPermission.objects.filter(user_contract=uca) + } + + for row in rows: + status = row['status'] + if status in existing_map: + perm = existing_map[status] + needs_update = ( + perm.can_view != row['can_view'] or + perm.can_edit != row['can_edit'] or + perm.can_change_status_to != row['can_change_status_to'] + ) + if needs_update and do_update: + if dry_run: + self.stdout.write( + f' [MAJ cont.] uca={uca.pk} statut={status} ' + f'view={row["can_view"]} edit={row["can_edit"]} ' + f'transition={row["can_change_status_to"]}' + ) + else: + perm.can_view = row['can_view'] + perm.can_edit = row['can_edit'] + perm.can_change_status_to = row['can_change_status_to'] + perm.save(update_fields=['can_view', 'can_edit', 'can_change_status_to']) + updated += 1 + else: + skipped += 1 + else: + if dry_run: + self.stdout.write( + f' [CRÉE cont.] uca={uca.pk} statut={status} ' + f'view={row["can_view"]} edit={row["can_edit"]} ' + f'transition={row["can_change_status_to"]}' + ) + else: + UserContractStatusPermission.objects.get_or_create( + user_contract=uca, + status=status, + defaults={ + 'can_view': row['can_view'], + 'can_edit': row['can_edit'], + 'can_change_status_to': row['can_change_status_to'], + }, + ) + created += 1 + + return created, updated, skipped diff --git a/streetup/common/management/commands/import_user_configs.py b/streetup/common/management/commands/import_user_configs.py new file mode 100644 index 0000000..9810d6f --- /dev/null +++ b/streetup/common/management/commands/import_user_configs.py @@ -0,0 +1,402 @@ +""" +Importe les configurations utilisateur depuis un fichier JSON produit par +export_user_configs. + +Modes : + create (défaut) — crée uniquement les configs absentes ; ignore les existantes + update — crée les absentes ET met à jour les existantes (thématiques / + contrats / entrepôts / équipes et leurs permissions par statut) + reset — supprime les relations (thématiques, contrats, etc.) des configs + existantes avant de les recréer depuis le JSON ; ne supprime pas + le UserConfig lui-même ni le compte User. + +Dans tous les modes, un User inexistant est IGNORÉ (l'import ne crée pas de +comptes utilisateurs pour ne pas contourner la gestion des mots de passe). + +Usage : + python manage.py import_user_configs exports/user_configs.json + python manage.py import_user_configs exports/user_configs.json --mode update + python manage.py import_user_configs exports/user_configs.json --mode reset --no-input + python manage.py import_user_configs exports/user_configs.json --dry-run +""" + +import json +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth import get_user_model +from django.db import transaction + +from common.models import ( + UserConfig, Role, UserThematics, UserContractAccess, + UserWarehouseAccess, UserWarehouseLocationAccess, UserThematicStatusPermission, + UserContractStatusPermission, AppView, +) +from contracts.models import UserCompanyTeamAccess, CompanyTeam, Company +from contracts.models import Contract # contract_number lookup + +User = get_user_model() + +SUPPORTED_VERSIONS = {1, 2} + + +class Command(BaseCommand): + help = 'Importe les configurations utilisateur depuis un fichier JSON.' + + def add_arguments(self, parser): + parser.add_argument( + 'file', + metavar='FICHIER', + help='Chemin vers le fichier JSON exporté par export_user_configs.', + ) + parser.add_argument( + '--mode', + choices=['create', 'update', 'reset'], + default='create', + help=( + 'create : crée uniquement les lignes manquantes (défaut). ' + 'update : crée + met à jour les existantes. ' + 'reset : vide les relations existantes avant de recréer.' + ), + ) + parser.add_argument( + '--dry-run', + action='store_true', + default=False, + dest='dry_run', + help='Affiche ce qui serait fait sans rien écrire.', + ) + parser.add_argument( + '--no-input', '--noinput', + action='store_true', + default=False, + dest='no_input', + ) + + # ------------------------------------------------------------------ + + def handle(self, *args, **options): + mode = options['mode'] + dry_run = options['dry_run'] + no_input = options['no_input'] + + if dry_run: + self.stdout.write(self.style.WARNING('Mode DRY-RUN : aucune modification.\n')) + + # Charger le JSON + try: + with open(options['file'], encoding='utf-8') as fh: + payload = json.load(fh) + except FileNotFoundError: + raise CommandError(f"Fichier introuvable : {options['file']}") + except json.JSONDecodeError as exc: + raise CommandError(f"JSON invalide : {exc}") + + version = payload.get('version', 1) + if version not in SUPPORTED_VERSIONS: + raise CommandError(f"Version {version} non supportée (supportées : {SUPPORTED_VERSIONS}).") + + users_data = payload.get('users', []) + self.stdout.write(f"{len(users_data)} configuration(s) à traiter (mode={mode}).\n") + + if mode == 'reset' and not dry_run: + if not no_input: + confirm = input( + 'ATTENTION : --mode reset effacera toutes les relations ' + '(thématiques, contrats, entrepôts, équipes, rôles, vues) ' + 'des configs présentes dans le fichier avant de les recréer.\n' + 'Continuer ? [o/N] ' + ) + if confirm.strip().lower() not in ('o', 'oui', 'y', 'yes'): + self.stdout.write(self.style.WARNING('Opération annulée.')) + return + + stats = {'created': 0, 'updated': 0, 'skipped': 0, 'missing_user': 0, 'errors': 0} + + with transaction.atomic(): + for entry in users_data: + try: + self._process_entry(entry, mode, dry_run, stats) + except Exception as exc: + self.stderr.write(self.style.ERROR( + f'Erreur pour {entry.get("username", "?")} : {exc}' + )) + stats['errors'] += 1 + if not dry_run: + raise # rollback + + self.stdout.write('') + self.stdout.write(self.style.SUCCESS( + f'Résultat — créées: {stats["created"]} | mises à jour: {stats["updated"]} ' + f'| ignorées: {stats["skipped"]} | utilisateur manquant: {stats["missing_user"]} ' + f'| erreurs: {stats["errors"]}' + )) + + # ------------------------------------------------------------------ + + def _process_entry(self, entry, mode, dry_run, stats): + username = entry.get('username') or '' + email = entry.get('email') or '' + + # Résoudre l'utilisateur (ne crée pas de User) + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + if email: + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + self.stdout.write( + self.style.WARNING(f' [IGNORÉ] utilisateur introuvable : {username} / {email}') + ) + stats['missing_user'] += 1 + return + else: + self.stdout.write( + self.style.WARNING(f' [IGNORÉ] utilisateur introuvable : {username}') + ) + stats['missing_user'] += 1 + return + + # Chercher ou créer le UserConfig + try: + cfg = UserConfig.objects.get(user=user) + exists = True + except UserConfig.DoesNotExist: + exists = False + cfg = None + + if exists and mode == 'create': + self.stdout.write(f' [IGNORÉ] config existante pour {username}') + stats['skipped'] += 1 + return + + label = 'MAJ' if exists else 'CRÉE' + self.stdout.write(f' [{label}] {username}') + + if dry_run: + stats['updated' if exists else 'created'] += 1 + return + + # Résoudre les FK optionnelles + default_thematic = None + if entry.get('default_thematic'): + from common.models import Thematic + default_thematic = Thematic.objects.filter(code=entry['default_thematic']).first() + + company_obj = None + if entry.get('company'): + company_obj, created = Company.objects.get_or_create(name=entry['company']) + if created: + self.stdout.write(self.style.WARNING( + f' [+] société créée : {entry["company"]}' + )) + + scalar_fields = { + 'is_intern': entry.get('is_intern', False), + 'generic_email': entry.get('generic_email'), + 'limit_assets_to_contracts': entry.get('limit_assets_to_contracts', True), + 'limit_interventions_to_contracts': entry.get('limit_interventions_to_contracts', True), + 'limit_shortcuts_to_contracts': entry.get('limit_shortcuts_to_contracts', False), + 'hide_intervention_map_by_default': entry.get('hide_intervention_map_by_default', False), + 'auto_logout_hours': entry.get('auto_logout_hours'), + 'can_add_unique_tag_documents': entry.get('can_add_unique_tag_documents', False), + 'default_thematic': default_thematic, + 'company': company_obj, + } + + if not exists: + cfg = UserConfig.objects.create(user=user, **scalar_fields) + else: + for field, value in scalar_fields.items(): + setattr(cfg, field, value) + cfg.save() + + # Rôles + roles = Role.objects.filter(name__in=entry.get('roles', [])) + cfg.roles.set(roles) + + # Vues accessibles + views = AppView.objects.filter(code__in=entry.get('accessible_views', [])) + cfg.accessible_views.set(views) + + # Linked configs (best-effort — peut rater si l'user n'existe pas encore) + linked_usernames = entry.get('linked_configs', []) + linked_cfgs = UserConfig.objects.filter(user__username__in=linked_usernames) + cfg.linked_configs.set(linked_cfgs) + + # Relations avec reset possible + if mode == 'reset' and exists: + cfg.userthematics.all().delete() + cfg.usercontractaccess_set.all().delete() + cfg.userwarehouses.all().delete() + cfg.userlocations.all().delete() + cfg.team_accesses.all().delete() + + # Thématiques + for ut_data in entry.get('thematics', []): + self._apply_thematic(cfg, ut_data, mode) + + # Contrats + for uc_data in entry.get('contracts', []): + self._apply_contract(cfg, uc_data, mode) + + # Entrepôts + for wa_data in entry.get('warehouses', []): + self._apply_warehouse(cfg, wa_data, mode) + + # Emplacements + for la_data in entry.get('locations', []): + self._apply_location(cfg, la_data, mode) + + # Équipes + for ta_data in entry.get('team_accesses', []): + self._apply_team(cfg, ta_data, mode) + + stats['updated' if exists else 'created'] += 1 + + # ------------------------------------------------------------------ + + def _apply_thematic(self, cfg, data, mode): + from common.models import Thematic + thematic = Thematic.objects.filter(code=data['thematic']).first() + if not thematic: + self.stdout.write(self.style.WARNING( + f' [⚠] thématique introuvable : {data["thematic"]}' + )) + return + + ut, created = UserThematics.objects.update_or_create( + user_config=cfg, + thematic=thematic, + defaults={ + 'can_view_assets': data.get('can_view_assets', True), + 'can_edit_assets': data.get('can_edit_assets', False), + 'can_view_interventions': data.get('can_view_interventions', True), + 'can_edit_interventions': data.get('can_edit_interventions', False), + 'can_edit_locations': data.get('can_edit_locations', False), + 'can_process_observations': data.get('can_process_observations', False), + }, + ) + + # Permissions par statut + if mode in ('update', 'reset') or created: + self._apply_status_permissions_thematic(ut, data.get('status_permissions', []), mode) + + def _apply_status_permissions_thematic(self, ut, sp_list, mode): + if mode == 'reset': + ut.status_permissions.all().delete() + for sp in sp_list: + UserThematicStatusPermission.objects.update_or_create( + user_thematic=ut, + status=sp['status'], + defaults={ + 'can_view': sp.get('can_view', False), + 'can_edit': sp.get('can_edit', False), + 'can_change_status_to': sp.get('can_change_status_to', False), + }, + ) + + # ------------------------------------------------------------------ + + def _apply_contract(self, cfg, data, mode): + try: + contract = Contract.objects.get(contract_number=data['contract']) + except Contract.DoesNotExist: + self.stdout.write(self.style.WARNING( + f' [⚠] contrat introuvable : {data["contract"]}' + )) + return + + uc, created = UserContractAccess.objects.update_or_create( + user_config=cfg, + contract=contract, + defaults={ + 'can_view_posts': data.get('can_view_posts', False), + 'can_view_prices': data.get('can_view_prices', False), + 'can_view_interventions': data.get('can_view_interventions', True), + 'can_view_assets': data.get('can_view_assets', True), + 'can_create_interventions': data.get('can_create_interventions', False), + 'can_check_interventions': data.get('can_check_interventions', False), + 'can_approve': data.get('can_approve', False), + }, + ) + + if mode in ('update', 'reset') or created: + self._apply_status_permissions_contract(uc, data.get('status_permissions', []), mode) + + def _apply_status_permissions_contract(self, uc, sp_list, mode): + if mode == 'reset': + uc.status_permissions.all().delete() + for sp in sp_list: + UserContractStatusPermission.objects.update_or_create( + user_contract=uc, + status=sp['status'], + defaults={ + 'can_view': sp.get('can_view', False), + 'can_edit': sp.get('can_edit', False), + 'can_change_status_to': sp.get('can_change_status_to', False), + }, + ) + + # ------------------------------------------------------------------ + + def _apply_warehouse(self, cfg, data, mode): + from stock.models import Warehouse + warehouse = Warehouse.objects.filter(code=data['warehouse']).first() + if not warehouse: + self.stdout.write(self.style.WARNING( + f' [⚠] entrepôt introuvable : {data["warehouse"]}' + )) + return + + UserWarehouseAccess.objects.update_or_create( + user_config=cfg, + warehouse=warehouse, + defaults={ + 'can_view': data.get('can_view', True), + 'can_manage': data.get('can_manage', False), + 'can_administer': data.get('can_administer', False), + 'can_manage_preparation_orders': data.get('can_manage_preparation_orders', False), + 'can_manage_purchase_orders': data.get('can_manage_purchase_orders', False), + 'can_validate_inventory': data.get('can_validate_inventory', False), + 'is_default': data.get('is_default', False), + }, + ) + + # ------------------------------------------------------------------ + + def _apply_location(self, cfg, data, mode): + from stock.models import WarehouseLocation + location = WarehouseLocation.objects.filter(code=data['location_code']).first() + if not location: + self.stdout.write(self.style.WARNING( + f' [⚠] emplacement introuvable : {data["location_code"]}' + )) + return + + UserWarehouseLocationAccess.objects.update_or_create( + user_config=cfg, + warehouse_location=location, + defaults={ + 'can_perform_inventory': data.get('can_perform_inventory', False), + 'can_validate_inventory': data.get('can_validate_inventory', False), + }, + ) + + # ------------------------------------------------------------------ + + def _apply_team(self, cfg, data, mode): + team = CompanyTeam.objects.filter(name=data['team']).first() + if not team: + self.stdout.write(self.style.WARNING( + f' [⚠] équipe introuvable : {data["team"]}' + )) + return + + UserCompanyTeamAccess.objects.update_or_create( + user_config=cfg, + team=team, + defaults={ + 'can_view': data.get('can_view', True), + 'can_edit': data.get('can_edit', False), + }, + ) diff --git a/streetup/common/management/commands/init_scheduled_tasks.py b/streetup/common/management/commands/init_scheduled_tasks.py new file mode 100644 index 0000000..33511bb --- /dev/null +++ b/streetup/common/management/commands/init_scheduled_tasks.py @@ -0,0 +1,299 @@ +""" +Script pour initialiser les tâches planifiées. + +Ce script crée la tâche d'export mensuel des interventions. + +Usage: + python manage.py init_scheduled_tasks +""" + +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.conf import settings +from common.models_scheduled_tasks import ScheduledCommand +import os + + +class Command(BaseCommand): + help = 'Initialise les tâches planifiées par défaut' + + def handle(self, *args, **options): + self.stdout.write(self.style.WARNING('\n=== Initialisation des tâches planifiées ===\n')) + + # Créer le répertoire exports s'il n'existe pas + exports_dir = os.path.join('exports') + if not os.path.exists(exports_dir): + os.makedirs(exports_dir) + self.stdout.write(self.style.SUCCESS(f'✓ Répertoire créé: {exports_dir}')) + + # 1. Tâche d'export mensuel des interventions + task_name = "Export mensuel des interventions" + + task, created = ScheduledCommand.objects.get_or_create( + name=task_name, + defaults={ + 'command_name': 'export_interventions_xlsx', + 'command_args': { + 'output': 'exports/interventions_export_%(year)s_%(month)s.xlsx' + }, + 'frequency': 'monthly', + 'hour': 4, + 'minute': 0, + 'day_of_month': 3, + 'is_active': True, + 'send_email': True, + 'email_recipients': getattr(settings, 'EMAIL_INTERNAL_FOLLOWUP', 'kdeterme@sprb.brussels'), + 'email_subject': '[StreetUp] Export mensuel des interventions', + 'description': ''' +Export mensuel automatique des interventions du mois précédent. + +La tâche s'exécute le 3 de chaque mois à 4h du matin. +Les interventions exportées sont celles du mois précédent (exemple: si nous sommes en février, exporte janvier). +Un email est automatiquement envoyé avec le fichier Excel en pièce jointe. + +Le fichier est sauvegardé dans le répertoire exports/ avec le nom: +interventions_export_YYYY_MM.xlsx + '''.strip() + } + ) + + if created: + # Calculer la prochaine exécution + task.next_run = task.calculate_next_run() + task.save() + + self.stdout.write(self.style.SUCCESS(f'\n✓ Tâche créée: {task_name}')) + self.stdout.write(f' - Fréquence: {task.get_frequency_display()}') + self.stdout.write(f' - Planning: Le {task.day_of_month} de chaque mois à {task.hour:02d}:{task.minute:02d}') + self.stdout.write(f' - Prochaine exécution: {task.next_run.strftime("%Y-%m-%d %H:%M")}') + self.stdout.write(f' - Email: {task.email_recipients}') + else: + self.stdout.write(self.style.WARNING(f'\n! Tâche déjà existante: {task_name}')) + self.stdout.write(f' - Statut: {"Active" if task.is_active else "Inactive"}') + if task.next_run: + self.stdout.write(f' - Prochaine exécution: {task.next_run.strftime("%Y-%m-%d %H:%M")}') + + # 2. Tâche de nettoyage quotidien des analytics + task_name_analytics = "Nettoyage mensuel des analytics" + + task2, created2 = ScheduledCommand.objects.get_or_create( + name=task_name_analytics, + defaults={ + 'command_name': 'cleanup_analytics', + 'command_args': { + 'days': 30 + }, + 'frequency': 'daily', + 'hour': 3, + 'minute': 0, + 'is_active': True, + 'send_email': False, + 'description': ''' +Nettoyage quotidien des données analytics de plus de 30 jours. + +La tâche s'exécute tous les jours à 3h du matin. +Avant de supprimer les PageView et UserSession anciennes, +un résumé quotidien (DailyPageViewSummary) est créé pour +conserver les statistiques d'utilisation agrégées par jour. + +Données conservées dans le résumé: +- Nombre total de pages vues +- Utilisateurs et sessions uniques +- Temps de réponse moyen et max +- Top 20 des pages visitées +- Top 20 des utilisateurs actifs +- Répartition par méthode HTTP + '''.strip() + } + ) + + if created2: + task2.next_run = task2.calculate_next_run() + task2.save() + + self.stdout.write(self.style.SUCCESS(f'\n✓ Tâche créée: {task_name_analytics}')) + self.stdout.write(f' - Fréquence: {task2.get_frequency_display()}') + self.stdout.write(f' - Planning: Tous les jours à {task2.hour:02d}:{task2.minute:02d}') + self.stdout.write(f' - Prochaine exécution: {task2.next_run.strftime("%Y-%m-%d %H:%M")}') + else: + self.stdout.write(self.style.WARNING(f'\n! Tâche déjà existante: {task_name_analytics}')) + self.stdout.write(f' - Statut: {"Active" if task2.is_active else "Inactive"}') + if task2.next_run: + self.stdout.write(f' - Prochaine exécution: {task2.next_run.strftime("%Y-%m-%d %H:%M")}') + + # 3. Tâche d'envoi groupé journalier des interventions + task_name_daily = "Résumé journalier des interventions" + + task3, created3 = ScheduledCommand.objects.get_or_create( + name=task_name_daily, + defaults={ + 'command_name': 'send_daily_intervention_summary', + 'command_args': {}, + 'frequency': 'daily', + 'hour': 18, + 'minute': 0, + 'is_active': False, # Désactivé par défaut, à activer si des contrats utilisent l'envoi groupé + 'send_email': False, + 'description': ''' +Envoie un résumé journalier des interventions pour les contrats configurés en mode "envoi groupé". + +Au lieu d'envoyer un email par intervention, toutes les fiches PDF du jour sont +regroupées dans un seul email par contrat. + +Pour activer l'envoi groupé sur un contrat : + 1. Aller dans Admin > Contracts > Contrats > [contrat] + 2. Cocher "Envoi groupé journalier" + 3. Définir l'heure d'envoi souhaitée dans "Heure d'envoi du résumé journalier" + 4. Activer cette tâche planifiée et configurer son heure en conséquence + +Arguments disponibles : + --date YYYY-MM-DD : traiter une date spécifique (défaut: aujourd'hui) + --contract ID : traiter un seul contrat + --dry-run : simulation sans envoi réel + '''.strip() + } + ) + + if created3: + task3.next_run = task3.calculate_next_run() + task3.save() + + self.stdout.write(self.style.SUCCESS(f'\n✓ Tâche créée: {task_name_daily}')) + self.stdout.write(f' - Fréquence: {task3.get_frequency_display()}') + self.stdout.write(f' - Planning: Tous les jours à {task3.hour:02d}:{task3.minute:02d}') + self.stdout.write(f' - Statut: {"Active" if task3.is_active else "Inactive (à activer si nécessaire)"}') + else: + self.stdout.write(self.style.WARNING(f'\n! Tâche déjà existante: {task_name_daily}')) + self.stdout.write(f' - Statut: {"Active" if task3.is_active else "Inactive"}') + if task3.next_run: + self.stdout.write(f' - Prochaine exécution: {task3.next_run.strftime("%Y-%m-%d %H:%M")}') + + # 4. Rafraîchissement des vues matérialisées de reporting + task_name_reporting = "Rafraîchissement des vues matérialisées" + + task4, created4 = ScheduledCommand.objects.get_or_create( + name=task_name_reporting, + defaults={ + 'command_name': 'refresh_reporting_views', + 'command_args': {}, + 'frequency': 'daily', + 'hour': 3, + 'minute': 0, + 'is_active': True, + 'send_email': False, + 'description': ( + 'Rafraîchit quotidiennement les vues matérialisées du schéma reporting.\n\n' + 'Vues concernées (dans l\'ordre) :\n' + ' - reporting.mv_location_hierarchy\n' + ' - reporting.mv_interventions\n\n' + 'Utilise REFRESH MATERIALIZED VIEW CONCURRENTLY pour ne pas bloquer les ' + 'lectures en cours d\'exécution.\n' + 'Lors de la première exécution (ou avec --force-recreate), les vues sont ' + 'créées à partir des fichiers SQL dans reporting/sql/.\n\n' + 'Arguments disponibles :\n' + ' --force-recreate : force le DROP + CREATE des vues (bloquant).' + ), + } + ) + + if created4: + task4.next_run = task4.calculate_next_run() + task4.save() + + self.stdout.write(self.style.SUCCESS(f'\n✓ Tâche créée: {task_name_reporting}')) + self.stdout.write(f' - Fréquence: {task4.get_frequency_display()}') + self.stdout.write(f' - Planning: Tous les jours à {task4.hour:02d}:{task4.minute:02d}') + self.stdout.write(f' - Prochaine exécution: {task4.next_run.strftime("%Y-%m-%d %H:%M")}') + else: + self.stdout.write(self.style.WARNING(f'\n! Tâche déjà existante: {task_name_reporting}')) + self.stdout.write(f' - Statut: {"Active" if task4.is_active else "Inactive"}') + if task4.next_run: + self.stdout.write(f' - Prochaine exécution: {task4.next_run.strftime("%Y-%m-%d %H:%M")}') + + # 5. Clôture automatique des interventions + task_name_auto_close = "Clôture automatique des interventions" + + task5, created5 = ScheduledCommand.objects.get_or_create( + name=task_name_auto_close, + defaults={ + 'command_name': 'close_expired_interventions', + 'command_args': {}, + 'frequency': 'daily', + 'hour': 2, + 'minute': 0, + 'is_active': True, + 'send_email': False, + 'description': ''' +Clôture automatique des interventions après un certain délai défini par contrat. + +La tâche s'exécute tous les jours à 2h00 du matin. +Les interventions concernées sont celles dont le statut est finalisé, traité, validé ou facturé, et dont le délai depuis la fin est supérieur au délai défini sur leur contrat. +Les observations liées sont également clôturées. + '''.strip() + } + ) + + if created5: + task5.next_run = task5.calculate_next_run() + task5.save() + + self.stdout.write(self.style.SUCCESS(f'\n✓ Tâche créée: {task_name_auto_close}')) + self.stdout.write(f' - Fréquence: {task5.get_frequency_display()}') + self.stdout.write(f' - Planning: Tous les jours à {task5.hour:02d}:{task5.minute:02d}') + self.stdout.write(f' - Prochaine exécution: {task5.next_run.strftime("%Y-%m-%d %H:%M")}') + else: + self.stdout.write(self.style.WARNING(f'\n! Tâche déjà existante: {task_name_auto_close}')) + self.stdout.write(f' - Statut: {"Active" if task5.is_active else "Inactive"}') + if task5.next_run: + self.stdout.write(f' - Prochaine exécution: {task5.next_run.strftime("%Y-%m-%d %H:%M")}') + + # 6. Sauvegarde de la base de données + task_name_backup = "Sauvegarde de la base de données" + + task6, created6 = ScheduledCommand.objects.get_or_create( + name=task_name_backup, + defaults={ + 'command_name': 'backup_database', + 'command_args': {}, + 'frequency': 'daily', + 'hour': 4, + 'minute': 0, + 'is_active': True, + 'send_email': True, + 'email_recipients': getattr(settings, 'EMAIL_INTERNAL_FOLLOWUP', 'kdeterme@sprb.brussels'), + 'email_subject': '[StreetUp] Sauvegarde de la base de données', + 'description': ''' +Sauvegarde quotidienne de la base de données PostgreSQL. + +La tâche s'exécute tous les jours à 4h00 du matin. +Elle effectue les actions suivantes : +1. Crée un dump PostgreSQL (pg_dump) des schémas django et public. +2. Compresse le dump en fichier .sql.gz. +3. Téléverse le backup compressé vers le stockage distant MinIO (si BACKUP_MINIO_ENABLED est à true). +4. Synchronise le backup vers le serveur de test via Rsync (si BACKUP_REMOTE_ENABLED est à true). +5. Supprime les sauvegardes locales de plus de 30 jours (configuré via BACKUP_RETENTION_DAYS). + '''.strip() + } + ) + + if created6: + task6.next_run = task6.calculate_next_run() + task6.save() + + self.stdout.write(self.style.SUCCESS(f'\n✓ Tâche créée: {task_name_backup}')) + self.stdout.write(f' - Fréquence: {task6.get_frequency_display()}') + self.stdout.write(f' - Planning: Tous les jours à {task6.hour:02d}:{task6.minute:02d}') + self.stdout.write(f' - Prochaine exécution: {task6.next_run.strftime("%Y-%m-%d %H:%M")}') + else: + self.stdout.write(self.style.WARNING(f'\n! Tâche déjà existante: {task_name_backup}')) + self.stdout.write(f' - Statut: {"Active" if task6.is_active else "Inactive"}') + if task6.next_run: + self.stdout.write(f' - Prochaine exécution: {task6.next_run.strftime("%Y-%m-%d %H:%M")}') + + self.stdout.write(self.style.SUCCESS('\n=== Initialisation terminée ===')) + self.stdout.write('\nPour gérer les tâches planifiées:') + self.stdout.write(' 1. Accédez à l\'admin Django: /admin/') + self.stdout.write(' 2. Section: Common > Commandes planifiées') + self.stdout.write('\nPour tester l\'exécution:') + self.stdout.write(' python manage.py run_scheduled_commands') + self.stdout.write('\n') diff --git a/streetup/common/management/commands/refresh_reporting_views.py b/streetup/common/management/commands/refresh_reporting_views.py new file mode 100644 index 0000000..8ab87e1 --- /dev/null +++ b/streetup/common/management/commands/refresh_reporting_views.py @@ -0,0 +1,151 @@ +""" +Management command : refresh_reporting_views + +Crée (première exécution) ou rafraîchit (exécutions suivantes) les vues +matérialisées du schéma "reporting". + +Utilise REFRESH MATERIALIZED VIEW CONCURRENTLY pour ne pas bloquer les +lectures — exige qu'un index unique existe sur chaque vue (créé lors du +premier CREATE via le fichier SQL correspondant). + +Usage : + python manage.py refresh_reporting_views + python manage.py refresh_reporting_views --force-recreate +""" + +import time +from pathlib import Path + +from django.core.management.base import BaseCommand +from django.db import connection + + +# Ordre de création/refresh : les dépendances d'abord. +# Clés : "schema.view_name" (doit correspondre au nom du fichier SQL sans extension) +VIEWS_IN_ORDER = [ + "reporting.mv_location_hierarchy", + "reporting.mv_interventions", +] + +SQL_DIR = ( + Path(__file__).resolve() + .parent # commands/ + .parent # management/ + .parent # common/ + .parent # streetup/ (racine Django) + / "reporting" + / "sql" +) + + +def _view_exists(cursor, view_name: str) -> bool: + """Vérifie si la vue matérialisée existe dans pg_matviews.""" + schema, name = view_name.split(".") + cursor.execute( + "SELECT 1 FROM pg_matviews WHERE schemaname = %s AND matviewname = %s", + [schema, name], + ) + return cursor.fetchone() is not None + + +class Command(BaseCommand): + help = ( + "Crée ou rafraîchit les vues matérialisées du schéma reporting. " + "Destiné à être exécuté nuitamment via ScheduledCommand (3h00)." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--force-recreate", + action="store_true", + default=False, + help=( + "Force la recréation complète des vues (DROP + CREATE + INDEX). " + "Bloquant pendant la recréation. Nécessaire après un changement " + "de structure dans les fichiers SQL." + ), + ) + + def handle(self, *args, **options): + force = options["force_recreate"] + total_start = time.time() + + self.stdout.write(self.style.MIGRATE_HEADING("Reporting views refresh")) + + with connection.cursor() as cursor: + + # 1. Créer le schéma si nécessaire + cursor.execute("CREATE SCHEMA IF NOT EXISTS reporting;") + self.stdout.write(" Schéma reporting : OK") + + for full_name in VIEWS_IN_ORDER: + _, name = full_name.split(".") + start = time.time() + + sql_file = SQL_DIR / f"{name}.sql" + if not sql_file.exists(): + self.stderr.write( + self.style.ERROR(f" Fichier SQL introuvable : {sql_file}") + ) + continue + + if not _view_exists(cursor, full_name) or force: + # ── Première création ou recréation forcée ────────────── + try: + sql = sql_file.read_text(encoding="utf-8") + + # Adapter le schéma django. pour l'environnement de test ou de développement + from django.conf import settings + import os + db_schema = os.getenv("DB_SCHEMA") + if not db_schema: + options = settings.DATABASES.get('default', {}).get('OPTIONS', {}).get('options', '') + if 'search_path=' in options: + db_schema = options.split('search_path=')[1].split(',')[0].strip() + else: + db_schema = 'public' + if settings.DATABASES['default']['NAME'].startswith('test_'): + db_schema = 'public' + + sql = sql.replace("django.", f"{db_schema}.") + cursor.execute(sql) + elapsed = time.time() - start + verb = "recréée" if force else "créée" + self.stdout.write( + self.style.SUCCESS( + f" {full_name} : {verb} ({elapsed:.1f}s)" + ) + ) + except Exception as exc: + self.stderr.write( + self.style.ERROR( + f" {full_name} : ERREUR lors de la création — {exc}" + ) + ) + raise + else: + # ── Refresh non-bloquant ──────────────────────────────── + try: + cursor.execute( + f"REFRESH MATERIALIZED VIEW CONCURRENTLY {full_name};" + ) + elapsed = time.time() - start + self.stdout.write( + self.style.SUCCESS( + f" {full_name} : rafraîchie ({elapsed:.1f}s)" + ) + ) + except Exception as exc: + self.stderr.write( + self.style.ERROR( + f" {full_name} : ERREUR lors du refresh — {exc}" + ) + ) + raise + + total = time.time() - total_start + self.stdout.write( + self.style.SUCCESS( + f"\nReporting views refresh terminé en {total:.1f}s" + ) + ) diff --git a/streetup/common/management/commands/run_scheduled_commands.py b/streetup/common/management/commands/run_scheduled_commands.py new file mode 100644 index 0000000..0bd69ca --- /dev/null +++ b/streetup/common/management/commands/run_scheduled_commands.py @@ -0,0 +1,242 @@ +""" +Commande Django pour exécuter les tâches planifiées. + +Cette commande doit être appelée régulièrement (par exemple via cron toutes les heures). +Elle vérifie quelles tâches doivent être exécutées et les lance. + +Usage: + python manage.py run_scheduled_commands +""" + +from django.core.management.base import BaseCommand +from django.core.management import call_command +from django.utils import timezone +from django.core.mail import EmailMessage +from django.conf import settings +from common.models_scheduled_tasks import ScheduledCommand, CommandExecutionLog +from io import StringIO +import sys +import traceback +import os + + +class Command(BaseCommand): + help = 'Exécute les commandes planifiées qui doivent être exécutées' + + def handle(self, *args, **options): + self.stdout.write(self.style.WARNING(f'\n=== Vérification des tâches planifiées - {timezone.now()} ===\n')) + + # Récupérer toutes les tâches actives qui doivent être exécutées + tasks = ScheduledCommand.objects.filter(is_active=True) + + if not tasks.exists(): + self.stdout.write(self.style.WARNING('Aucune tâche planifiée active trouvée.')) + return + + self.stdout.write(f'Nombre de tâches actives: {tasks.count()}') + + tasks_to_run = [] + for task in tasks: + if task.should_run_now(): + tasks_to_run.append(task) + self.stdout.write(self.style.SUCCESS(f' ✓ {task.name} doit être exécutée')) + else: + next_run = task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée' + self.stdout.write(f' - {task.name} : prochaine exécution le {next_run}') + + if not tasks_to_run: + self.stdout.write(self.style.WARNING('\nAucune tâche à exécuter maintenant.')) + return + + self.stdout.write(self.style.SUCCESS(f'\n{len(tasks_to_run)} tâche(s) à exécuter\n')) + + # Exécuter chaque tâche + for task in tasks_to_run: + self.execute_task(task) + + self.stdout.write(self.style.SUCCESS('\n=== Vérification terminée ===\n')) + + def execute_task(self, task): + """ + Exécute une tâche planifiée. + """ + self.stdout.write(self.style.WARNING(f'\n>>> Exécution de: {task.name}')) + self.stdout.write(f' Commande: {task.command_name}') + self.stdout.write(f' Arguments: {task.command_args}') + + # Créer un log d'exécution + log = CommandExecutionLog.objects.create( + scheduled_command=task, + status='running' + ) + + # Capturer la sortie + output = StringIO() + error_output = StringIO() + + old_stdout = sys.stdout + old_stderr = sys.stderr + + try: + # Rediriger stdout et stderr + sys.stdout = output + sys.stderr = error_output + + # Exécuter la commande + call_command(task.command_name, **task.command_args) + + # Restaurer stdout/stderr + sys.stdout = old_stdout + sys.stderr = old_stderr + + # Marquer comme succès + log.status = 'success' + log.output = output.getvalue() + log.finished_at = timezone.now() + log.calculate_duration() + log.save() + + # Mettre à jour la tâche + task.last_run = timezone.now() + task.next_run = task.calculate_next_run() + task.save() + + self.stdout.write(self.style.SUCCESS(f' ✓ Succès (durée: {log.duration_seconds:.1f}s)')) + + # Envoyer l'email si configuré + if task.send_email and task.email_recipients: + self.send_success_email(task, log) + + except Exception as e: + # Restaurer stdout/stderr + sys.stdout = old_stdout + sys.stderr = old_stderr + + # Marquer comme erreur + log.status = 'error' + log.output = output.getvalue() + log.error_message = f"{str(e)}\n\n{traceback.format_exc()}\n\n{error_output.getvalue()}" + log.finished_at = timezone.now() + log.calculate_duration() + log.save() + + # Mettre à jour la tâche quand même + task.last_run = timezone.now() + task.next_run = task.calculate_next_run() + task.save() + + self.stdout.write(self.style.ERROR(f' ✗ Erreur: {str(e)}')) + + # Envoyer l'email d'erreur si configuré + if task.send_email and task.email_recipients: + self.send_error_email(task, log) + + def send_success_email(self, task, log): + """ + Envoie un email de succès. + """ + try: + subject = task.email_subject or f"[StreetUp] Tâche planifiée réussie: {task.name}" + + # Vérifier si un fichier a été généré + output_file = task.command_args.get('output', '') + + body = f""" +Bonjour, + +La tâche planifiée "{task.name}" s'est exécutée avec succès. + +Détails: +- Commande: {task.command_name} +- Démarré: {log.started_at.strftime('%Y-%m-%d %H:%M:%S')} +- Terminé: {log.finished_at.strftime('%Y-%m-%d %H:%M:%S')} +- Durée: {log.duration_seconds:.1f} secondes + +Prochaine exécution: {task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée'} + +--- +Sortie de la commande: +{log.output} + +--- +Cordialement, +StreetUp - Système de tâches planifiées + """ + + recipients = [email.strip() for email in task.email_recipients.split(',')] + + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=recipients, + ) + + # Attacher le fichier si il existe + if output_file and os.path.exists(output_file): + email.attach_file(output_file) + self.stdout.write(self.style.SUCCESS(f' ✓ Fichier attaché: {output_file}')) + + email.send(fail_silently=False) + + log.email_sent = True + log.save(update_fields=['email_sent']) + + self.stdout.write(self.style.SUCCESS(f' ✓ Email envoyé à: {", ".join(recipients)}')) + + except Exception as e: + self.stdout.write(self.style.ERROR(f' ✗ Erreur lors de l\'envoi de l\'email: {str(e)}')) + + def send_error_email(self, task, log): + """ + Envoie un email d'erreur. + """ + try: + subject = f"[StreetUp] ERREUR - Tâche planifiée: {task.name}" + + body = f""" +Bonjour, + +La tâche planifiée "{task.name}" a rencontré une erreur lors de son exécution. + +Détails: +- Commande: {task.command_name} +- Démarré: {log.started_at.strftime('%Y-%m-%d %H:%M:%S')} +- Terminé: {log.finished_at.strftime('%Y-%m-%d %H:%M:%S')} +- Durée: {log.duration_seconds:.1f} secondes + +Prochaine exécution: {task.next_run.strftime('%Y-%m-%d %H:%M') if task.next_run else 'Non planifiée'} + +--- +Message d'erreur: +{log.error_message} + +--- +Sortie de la commande: +{log.output} + +--- +Veuillez vérifier les logs dans l'administration Django pour plus de détails. + +Cordialement, +StreetUp - Système de tâches planifiées + """ + + recipients = [email.strip() for email in task.email_recipients.split(',')] + + email = EmailMessage( + subject=subject, + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=recipients, + ) + + email.send(fail_silently=False) + + log.email_sent = True + log.save(update_fields=['email_sent']) + + self.stdout.write(self.style.WARNING(f' ! Email d\'erreur envoyé à: {", ".join(recipients)}')) + + except Exception as e: + self.stdout.write(self.style.ERROR(f' ✗ Erreur lors de l\'envoi de l\'email d\'erreur: {str(e)}')) diff --git a/streetup/common/middleware.py b/streetup/common/middleware.py new file mode 100644 index 0000000..2d8544f --- /dev/null +++ b/streetup/common/middleware.py @@ -0,0 +1,208 @@ +""" +Middleware pour la gestion de la déconnexion automatique des utilisateurs. + +Ce middleware vérifie si l'utilisateur connecté a une configuration de +déconnexion automatique (auto_logout_hours) et le déconnecte si le temps +depuis sa dernière connexion dépasse cette durée. + +Typiquement utilisé pour les utilisateurs qui partagent un ordinateur +en équipes de travail (shifts). +""" + +from django.contrib.auth import logout +from django.shortcuts import redirect +from django.utils import timezone +from django.contrib import messages +from django.utils.translation import gettext_lazy as _ +import traceback +import sys + + +# Clé de session pour stocker l'heure de connexion +SESSION_LOGIN_TIME_KEY = 'login_timestamp' + + +class RequestLoggingMiddleware: + """ + Middleware qui capture les erreurs HTTP (4xx, 5xx) et les exceptions + non gérées pour les enregistrer dans la base de données. + + Cela permet de visualiser les erreurs dans l'admin Django. + """ + + # URLs à ignorer pour éviter de logger des requêtes non pertinentes + IGNORED_PATHS = [ + '/static/', + '/media/', + '/favicon.ico', + '/__debug__/', + ] + + # Codes de statut à logger + LOGGED_STATUS_CODES = [400, 401, 403, 404, 405, 500, 502, 503, 504] + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Vérifier si cette URL doit être ignorée + if self._should_ignore(request): + return self.get_response(request) + + response = None + try: + response = self.get_response(request) + + # Logger les erreurs HTTP (4xx, 5xx) + if response.status_code in self.LOGGED_STATUS_CODES: + self._log_request( + request, + status_code=response.status_code, + level=self._get_log_level(response.status_code) + ) + + return response + + except Exception as exc: + # Logger les exceptions non gérées + self._log_request( + request, + exception=exc, + level='CRITICAL', + status_code=500, + traceback_str=traceback.format_exc() + ) + raise # Re-raise l'exception pour que Django la gère + + def process_exception(self, request, exception): + """ + Appelé quand une vue lève une exception. + """ + if self._should_ignore(request): + return None + + self._log_request( + request, + exception=exception, + level='ERROR', + status_code=500, + traceback_str=traceback.format_exc() + ) + return None # Laisser Django gérer l'exception + + def _should_ignore(self, request): + """Vérifie si la requête doit être ignorée.""" + for path in self.IGNORED_PATHS: + if request.path.startswith(path): + return True + return False + + def _get_log_level(self, status_code): + """Retourne le niveau de log approprié selon le code de statut.""" + if status_code >= 500: + return 'ERROR' + elif status_code == 404: + return 'WARNING' + elif status_code >= 400: + return 'WARNING' + return 'INFO' + + def _log_request(self, request, exception=None, level='ERROR', status_code=None, traceback_str=None): + """Enregistre le log dans la base de données.""" + try: + from common.models import RequestLog + RequestLog.log_request( + request, + exception=exception, + level=level, + status_code=status_code, + traceback_str=traceback_str + ) + except Exception as e: + # En cas d'erreur lors de l'enregistrement du log, ne pas faire planter la requête + # On peut quand même logger dans le fichier standard + import logging + logger = logging.getLogger('django') + logger.error(f"Erreur lors de l'enregistrement du log: {e}") + + +class AutoLogoutMiddleware: + """ + Middleware qui déconnecte automatiquement les utilisateurs après un + certain nombre d'heures configuré dans leur UserConfig. + + Si l'utilisateur a auto_logout_hours défini dans son UserConfig, + il sera automatiquement déconnecté après ce nombre d'heures depuis + sa dernière connexion. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Ne vérifier que pour les utilisateurs authentifiés + if request.user.is_authenticated: + # Enregistrer l'heure de connexion si pas encore fait + if SESSION_LOGIN_TIME_KEY not in request.session: + request.session[SESSION_LOGIN_TIME_KEY] = timezone.now().isoformat() + + # Vérifier si l'utilisateur doit être déconnecté + if self._should_logout(request): + # Stocker un message avant la déconnexion + messages.info( + request, + _("Votre session a expiré après le délai configuré. Veuillez vous reconnecter.") + ) + logout(request) + return redirect('login') + + response = self.get_response(request) + return response + + def _should_logout(self, request): + """ + Vérifie si l'utilisateur doit être déconnecté en fonction de + son auto_logout_hours configuré. + """ + try: + # Récupérer la configuration utilisateur + user_config = getattr(request.user, 'config', None) + if not user_config: + return False + + # Vérifier si auto_logout_hours est défini + auto_logout_hours = user_config.auto_logout_hours + if not auto_logout_hours: + return False + + # Récupérer l'heure de connexion depuis la session + login_time_str = request.session.get(SESSION_LOGIN_TIME_KEY) + if not login_time_str: + return False + + # Parser l'heure de connexion + from datetime import datetime + login_time = datetime.fromisoformat(login_time_str) + if timezone.is_naive(login_time): + login_time = timezone.make_aware(login_time) + + # Calculer la durée depuis la connexion + now = timezone.now() + hours_since_login = (now - login_time).total_seconds() / 3600 + + # Déconnecter si le temps dépasse la limite + return hours_since_login >= auto_logout_hours + + except Exception: + # En cas d'erreur, ne pas déconnecter + return False + + +def set_login_time(sender, user, request, **kwargs): + """ + Signal handler pour enregistrer l'heure de connexion quand un + utilisateur se connecte. + + À connecter au signal user_logged_in de Django. + """ + request.session[SESSION_LOGIN_TIME_KEY] = timezone.now().isoformat() diff --git a/streetup/common/migrations/0001_initial.py b/streetup/common/migrations/0001_initial.py new file mode 100644 index 0000000..81a14a3 --- /dev/null +++ b/streetup/common/migrations/0001_initial.py @@ -0,0 +1,327 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.contrib.gis.db.models.fields +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('assets', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AppView', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50, unique=True, verbose_name='Code')), + ('name_fr', models.CharField(max_length=100, verbose_name='Nom')), + ('name_nl', models.CharField(max_length=100, verbose_name='Naam')), + ('icon', models.CharField(blank=True, max_length=50, null=True, verbose_name='Icône Bootstrap')), + ('url_name', models.CharField(help_text="Ex: 'home:index' ou 'sign:index'", max_length=100, verbose_name="Nom de l'URL")), + ('is_default', models.BooleanField(default=False, help_text='Vue affichée par défaut pour les nouveaux utilisateurs', verbose_name='Vue par défaut')), + ('order', models.PositiveIntegerField(default=0, verbose_name="Ordre d'affichage")), + ], + options={ + 'verbose_name': 'Vue applicative', + 'verbose_name_plural': 'Vues applicatives', + 'ordering': ['order', 'name_fr'], + }, + ), + migrations.CreateModel( + name='Municipality', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fid', models.CharField(max_length=100)), + ('name_fr', models.CharField(max_length=255, verbose_name='Name FR')), + ('name_nl', models.CharField(max_length=255, verbose_name='Name NL')), + ('niscode', models.CharField(blank=True, max_length=30, null=True, verbose_name='Postal code')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ], + ), + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ], + ), + migrations.CreateModel( + name='Thematic', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=100, unique=True, verbose_name='Code')), + ('name_fr', models.CharField(max_length=255, verbose_name='Nom')), + ('name_nl', models.CharField(max_length=255, verbose_name='Naam')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description')), + ('icon', models.CharField(blank=True, max_length=100, null=True, verbose_name='Icône Bootstrap')), + ('color', models.CharField(default='#000000', max_length=7, verbose_name='Couleur associée')), + ('uses_occupations', models.BooleanField(default=False, verbose_name='Utilise les occupations')), + ('include_in_daily_report', models.BooleanField(default=False, help_text="Si coché, cette thématique apparaît toujours dans le rapport journalier, même si le nombre d'interventions est 0.", verbose_name='Inclure dans rapport journalier')), + ('allow_planning_without_contract', models.BooleanField(default=False, help_text="Si coché, les interventions de cette thématique peuvent passer au statut 'À planifier' sans qu'un prestataire ou un contrat soit renseigné. Le prestataire et le contrat restent modifiables dans ce statut.", verbose_name='Planification sans contrat autorisée')), + ], + options={ + 'verbose_name': 'Thématique', + 'verbose_name_plural': 'Thématiques', + }, + ), + migrations.CreateModel( + name='UserContractAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_view_posts', models.BooleanField(default=False)), + ('can_view_prices', models.BooleanField(default=False)), + ('can_view_interventions', models.BooleanField(default=True)), + ('can_view_assets', models.BooleanField(default=True)), + ('can_create_interventions', models.BooleanField(default=False, help_text='Permet au prestataire de créer des interventions pour ce contrat.', verbose_name='Peut créer des interventions')), + ('can_check_interventions', models.BooleanField(default=False, help_text='Permet de vérifier les interventions pour ce contrat.', verbose_name='Peut vérifier les interventions')), + ('can_approve', models.BooleanField(default=False, help_text="Permet de faire passer les interventions du statut 'à approuver' vers un autre statut pour ce contrat.", verbose_name='Peut approuver les interventions')), + ], + ), + migrations.CreateModel( + name='UserContractStatusPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('in_preparation', 'En préparation'), ('to_be_approved', 'À approuver'), ('to_be_planned', 'À planifier'), ('to_be_processed', 'À traiter'), ('assigned', 'Pris en charge'), ('in_progress', 'Démarré'), ('on_pause', 'En pause'), ('finished', 'Terminé'), ('processed', 'Traité'), ('to_be_corrected', 'À corriger'), ('corrected', 'Corrigé'), ('validated', 'Validé'), ('invoiced', 'Comptabilisé'), ('closed', 'Finalisé'), ('canceled', 'Annulé')], max_length=30, verbose_name='Statut')), + ('can_view', models.BooleanField(default=False, help_text='Peut voir les interventions à ce statut.', verbose_name='Voir')), + ('can_edit', models.BooleanField(default=False, help_text='Peut modifier les interventions à ce statut.', verbose_name='Modifier')), + ('can_change_status_to', models.BooleanField(default=False, help_text='Peut déclencher une transition vers ce statut (selon le graphe global des transitions).', verbose_name='Changer vers')), + ], + options={ + 'verbose_name': 'Permission par statut (contrat)', + 'verbose_name_plural': 'Permissions par statut (contrat)', + }, + ), + migrations.CreateModel( + name='UserThematics', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_view_assets', models.BooleanField(default=True)), + ('can_edit_assets', models.BooleanField(default=False)), + ('can_view_interventions', models.BooleanField(default=True)), + ('can_edit_interventions', models.BooleanField(default=False)), + ('can_edit_locations', models.BooleanField(default=False)), + ('can_process_observations', models.BooleanField(default=False, verbose_name='Peut traiter les observations')), + ('can_view_projects', models.BooleanField(default=False, verbose_name='Peut voir les projets')), + ('can_edit_projects', models.BooleanField(default=False, verbose_name='Peut éditer les projets')), + ], + options={ + 'verbose_name': 'Thématique utilisateur', + 'verbose_name_plural': 'Thématiques utilisateur', + }, + ), + migrations.CreateModel( + name='UserThematicStatusPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('in_preparation', 'En préparation'), ('to_be_approved', 'À approuver'), ('to_be_planned', 'À planifier'), ('to_be_processed', 'À traiter'), ('assigned', 'Pris en charge'), ('in_progress', 'Démarré'), ('on_pause', 'En pause'), ('finished', 'Terminé'), ('processed', 'Traité'), ('to_be_corrected', 'À corriger'), ('corrected', 'Corrigé'), ('validated', 'Validé'), ('invoiced', 'Comptabilisé'), ('closed', 'Finalisé'), ('canceled', 'Annulé')], max_length=30, verbose_name='Statut')), + ('can_view', models.BooleanField(default=False, help_text='Peut voir les interventions à ce statut.', verbose_name='Voir')), + ('can_edit', models.BooleanField(default=False, help_text='Peut modifier les interventions à ce statut.', verbose_name='Modifier')), + ('can_change_status_to', models.BooleanField(default=False, help_text='Peut déclencher une transition vers ce statut (selon le graphe global des transitions).', verbose_name='Changer vers')), + ], + options={ + 'verbose_name': 'Permission par statut (thématique)', + 'verbose_name_plural': 'Permissions par statut (thématique)', + }, + ), + migrations.CreateModel( + name='UserWarehouseAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_view', models.BooleanField(default=True, verbose_name='Peut visualiser')), + ('can_manage', models.BooleanField(default=False, verbose_name='Peut gérer')), + ('can_administer', models.BooleanField(default=False, verbose_name='Peut administrer')), + ('can_manage_preparation_orders', models.BooleanField(default=False, verbose_name='Peut gérer les bons de préparation')), + ('can_manage_purchase_orders', models.BooleanField(default=False, verbose_name='Peut gérer les bons de commande')), + ('can_validate_inventory', models.BooleanField(default=False, verbose_name='Peut valider les inventaires')), + ('is_default', models.BooleanField(default=False, verbose_name='Entrepôt par défaut')), + ], + options={ + 'verbose_name': 'Accès Entrepôt', + 'verbose_name_plural': 'Accès Entrepôts', + }, + ), + migrations.CreateModel( + name='AssetTypePermissionConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('requires_explicit_permissions', models.BooleanField(default=False, help_text="Si coché, les droits thématiques seuls ne suffisent pas. Chaque utilisateur doit avoir une entrée explicite dans « Accès assets spécifiques » pour ce type d'objet.", verbose_name='Droits explicites requis')), + ('content_type', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='asset_permission_config', to='contenttypes.contenttype', verbose_name="Type d'asset")), + ], + options={ + 'verbose_name': "Config permissions par type d'asset", + 'verbose_name_plural': "Configs permissions par type d'asset", + }, + ), + migrations.CreateModel( + name='AdrStreet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fid', models.CharField(max_length=100, unique=True)), + ('name_fr', models.CharField(max_length=255, verbose_name='Nom')), + ('name_nl', models.CharField(max_length=255, verbose_name='Naam')), + ('full_street_fr', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Nom complet FR')), + ('full_street_nl', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Nom complet NL')), + ('centroid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812)), + ('municipality', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='street_names', to='common.municipality')), + ], + ), + migrations.CreateModel( + name='AdrPoint', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fid', models.CharField(max_length=100, unique=True)), + ('number', models.CharField(max_length=255, verbose_name='Numéro')), + ('zipcode', models.CharField(blank=True, max_length=255, null=True, verbose_name='Code postal')), + ('full_address_fr', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Adresse complète FR')), + ('full_address_nl', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Adresse complète NL')), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('street_name', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='common.adrstreet')), + ('municipality', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='addresses', to='common.municipality')), + ], + ), + migrations.CreateModel( + name='RequestLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date et heure')), + ('level', models.CharField(choices=[('DEBUG', 'Debug'), ('INFO', 'Info'), ('WARNING', 'Warning'), ('ERROR', 'Error'), ('CRITICAL', 'Critical')], default='ERROR', max_length=10, verbose_name='Niveau')), + ('username', models.CharField(blank=True, max_length=150, null=True, verbose_name="Nom d'utilisateur")), + ('method', models.CharField(blank=True, max_length=10, null=True, verbose_name='Méthode HTTP')), + ('path', models.CharField(max_length=2048, verbose_name='URL')), + ('full_url', models.CharField(blank=True, max_length=4096, null=True, verbose_name='URL complète')), + ('status_code', models.IntegerField(blank=True, null=True, verbose_name='Code HTTP')), + ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='Adresse IP')), + ('user_agent', models.TextField(blank=True, null=True, verbose_name='User Agent')), + ('referer', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Referer')), + ('exception_type', models.CharField(blank=True, max_length=255, null=True, verbose_name="Type d'exception")), + ('exception_message', models.TextField(blank=True, null=True, verbose_name="Message d'erreur")), + ('traceback', models.TextField(blank=True, null=True, verbose_name='Traceback')), + ('request_data', models.TextField(blank=True, null=True, verbose_name='Données de la requête')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='request_logs', to=settings.AUTH_USER_MODEL, verbose_name='Utilisateur')), + ], + options={ + 'verbose_name': 'Log de requête', + 'verbose_name_plural': 'Logs de requêtes', + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='ScheduledCommand', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Nom descriptif de la tâche planifiée', max_length=200, unique=True, verbose_name='Nom de la tâche')), + ('command_name', models.CharField(help_text='Nom de la commande Django (ex: export_interventions_xlsx)', max_length=200, verbose_name='Nom de la commande')), + ('command_args', models.JSONField(blank=True, default=dict, help_text='Arguments de la commande au format JSON (ex: {"output": "file.xlsx"})', verbose_name='Arguments de la commande')), + ('frequency', models.CharField(choices=[('hourly', 'Toutes les heures'), ('daily', 'Quotidien'), ('weekly', 'Hebdomadaire'), ('monthly', 'Mensuel')], default='daily', max_length=20, verbose_name='Fréquence')), + ('hour', models.IntegerField(default=0, help_text="Heure d'exécution (0-23)", validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(23)], verbose_name='Heure')), + ('minute', models.IntegerField(default=0, help_text="Minute d'exécution (0-59)", validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(59)], verbose_name='Minute')), + ('day_of_month', models.IntegerField(blank=True, help_text='Pour les tâches mensuelles : jour du mois (1-31)', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(31)], verbose_name='Jour du mois')), + ('day_of_week', models.IntegerField(blank=True, choices=[(0, 'Lundi'), (1, 'Mardi'), (2, 'Mercredi'), (3, 'Jeudi'), (4, 'Vendredi'), (5, 'Samedi'), (6, 'Dimanche')], help_text='Pour les tâches hebdomadaires : 0=Lundi, 6=Dimanche', null=True, verbose_name='Jour de la semaine')), + ('is_active', models.BooleanField(default=True, help_text='Si décoché, la tâche ne sera pas exécutée', verbose_name='Active')), + ('send_email', models.BooleanField(default=False, help_text="Envoyer un email après l'exécution", verbose_name='Envoyer un email')), + ('email_recipients', models.TextField(blank=True, help_text='Adresses email séparées par des virgules', verbose_name='Destinataires')), + ('email_subject', models.CharField(blank=True, max_length=200, verbose_name="Sujet de l'email")), + ('last_run', models.DateTimeField(blank=True, null=True, verbose_name='Dernière exécution')), + ('next_run', models.DateTimeField(blank=True, null=True, verbose_name='Prochaine exécution')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Créé le')), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_scheduled_commands', to=settings.AUTH_USER_MODEL, verbose_name='Créé par')), + ], + options={ + 'verbose_name': 'Commande planifiée', + 'verbose_name_plural': 'Commandes planifiées', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='CommandExecutionLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('started_at', models.DateTimeField(auto_now_add=True, verbose_name='Démarré à')), + ('finished_at', models.DateTimeField(blank=True, null=True, verbose_name='Terminé à')), + ('status', models.CharField(choices=[('running', 'En cours'), ('success', 'Succès'), ('error', 'Erreur')], default='running', max_length=20, verbose_name='Statut')), + ('output', models.TextField(blank=True, verbose_name='Sortie')), + ('error_message', models.TextField(blank=True, verbose_name="Message d'erreur")), + ('duration_seconds', models.FloatField(blank=True, null=True, verbose_name='Durée (secondes)')), + ('email_sent', models.BooleanField(default=False, verbose_name='Email envoyé')), + ('scheduled_command', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution_logs', to='common.scheduledcommand', verbose_name='Commande planifiée')), + ], + options={ + 'verbose_name': "Journal d'exécution", + 'verbose_name_plural': "Journaux d'exécution", + 'ordering': ['-started_at'], + }, + ), + migrations.CreateModel( + name='UserAssetAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField(verbose_name="ID de l'objet")), + ('can_view', models.BooleanField(default=False, verbose_name='Peut visualiser')), + ('can_edit', models.BooleanField(default=False, verbose_name='Peut modifier')), + ('can_delete', models.BooleanField(default=False, verbose_name='Peut supprimer')), + ('editable_fields', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Liste des noms de champs que cet utilisateur peut modifier sur cet objet. Si renseignée (non vide), remplace complètement les champs autorisés par le rôle.', null=True, size=None, verbose_name='Champs modifiables')), + ('visible_fields', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Liste des noms de champs que cet utilisateur peut voir sur cet objet. Si renseignée (non vide), remplace complètement les champs affichés par défaut.', null=True, size=None, verbose_name='Champs visibles')), + ('content_type', models.ForeignKey(help_text="Le modèle Django de l'asset ou de la location concernée.", on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'objet")), + ], + options={ + 'verbose_name': 'Accès asset spécifique', + 'verbose_name_plural': 'Accès assets spécifiques', + }, + ), + migrations.CreateModel( + name='UserAssetTypeAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_view', models.BooleanField(default=False, verbose_name='Peut visualiser')), + ('can_edit', models.BooleanField(default=False, verbose_name='Peut modifier')), + ('can_delete', models.BooleanField(default=False, verbose_name='Peut supprimer')), + ('editable_fields', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Liste des noms de champs modifiables pour tous les objets de ce type. Si renseignée (non vide), remplace les champs autorisés par le rôle. Peut être écrasée objet par objet via UserAssetAccess.', null=True, size=None, verbose_name='Champs modifiables')), + ('visible_fields', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Liste des noms de champs visibles pour tous les objets de ce type. Si renseignée (non vide), remplace les champs affichés par défaut. Peut être écrasée objet par objet via UserAssetAccess.', null=True, size=None, verbose_name='Champs visibles')), + ('content_type', models.ForeignKey(help_text="Le modèle Django concerné (s'applique à toutes ses instances).", on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset")), + ], + options={ + 'verbose_name': "Accès par type d'asset", + 'verbose_name_plural': "Accès par type d'asset", + }, + ), + migrations.CreateModel( + name='UserConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_intern', models.BooleanField(default=False)), + ('limit_assets_to_contracts', models.BooleanField(default=True)), + ('limit_interventions_to_contracts', models.BooleanField(default=True)), + ('limit_shortcuts_to_contracts', models.BooleanField(default=False, help_text="Limite les raccourcis et les compteurs de la page d'accueil aux contrats pour lesquels l'utilisateur dispose de permissions explicites.", verbose_name="Limiter la page d'accueil aux contrats")), + ('generic_email', models.EmailField(blank=True, max_length=254, null=True)), + ('hide_intervention_map_by_default', models.BooleanField(default=False, help_text="Affiche un bouton permettant d'afficher la carte au besoin.", verbose_name='Masquer la carte des interventions par défaut')), + ('intervention_list_layout', models.CharField(choices=[('both', 'Tableau et carte'), ('table_only', 'Tableau seul'), ('map_only', 'Carte seule')], default='both', help_text='Préférence de disposition pour la vue liste des interventions.', max_length=20, verbose_name='Disposition de la liste des interventions')), + ('intervention_list_filters', models.JSONField(blank=True, default=dict, help_text='Préférences de filtrage pour la vue liste des interventions.', verbose_name='Filtres de la liste des interventions')), + ('intervention_list_sort', models.JSONField(blank=True, default=dict, help_text='Préférences de tri pour la vue liste des interventions.', verbose_name='Tri de la liste des interventions')), + ('intervention_list_columns', models.JSONField(blank=True, default=None, help_text='Liste des champs masqués dans le tableau de la vue liste des interventions.', null=True, verbose_name='Colonnes masquées de la liste des interventions')), + ('observation_list_filters', models.JSONField(blank=True, default=dict, help_text='Préférences de filtrage pour la vue liste des observations.', verbose_name='Filtres de la liste des observations')), + ('observation_list_sort', models.JSONField(blank=True, default=dict, help_text='Préférences de tri pour la vue liste des observations.', verbose_name='Tri de la liste des observations')), + ('assets_list_layout', models.CharField(choices=[('both', 'Tableau et carte'), ('table_only', 'Tableau seul'), ('map_only', 'Carte seule')], default='both', help_text='Préférence de disposition pour la vue liste des assets.', max_length=20, verbose_name='Disposition de la liste des assets')), + ('auto_logout_hours', models.PositiveIntegerField(blank=True, help_text="Nombre d'heures après la connexion avant déconnexion automatique. Laisser vide pour désactiver.", null=True, verbose_name='Déconnexion automatique (heures)')), + ('can_add_unique_tag_documents', models.BooleanField(default=False, help_text="Permet aux utilisateurs externes d'ajouter/modifier des documents avec des tags uniques (CME, PIS, As-Built, etc.).", verbose_name='Peut ajouter des documents avec tags uniques')), + ('reporting_preferences', models.JSONField(blank=True, default=dict, help_text='Préférences de filtrage pour les différentes pages du reporting.', verbose_name='Préférences du reporting')), + ('accessible_views', models.ManyToManyField(blank=True, help_text="Les vues/applications auxquelles l'utilisateur a accès.", related_name='user_configs', to='common.appview', verbose_name='Vues accessibles')), + ('approval_categories', models.ManyToManyField(blank=True, help_text="Catégories d'assets pour lesquelles l'utilisateur approuve des interventions. Si vide, aucun filtre de catégorie n'est appliqué.", related_name='approver_user_configs', to='assets.assetcategory', verbose_name="Catégories d'approbation")), + ], + ), + ] diff --git a/streetup/common/migrations/0002_initial.py b/streetup/common/migrations/0002_initial.py new file mode 100644 index 0000000..3a6a245 --- /dev/null +++ b/streetup/common/migrations/0002_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('common', '0001_initial'), + ('contracts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='company', + field=models.ForeignKey(blank=True, help_text='Société/organisme auquel appartient cet utilisateur (utile pour filtrer les membres visibles).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_configs', to='contracts.company', verbose_name='Société'), + ), + migrations.AddField( + model_name='userconfig', + name='default_thematic', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_user_accesses', to='common.thematic'), + ), + migrations.AddField( + model_name='userconfig', + name='linked_configs', + field=models.ManyToManyField(blank=True, to='common.userconfig', verbose_name='Comptes liés'), + ), + migrations.AddField( + model_name='userconfig', + name='roles', + field=models.ManyToManyField(blank=True, to='common.role'), + ), + migrations.AddField( + model_name='userconfig', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='config', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='userassettypeaccess', + name='user_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='asset_type_accesses', to='common.userconfig', verbose_name='Configuration utilisateur'), + ), + migrations.AddField( + model_name='userassetaccess', + name='user_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='asset_accesses', to='common.userconfig', verbose_name='Configuration utilisateur'), + ), + migrations.CreateModel( + name='UserApprovalConfig', + fields=[ + ], + options={ + 'verbose_name': 'Configuration approbateur', + 'verbose_name_plural': 'Configurations approbateurs', + 'ordering': ['user__username'], + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('common.userconfig',), + ), + migrations.AddField( + model_name='usercontractaccess', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contracts.contract'), + ), + migrations.AddField( + model_name='usercontractaccess', + name='user_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='common.userconfig'), + ), + migrations.AddField( + model_name='userconfig', + name='contracts', + field=models.ManyToManyField(related_name='user_accesses', through='common.UserContractAccess', to='contracts.contract'), + ), + migrations.AddField( + model_name='usercontractstatuspermission', + name='user_contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_permissions', to='common.usercontractaccess'), + ), + migrations.AddField( + model_name='userthematics', + name='thematic', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='common.thematic'), + ), + migrations.AddField( + model_name='userthematics', + name='user_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userthematics', to='common.userconfig'), + ), + migrations.AddField( + model_name='userconfig', + name='thematics', + field=models.ManyToManyField(related_name='user_accesses', through='common.UserThematics', to='common.thematic'), + ), + migrations.AddField( + model_name='userthematicstatuspermission', + name='user_thematic', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_permissions', to='common.userthematics'), + ), + migrations.AddField( + model_name='userwarehouseaccess', + name='user_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userwarehouses', to='common.userconfig'), + ), + ] diff --git a/streetup/common/migrations/0003_initial.py b/streetup/common/migrations/0003_initial.py new file mode 100644 index 0000000..000c9dd --- /dev/null +++ b/streetup/common/migrations/0003_initial.py @@ -0,0 +1,83 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('common', '0002_initial'), + ('stock', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userwarehouseaccess', + name='warehouse', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stock.warehouse'), + ), + migrations.AddField( + model_name='userconfig', + name='warehouses', + field=models.ManyToManyField(blank=True, related_name='user_accesses', through='common.UserWarehouseAccess', to='stock.warehouse'), + ), + migrations.AddIndex( + model_name='requestlog', + index=models.Index(fields=['level'], name='common_requ_level_55ef16_idx'), + ), + migrations.AddIndex( + model_name='requestlog', + index=models.Index(fields=['user'], name='common_requ_user_id_3781cb_idx'), + ), + migrations.AddIndex( + model_name='requestlog', + index=models.Index(fields=['status_code'], name='common_requ_status__b046e1_idx'), + ), + migrations.AddIndex( + model_name='requestlog', + index=models.Index(fields=['timestamp', 'level'], name='common_requ_timesta_264745_idx'), + ), + migrations.AlterUniqueTogether( + name='userassettypeaccess', + unique_together={('user_config', 'content_type')}, + ), + migrations.AddIndex( + model_name='userassetaccess', + index=models.Index(fields=['content_type', 'object_id'], name='common_user_content_e5ef8d_idx'), + ), + migrations.AlterUniqueTogether( + name='userassetaccess', + unique_together={('user_config', 'content_type', 'object_id')}, + ), + migrations.AlterUniqueTogether( + name='usercontractaccess', + unique_together={('user_config', 'contract')}, + ), + migrations.AddIndex( + model_name='usercontractstatuspermission', + index=models.Index(fields=['user_contract', 'status', 'can_view'], name='ucsp_status_canview_idx'), + ), + migrations.AlterUniqueTogether( + name='usercontractstatuspermission', + unique_together={('user_contract', 'status')}, + ), + migrations.AlterUniqueTogether( + name='userthematics', + unique_together={('user_config', 'thematic')}, + ), + migrations.AddIndex( + model_name='userthematicstatuspermission', + index=models.Index(fields=['user_thematic', 'status', 'can_view'], name='utsp_status_canview_idx'), + ), + migrations.AlterUniqueTogether( + name='userthematicstatuspermission', + unique_together={('user_thematic', 'status')}, + ), + migrations.AlterUniqueTogether( + name='userwarehouseaccess', + unique_together={('user_config', 'warehouse')}, + ), + ] diff --git a/streetup/common/migrations/0004_userconfig_assets_index_preferences.py b/streetup/common/migrations/0004_userconfig_assets_index_preferences.py new file mode 100644 index 0000000..f04026d --- /dev/null +++ b/streetup/common/migrations/0004_userconfig_assets_index_preferences.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-05-23 05:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0003_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='assets_index_preferences', + field=models.JSONField(blank=True, default=dict, help_text='Préférences de la page assets (thématiques actives, catégories, filtres, position carte).', verbose_name='Préférences de la page index unifiée'), + ), + ] diff --git a/streetup/common/migrations/0005_userconfig_calendar_preferences.py b/streetup/common/migrations/0005_userconfig_calendar_preferences.py new file mode 100644 index 0000000..393aa27 --- /dev/null +++ b/streetup/common/migrations/0005_userconfig_calendar_preferences.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-05-24 09:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0004_userconfig_assets_index_preferences'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='calendar_preferences', + field=models.JSONField(blank=True, default=dict, help_text="Préférences de la page calendrier (mode d'affichage, zoom, filtres actifs, état d'expansion du backlog).", null=True, verbose_name='Préférences du calendrier'), + ), + ] diff --git a/streetup/common/migrations/0006_userconfig_esign_can_edit_and_more.py b/streetup/common/migrations/0006_userconfig_esign_can_edit_and_more.py new file mode 100644 index 0000000..eb82698 --- /dev/null +++ b/streetup/common/migrations/0006_userconfig_esign_can_edit_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.8 on 2026-05-30 08:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0005_userconfig_calendar_preferences'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='esign_can_edit', + field=models.BooleanField(default=False, help_text='Permission pour éditer les panneaux et les poteaux.', verbose_name='eSign : Peut éditer'), + ), + migrations.AddField( + model_name='userconfig', + name='esign_can_manage_rcp', + field=models.BooleanField(default=False, help_text='Permission pour créer, modifier et supprimer les RCP.', verbose_name='eSign : Peut gérer les RCP'), + ), + migrations.AddField( + model_name='userconfig', + name='esign_can_validate_rcp', + field=models.BooleanField(default=False, help_text='Permission pour valider les RCP.', verbose_name='eSign : Peut valider les RCP'), + ), + ] diff --git a/streetup/common/migrations/0007_userconfig_esign_organizations.py b/streetup/common/migrations/0007_userconfig_esign_organizations.py new file mode 100644 index 0000000..51a553f --- /dev/null +++ b/streetup/common/migrations/0007_userconfig_esign_organizations.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.8 on 2026-05-30 10:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0006_userconfig_esign_can_edit_and_more'), + ('sign', '0003_alter_signpanel_street_alter_signpole_street'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='esign_organizations', + field=models.ManyToManyField(blank=True, help_text="Les gestionnaires (communes, Bruxelles Mobilité) pour lesquels l'utilisateur a des permissions.", related_name='user_configs', to='sign.organization', verbose_name='eSign : Gestionnaires'), + ), + ] diff --git a/streetup/common/migrations/0008_create_agents_regie.py b/streetup/common/migrations/0008_create_agents_regie.py new file mode 100644 index 0000000..b79d5c7 --- /dev/null +++ b/streetup/common/migrations/0008_create_agents_regie.py @@ -0,0 +1,156 @@ +# Generated by Django on 2026-06-10 + +from django.db import migrations +from django.contrib.auth.hashers import make_password +from django.conf import settings +import zipfile +import xml.etree.ElementTree as ET +import unicodedata +import random +import string +import os + +def normalize_text(text): + nfkd_form = unicodedata.normalize('NFKD', text) + only_ascii = "".join([c for c in nfkd_form if not unicodedata.combining(c)]) + cleaned = "".join([c.lower() for c in only_ascii if c.isalnum()]) + return cleaned + +def generate_password(): + first_char = random.choice(string.ascii_uppercase) + rest_chars = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(11)) + return first_char + rest_chars + +def create_agents_regie(apps, schema_editor): + User = apps.get_model('auth', 'User') + Role = apps.get_model('common', 'Role') + UserConfig = apps.get_model('common', 'UserConfig') + Company = apps.get_model('contracts', 'Company') + CompanyMember = apps.get_model('contracts', 'CompanyMember') + + # Find the ODS file path + ods_path = os.path.join(settings.BASE_DIR, '..', 'external_files', 'agents_regie.ods') + if not os.path.exists(ods_path): + ods_path = os.path.join(settings.BASE_DIR, 'external_files', 'agents_regie.ods') + + if not os.path.exists(ods_path): + print(f"\n[WARNING] agents_regie.ods not found at {ods_path}. Skipping user creation.") + return + + # Parse ODS file + namespaces = { + 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0', + 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', + 'office': 'urn:oasis:names:tc:opendocument:xmlns:office:1.0' + } + + agents = [] + with zipfile.ZipFile(ods_path, 'r') as z: + content_xml = z.read('content.xml') + root = ET.fromstring(content_xml) + tables = root.findall('.//table:table', namespaces) + for table in tables: + rows = table.findall('.//table:table-row', namespaces) + for idx, row in enumerate(rows): + cells = [] + for cell in row.findall('.//table:table-cell', namespaces): + repeated = cell.get('{' + namespaces['table'] + '}number-columns-repeated') + rep_count = int(repeated) if repeated else 1 + text_elems = cell.findall('.//text:p', namespaces) + cell_text = "".join([t.text if t.text else "" for t in text_elems]) + for _ in range(rep_count): + cells.append(cell_text) + + if any(cells): + while cells and cells[-1] == "": + cells.pop() + if idx == 0: + continue + if len(cells) >= 2: + name = cells[0].strip() + firstname = cells[1].strip() + if name and firstname: + agents.append((name, firstname)) + + if not agents: + print("\n[WARNING] No agents found in agents_regie.ods. Skipping.") + return + + role_tech, _ = Role.objects.get_or_create(name='technician') + company_regie, _ = Company.objects.get_or_create(name='BM - Régie') + + created_users = [] + passwords_file_lines = [ + "=== BM - Régie : Nouveaux Techniciens ===", + f"Généré le : {os.getenv('CURRENT_TIME', '2026-06-10')}", + "-" * 50 + ] + + for name, firstname in agents: + last_name_formatted = name.capitalize() + first_name_formatted = firstname + + first_letter = normalize_text(first_name_formatted)[:1] + last_name_normalized = normalize_text(last_name_formatted) + username = f"{first_letter}{last_name_normalized}" + + if User.objects.filter(username=username).exists(): + continue + + password = generate_password() + + # Create user + user = User.objects.create( + username=username, + first_name=first_name_formatted, + last_name=last_name_formatted, + password=make_password(password) + ) + + # Create UserConfig + config = UserConfig.objects.create( + user=user, + company=company_regie + ) + config.roles.add(role_tech) + + # Create CompanyMember + CompanyMember.objects.create( + user=user, + name=first_name_formatted + ) + + created_users.append((username, password)) + passwords_file_lines.append(f"Login: {username:<15} | Password: {password}") + + if created_users: + print("\n" + "="*80) + print("BM - RÉGIE : UTILISATEURS CRÉÉS") + print("="*80) + for username, password in created_users: + print(f" Login: {username:<15} | Mot de passe: {password}") + print("="*80) + + # Write to generated_passwords.txt on production server + passwords_file_path = os.path.join(settings.BASE_DIR, 'generated_passwords.txt') + try: + with open(passwords_file_path, 'w', encoding='utf-8') as f: + f.write("\n".join(passwords_file_lines) + "\n") + print(f"Mots de passe sauvegardés dans : {passwords_file_path}") + except Exception as e: + print(f"[ERROR] Impossible d'écrire les mots de passe dans un fichier: {e}") + print("="*80 + "\n") + +def delete_agents_regie(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0007_userconfig_esign_organizations'), + ('contracts', '0006_companymember_teams_m2m'), + ] + + operations = [ + migrations.RunPython(create_agents_regie, reverse_code=delete_agents_regie), + ] diff --git a/streetup/common/migrations/0008_userconfig_mobile_intervention_list_filters.py b/streetup/common/migrations/0008_userconfig_mobile_intervention_list_filters.py new file mode 100644 index 0000000..6b6a86a --- /dev/null +++ b/streetup/common/migrations/0008_userconfig_mobile_intervention_list_filters.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-06-11 11:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0007_userconfig_esign_organizations'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='mobile_intervention_list_filters', + field=models.JSONField(blank=True, default=dict, help_text='Préférences de filtrage pour la vue mobile de la liste des interventions.', verbose_name='Filtres de la liste des interventions mobile'), + ), + ] diff --git a/streetup/common/migrations/0009_merge_20260611_1539.py b/streetup/common/migrations/0009_merge_20260611_1539.py new file mode 100644 index 0000000..86b7ed5 --- /dev/null +++ b/streetup/common/migrations/0009_merge_20260611_1539.py @@ -0,0 +1,14 @@ +# Generated by Django 5.2.8 on 2026-06-11 13:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0008_create_agents_regie'), + ('common', '0008_userconfig_mobile_intervention_list_filters'), + ] + + operations = [ + ] diff --git a/streetup/common/migrations/0010_userconfig_project_list_filters_and_more.py b/streetup/common/migrations/0010_userconfig_project_list_filters_and_more.py new file mode 100644 index 0000000..0a99008 --- /dev/null +++ b/streetup/common/migrations/0010_userconfig_project_list_filters_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2026-06-16 10:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0009_merge_20260611_1539'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='project_list_filters', + field=models.JSONField(blank=True, default=dict, help_text='Préférences de filtrage pour la vue liste des projets.', verbose_name='Filtres de la liste des projets'), + ), + migrations.AddField( + model_name='userconfig', + name='project_list_sort', + field=models.JSONField(blank=True, default=dict, help_text='Préférences de tri pour la vue liste des projets.', verbose_name='Tri de la liste des projets'), + ), + ] diff --git a/streetup/common/migrations/0011_userwarehouselocationaccess_and_more.py b/streetup/common/migrations/0011_userwarehouselocationaccess_and_more.py new file mode 100644 index 0000000..80723b0 --- /dev/null +++ b/streetup/common/migrations/0011_userwarehouselocationaccess_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.8 on 2026-06-17 07:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0010_userconfig_project_list_filters_and_more'), + ('stock', '0007_add_comment_by_to_inventoryitem'), + ] + + operations = [ + migrations.CreateModel( + name='UserWarehouseLocationAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_perform_inventory', models.BooleanField(default=False, verbose_name="Peut faire l'inventaire")), + ('can_validate_inventory', models.BooleanField(default=False, verbose_name="Peut valider l'inventaire")), + ('user_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userlocations', to='common.userconfig')), + ('warehouse_location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accesses', to='stock.warehouselocation', verbose_name='Emplacement')), + ], + options={ + 'verbose_name': 'Accès Emplacement', + 'verbose_name_plural': 'Accès Emplacements', + 'unique_together': {('user_config', 'warehouse_location')}, + }, + ), + migrations.AddField( + model_name='userconfig', + name='warehouse_locations', + field=models.ManyToManyField(blank=True, related_name='user_configs', through='common.UserWarehouseLocationAccess', to='stock.warehouselocation'), + ), + ] diff --git a/streetup/common/migrations/0012_create_water_thematic_and_transfer_data.py b/streetup/common/migrations/0012_create_water_thematic_and_transfer_data.py new file mode 100644 index 0000000..fe71809 --- /dev/null +++ b/streetup/common/migrations/0012_create_water_thematic_and_transfer_data.py @@ -0,0 +1,112 @@ +from django.db import migrations, models + +def create_water_thematic_and_transfer_data(apps, schema_editor): + Thematic = apps.get_model('common', 'Thematic') + UserThematics = apps.get_model('common', 'UserThematics') + AssetCategory = apps.get_model('assets', 'AssetCategory') + Symptom = apps.get_model('interventions', 'Symptom') + InspectionConfiguration = apps.get_model('interventions', 'InspectionConfiguration') + Intervention = apps.get_model('interventions', 'Intervention') + + # 1. Create or get water thematic + water_thematic, created = Thematic.objects.get_or_create( + code='water', + defaults={ + 'name_fr': 'Eau', + 'name_nl': 'Water', + 'icon': 'bi-droplet', + 'color': '#0d6efd', + } + ) + + # 2. Replicate UserThematics permissions for 'water' from 'nature' + nature_permissions = UserThematics.objects.filter(thematic__code='nature') + for np in nature_permissions: + UserThematics.objects.get_or_create( + user_config=np.user_config, + thematic=water_thematic, + defaults={ + 'can_view_assets': np.can_view_assets, + 'can_edit_assets': np.can_edit_assets, + 'can_view_interventions': np.can_view_interventions, + 'can_edit_interventions': np.can_edit_interventions, + 'can_edit_locations': np.can_edit_locations, + 'can_process_observations': np.can_process_observations, + 'can_view_projects': np.can_view_projects, + 'can_edit_projects': np.can_edit_projects, + } + ) + + # 3. Transfer AssetCategory NAT_RWI + try: + giep_cat = AssetCategory.objects.get(code='NAT_RWI') + giep_cat.thematic = water_thematic + giep_cat.save() + + # Transfer any subcategories recursively (in case any exist) + def transfer_children(parent_cat): + for child in AssetCategory.objects.filter(parent=parent_cat): + child.thematic = water_thematic + child.save() + transfer_children(child) + transfer_children(giep_cat) + except AssetCategory.DoesNotExist: + pass + + # 4. Transfer Symptom instances linked to NAT_RWI + giep_cats = AssetCategory.objects.filter(thematic=water_thematic) + symptoms_to_transfer = Symptom.objects.filter( + models.Q(thematic__code='nature') & + (models.Q(asset_categories__in=giep_cats) | models.Q(name_fr__icontains='giep') | models.Q(name_nl__icontains='giep')) + ).distinct() + for symp in symptoms_to_transfer: + symp.thematic = water_thematic + symp.save() + + # 5. Transfer InspectionConfiguration + configs_to_transfer = InspectionConfiguration.objects.filter( + models.Q(thematic__code='nature') & + (models.Q(name_fr__icontains='giep') | models.Q(name_nl__icontains='giep') | models.Q(default_category__thematic=water_thematic)) + ).distinct() + for config in configs_to_transfer: + config.thematic = water_thematic + config.save() + + # 6. Transfer Interventions + ContentType = apps.get_model('contenttypes', 'ContentType') + try: + rwiz_ct = ContentType.objects.get(app_label='assets', model='naturerwiz') + rwiasset_ct = ContentType.objects.get(app_label='assets', model='naturerwiasset') + + # Interventions with category in water thematic + itvs_by_cat = Intervention.objects.filter(thematic__code='nature', asset_category__thematic=water_thematic) + + # Interventions with location in NatureRWIZ + InterventionLocation = apps.get_model('interventions', 'InterventionLocation') + itv_ids_by_loc = InterventionLocation.objects.filter(content_type=rwiz_ct).values_list('intervention_id', flat=True) + itvs_by_loc = Intervention.objects.filter(thematic__code='nature', id__in=itv_ids_by_loc) + + # Interventions with asset in NatureRWIAsset + InterventionAsset = apps.get_model('interventions', 'InterventionAsset') + itv_ids_by_asset = InterventionAsset.objects.filter(content_type=rwiasset_ct).values_list('intervention_id', flat=True) + itvs_by_asset = Intervention.objects.filter(thematic__code='nature', id__in=itv_ids_by_asset) + + all_giep_itvs = (itvs_by_cat | itvs_by_loc | itvs_by_asset).distinct() + for itv in all_giep_itvs: + itv.thematic = water_thematic + itv.save() + except ContentType.DoesNotExist: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0011_userwarehouselocationaccess_and_more'), + ('assets', '0020_alter_trafficlightradarpole_model'), + ('interventions', '0016_inspectionconfiguration_show_equipment'), + ] + + operations = [ + migrations.RunPython(create_water_thematic_and_transfer_data, reverse_code=migrations.RunPython.noop), + ] diff --git a/streetup/common/migrations/0013_userthematics_can_validate_assets.py b/streetup/common/migrations/0013_userthematics_can_validate_assets.py new file mode 100644 index 0000000..878f49e --- /dev/null +++ b/streetup/common/migrations/0013_userthematics_can_validate_assets.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-07-03 09:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0012_create_water_thematic_and_transfer_data'), + ] + + operations = [ + migrations.AddField( + model_name='userthematics', + name='can_validate_assets', + field=models.BooleanField(default=False, verbose_name='Peut valider les assets'), + ), + ] diff --git a/streetup/common/migrations/0014_userconfig_panoramax_can_upload.py b/streetup/common/migrations/0014_userconfig_panoramax_can_upload.py new file mode 100644 index 0000000..f301237 --- /dev/null +++ b/streetup/common/migrations/0014_userconfig_panoramax_can_upload.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-07-19 05:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0013_userthematics_can_validate_assets'), + ] + + operations = [ + migrations.AddField( + model_name='userconfig', + name='panoramax_can_upload', + field=models.BooleanField(default=False, help_text="Permet à l'utilisateur d'uploader de nouvelles photos ou vidéos 360 pour Panoramax.", verbose_name='Peut uploader des photos 360'), + ), + ] diff --git a/streetup/common/migrations/__init__.py b/streetup/common/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/common/models.py b/streetup/common/models.py new file mode 100644 index 0000000..6d8d557 --- /dev/null +++ b/streetup/common/models.py @@ -0,0 +1,1166 @@ +from django.contrib.gis.db import models +from django.contrib.auth.models import User +from django.utils.translation import get_language +from django.contrib.postgres.indexes import GinIndex, OpClass +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Lower +from django.conf import settings + +import os +from PIL import Image, ExifTags +from pdf2image import convert_from_path + + +class LanguageManager(models.Manager): + def get_queryset(self): + lang = get_language() + if lang == 'nl': + return super().get_queryset().order_by('name_nl') + return super().get_queryset().order_by('name_fr') + +class Thematic(models.Model): + code = models.CharField(max_length=100, unique=True, verbose_name="Code") + name_fr = models.CharField(max_length=255, verbose_name="Nom") + name_nl = models.CharField(max_length=255, verbose_name="Naam") + description = models.TextField(blank=True, null=True, verbose_name="Description") + icon = models.CharField(max_length=100, blank=True, null=True, verbose_name="Icône Bootstrap") + color = models.CharField(max_length=7, default="#000000", verbose_name="Couleur associée") + uses_occupations = models.BooleanField(default=False, verbose_name="Utilise les occupations") + include_in_daily_report = models.BooleanField( + default=False, + verbose_name="Inclure dans rapport journalier", + help_text="Si coché, cette thématique apparaît toujours dans le rapport journalier, même si le nombre d'interventions est 0.", + ) + allow_planning_without_contract = models.BooleanField( + default=False, + verbose_name="Planification sans contrat autorisée", + help_text="Si coché, les interventions de cette thématique peuvent passer au statut 'À planifier' sans qu'un prestataire ou un contrat soit renseigné. Le prestataire et le contrat restent modifiables dans ce statut.", + ) + + @property + def name(self): + """Renvoie le nom de la thématique dans la langue active.""" + lang = get_language() + if lang == 'nl': + return self.name_nl + return self.name_fr + + objects = LanguageManager() + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Thématique" + verbose_name_plural = "Thématiques" + + +class Role(models.Model): + name = models.CharField(max_length=50, unique=True) # ex: 'manager', 'technician', 'admin' + + def __str__(self): + return self.name + + +class AppView(models.Model): + """ + Defines available application views/modules. + This allows for a flexible permission system for different views. + """ + code = models.CharField(max_length=50, unique=True, verbose_name="Code") + name_fr = models.CharField(max_length=100, verbose_name="Nom") + name_nl = models.CharField(max_length=100, verbose_name="Naam") + icon = models.CharField(max_length=50, blank=True, null=True, verbose_name="Icône Bootstrap") + url_name = models.CharField(max_length=100, verbose_name="Nom de l'URL", help_text="Ex: 'home:index' ou 'sign:index'") + is_default = models.BooleanField(default=False, verbose_name="Vue par défaut", help_text="Vue affichée par défaut pour les nouveaux utilisateurs") + order = models.PositiveIntegerField(default=0, verbose_name="Ordre d'affichage") + + @property + def name(self): + """Renvoie le nom dans la langue active.""" + lang = get_language() + if lang == 'nl': + return self.name_nl + return self.name_fr + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Vue applicative" + verbose_name_plural = "Vues applicatives" + ordering = ['order', 'name_fr'] + +class UserConfig(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="config") + is_intern = models.BooleanField(default=False) + roles = models.ManyToManyField(Role, blank=True) + default_thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True, related_name="default_user_accesses") + thematics = models.ManyToManyField("common.Thematic", through="UserThematics", related_name="user_accesses") + contracts = models.ManyToManyField("contracts.Contract", through="UserContractAccess", related_name="user_accesses") + warehouses = models.ManyToManyField("stock.Warehouse", through="UserWarehouseAccess", related_name="user_accesses", blank=True) + warehouse_locations = models.ManyToManyField("stock.WarehouseLocation", through="UserWarehouseLocationAccess", related_name="user_configs", blank=True) + limit_assets_to_contracts = models.BooleanField(default=True) + limit_interventions_to_contracts = models.BooleanField(default=True) + limit_shortcuts_to_contracts = models.BooleanField( + default=False, + verbose_name="Limiter la page d'accueil aux contrats", + help_text="Limite les raccourcis et les compteurs de la page d'accueil aux contrats pour lesquels l'utilisateur dispose de permissions explicites.", + ) + generic_email = models.EmailField(blank=True, null=True) # Email générique de l'utilisateur + hide_intervention_map_by_default = models.BooleanField( + default=False, + verbose_name="Masquer la carte des interventions par défaut", + help_text="Affiche un bouton permettant d'afficher la carte au besoin.", + ) + INTERVENTION_LIST_LAYOUT_CHOICES = [ + ('both', 'Tableau et carte'), + ('table_only', 'Tableau seul'), + ('map_only', 'Carte seule'), + ] + intervention_list_layout = models.CharField( + max_length=20, + choices=INTERVENTION_LIST_LAYOUT_CHOICES, + default='both', + verbose_name="Disposition de la liste des interventions", + help_text="Préférence de disposition pour la vue liste des interventions.", + ) + intervention_list_filters = models.JSONField( + default=dict, + blank=True, + verbose_name="Filtres de la liste des interventions", + help_text="Préférences de filtrage pour la vue liste des interventions.", + ) + mobile_intervention_list_filters = models.JSONField( + default=dict, + blank=True, + verbose_name="Filtres de la liste des interventions mobile", + help_text="Préférences de filtrage pour la vue mobile de la liste des interventions.", + ) + intervention_list_sort = models.JSONField( + default=dict, + blank=True, + verbose_name="Tri de la liste des interventions", + help_text="Préférences de tri pour la vue liste des interventions.", + ) + intervention_list_columns = models.JSONField( + default=None, + null=True, + blank=True, + verbose_name="Colonnes masquées de la liste des interventions", + help_text="Liste des champs masqués dans le tableau de la vue liste des interventions.", + ) + observation_list_filters = models.JSONField( + default=dict, + blank=True, + verbose_name="Filtres de la liste des observations", + help_text="Préférences de filtrage pour la vue liste des observations.", + ) + observation_list_sort = models.JSONField( + default=dict, + blank=True, + verbose_name="Tri de la liste des observations", + help_text="Préférences de tri pour la vue liste des observations.", + ) + project_list_filters = models.JSONField( + default=dict, + blank=True, + verbose_name="Filtres de la liste des projets", + help_text="Préférences de filtrage pour la vue liste des projets.", + ) + project_list_sort = models.JSONField( + default=dict, + blank=True, + verbose_name="Tri de la liste des projets", + help_text="Préférences de tri pour la vue liste des projets.", + ) + ASSETS_LIST_LAYOUT_CHOICES = [ + ('both', 'Tableau et carte'), + ('table_only', 'Tableau seul'), + ('map_only', 'Carte seule'), + ] + assets_list_layout = models.CharField( + max_length=20, + choices=ASSETS_LIST_LAYOUT_CHOICES, + default='both', + verbose_name="Disposition de la liste des assets", + help_text="Préférence de disposition pour la vue liste des assets.", + ) + linked_configs = models.ManyToManyField( + "self", + blank=True, + symmetrical=False, + verbose_name="Comptes liés", + ) + auto_logout_hours = models.PositiveIntegerField( + null=True, + blank=True, + verbose_name="Déconnexion automatique (heures)", + help_text="Nombre d'heures après la connexion avant déconnexion automatique. Laisser vide pour désactiver.", + ) + # Generic view access permissions + accessible_views = models.ManyToManyField( + AppView, + blank=True, + verbose_name="Vues accessibles", + help_text="Les vues/applications auxquelles l'utilisateur a accès.", + related_name="user_configs", + ) + panoramax_can_upload = models.BooleanField( + default=False, + verbose_name="Peut uploader des photos 360", + help_text="Permet à l'utilisateur d'uploader de nouvelles photos ou vidéos 360 pour Panoramax." + ) + # Société à laquelle appartient cet utilisateur (ex: prestataire) + company = models.ForeignKey( + 'contracts.Company', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='user_configs', + verbose_name="Société", + help_text="Société/organisme auquel appartient cet utilisateur (utile pour filtrer les membres visibles).", + ) + # Document management permissions for external users + can_add_unique_tag_documents = models.BooleanField( + default=False, + verbose_name="Peut ajouter des documents avec tags uniques", + help_text="Permet aux utilisateurs externes d'ajouter/modifier des documents avec des tags uniques (CME, PIS, As-Built, etc.).", + ) + esign_can_edit = models.BooleanField( + default=False, + verbose_name="eSign : Peut éditer", + help_text="Permission pour éditer les panneaux et les poteaux.", + ) + esign_can_manage_rcp = models.BooleanField( + default=False, + verbose_name="eSign : Peut gérer les RCP", + help_text="Permission pour créer, modifier et supprimer les RCP.", + ) + esign_can_validate_rcp = models.BooleanField( + default=False, + verbose_name="eSign : Peut valider les RCP", + help_text="Permission pour valider les RCP.", + ) + esign_organizations = models.ManyToManyField( + "sign.Organization", + blank=True, + related_name="user_configs", + verbose_name="eSign : Gestionnaires", + help_text="Les gestionnaires (communes, Bruxelles Mobilité) pour lesquels l'utilisateur a des permissions." + ) + # Catégories d'assets concernées pour l'approbation des interventions + approval_categories = models.ManyToManyField( + 'assets.AssetCategory', + blank=True, + verbose_name="Catégories d'approbation", + help_text="Catégories d'assets pour lesquelles l'utilisateur approuve des interventions. Si vide, aucun filtre de catégorie n'est appliqué.", + related_name='approver_user_configs', + ) + # Préférences de filtrage du reporting (structurées par page/widget) + # Ex : {"dashboard": {"granularity": "week", "contract_ids": [], "thematic_ids": [], ...}} + reporting_preferences = models.JSONField( + default=dict, + blank=True, + verbose_name="Préférences du reporting", + help_text="Préférences de filtrage pour les différentes pages du reporting.", + ) + assets_index_preferences = models.JSONField( + default=dict, + blank=True, + verbose_name="Préférences de la page index unifiée", + help_text="Préférences de la page assets (thématiques actives, catégories, filtres, position carte).", + ) + calendar_preferences = models.JSONField( + default=dict, + blank=True, + null=True, + verbose_name="Préférences du calendrier", + help_text="Préférences de la page calendrier (mode d'affichage, zoom, filtres actifs, état d'expansion du backlog).", + ) + + def __str__(self): + return f"Configuration de {self.user.username}" + + def has_role(self, role_str): + return self.roles.filter(name=role_str).exists() + + @property + def can_approve_any_intervention(self): + """Détermine si l'utilisateur dispose de permissions d'approbation sur au moins un contrat ou un rôle supérieur.""" + if self.has_role('admin') or self.has_role('top_manager') or self.user.is_superuser: + return True + return self.usercontractaccess_set.filter(can_approve=True).exists() + + def can_access_view(self, view_code): + """Check if the user has access to a specific view by its code.""" + return self.accessible_views.filter(code=view_code).exists() + + def get_accessible_views(self): + """Returns all accessible views ordered by their display order.""" + return self.accessible_views.all().order_by('order') + + def can_view_warehouse(self, warehouse): + """Check if the user can view a specific warehouse.""" + if self.user.is_superuser or self.user.is_staff or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return self.userlocations.filter(warehouse_location__warehouse=warehouse).exists() + return True + return self.userwarehouses.filter(warehouse=warehouse, can_view=True).exists() + + def can_manage_warehouse(self, warehouse): + """Check if the user can manage a specific warehouse.""" + if self.user.is_superuser or self.user.is_staff or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return False + return True + return self.userwarehouses.filter(warehouse=warehouse, can_manage=True).exists() + + def can_manage_preparation_orders_any(self): + """Vérifie si l'utilisateur peut gérer des bons de préparation sur au moins un entrepôt.""" + if self.user.is_superuser or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return False + return True + return self.userwarehouses.filter(can_manage_preparation_orders=True).exists() + + def can_manage_preparation_orders_for(self, warehouse): + """Vérifie si l'utilisateur peut gérer des bons de préparation pour un entrepôt spécifique.""" + if self.user.is_superuser or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return False + return True + return self.userwarehouses.filter(warehouse=warehouse, can_manage_preparation_orders=True).exists() + + def can_manage_purchase_orders_any(self): + """Vérifie si l'utilisateur peut gérer des bons de commande sur au moins un entrepôt.""" + if self.user.is_superuser or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return False + return True + return self.userwarehouses.filter(can_manage_purchase_orders=True).exists() + + def can_manage_purchase_orders_for(self, warehouse): + """Vérifie si l'utilisateur peut gérer des bons de commande pour un entrepôt spécifique.""" + if self.user.is_superuser or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return False + return True + return self.userwarehouses.filter(warehouse=warehouse, can_manage_purchase_orders=True).exists() + + def can_validate_inventory_for(self, warehouse): + """Vérifie si l'utilisateur peut valider les inventaires d'un entrepôt.""" + if self.user.is_superuser or self.has_role('admin'): + return True + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + return False + return True + return self.userwarehouses.filter(warehouse=warehouse, can_validate_inventory=True).exists() + + def can_perform_inventory_on(self, location): + """Vérifie si l'utilisateur peut effectuer l'inventaire d'un emplacement spécifique.""" + if self.user.is_superuser or self.user.is_staff or self.has_role('admin'): + return True + if self.can_manage_warehouse(location.warehouse) or self.can_validate_inventory_for(location.warehouse): + return True + return self.userlocations.filter(warehouse_location=location, can_perform_inventory=True).exists() + + def can_validate_inventory_on(self, location): + """Vérifie si l'utilisateur peut valider les inventaires d'un emplacement spécifique.""" + if self.user.is_superuser or self.has_role('admin'): + return True + if self.can_validate_inventory_for(location.warehouse): + return True + return self.userlocations.filter(warehouse_location=location, can_validate_inventory=True).exists() + + def get_accessible_warehouses(self, can_manage=False): + """Returns all warehouses the user can access.""" + from stock.models import Warehouse + if self.user.is_superuser or self.user.is_staff or self.has_role('admin'): + return Warehouse.objects.all() + + if not self.userwarehouses.exists(): + if self.userlocations.exists(): + if can_manage: + from django.db.models import Q + return Warehouse.objects.filter( + Q(locations__user_accesses__user_config=self, locations__user_accesses__can_perform_inventory=True) | + Q(locations__user_accesses__user_config=self, locations__user_accesses__can_validate_inventory=True) + ).distinct() + return Warehouse.objects.filter( + locations__user_accesses__user_config=self + ).distinct() + return Warehouse.objects.all() + + if can_manage: + return Warehouse.objects.filter( + userwarehouseaccess__user_config=self, + userwarehouseaccess__can_manage=True + ).distinct() + + return Warehouse.objects.filter( + userwarehouseaccess__user_config=self, + userwarehouseaccess__can_view=True + ).distinct() + + def get_default_warehouse(self): + """Returns the user's default warehouse if set.""" + default_access = self.userwarehouses.filter(is_default=True).first() + return default_access.warehouse if default_access else None + + def can_administer_warehouse(self, warehouse): + """Check if the user can administer a specific warehouse.""" + if self.user.is_superuser or self.has_role('admin'): + return True + return self.userwarehouses.filter(warehouse=warehouse, can_administer=True).exists() + + def can_administer_any_warehouse(self): + """Check if the user can administer at least one warehouse.""" + if self.user.is_superuser or self.has_role('admin'): + return True + return self.userwarehouses.filter(can_administer=True).exists() + + + +class UserThematics(models.Model): + """Table intermédiaire pour gérer les thématiques d'un utilisateur""" + user_config = models.ForeignKey(UserConfig, on_delete=models.CASCADE, related_name="userthematics") + thematic = models.ForeignKey(Thematic, on_delete=models.CASCADE) + can_view_assets = models.BooleanField(default=True) # Peut voir les assets de la thématique + can_edit_assets = models.BooleanField(default=False) # Peut éditer les assets de la thématique + can_view_interventions = models.BooleanField(default=True) # Peut voir les interventions de la thématique + can_edit_interventions = models.BooleanField(default=False) # Peut éditer les interventions de la thématique + can_edit_locations = models.BooleanField(default=False) # Peut créer/modifier/supprimer les localisations de la thématique + can_process_observations = models.BooleanField(default=False, verbose_name="Peut traiter les observations") # Peut changer le statut des observations (traiter, rediriger, archiver, etc.) + can_view_projects = models.BooleanField(default=False, verbose_name="Peut voir les projets") # Peut voir les projets de la thématique + can_edit_projects = models.BooleanField(default=False, verbose_name="Peut éditer les projets") # Peut créer/modifier les projets de la thématique + can_validate_assets = models.BooleanField(default=False, verbose_name="Peut valider les assets") + + class Meta: + unique_together = ("user_config", "thematic") # Empêche les doublons + verbose_name = "Thématique utilisateur" + verbose_name_plural = "Thématiques utilisateur" + + def __str__(self): + return f"{self.user_config.user.username} - {self.thematic.name}" + + +# Statuts d'intervention (copie locale pour éviter import circulaire avec interventions) +INTERVENTION_STATUS_CHOICES = [ + ('in_preparation', 'En préparation'), + ('to_be_approved', 'À approuver'), + ('to_be_planned', 'À planifier'), + ('to_be_processed', 'À traiter'), + ('assigned', 'Pris en charge'), + ('in_progress', 'Démarré'), + ('on_pause', 'En pause'), + ('finished', 'Terminé'), + ('processed', 'Traité'), + ('to_be_corrected', 'À corriger'), + ('corrected', 'Corrigé'), + ('validated', 'Validé'), + ('invoiced', 'Comptabilisé'), + ('closed', 'Finalisé'), + ('canceled', 'Annulé'), +] + +# Ordre d'affichage des statuts pour les colonnes de la table admin +INTERVENTION_STATUS_ORDER = [ + 'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', + 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', + 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled', +] + + +class UserThematicStatusPermission(models.Model): + """Permissions par statut d'intervention pour un accès thématique utilisateur.""" + user_thematic = models.ForeignKey( + UserThematics, + on_delete=models.CASCADE, + related_name='status_permissions', + ) + status = models.CharField( + max_length=30, + choices=INTERVENTION_STATUS_CHOICES, + verbose_name="Statut", + ) + can_view = models.BooleanField( + default=False, + verbose_name="Voir", + help_text="Peut voir les interventions à ce statut.", + ) + can_edit = models.BooleanField( + default=False, + verbose_name="Modifier", + help_text="Peut modifier les interventions à ce statut.", + ) + can_change_status_to = models.BooleanField( + default=False, + verbose_name="Changer vers", + help_text="Peut déclencher une transition vers ce statut (selon le graphe global des transitions).", + ) + + class Meta: + unique_together = ('user_thematic', 'status') + verbose_name = "Permission par statut (thématique)" + verbose_name_plural = "Permissions par statut (thématique)" + indexes = [ + # Index couvrant can_view : permet un index-only scan dans les sous-requêtes EXISTS + models.Index(fields=['user_thematic', 'status', 'can_view'], name='utsp_status_canview_idx'), + ] + + def __str__(self): + return f"{self.user_thematic} – {self.status}" + + +class UserContractAccess(models.Model): + """Table intermédiaire pour gérer les droits d'accès d'un utilisateur à un contrat""" + user_config = models.ForeignKey(UserConfig, on_delete=models.CASCADE) + contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE) + can_view_posts = models.BooleanField(default=False) # Peut voir les postes du contrat + can_view_prices = models.BooleanField(default=False) # Peut voir les prix des postes + can_view_interventions = models.BooleanField(default=True) # Peut voir les interventions du contrat + can_view_assets = models.BooleanField(default=True) # Peut voir les assets du contrat + can_create_interventions = models.BooleanField( + default=False, + verbose_name="Peut créer des interventions", + help_text="Permet au prestataire de créer des interventions pour ce contrat." + ) # Permet aux prestataires de créer des interventions + can_check_interventions = models.BooleanField( + default=False, + verbose_name="Peut vérifier les interventions", + help_text="Permet de vérifier les interventions pour ce contrat." + ) # Permet de vérifier les interventions + can_approve = models.BooleanField( + default=False, + verbose_name="Peut approuver les interventions", + help_text="Permet de faire passer les interventions du statut 'à approuver' vers un autre statut pour ce contrat." + ) # Permet d'approuver les interventions (passage depuis 'to_be_approved') + + class Meta: + unique_together = ("user_config", "contract") # Empêche les doublons + + def __str__(self): + return f"{self.user_config.user.username} - {self.contract.contract_number}" + + +class UserContractStatusPermission(models.Model): + """Permissions par statut d'intervention pour un accès contrat utilisateur.""" + user_contract = models.ForeignKey( + UserContractAccess, + on_delete=models.CASCADE, + related_name='status_permissions', + ) + status = models.CharField( + max_length=30, + choices=INTERVENTION_STATUS_CHOICES, + verbose_name="Statut", + ) + can_view = models.BooleanField( + default=False, + verbose_name="Voir", + help_text="Peut voir les interventions à ce statut.", + ) + can_edit = models.BooleanField( + default=False, + verbose_name="Modifier", + help_text="Peut modifier les interventions à ce statut.", + ) + can_change_status_to = models.BooleanField( + default=False, + verbose_name="Changer vers", + help_text="Peut déclencher une transition vers ce statut (selon le graphe global des transitions).", + ) + + class Meta: + unique_together = ('user_contract', 'status') + verbose_name = "Permission par statut (contrat)" + verbose_name_plural = "Permissions par statut (contrat)" + indexes = [ + # Index couvrant can_view : permet un index-only scan dans les sous-requêtes EXISTS + models.Index(fields=['user_contract', 'status', 'can_view'], name='ucsp_status_canview_idx'), + ] + + def __str__(self): + return f"{self.user_contract} – {self.status}" + + +class UserWarehouseAccess(models.Model): + """Table intermédiaire pour gérer les droits d'accès d'un utilisateur à un entrepôt""" + user_config = models.ForeignKey(UserConfig, on_delete=models.CASCADE, related_name="userwarehouses") + warehouse = models.ForeignKey("stock.Warehouse", on_delete=models.CASCADE) + can_view = models.BooleanField(default=True, verbose_name="Peut visualiser") # Peut voir l'entrepôt + can_manage = models.BooleanField(default=False, verbose_name="Peut gérer") # Peut gérer l'entrepôt (ajouter/modifier stock, commandes, etc.) + can_administer = models.BooleanField(default=False, verbose_name="Peut administrer") # Peut administrer l'entrepôt (gérer les accès, config, etc.) + can_manage_preparation_orders = models.BooleanField(default=False, verbose_name="Peut gérer les bons de préparation") + can_manage_purchase_orders = models.BooleanField(default=False, verbose_name="Peut gérer les bons de commande") + can_validate_inventory = models.BooleanField(default=False, verbose_name="Peut valider les inventaires") + is_default = models.BooleanField(default=False, verbose_name="Entrepôt par défaut") # Entrepôt par défaut pour cet utilisateur + + class Meta: + unique_together = ("user_config", "warehouse") # Empêche les doublons + verbose_name = "Accès Entrepôt" + verbose_name_plural = "Accès Entrepôts" + + def __str__(self): + return f"{self.user_config.user.username} - {self.warehouse.name}" + + def save(self, *args, **kwargs): + # Si cet entrepôt est défini comme par défaut, retirer le flag des autres + if self.is_default: + UserWarehouseAccess.objects.filter( + user_config=self.user_config, + is_default=True + ).exclude(pk=self.pk).update(is_default=False) + super().save(*args, **kwargs) + + +class UserWarehouseLocationAccess(models.Model): + """Table intermédiaire pour gérer les droits d'accès d'un utilisateur à un emplacement d'entrepôt (camionnette, etc.)""" + user_config = models.ForeignKey(UserConfig, on_delete=models.CASCADE, related_name="userlocations") + warehouse_location = models.ForeignKey("stock.WarehouseLocation", on_delete=models.CASCADE, related_name="user_accesses", verbose_name="Emplacement") + can_perform_inventory = models.BooleanField(default=False, verbose_name="Peut faire l'inventaire") + can_validate_inventory = models.BooleanField(default=False, verbose_name="Peut valider l'inventaire") + + class Meta: + unique_together = ("user_config", "warehouse_location") + verbose_name = "Accès Emplacement" + verbose_name_plural = "Accès Emplacements" + + def __str__(self): + return f"{self.user_config.user.username} - {self.warehouse_location.name}" + + +class UserAssetAccess(models.Model): + """ + Permissions explicites par instance d'asset ou de location pour un utilisateur. + + Logique de priorité (identique aux contrats) : + - Si une ligne existe → ses valeurs (True OU False) sont DÉFINITIVES, sans fallback thématique. + - Si aucune ligne n'existe → on consulte AssetTypePermissionConfig.requires_explicit_permissions : + * True → l'utilisateur n'a PAS accès (droits thématiques insuffisants). + * False → fallback sur les droits thématiques habituels. + - Le rôle admin bypass toujours ces vérifications. + + Le champ editable_fields, s'il est renseigné (liste non vide), REMPLACE complètement + les champs éditables déterminés par le rôle. + Le champ visible_fields, s'il est renseigné (liste non vide), REMPLACE complètement + les champs visibles par défaut (masque les champs absents de la liste). + """ + user_config = models.ForeignKey( + UserConfig, + on_delete=models.CASCADE, + related_name='asset_accesses', + verbose_name="Configuration utilisateur", + ) + content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + verbose_name="Type d'objet", + help_text="Le modèle Django de l'asset ou de la location concernée.", + ) + object_id = models.PositiveIntegerField( + verbose_name="ID de l'objet", + ) + content_object = GenericForeignKey('content_type', 'object_id') + + can_view = models.BooleanField( + default=False, + verbose_name="Peut visualiser", + ) + can_edit = models.BooleanField( + default=False, + verbose_name="Peut modifier", + ) + can_delete = models.BooleanField( + default=False, + verbose_name="Peut supprimer", + ) + editable_fields = ArrayField( + models.CharField(max_length=200), + null=True, + blank=True, + verbose_name="Champs modifiables", + help_text=( + "Liste des noms de champs que cet utilisateur peut modifier sur cet objet. " + "Si renseignée (non vide), remplace complètement les champs autorisés par le rôle." + ), + ) + visible_fields = ArrayField( + models.CharField(max_length=200), + null=True, + blank=True, + verbose_name="Champs visibles", + help_text=( + "Liste des noms de champs que cet utilisateur peut voir sur cet objet. " + "Si renseignée (non vide), remplace complètement les champs affichés par défaut." + ), + ) + + class Meta: + unique_together = [('user_config', 'content_type', 'object_id')] + verbose_name = "Accès asset spécifique" + verbose_name_plural = "Accès assets spécifiques" + indexes = [ + models.Index(fields=['content_type', 'object_id']), + ] + + def __str__(self): + return ( + f"{self.user_config.user.username} → {self.content_type.model} #{self.object_id}" + ) + + +class UserAssetTypeAccess(models.Model): + """ + Permissions explicites pour un utilisateur sur TOUS les objets d'un type d'asset donné. + + Niveau intermédiaire entre UserAssetAccess (par instance) et les droits thématiques. + + Logique de priorité : + - Plus prioritaire que les droits thématiques et que requires_explicit_permissions. + - Moins prioritaire qu'une ligne UserAssetAccess portant sur l'instance précise. + + Le champ editable_fields, s'il est renseigné (liste non vide), remplace les champs + éditables déterminés par le rôle **pour tous les objets de ce type**, sauf si une + ligne UserAssetAccess spécifique à l'instance définit ses propres editable_fields. + Le champ visible_fields, s'il est renseigné (liste non vide), remplace les champs + visibles par défaut pour tous les objets de ce type. Peut être écrasé par instance. + """ + user_config = models.ForeignKey( + UserConfig, + on_delete=models.CASCADE, + related_name='asset_type_accesses', + verbose_name="Configuration utilisateur", + ) + content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + verbose_name="Type d'asset", + help_text="Le modèle Django concerné (s'applique à toutes ses instances).", + ) + + can_view = models.BooleanField( + default=False, + verbose_name="Peut visualiser", + ) + can_edit = models.BooleanField( + default=False, + verbose_name="Peut modifier", + ) + can_delete = models.BooleanField( + default=False, + verbose_name="Peut supprimer", + ) + editable_fields = ArrayField( + models.CharField(max_length=200), + null=True, + blank=True, + verbose_name="Champs modifiables", + help_text=( + "Liste des noms de champs modifiables pour tous les objets de ce type. " + "Si renseignée (non vide), remplace les champs autorisés par le rôle. " + "Peut être écrasée objet par objet via UserAssetAccess." + ), + ) + visible_fields = ArrayField( + models.CharField(max_length=200), + null=True, + blank=True, + verbose_name="Champs visibles", + help_text=( + "Liste des noms de champs visibles pour tous les objets de ce type. " + "Si renseignée (non vide), remplace les champs affichés par défaut. " + "Peut être écrasée objet par objet via UserAssetAccess." + ), + ) + + class Meta: + unique_together = [('user_config', 'content_type')] + verbose_name = "Accès par type d'asset" + verbose_name_plural = "Accès par type d'asset" + + def __str__(self): + return ( + f"{self.user_config.user.username} → {self.content_type.model} (tous les objets)" + ) + + +class AssetTypePermissionConfig(models.Model): + """ + Configuration des permissions au niveau d'un type d'asset (modèle Django). + + Lorsque requires_explicit_permissions=True, les droits thématiques ne sont pas + suffisants pour accéder aux instances de ce type : chaque utilisateur doit avoir + une ligne UserAssetAccess ou UserAssetTypeAccess explicite. + """ + content_type = models.OneToOneField( + ContentType, + on_delete=models.CASCADE, + related_name='asset_permission_config', + verbose_name="Type d'asset", + ) + requires_explicit_permissions = models.BooleanField( + default=False, + verbose_name="Droits explicites requis", + help_text=( + "Si coché, les droits thématiques seuls ne suffisent pas. " + "Chaque utilisateur doit avoir une entrée explicite dans " + "« Accès assets spécifiques » pour ce type d'objet." + ), + ) + + class Meta: + verbose_name = "Config permissions par type d'asset" + verbose_name_plural = "Configs permissions par type d'asset" + + def __str__(self): + flag = "⚠ droits explicites requis" if self.requires_explicit_permissions else "droits thématiques suffisants" + return f"{self.content_type.model} — {flag}" + + +class UserApprovalConfig(UserConfig): + """Proxy model de UserConfig dédié à la gestion des approbateurs d'interventions.""" + + class Meta: + proxy = True + verbose_name = "Configuration approbateur" + verbose_name_plural = "Configurations approbateurs" + ordering = ['user__username'] + + + +DOCUMENT_STATUS_CHOICES = { + 'active': 'Actif', + 'archived': 'Archivé', +} + +class Document(models.Model): + uploaded_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="Ajouté par", + ) + description = models.TextField(blank=True,null=True,verbose_name="Description du document") + upload_date = models.DateTimeField(auto_now=True,verbose_name="Date de téléversement") + status = models.CharField(choices=DOCUMENT_STATUS_CHOICES, default='active') + managed_document = models.ForeignKey( + "documents.ManagedDocument", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="%(app_label)s_%(class)s_sources", + ) + + @property + def filename(self): + return os.path.basename(self.file.name) + + class Meta: + abstract = True + ordering = ["upload_date"] + + def __str__(self): + return f"{self.file.name}" + + + DOCUMENT_MANAGEMENT_SYNC = False # Désactive la synchronisation par défaut + + def get_document_context(self): + for field in self._meta.fields: + if not field.is_relation or not field.many_to_one: + continue + if field.name in {"uploaded_by", "managed_document"}: + continue + value = getattr(self, field.name, None) + if value is not None: + return value + return None + + + def get_document_title(self): + return getattr(self, "description", None) or self.filename + + + def generate_thumbnail(self): + """Générer une vignette pour les fichiers PDF.""" + if self.file.name.endswith(".pdf"): + try: + pdf_path = self.file.path + images = convert_from_path(pdf_path, first_page=1, last_page=1, size=(200, 200)) + thumbnail_path = f"{self.file.path}_thumbnail.jpg" + images[0].save(thumbnail_path, "JPEG") + self.thumbnail.name = f"{self.file.name}_thumbnail.jpg" + self.save() + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error generating PDF thumbnail for {self.file.name}: {e}") + else: + try: + with Image.open(self.file.path) as img: + # Conversion en mode RGB si nécessaire (évite les erreurs avec le format JPEG) + if img.mode in ("RGBA", "P"): # "P" correspond aux images en palette (GIF) + img = img.convert("RGB") + + img.thumbnail((200, 200)) + thumbnail_path = f"{self.file.path}_thumbnail.jpg" + img.save(thumbnail_path, "JPEG") + self.thumbnail.name = f"{self.file.name}_thumbnail.jpg" + self.save() + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error generating image thumbnail for {self.file.name}: {e}") + + + def save(self, *args, **kwargs): + is_update = self.pk is not None + old_file_name = None + if is_update: + old_file_name = ( + self.__class__.objects.filter(pk=self.pk) + .values_list("file", flat=True) + .first() + ) + + super().save(*args, **kwargs) # Save the file first + + if self.file: # Vérifier qu'un fichier est bien présent + try: + with Image.open(self.file.path) as img: + # Vérifier et corriger l'orientation EXIF si nécessaire + try: + exif = img._getexif() + if exif: + for tag, value in exif.items(): + if tag in ExifTags.TAGS and ExifTags.TAGS[tag] == "Orientation": + if value == 3: + img = img.rotate(180, expand=True) + elif value == 6: + img = img.rotate(270, expand=True) + elif value == 8: + img = img.rotate(90, expand=True) + except AttributeError: + pass # Pas d'EXIF, on ignore l'orientation + + # Sauvegarde de l'image corrigée en écrasant l'originale + img.save(self.file.path, format=img.format) + except Exception: + pass # Fichier non-image (PDF, etc.) + + if not self.thumbnail: + self.generate_thumbnail() + + + if not getattr(self, "DOCUMENT_MANAGEMENT_SYNC", False): + return + + try: + from documents.models import ensure_document_for_legacy + + ensure_document_for_legacy(self, old_file_name=old_file_name) + except Exception as exc: + print(f"Error while synchronising document management for {self}: {exc}") + + + + +class Municipality(models.Model): + fid = models.CharField(max_length=100) + name_fr = models.CharField(max_length=255, verbose_name="Name FR") + name_nl = models.CharField(max_length=255, verbose_name="Name NL") + niscode = models.CharField(max_length=30, verbose_name="Postal code", null=True, blank=True) + geom = models.MultiPolygonField(srid=3812, null=True, blank=True) + + @property + def name(self): + """Renvoie le nom dans la langue active.""" + lang = get_language() + if lang == 'nl': + return self.name_nl + return self.name_fr + + objects = LanguageManager() + + def __str__(self): + return self.name + + +class AdrStreet(models.Model): + fid = models.CharField(max_length=100, unique=True) + name_fr = models.CharField(max_length=255, verbose_name="Nom") + name_nl = models.CharField(max_length=255, verbose_name="Naam") + municipality = models.ForeignKey(Municipality, on_delete=models.CASCADE, related_name="street_names", null=True, blank=True) + full_street_fr = models.CharField(max_length=2048, verbose_name="Nom complet FR", null=True, blank=True) + full_street_nl = models.CharField(max_length=2048, verbose_name="Nom complet NL", null=True, blank=True) + centroid = models.PointField(srid=3812, null=True, blank=True) + lon = models.FloatField(null=True, blank=True) + lat = models.FloatField(null=True, blank=True) + geom = models.MultiPolygonField(srid=3812, null=True, blank=True) + + @property + def name(self): + """Renvoie le nom dans la langue active.""" + lang = get_language() + if lang == 'nl': + street_name = self.name_nl + else: + street_name = self.name_fr + + return f"{street_name} ({self.municipality.name})" if self.municipality else street_name + + def get_name(self): + """Compatibilité avec les appels get_name()""" + return self.name + + objects = LanguageManager() + + def __str__(self): + return self.name + + + +class AdrPoint(models.Model): + fid = models.CharField(max_length=100, unique=True) + number = models.CharField(max_length=255, verbose_name="Numéro") + street_name = models.ForeignKey(AdrStreet, on_delete=models.CASCADE, related_name="addresses", null=True, blank=True) + zipcode = models.CharField(max_length=255, verbose_name="Code postal", null=True, blank=True) + municipality = models.ForeignKey(Municipality, on_delete=models.SET_NULL, related_name="addresses", null=True, blank=True) + full_address_fr = models.CharField(max_length=2048, verbose_name="Adresse complète FR", null=True, blank=True) + full_address_nl = models.CharField(max_length=2048, verbose_name="Adresse complète NL", null=True, blank=True) + geom = models.PointField(srid=3812, null=True, blank=True) + lon = models.FloatField(null=True, blank=True) + lat = models.FloatField(null=True, blank=True) + + def __str__(self): + if self.street_name: + return f"{self.number} {self.street_name.name} ({self.municipality.name})" if self.municipality else f"{self.number} {self.street_name.name}" + return f"{self.number} ({self.municipality.name})" if self.municipality else self.number + + +LOG_LEVEL_CHOICES = { + 'DEBUG': 'Debug', + 'INFO': 'Info', + 'WARNING': 'Warning', + 'ERROR': 'Error', + 'CRITICAL': 'Critical', +} + + +class RequestLog(models.Model): + """ + Modèle pour stocker les logs des requêtes et erreurs de l'application. + Permet de visualiser les erreurs dans l'admin Django. + """ + timestamp = models.DateTimeField(auto_now_add=True, verbose_name="Date et heure", db_index=True) + level = models.CharField(max_length=10, choices=LOG_LEVEL_CHOICES, default='ERROR', verbose_name="Niveau") + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="Utilisateur", + related_name="request_logs" + ) + username = models.CharField(max_length=150, blank=True, null=True, verbose_name="Nom d'utilisateur") + method = models.CharField(max_length=10, blank=True, null=True, verbose_name="Méthode HTTP") + path = models.CharField(max_length=2048, verbose_name="URL") + full_url = models.CharField(max_length=4096, blank=True, null=True, verbose_name="URL complète") + status_code = models.IntegerField(null=True, blank=True, verbose_name="Code HTTP") + ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name="Adresse IP") + user_agent = models.TextField(blank=True, null=True, verbose_name="User Agent") + referer = models.CharField(max_length=2048, blank=True, null=True, verbose_name="Referer") + exception_type = models.CharField(max_length=255, blank=True, null=True, verbose_name="Type d'exception") + exception_message = models.TextField(blank=True, null=True, verbose_name="Message d'erreur") + traceback = models.TextField(blank=True, null=True, verbose_name="Traceback") + request_data = models.TextField(blank=True, null=True, verbose_name="Données de la requête") + + class Meta: + verbose_name = "Log de requête" + verbose_name_plural = "Logs de requêtes" + ordering = ["-timestamp"] + indexes = [ + models.Index(fields=['level']), + models.Index(fields=['user']), + models.Index(fields=['status_code']), + models.Index(fields=['timestamp', 'level']), + ] + + def __str__(self): + return f"[{self.level}] {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')} - {self.path}" + + @classmethod + def log_request(cls, request, exception=None, level='ERROR', status_code=None, traceback_str=None): + """ + Crée une entrée de log à partir d'une requête HTTP. + """ + import json + + # Récupérer l'adresse IP + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip_address = x_forwarded_for.split(',')[0].strip() + else: + ip_address = request.META.get('REMOTE_ADDR') + + # Récupérer les données de la requête (sans données sensibles) + request_data = None + try: + if request.method in ['POST', 'PUT', 'PATCH']: + data = dict(request.POST) + # Masquer les mots de passe et autres données sensibles + sensitive_keys = ['password', 'password1', 'password2', 'token', 'secret', 'csrfmiddlewaretoken'] + for key in sensitive_keys: + if key in data: + data[key] = ['***MASKED***'] + request_data = json.dumps(data, default=str) + except Exception: + pass + + # Informations sur l'exception + exception_type = None + exception_message = None + if exception: + exception_type = type(exception).__name__ + exception_message = str(exception) + + # Créer le log + log_entry = cls.objects.create( + level=level, + user=request.user if request.user.is_authenticated else None, + username=request.user.username if request.user.is_authenticated else None, + method=request.method, + path=request.path, + full_url=request.build_absolute_uri(), + status_code=status_code, + ip_address=ip_address, + user_agent=request.META.get('HTTP_USER_AGENT', '')[:2000] if request.META.get('HTTP_USER_AGENT') else None, + referer=request.META.get('HTTP_REFERER', '')[:2048] if request.META.get('HTTP_REFERER') else None, + exception_type=exception_type, + exception_message=exception_message, + traceback=traceback_str, + request_data=request_data, + ) + return log_entry + + +# Import des modèles de tâches planifiées +from .models_scheduled_tasks import ScheduledCommand, CommandExecutionLog diff --git a/streetup/common/models_scheduled_tasks.py b/streetup/common/models_scheduled_tasks.py new file mode 100644 index 0000000..573e55a --- /dev/null +++ b/streetup/common/models_scheduled_tasks.py @@ -0,0 +1,289 @@ +""" +Modèles pour la gestion des tâches planifiées. +""" + +from django.db import models +from django.conf import settings +from django.utils import timezone +from django.core.validators import MinValueValidator, MaxValueValidator +import json + + +class ScheduledCommand(models.Model): + """ + Commande Django planifiée pour exécution automatique. + """ + FREQUENCY_CHOICES = [ + ('hourly', 'Toutes les heures'), + ('daily', 'Quotidien'), + ('weekly', 'Hebdomadaire'), + ('monthly', 'Mensuel'), + ] + + WEEKDAY_CHOICES = [ + (0, 'Lundi'), + (1, 'Mardi'), + (2, 'Mercredi'), + (3, 'Jeudi'), + (4, 'Vendredi'), + (5, 'Samedi'), + (6, 'Dimanche'), + ] + + name = models.CharField( + max_length=200, + unique=True, + verbose_name="Nom de la tâche", + help_text="Nom descriptif de la tâche planifiée" + ) + + command_name = models.CharField( + max_length=200, + verbose_name="Nom de la commande", + help_text="Nom de la commande Django (ex: export_interventions_xlsx)" + ) + + command_args = models.JSONField( + default=dict, + blank=True, + verbose_name="Arguments de la commande", + help_text="Arguments de la commande au format JSON (ex: {\"output\": \"file.xlsx\"})" + ) + + frequency = models.CharField( + max_length=20, + choices=FREQUENCY_CHOICES, + default='daily', + verbose_name="Fréquence" + ) + + hour = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(23)], + default=0, + verbose_name="Heure", + help_text="Heure d'exécution (0-23)" + ) + + minute = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(59)], + default=0, + verbose_name="Minute", + help_text="Minute d'exécution (0-59)" + ) + + day_of_month = models.IntegerField( + null=True, + blank=True, + validators=[MinValueValidator(1), MaxValueValidator(31)], + verbose_name="Jour du mois", + help_text="Pour les tâches mensuelles : jour du mois (1-31)" + ) + + day_of_week = models.IntegerField( + null=True, + blank=True, + choices=WEEKDAY_CHOICES, + verbose_name="Jour de la semaine", + help_text="Pour les tâches hebdomadaires : 0=Lundi, 6=Dimanche" + ) + + is_active = models.BooleanField( + default=True, + verbose_name="Active", + help_text="Si décoché, la tâche ne sera pas exécutée" + ) + + send_email = models.BooleanField( + default=False, + verbose_name="Envoyer un email", + help_text="Envoyer un email après l'exécution" + ) + + email_recipients = models.TextField( + blank=True, + verbose_name="Destinataires", + help_text="Adresses email séparées par des virgules" + ) + + email_subject = models.CharField( + max_length=200, + blank=True, + verbose_name="Sujet de l'email" + ) + + last_run = models.DateTimeField( + null=True, + blank=True, + verbose_name="Dernière exécution" + ) + + next_run = models.DateTimeField( + null=True, + blank=True, + verbose_name="Prochaine exécution" + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Créé le" + ) + + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="created_scheduled_commands", + verbose_name="Créé par" + ) + + description = models.TextField( + blank=True, + verbose_name="Description" + ) + + class Meta: + verbose_name = "Commande planifiée" + verbose_name_plural = "Commandes planifiées" + ordering = ['name'] + + def __str__(self): + return f"{self.name} ({self.get_frequency_display()})" + + def should_run_now(self): + """ + Vérifie si la commande doit être exécutée maintenant. + """ + if not self.is_active: + return False + + now = timezone.now() + + # Si jamais exécutée ou si next_run est dépassé + if self.next_run is None or now >= self.next_run: + return True + + return False + + def calculate_next_run(self, from_time=None): + """ + Calcule la prochaine date d'exécution. + """ + if from_time is None: + from_time = timezone.now() + + next_run = from_time.replace(hour=self.hour, minute=self.minute, second=0, microsecond=0) + + if self.frequency == 'hourly': + # Prochaine heure + if next_run <= from_time: + next_run = from_time.replace(minute=self.minute, second=0, microsecond=0) + if next_run <= from_time: + next_run = (next_run + timezone.timedelta(hours=1)) + + elif self.frequency == 'daily': + # Lendemain si l'heure est dépassée aujourd'hui + if next_run <= from_time: + next_run += timezone.timedelta(days=1) + + elif self.frequency == 'weekly': + # Prochain jour de la semaine spécifié + if self.day_of_week is not None: + days_ahead = self.day_of_week - next_run.weekday() + if days_ahead <= 0 or (days_ahead == 0 and next_run <= from_time): + days_ahead += 7 + next_run += timezone.timedelta(days=days_ahead) + + elif self.frequency == 'monthly': + # Prochain jour du mois spécifié + if self.day_of_month is not None: + next_run = next_run.replace(day=min(self.day_of_month, 28)) # Sécurité pour éviter les erreurs + if next_run <= from_time: + # Mois suivant + if next_run.month == 12: + next_run = next_run.replace(year=next_run.year + 1, month=1) + else: + next_run = next_run.replace(month=next_run.month + 1) + + return next_run + + def get_command_args_display(self): + """Retourne les arguments sous forme lisible.""" + if not self.command_args: + return "Aucun argument" + try: + return json.dumps(self.command_args, indent=2) + except: + return str(self.command_args) + + +class CommandExecutionLog(models.Model): + """ + Historique d'exécution des commandes planifiées. + """ + STATUS_CHOICES = [ + ('running', 'En cours'), + ('success', 'Succès'), + ('error', 'Erreur'), + ] + + scheduled_command = models.ForeignKey( + ScheduledCommand, + on_delete=models.CASCADE, + related_name="execution_logs", + verbose_name="Commande planifiée" + ) + + started_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Démarré à" + ) + + finished_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Terminé à" + ) + + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='running', + verbose_name="Statut" + ) + + output = models.TextField( + blank=True, + verbose_name="Sortie" + ) + + error_message = models.TextField( + blank=True, + verbose_name="Message d'erreur" + ) + + duration_seconds = models.FloatField( + null=True, + blank=True, + verbose_name="Durée (secondes)" + ) + + email_sent = models.BooleanField( + default=False, + verbose_name="Email envoyé" + ) + + class Meta: + verbose_name = "Journal d'exécution" + verbose_name_plural = "Journaux d'exécution" + ordering = ['-started_at'] + + def __str__(self): + return f"{self.scheduled_command.name} - {self.started_at.strftime('%Y-%m-%d %H:%M')} - {self.get_status_display()}" + + def calculate_duration(self): + """Calcule la durée d'exécution.""" + if self.finished_at and self.started_at: + delta = self.finished_at - self.started_at + self.duration_seconds = delta.total_seconds() + self.save(update_fields=['duration_seconds']) diff --git a/streetup/common/private_files/helpers.py b/streetup/common/private_files/helpers.py new file mode 100644 index 0000000..4aafca9 --- /dev/null +++ b/streetup/common/private_files/helpers.py @@ -0,0 +1,16 @@ +# common/private_files/helpers.py +from django.urls import reverse +from django.core import signing + + +def build_private_url(app_label: str, model_name: str, pk: int | str, field_name: str, *, sign: bool = True) -> str: + params = f"?app={app_label}&model={model_name}&pk={pk}&field={field_name}" + + if sign: + token = signing.dumps( + {"app": app_label, "model": model_name, "pk": pk, "field": field_name}, + salt="private-files", + compress=True, + ) + params += f"&token={token}" + return reverse("private_file_download") + params diff --git a/streetup/common/private_files/registry.py b/streetup/common/private_files/registry.py new file mode 100644 index 0000000..60ba7c8 --- /dev/null +++ b/streetup/common/private_files/registry.py @@ -0,0 +1,13 @@ +# common/private_files/registry.py +from typing import Callable, Tuple, Dict + +Guard = Callable[[object, "django.contrib.auth.models.User"], bool] + +_REGISTRY: Dict[Tuple[str, str, str], Guard] = {} # (app_label, model_name, field_name) -> guard + +def register(app_label: str, model_name: str, field_name: str, guard: Guard): + key = (app_label.lower(), model_name.lower(), field_name) + _REGISTRY[key] = guard + +def get_guard(app_label: str, model_name: str, field_name: str) -> Guard | None: + return _REGISTRY.get((app_label.lower(), model_name.lower(), field_name)) diff --git a/streetup/common/private_files/views.py b/streetup/common/private_files/views.py new file mode 100644 index 0000000..6a3fa3b --- /dev/null +++ b/streetup/common/private_files/views.py @@ -0,0 +1,68 @@ +# common/private_files/views.py +import os +from django.apps import apps +from django.http import FileResponse, Http404, HttpResponseBadRequest +from django.contrib.auth.decorators import login_required +from django.core import signing +from django.core.signing import BadSignature, SignatureExpired +from django.utils.encoding import smart_str +from django.utils.translation import gettext as _ + +from django.conf import settings +from .registry import get_guard + +_TOKEN_MAX_AGE = getattr(settings, "PRIVATE_FILES_TOKEN_MAX_AGE", 14400) + + + +@login_required +def private_file_download(request): + """ + Accès générique: app=&model=&pk=&field=&token= + """ + app_label = request.GET.get("app") + model_name = request.GET.get("model") + pk = request.GET.get("pk") + field_name = request.GET.get("field") + token = request.GET.get("token") + + if not all([app_label, model_name, pk, field_name]): + return HttpResponseBadRequest("Missing params") + + # Vérifie le token signé (facultatif si tu ne veux pas signer) + if token: + try: + signing.loads(token, salt="private-files", max_age=_TOKEN_MAX_AGE) + except (BadSignature, SignatureExpired): + raise Http404(_("Lien expiré ou invalide")) + + # Récupère le modèle et l’objet + try: + model = apps.get_model(app_label, model_name) + except LookupError: + raise Http404(_("Modèle inconnu")) + + obj = model._default_manager.select_related().filter(pk=pk).first() + if not obj: + raise Http404(_("Objet introuvable")) + + # Récupère le guard + guard = get_guard(app_label, model_name, field_name) + if guard is None: + # Par défaut, refuse si pas de guard enregistré (sécurité par défaut) + raise Http404(_("Fichier non exposé")) + + # Vérif d’accès + if not guard(obj, request.user): + from django.core.exceptions import PermissionDenied + raise PermissionDenied(_("Accès refusé")) + + # Ouvre le FileField + f = getattr(obj, field_name, None) + if not f or not getattr(f, "storage", None) or not f.name or not f.storage.exists(f.name): + raise Http404(_("Fichier introuvable")) + + resp = FileResponse(f.open("rb")) + resp["Content-Disposition"] = f'inline; filename="{smart_str(os.path.basename(f.name))}"' + resp['X-Frame-Options'] = 'ALLOWALL' # Permet l’affichage dans un iframe + return resp diff --git a/streetup/common/static/common/SPRB-GOB-FR-NL-LOGO-RGB.png b/streetup/common/static/common/SPRB-GOB-FR-NL-LOGO-RGB.png new file mode 100644 index 0000000..2245cc1 Binary files /dev/null and b/streetup/common/static/common/SPRB-GOB-FR-NL-LOGO-RGB.png differ diff --git a/streetup/common/static/common/address_search_control.js b/streetup/common/static/common/address_search_control.js new file mode 100644 index 0000000..fba4f82 --- /dev/null +++ b/streetup/common/static/common/address_search_control.js @@ -0,0 +1,405 @@ +/** + * AddressSearchControl – MapLibre GL custom control + * + * A reusable address-search control that queries the internal + * `autocomplete_street` endpoint and places a draggable marker + * on the selected address. + * + * Follows the same patterns as base_map.js (EventTarget) and + * layer-switcher-control.js (IControl interface + IIFE). + * + * Usage + * ----- + * // 1. Load after maplibre-gl.js and base_map.js: + * // + * + * // 2. Instantiate and add to a map: + * const ctrl = new AddressSearchControl({ + * autocompleteUrl : '/common/autocomplete-street/', + * placeholder : 'Rechercher une adresse…', + * lonInputId : 'id_lon', // optional – auto-syncs a form input + * latInputId : 'id_lat', // optional – auto-syncs a form input + * }); + * map.addControl(ctrl, 'top-left'); + * + * // 3. Listen to custom events (the control extends EventTarget): + * ctrl.addEventListener('address-selected', e => console.log(e.detail)); + * ctrl.addEventListener('marker-placed', e => console.log(e.detail)); + * ctrl.addEventListener('marker-moved', e => console.log(e.detail)); + * ctrl.addEventListener('marker-cleared', () => { ... }); + * + * // 4. Public methods: + * ctrl.setMarker(lon, lat, fly = true); // place marker programmatically + * ctrl.clearMarker(); // remove marker + clear inputs + * + * Events dispatched + * ----------------- + * address-selected { lon, lat, label } – user picked a result + * marker-placed { lon, lat } – marker placed / moved by click + * marker-moved { lon, lat } – marker dragged by user + * marker-cleared – marker removed + * + * @version 1.0.0 + * @requires maplibre-gl + */ + +(function (window) { + 'use strict'; + + /* ── Injected CSS (once per page) ─────────────────────────────────── */ + const _CSS_ID = 'asc-ctrl-styles'; + const _CSS = ` + .asc-container { + background : #fff; + border-radius : 4px; + box-shadow : 0 0 0 2px rgba(0,0,0,.1); + padding : 4px; + min-width : 220px; + position : relative; + } + .asc-input-row { + display : flex; + align-items : center; + gap : 4px; + } + .asc-input { + flex : 1; + min-width : 0; + border : 1px solid #ccc; + border-radius : 4px; + padding : 4px 8px; + font-size : 13px; + line-height : 1.4; + outline : none; + transition : border-color .15s; + } + .asc-input:focus { border-color : #0d6efd; } + .asc-clear-btn { + flex-shrink : 0; + background : none; + border : none; + cursor : pointer; + color : #6c757d; + font-size : 14px; + line-height : 1; + padding : 2px 4px; + border-radius : 3px; + display : none; + } + .asc-clear-btn:hover { color : #212529; background : #f0f0f0; } + .asc-clear-btn.visible { display : block; } + .asc-results { + position : absolute; + left : 0; + right : 0; + top : calc(100% + 4px); + background : #fff; + border : 1px solid #dee2e6; + border-radius : 4px; + box-shadow : 0 4px 12px rgba(0,0,0,.12); + max-height : 220px; + overflow-y : auto; + z-index : 9999; + display : none; + } + .asc-results.open { display : block; } + .asc-result-item { + padding : 6px 10px; + font-size : 13px; + cursor : pointer; + border-bottom : 1px solid #f0f0f0; + white-space : nowrap; + overflow : hidden; + text-overflow : ellipsis; + } + .asc-result-item:last-child { border-bottom : none; } + .asc-result-item:hover { background : #eef2ff; } + .asc-no-results { + padding : 8px 10px; + font-size : 13px; + color : #6c757d; + } + .asc-loading { + padding : 8px 10px; + font-size : 12px; + color : #999; + font-style : italic; + } + `; + + /* ── AddressSearchControl ─────────────────────────────────────────── */ + + class AddressSearchControl extends EventTarget { + + /** + * @param {object} opts + * @param {string} opts.autocompleteUrl URL of autocomplete_street endpoint (required) + * @param {string} [opts.placeholder] Input placeholder text + * @param {number} [opts.minChars=2] Minimum characters before searching + * @param {number} [opts.debounce=250] Debounce delay (ms) + * @param {string} [opts.markerColor] Hex colour for the marker (default: #0d6efd) + * @param {boolean} [opts.markerDraggable] Whether the marker can be dragged (default: true) + * @param {string} [opts.lonInputId] DOM id of a hidden lon form field to auto-sync + * @param {string} [opts.latInputId] DOM id of a hidden lat form field to auto-sync + */ + constructor(opts = {}) { + super(); + this._opts = Object.assign({ + placeholder : 'Rechercher une adresse…', + minChars : 2, + debounce : 250, + markerColor : '#0d6efd', + markerDraggable: true, + lonInputId : null, + latInputId : null, + }, opts); + + if (!this._opts.autocompleteUrl) { + console.error('[AddressSearchControl] autocompleteUrl option is required.'); + } + + this._map = null; + this._marker = null; + this._container = null; + this._input = null; + this._results = null; + this._clearBtn = null; + this._timer = null; + } + + /* ── MapLibre IControl interface ────────────────────────────── */ + + onAdd(map) { + this._map = map; + this._injectCSS(); + this._container = this._buildDOM(); + return this._container; + } + + onRemove() { + this.clearMarker(); + this._container?.remove(); + this._map = null; + } + + /* ── Public API ─────────────────────────────────────────────── */ + + /** + * Programmatically place the marker and optionally fly to it. + * @param {number} lon + * @param {number} lat + * @param {boolean} [fly=true] + */ + setMarker(lon, lat, fly = true) { + this._placeMarker(lon, lat, fly); + } + + /** + * Remove the marker and clear any synced form inputs. + */ + clearMarker() { + if (this._marker) { + this._marker.remove(); + this._marker = null; + } + this._syncInputs(null, null); + this.dispatchEvent(new CustomEvent('marker-cleared')); + } + + /* ── Private ────────────────────────────────────────────────── */ + + _injectCSS() { + if (document.getElementById(_CSS_ID)) return; + const style = document.createElement('style'); + style.id = _CSS_ID; + style.textContent = _CSS; + document.head.appendChild(style); + } + + _buildDOM() { + /* Outer wrapper */ + const container = document.createElement('div'); + container.className = 'maplibregl-ctrl asc-container'; + + /* Input row */ + const row = document.createElement('div'); + row.className = 'asc-input-row'; + + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'asc-input'; + input.placeholder = this._opts.placeholder; + input.autocomplete = 'off'; + input.spellcheck = false; + this._input = input; + + const clearBtn = document.createElement('button'); + clearBtn.type = 'button'; + clearBtn.className = 'asc-clear-btn'; + clearBtn.innerHTML = '✕'; + clearBtn.title = 'Effacer'; + this._clearBtn = clearBtn; + + row.append(input, clearBtn); + + /* Dropdown results */ + const results = document.createElement('div'); + results.className = 'asc-results'; + this._results = results; + + container.append(row, results); + this._bindEvents(); + return container; + } + + _bindEvents() { + const { _input: input, _clearBtn: clearBtn, _results: results } = this; + + /* Typing */ + input.addEventListener('input', () => { + const q = input.value.trim(); + clearBtn.classList.toggle('visible', q.length > 0); + clearTimeout(this._timer); + + if (q.length < this._opts.minChars) { + this._closeResults(); + return; + } + + this._showLoading(); + this._timer = setTimeout(() => this._search(q), this._opts.debounce); + }); + + /* Keyboard navigation */ + input.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { input.blur(); this._closeResults(); } + }); + + /* Close on blur (allow click on result first) */ + input.addEventListener('blur', () => { + setTimeout(() => this._closeResults(), 180); + }); + + /* Clear search text + marker */ + clearBtn.addEventListener('click', () => { + input.value = ''; + clearBtn.classList.remove('visible'); + this._closeResults(); + this.clearMarker(); + input.focus(); + }); + + /* Result selection */ + results.addEventListener('click', (e) => { + const item = e.target.closest('[data-lon]'); + if (!item) return; + + const lon = parseFloat(item.dataset.lon); + const lat = parseFloat(item.dataset.lat); + const label = item.dataset.label || ''; + + input.value = label; + clearBtn.classList.add('visible'); + this._closeResults(); + + this._placeMarker(lon, lat, true); + this.dispatchEvent(new CustomEvent('address-selected', { + detail: { lon, lat, label } + })); + }); + } + + async _search(q) { + const url = `${this._opts.autocompleteUrl}?q=${encodeURIComponent(q)}`; + try { + const resp = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); + if (!resp.ok) { this._closeResults(); return; } + + const items = await resp.json(); + this._renderResults(items); + } catch { + this._closeResults(); + } + } + + _renderResults(items) { + const r = this._results; + if (!Array.isArray(items) || !items.length) { + r.innerHTML = '
Aucun résultat
'; + r.classList.add('open'); + return; + } + r.innerHTML = items.map(it => { + const label = (it.label || '').replace(/"/g, '"'); + return `
+ ${it.label} +
`; + }).join(''); + r.classList.add('open'); + } + + _showLoading() { + const r = this._results; + r.innerHTML = '
Recherche…
'; + r.classList.add('open'); + } + + _closeResults() { + this._results.classList.remove('open'); + this._results.innerHTML = ''; + } + + _placeMarker(lon, lat, fly = true) { + if (!this._map) return; + + if (this._marker) this._marker.remove(); + + this._marker = new maplibregl.Marker({ + color : this._opts.markerColor, + draggable : this._opts.markerDraggable, + }) + .setLngLat([lon, lat]) + .addTo(this._map); + + if (this._opts.markerDraggable) { + this._marker.on('dragend', () => { + const ll = this._marker.getLngLat(); + this._syncInputs(ll.lng, ll.lat); + this.dispatchEvent(new CustomEvent('marker-moved', { + detail: { lon: ll.lng, lat: ll.lat } + })); + }); + } + + this._syncInputs(lon, lat); + + if (fly) { + this._map.flyTo({ + center : [lon, lat], + zoom : Math.max(this._map.getZoom(), 17), + duration : 800, + }); + } + + this.dispatchEvent(new CustomEvent('marker-placed', { detail: { lon, lat } })); + } + + _syncInputs(lon, lat) { + const lonEl = this._opts.lonInputId + ? document.getElementById(this._opts.lonInputId) : null; + const latEl = this._opts.latInputId + ? document.getElementById(this._opts.latInputId) : null; + + if (lonEl) lonEl.value = lon !== null ? Number(lon).toFixed(6) : ''; + if (latEl) latEl.value = lat !== null ? Number(lat).toFixed(6) : ''; + } + } + + /* ── Expose globally ─────────────────────────────────────────────── */ + window.AddressSearchControl = AddressSearchControl; + +})(window); diff --git a/streetup/common/static/common/android-192.png b/streetup/common/static/common/android-192.png new file mode 100644 index 0000000..40e9e58 Binary files /dev/null and b/streetup/common/static/common/android-192.png differ diff --git a/streetup/common/static/common/android-512.png b/streetup/common/static/common/android-512.png new file mode 100644 index 0000000..60ea378 Binary files /dev/null and b/streetup/common/static/common/android-512.png differ diff --git a/streetup/common/static/common/apple-touch-180.png b/streetup/common/static/common/apple-touch-180.png new file mode 100644 index 0000000..f4434fd Binary files /dev/null and b/streetup/common/static/common/apple-touch-180.png differ diff --git a/streetup/common/static/common/arial.cw127.pkl b/streetup/common/static/common/arial.cw127.pkl new file mode 100644 index 0000000..a24caae Binary files /dev/null and b/streetup/common/static/common/arial.cw127.pkl differ diff --git a/streetup/common/static/common/arial.pkl b/streetup/common/static/common/arial.pkl new file mode 100644 index 0000000..6cca994 Binary files /dev/null and b/streetup/common/static/common/arial.pkl differ diff --git a/streetup/common/static/common/arial.ttf b/streetup/common/static/common/arial.ttf new file mode 100644 index 0000000..ff0815c Binary files /dev/null and b/streetup/common/static/common/arial.ttf differ diff --git a/streetup/common/static/common/arial_bold.pkl b/streetup/common/static/common/arial_bold.pkl new file mode 100644 index 0000000..7b52ce2 Binary files /dev/null and b/streetup/common/static/common/arial_bold.pkl differ diff --git a/streetup/common/static/common/arial_bold.ttf b/streetup/common/static/common/arial_bold.ttf new file mode 100644 index 0000000..addc76c Binary files /dev/null and b/streetup/common/static/common/arial_bold.ttf differ diff --git a/streetup/common/static/common/arial_italic.pkl b/streetup/common/static/common/arial_italic.pkl new file mode 100644 index 0000000..1091b5c Binary files /dev/null and b/streetup/common/static/common/arial_italic.pkl differ diff --git a/streetup/common/static/common/arial_italic.ttf b/streetup/common/static/common/arial_italic.ttf new file mode 100644 index 0000000..eac8b35 Binary files /dev/null and b/streetup/common/static/common/arial_italic.ttf differ diff --git a/streetup/common/static/common/background_bm_A4_fr.png b/streetup/common/static/common/background_bm_A4_fr.png new file mode 100644 index 0000000..6dd1e02 Binary files /dev/null and b/streetup/common/static/common/background_bm_A4_fr.png differ diff --git a/streetup/common/static/common/base_map.js b/streetup/common/static/common/base_map.js new file mode 100644 index 0000000..d4577fa --- /dev/null +++ b/streetup/common/static/common/base_map.js @@ -0,0 +1,318 @@ + +var map; + +const popup = new maplibregl.Popup({ + closeButton: false, + closeOnClick: false, + anchor: 'bottom-left' +}); + + +/** + * Creates a handler to load the map with custom events. + */ +class MapLoader extends EventTarget { + constructor() { + super(); + } + + /** + * Loads the map wihtin the given container + * + * @param {string} url - name of the container + */ + async loadMap(containerId) { + try { + map = new maplibregl.Map({ + container: containerId, + //style: 'https://demotiles.maplibre.org/style.json', + style: window.MAP_STYLE_LIGHT, + //style: 'https://tiles.openfreemap.org/styles/positron', + center: [4.3572, 50.8477], + zoom: 11, + minzoom: 0, + maxzoom: 24 + }); + map.on("load", () => { + this.dispatchEvent(new CustomEvent("mapLoaded", { detail: map })); + }); + + } catch (error) { + console.error("Erreur when loading the map :", error.message); + + // Déclenche un événement pour signaler une erreur + this.dispatchEvent(new CustomEvent("mapError", { detail: error })); + } + } +} +const maploader = new MapLoader(); + + +/** + * Creates a handler to load a GeoJSON with custom events. + */ +class GeojsonLoader extends EventTarget { + constructor() { + super(); + } + + /** + * Loads a GeoJSON from a URL and triggers an event once loaded. + * + * @param {string} url - The URL of the GeoJSON file. + */ + async loadGeojson(url) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Erreur when loading GeoJSON : ${response.statusText}`); + } + + var geojson = await response.json(); + + if (!geojson || geojson.type !== "FeatureCollection") { + throw new Error("The GeoJSON must be an object of type FeatureCollection."); + } + + // Triggers a custom event with the loaded GeoJSON + this.dispatchEvent(new CustomEvent("geojsonLoaded", { detail: geojson })); + + } catch (error) { + console.error("Error loading GeoJSON: ", error.message); + + // Triggers an event to report an error + this.dispatchEvent(new CustomEvent("geojsonError", { detail: error })); + } + } +} + + + + +/** + * updates the map when geojson is loaded and map is loaded. + */ +async function handleGeojsonMapLoading(init=true, geojsonloader, geojsonsource, geojsonlayers=[], cluster=false) { + try { + var geojson = {}; + + if (init) { + // Wait for both events simultaneously + [geojson, map] = await Promise.all([ + waitForEvent(geojsonloader, "geojsonLoaded"), + waitForEvent(maploader, "mapLoaded") + ]); + } + else { + // Wait for new geojson loading + [geojson] = await Promise.all([ + waitForEvent(geojsonloader, "geojsonLoaded"), + ]); + } + + // Update Map + if (map.getSource(geojsonsource)) { + map.getSource(geojsonsource).setData(reverseGeoJson(geojson)); + } + else { + if (cluster) { + map.addSource(geojsonsource, { + 'type': 'geojson', + 'data': geojson, + cluster: true, + clusterMaxZoom: 24, + clusterRadius: 25, + clusterMinPoints: 1 + }); + } + else { + map.addSource(geojsonsource, { + 'type': 'geojson', + 'data': reverseGeoJson(geojson) + }); + } + + geojsonlayers.forEach(layer => { + if (!map.getLayer(layer.id)) { + map.addLayer(layer); + } + }); + } + + } catch (error) { + console.error("Error while updating map :", error.message); + } +} + + + + +// Function to update a table with the elements visible on the map +function filterTableByMapBounds(map, geojson_data, callback) { + // Récupérer les limites actuelles de la carte + const bounds = map.getBounds(); + + // Filtrer les points visibles dans les limites de la carte + const visibleFeatures = geojson_data.features.filter((feature) => { + if (feature.geometry && feature.geometry.type === 'Point') { + const [lon, lat] = feature.geometry.coordinates; + return bounds.contains([lon, lat]); // Vérifie si le point est dans les limites + } + return false; // Ignorer les autres types de géométries + }); + + callback(visibleFeatures); +} + + + +/** + * Registers a mouseenter event on a specific MapLibre layer. + * Displays a popup with feature properties if the zoom level is above the threshold. + * + * @param {object} map - The MapLibre GL map instance. + * @param {string} layerId - The ID of the layer to listen for mouseenter events. + * @param {object} popup - The MapLibre popup instance to display information. + * @param {string} property - The feature property to display in the popup. + * @param {number} zoomThreshold - The minimum zoom level required to show the popup. + */ +function registerLayerMouseEnter(map, layerId, popup, property, zoomThreshold = 13) { + map.on('mouseenter', layerId, (e) => { + const currentZoom = map.getZoom(); + + if (currentZoom > zoomThreshold) { + // Change the cursor style to indicate interactivity + map.getCanvas().style.cursor = 'pointer'; + + const description = e.features[0].properties[property] || 'No data'; + + // const coordinates = e.features[0].geometry.coordinates.slice(); + // // Handle longitude + // while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) { + // coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360; + // } + const coordinates = e.lngLat; + + // Set and display the popup + popup.setLngLat(coordinates).setHTML(description).addTo(map); + } + }); +} + +/** + * Registers a mouseleave event on a specific MapLibre layer. + * Resets the cursor and removes the popup when the mouse leaves the layer. + * + * @param {object} map - The MapLibre GL map instance. + * @param {string} layerId - The ID of the layer to listen for mouseleave events. + * @param {object} popup - The MapLibre popup instance to be removed. + */ +function registerLayerMouseLeave(map, layerId, popup) { + map.on('mouseleave', layerId, () => { + // Reset cursor style + map.getCanvas().style.cursor = ''; + + // Remove popup from the map + popup.remove(); + }); +} + + +/** + * Registers a mousemove event on a specific MapLibre layer. + * Displays a popup with feature properties if the zoom level is above the threshold. + * + * @param {object} map - The MapLibre GL map instance. + * @param {string} layerId - The ID of the layer to listen for mouseenter events. + * @param {object} popup - The MapLibre popup instance to display information. + * @param {string} property - The feature property to display in the popup. + * @param {number} zoomThreshold - The minimum zoom level required to show the popup. + */ +function registerLayerMouseMove(map, layerId, popup, property, zoomThreshold = 13) { + map.on('mousemove', layerId, (e) => { + const currentZoom = map.getZoom(); + + if (currentZoom > zoomThreshold) { + map.getCanvas().style.cursor = 'pointer'; + const coordinates = e.lngLat; + const description = e.features[0].properties[property] || 'No data'; + popup.setLngLat(coordinates).setHTML(description); + if (!popup.isOpen()) { + popup.addTo(map); + } + } + }); +} + + +/** + * Registers a click event on a specific table for zooming to a location on the map. + * + * @param {string} tableSelector - The selector for the table(s) containing zoom buttons (e.g., '#intersections-table'). + * @param {string} buttonClass - The class of the zoom button (e.g., '.zoom-btn'). + * @param {object} map - The MapLibre GL map instance. + * @param {number} zoomLevel - The zoom level to apply when flying to the location (default: 16). + */ +function registerZoomAction(tableSelector, buttonClass, map, zoomLevel = 16) { + $(tableSelector).on('click', buttonClass, function () { + const lon = parseFloat($(this).data('lon')); + const lat = parseFloat($(this).data('lat')); + + if (!isNaN(lon) && !isNaN(lat)) { + map.flyTo({ + center: [lon, lat], + zoom: zoomLevel, + essential: true // Ensures smooth animation + }); + } + }); +} + + +/** + * Highlights a specific feature on a MapLibre layer while removing the highlight from the previous feature. + * + * @param {object} map - The MapLibre GL map instance. + * @param {string} sourceId - The source ID containing the features. + * @param {string} featureId - The ID of the feature to highlight. + * @param {object} highlightState - Object to track the currently highlighted feature. + */ +function highlightMapFeature(map, sourceId, featureId, highlightState) { + // Remove highlight from the previous feature + if (highlightState.previousId !== null) { + map.setFeatureState( + { source: sourceId, id: highlightState.previousId }, + { highlighted: false } + ); + } + + // Set highlight on the newly selected feature + highlightState.previousId = featureId; + map.setFeatureState( + { source: sourceId, id: featureId }, + { highlighted: true } + ); +} + + + +function reverseGeoJson(data) { + if (data.type === 'FeatureCollection' && Array.isArray(data.features)) { + return { + ...data, + features: [...data.features].reverse() + }; + } + return data; +} + + +// --------------------------------------------------------------------------- +// Companion controls (loaded separately, depend on maplibre-gl.js) +// --------------------------------------------------------------------------- +// address_search_control.js – AddressSearchControl +// A reusable MapLibre IControl that queries the internal autocomplete_street +// endpoint and places a draggable marker on the selected address. +// Usage: map.addControl(new AddressSearchControl({ autocompleteUrl, ... }), 'top-left'); +// Events: 'address-selected' | 'marker-placed' | 'marker-moved' | 'marker-cleared' +// --------------------------------------------------------------------------- \ No newline at end of file diff --git a/streetup/common/static/common/base_script.js b/streetup/common/static/common/base_script.js new file mode 100644 index 0000000..0f4a2cf --- /dev/null +++ b/streetup/common/static/common/base_script.js @@ -0,0 +1,106 @@ + +/** + * Waits for a specific event and returns a promise that resolves when the event is triggered. + * + * @param {EventTarget} target - The target object that triggers the event. + * @param {string} eventName - The name of the event to listen to. + * @returns {Promise} - A promise resolved with event data. + */ +function waitForEvent(target, eventName) { + return new Promise(resolve => { + const handler = event => { + // Resolve the promise with event details + resolve(event.detail); + // Remove handler after resolution + target.removeEventListener(eventName, handler); + }; + target.addEventListener(eventName, handler); + }); +} + + +/** + * Adds a click event listener to a table for handling detail button actions. + * When a button is clicked, it retrieves the data-id attribute and redirects to a new page. + * + * @param {string} tableId - The ID of the table where the event should be attached (e.g., '#intersections-table'). + * @param {string} buttonClass - The class of the button that triggers the action (e.g., '.detail-btn'). + * @param {string} baseUrl - The base URL to which the feature ID will be appended (e.g., 'https://example.com/details/'). + */ +function registerDetailAction(tableId, buttonClass, baseUrl) { + $(tableId).on('click', buttonClass, function () { + const explicitUrl = $(this).data('url'); + if (explicitUrl) { + window.open(explicitUrl, "_self"); + return; + } + + const featureId = $(this).data('id'); // Retrieve the ID from the button's data attribute + + if (featureId) { + window.open(baseUrl + featureId + "/", "_self"); // Open the detail page in the same tab + } + }); +} + + +/** + * Highlights a row in the Bootstrap Table, navigates to its page, and centers it in the view. + * + * @param {string} tableId - The ID of the Bootstrap Table (e.g., '#intersections-table'). + * @param {string} field - The column field to match (e.g., 'code' or 'id'). + * @param {any} value - The value to search for in the column. + */ +function highlightRow(tableId, field, value, scroll=true) { + const $table = $(tableId); + const data = $table.bootstrapTable('getData'); + + // Find the row index in the full dataset + const rowIndex = data.findIndex(row => row[field] === value); + + if (rowIndex === -1) { + console.warn(`Row with ${field}=${value} not found.`); + return; + } + + // Get the page size and compute which page contains the row + const pageSize = $table.bootstrapTable('getOptions').pageSize || data.length; + const pageNumber = Math.floor(rowIndex / pageSize) + 1; + + // Change the table to the correct page + $table.bootstrapTable('selectPage', pageNumber); + + // Delay to ensure UI updates before applying the highlight + setTimeout(() => { + // Remove highlight from all previously highlighted cells + $table.find('tbody td').removeClass('rowhighlight'); + + // Get the row element using its index within the page + const $rowElement = $table.find(`tbody tr[data-index="${rowIndex}"]`); + + if ($rowElement.length) { + // Apply the highlight class to all `` cells in the row + $rowElement.find('td').addClass('rowhighlight'); + + // Scroll the row into view + if (scroll) { + $rowElement[0].scrollIntoView({ behavior: "smooth", block: "center" }); + } + } + }, 300); // Delay to ensure the page has changed before searching for the row +} + + +/** + * Formats a date value into a localized string. + * Uses the page language (set by Django) to determine the locale, + * so fr/nl always render dd/mm/yyyy regardless of browser settings. + */ +function dateFormatter(value) { + if (!value) return '-'; // Return a dash if the value is empty + const date = new Date(value); + const lang = (document.documentElement.lang || 'fr').split('-')[0]; + const localeMap = { 'fr': 'fr-BE', 'nl': 'nl-BE', 'en': 'en-GB' }; + const locale = localeMap[lang] || 'fr-BE'; + return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }); +} diff --git a/streetup/common/static/common/base_style.css b/streetup/common/static/common/base_style.css new file mode 100644 index 0000000..e3cc4d4 --- /dev/null +++ b/streetup/common/static/common/base_style.css @@ -0,0 +1,566 @@ +@keyframes fadeIn75 { + 0% { + opacity: 0; + } + + 100% { + opacity: 0.75; + } +} + +:root { + --brand-color: rgba(47, 62, 139, 1); + --labelblue: rgba(0, 169, 252, 0.75); + --bg-light-grey: #f8f8f8; +} + +body { + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; +} + + +#header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 60px; + background-color: #fff; + color: #4a7d9b; + border-bottom: 1px solid #4a7d9b; + display: flex; + align-items: center; + padding: 0 1rem; + z-index: 1030; +} + +nav.gradient-custom { + border-bottom: 1px solid #2f3e8b; + box-shadow: 0 4px 8px rgba(47, 62, 139, 0.1); + z-index: 1030; +} + +.navbar .form-control:focus { + outline: none; + box-shadow: none; +} + +#main { + display: flex; + flex-grow: 1; + overflow: hidden; +} + +#sidebar { + color: #2f3e8b; + /* background-color: #fcfcfc; */ + /* background-color: var(--bg-light-grey); */ + border-right: 1px solid rgba(47, 62, 139, 0.25); + z-index: 100; +} + +.sidebar { + transition: width 0.3s ease; + flex-shrink: 0; +} + +.sidebar.collapsed { + width: 60px; + /* Largeur réduite pour les icônes uniquement */ +} + +.sidebar .text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + +#content-wrapper { + flex-grow: 1; + overflow-y: auto; + padding: 1rem; + padding-top: 3px; +} + +#map { + position: relative; + height: calc(100vh - 322px); +} + +.maplibregl-popup { + max-width: 400px; + font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif; +} + + +ul>li>a.active { + font-weight: bold; +} + +.h3, +h5 { + color: var(--brand-color); +} + +.h4 { + background-color: #e8f3f7; + padding: 5px; +} + + +.filterzone { + position: relative; + z-index: 10001; + border-color: rgb(77, 99, 117, 0.75); + opacity: 0; + animation: fadeIn75 2s 2s; + animation-fill-mode: forwards; +} + +.filter-dropdown { + background-color: #f8f8f8; +} + +.table-spinner { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + /* Ajustez selon vos besoins */ +} + + +.nav-tabs .nav-link { + border-color: rgb(240, 240, 240); + color: black +} + +.nav-link.active { + font-weight: bold; +} + +.nav-tabs .nav-link.active { + background-color: white; + color: var(--brand-color); +} + +.dropdown-item-form { + margin: 0; +} + +.dropdown-item-form .dropdown-item { + width: 100%; + text-align: left; +} + +label, +.label { + color: var(--labelblue); +} + +.bootstrap-table .fixed-table-container .table tbody tr.selected td { + background-color: rgba(1, 123, 197, 0.15); +} + +.rowhighlight { + background-color: rgb(255, 235, 59, 0.35) !important; + /* Yellow highlight */ +} + +.btn-light { + /* border-color: rgba(0, 123, 197, 0.25); */ + /* border-color: rgba(47, 62, 139,0.5); */ + background-color: var(--bg-light-grey); +} + +.detail-btn, +.zoom-btn, +.table-btn { + color: var(--brand-color); + margin-right: 4px; +} + +.status_active { + color: rgb(0, 148, 255) +} + +.status_removed { + color: rgb(108, 117, 125) +} + +.status_in_preparation { + color: rgb(245, 159, 0) +} + +.status_under_repair { + color: rgb(247, 103, 7) +} + +.status_voluntarily_out_of_service { + color: rgb(224, 49, 49) +} + +.status_inactive { + color: rgb(224, 49, 49) +} + +.status_archived { + color: rgb(108, 117, 125) +} + + + + +.totals-overlay { + position: absolute; + inset: 0; + /* top/right/bottom/left: 0 */ + background: rgba(255, 255, 255, 0.98); + z-index: 10; + /* au-dessus des contrôles map */ + display: none; + /* masqué par défaut */ + overflow: auto; + /* scroll si contenu long */ +} + +/* petite barre supérieure pour le titre et les actions */ +.totals-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: .75rem; + padding: .75rem 1rem; + border-bottom: 1px solid #e5e7eb; + position: sticky; + top: 0; + background: #fff; +} + +.totals-content { + padding: 1rem; +} + +/* état visible */ +.totals-overlay.show { + display: block; +} + + +.errorlist { + color: red; + margin-top: 0.25rem; + margin-bottom: 0; + list-style-type: none; + padding-left: 0; + font-size: 0.875rem; +} + +/* Texte secondaire (text-muted) légèrement plus foncé que le défaut Bootstrap */ +.text-muted, +.form-text { + color: #bbb !important; +} + +/* Mode carte maximisée */ +#map.maximized, +#asset-map.maximized { + position: fixed !important; + top: 20px !important; + left: 20px !important; + right: 20px !important; + bottom: 20px !important; + width: calc(100vw - 40px) !important; + height: calc(100vh - 40px) !important; + z-index: 1040 !important; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35); + border: 1px solid rgba(0, 0, 0, 0.15); + background-color: white; +} + +/* Décalage des contrôles en haut à droite pour ne pas chevaucher le bouton d'agrandissement */ +#map .maplibregl-ctrl-top-right, +#map .mapboxgl-ctrl-top-right, +#asset-map .maplibregl-ctrl-top-right, +#asset-map .mapboxgl-ctrl-top-right { + top: 50px !important; +} + +/* Surcharges de couleur personnalisées pour les 15 statuts d'interventions */ +.badge.status-in_preparation { + background-color: #757575 !important; + color: #ffffff !important; +} + +.badge.status-to_be_approved { + background-color: #ffb74d !important; + color: #000000 !important; +} + +.badge.status-to_be_planned { + background-color: #ff8a80 !important; + color: #000000 !important; +} + +.badge.status-to_be_processed { + background-color: #f4511e !important; + color: #ffffff !important; +} + +.badge.status-assigned { + background-color: #63b3ed !important; + color: #000000 !important; +} + +.badge.status-in_progress { + background-color: #3182ce !important; + color: #ffffff !important; +} + +.badge.status-on_pause { + background-color: #90a4ae !important; + color: #000000 !important; +} + +.badge.status-finished { + background-color: #26a69a !important; + color: #ffffff !important; +} + +.badge.status-processed { + background-color: #9ae9e2 !important; + color: #000000 !important; +} + +.badge.status-to_be_corrected { + background-color: #9e87f6 !important; + color: #000000 !important; +} + +.badge.status-corrected { + background-color: #81c784 !important; + color: #000000 !important; +} + +.badge.status-validated { + background-color: #388e3c !important; + color: #ffffff !important; +} + +.badge.status-invoiced { + background-color: #738f7a !important; + color: #ffffff !important; +} + +.badge.status-closed { + background-color: #09425f !important; + color: #ffffff !important; +} + +.badge.status-canceled { + background-color: #cfd8dc !important; + color: #000000 !important; +} + +/* Surcharges pour les boutons de filtres des statuts d'interventions */ +.filter-option label.status-in_preparation { + border-color: #757575 !important; + color: #757575 !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-in_preparation, +.filter-option input[type="checkbox"]:checked+label.status-in_preparation { + background-color: #757575 !important; + border-color: #757575 !important; + color: #ffffff !important; +} + +.filter-option label.status-to_be_approved { + border-color: #ffb74d !important; + color: #ffb74d !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-to_be_approved, +.filter-option input[type="checkbox"]:checked+label.status-to_be_approved { + background-color: #ffb74d !important; + border-color: #ffb74d !important; + color: #000000 !important; +} + +.filter-option label.status-to_be_planned { + border-color: #ff8a80 !important; + color: #ff8a80 !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-to_be_planned, +.filter-option input[type="checkbox"]:checked+label.status-to_be_planned { + background-color: #ff8a80 !important; + border-color: #ff8a80 !important; + color: #000000 !important; +} + +.filter-option label.status-to_be_processed { + border-color: #f4511e !important; + color: #f4511e !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-to_be_processed, +.filter-option input[type="checkbox"]:checked+label.status-to_be_processed { + background-color: #f4511e !important; + border-color: #f4511e !important; + color: #ffffff !important; +} + +.filter-option label.status-assigned { + border-color: #63b3ed !important; + color: #63b3ed !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-assigned, +.filter-option input[type="checkbox"]:checked+label.status-assigned { + background-color: #63b3ed !important; + border-color: #63b3ed !important; + color: #000000 !important; +} + +.filter-option label.status-in_progress { + border-color: #3182ce !important; + color: #3182ce !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-in_progress, +.filter-option input[type="checkbox"]:checked+label.status-in_progress { + background-color: #3182ce !important; + border-color: #3182ce !important; + color: #ffffff !important; +} + +.filter-option label.status-on_pause { + border-color: #90a4ae !important; + color: #90a4ae !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-on_pause, +.filter-option input[type="checkbox"]:checked+label.status-on_pause { + background-color: #90a4ae !important; + border-color: #90a4ae !important; + color: #000000 !important; +} + +.filter-option label.status-finished { + border-color: #26a69a !important; + color: #26a69a !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-finished, +.filter-option input[type="checkbox"]:checked+label.status-finished { + background-color: #26a69a !important; + border-color: #26a69a !important; + color: #ffffff !important; +} + +.filter-option label.status-processed { + border-color: #9ae9e2 !important; + color: #9ae9e2 !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-processed, +.filter-option input[type="checkbox"]:checked+label.status-processed { + background-color: #9ae9e2 !important; + border-color: #9ae9e2 !important; + color: #000000 !important; +} + +.filter-option label.status-to_be_corrected { + border-color: #9e87f6 !important; + color: #9e87f6 !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-to_be_corrected, +.filter-option input[type="checkbox"]:checked+label.status-to_be_corrected { + background-color: #9e87f6 !important; + border-color: #9e87f6 !important; + color: #000000 !important; +} + +.filter-option label.status-corrected { + border-color: #81c784 !important; + color: #81c784 !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-corrected, +.filter-option input[type="checkbox"]:checked+label.status-corrected { + background-color: #81c784 !important; + border-color: #81c784 !important; + color: #000000 !important; +} + +.filter-option label.status-validated { + border-color: #388e3c !important; + color: #388e3c !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-validated, +.filter-option input[type="checkbox"]:checked+label.status-validated { + background-color: #388e3c !important; + border-color: #388e3c !important; + color: #ffffff !important; +} + +.filter-option label.status-invoiced { + border-color: #738f7a !important; + color: #738f7a !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-invoiced, +.filter-option input[type="checkbox"]:checked+label.status-invoiced { + background-color: #738f7a !important; + border-color: #738f7a !important; + color: #ffffff !important; +} + +.filter-option label.status-closed { + border-color: #09425f !important; + color: #09425f !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-closed, +.filter-option input[type="checkbox"]:checked+label.status-closed { + background-color: #09425f !important; + border-color: #09425f !important; + color: #ffffff !important; +} + +.filter-option label.status-canceled { + border-color: #cfd8dc !important; + color: #cfd8dc !important; + background-color: transparent !important; +} +.filter-option input[type="checkbox"]:hover+label.status-canceled, +.filter-option input[type="checkbox"]:checked+label.status-canceled { + background-color: #cfd8dc !important; + border-color: #cfd8dc !important; + color: #000000 !important; +} + +/* Classes pour la couleur de texte / d'icône des statuts */ +.text-status-in_preparation { color: #757575 !important; } +.text-status-to_be_approved { color: #ffb74d !important; } +.text-status-to_be_planned { color: #ff8a80 !important; } +.text-status-to_be_processed { color: #f4511e !important; } +.text-status-assigned { color: #63b3ed !important; } +.text-status-in_progress { color: #3182ce !important; } +.text-status-on_pause { color: #90a4ae !important; } +.text-status-finished { color: #26a69a !important; } +.text-status-processed { color: #9ae9e2 !important; } +.text-status-to_be_corrected { color: #9e87f6 !important; } +.text-status-corrected { color: #81c784 !important; } +.text-status-validated { color: #388e3c !important; } +.text-status-invoiced { color: #738f7a !important; } +.text-status-closed { color: #09425f !important; } +.text-status-canceled { color: #cfd8dc !important; } \ No newline at end of file diff --git a/streetup/common/static/common/bootstrap-icons.min.css b/streetup/common/static/common/bootstrap-icons.min.css new file mode 100644 index 0000000..dadd6dc --- /dev/null +++ b/streetup/common/static/common/bootstrap-icons.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?dd67030699838ea613ee6dbda90effa6") format("woff2"),url("fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"} \ No newline at end of file diff --git a/streetup/common/static/common/bootstrap-table.min.css b/streetup/common/static/common/bootstrap-table.min.css new file mode 100644 index 0000000..b9b91a3 --- /dev/null +++ b/streetup/common/static/common/bootstrap-table.min.css @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.23.5 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +@charset "UTF-8";.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table tfoot th,.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table tfoot th:focus,.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table tfoot th.detail,.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table tfoot th .th-inner,.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table tfoot th .sortable,.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table tfoot th .sortable.sortable-center,.bootstrap-table .fixed-table-container .table thead th .sortable.sortable-center{padding-left:20px!important;padding-right:20px!important}.bootstrap-table .fixed-table-container .table tfoot th .both,.bootstrap-table .fixed-table-container .table thead th .both{background-image:url('data:image/svg+xml;utf8,');background-size:16px 16px;background-position:center right 2px}.bootstrap-table .fixed-table-container .table tfoot th .asc,.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url('data:image/svg+xml;utf8,')}.bootstrap-table .fixed-table-container .table tfoot th .desc,.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url('data:image/svg+xml;utf8,')}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important;text-align:left!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.25rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"⬅"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"➡"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:100vh;overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} \ No newline at end of file diff --git a/streetup/common/static/common/bootstrap-table.min.js b/streetup/common/static/common/bootstrap-table.min.js new file mode 100644 index 0000000..d152b23 --- /dev/null +++ b/streetup/common/static/common/bootstrap-table.min.js @@ -0,0 +1,10 @@ +/** + * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) + * + * @version v1.23.5 + * @homepage https://bootstrap-table.com + * @author wenzhixin (http://wenzhixin.net.cn/) + * @license MIT + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).BootstrapTable=e(t.jQuery)}(this,(function(t){"use strict";function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,a=!0,s=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){s=!0,o=t},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw o}}}}function o(t,e,n){return(e=u(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function a(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function s(t){for(var e=1;e0&&t[0]<4?1:+(t[0]+t[1])),!e&&i&&(!(t=i.match(/Edge\/(\d+)/))||t[1]>=74)&&(t=i.match(/Chrome\/(\d+)/))&&(e=+t[1]),nt=e}function jt(){if(ot)return rt;ot=1;var t=Et(),e=C(),n=b().String;return rt=!!Object.getOwnPropertySymbols&&!e((function(){var e=Symbol("symbol detection");return!n(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&t&&t<41}))}function Nt(){if(st)return at;st=1;var t=jt();return at=t&&!Symbol.sham&&"symbol"==typeof Symbol.iterator}function Ft(){if(ct)return lt;ct=1;var t=It(),e=Ct(),n=$t(),i=Nt(),r=Object;return lt=i?function(t){return"symbol"==typeof t}:function(i){var o=t("Symbol");return e(o)&&n(o.prototype,r(i))}}function Dt(){if(ht)return ut;ht=1;var t=String;return ut=function(e){try{return t(e)}catch(t){return"Object"}}}function Lt(){if(dt)return ft;dt=1;var t=Ct(),e=Dt(),n=TypeError;return ft=function(i){if(t(i))return i;throw new n(e(i)+" is not a function")}}function _t(){if(gt)return pt;gt=1;var t=Lt(),e=kt();return pt=function(n,i){var r=n[i];return e(r)?void 0:t(r)}}function Vt(){if(bt)return vt;bt=1;var t=$(),e=Ct(),n=At(),i=TypeError;return vt=function(r,o){var a,s;if("string"===o&&e(a=r.toString)&&!n(s=t(a,r)))return s;if(e(a=r.valueOf)&&!n(s=t(a,r)))return s;if("string"!==o&&e(a=r.toString)&&!n(s=t(a,r)))return s;throw new i("Can't convert object to primitive value")}}var Bt,Ht,Mt,Ut,zt,qt,Wt,Gt,Kt,Jt,Yt,Qt,Xt,Zt,te,ee,ne,ie,re,oe,ae,se,le,ce,ue={exports:{}};function he(){return Ht?Bt:(Ht=1,Bt=!1)}function fe(){if(Ut)return Mt;Ut=1;var t=b(),e=Object.defineProperty;return Mt=function(n,i){try{e(t,n,{value:i,configurable:!0,writable:!0})}catch(e){t[n]=i}return i}}function de(){if(zt)return ue.exports;zt=1;var t=he(),e=b(),n=fe(),i="__core-js_shared__",r=ue.exports=e[i]||n(i,{});return(r.versions||(r.versions=[])).push({version:"3.38.1",mode:t?"pure":"global",copyright:"© 2014-2024 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.38.1/LICENSE",source:"https://github.com/zloirock/core-js"}),ue.exports}function pe(){if(Wt)return qt;Wt=1;var t=de();return qt=function(e,n){return t[e]||(t[e]=n||{})}}function ge(){if(Kt)return Gt;Kt=1;var t=Tt(),e=Object;return Gt=function(n){return e(t(n))}}function ve(){if(Yt)return Jt;Yt=1;var t=St(),e=ge(),n=t({}.hasOwnProperty);return Jt=Object.hasOwn||function(t,i){return n(e(t),i)}}function be(){if(Xt)return Qt;Xt=1;var t=St(),e=0,n=Math.random(),i=t(1..toString);return Qt=function(t){return"Symbol("+(void 0===t?"":t)+")_"+i(++e+n,36)}}function me(){if(te)return Zt;te=1;var t=b(),e=pe(),n=ve(),i=be(),r=jt(),o=Nt(),a=t.Symbol,s=e("wks"),l=o?a.for||a:a&&a.withoutSetter||i;return Zt=function(t){return n(s,t)||(s[t]=r&&n(a,t)?a[t]:l("Symbol."+t)),s[t]}}function ye(){if(ne)return ee;ne=1;var t=$(),e=At(),n=Ft(),i=_t(),r=Vt(),o=me(),a=TypeError,s=o("toPrimitive");return ee=function(o,l){if(!e(o)||n(o))return o;var c,u=i(o,s);if(u){if(void 0===l&&(l="default"),c=t(u,o,l),!e(c)||n(c))return c;throw new a("Can't convert object to primitive value")}return void 0===l&&(l="number"),r(o,l)}}function we(){if(re)return ie;re=1;var t=ye(),e=Ft();return ie=function(n){var i=t(n,"string");return e(i)?i:i+""}}function Se(){if(ae)return oe;ae=1;var t=b(),e=At(),n=t.document,i=e(n)&&e(n.createElement);return oe=function(t){return i?n.createElement(t):{}}}function xe(){if(le)return se;le=1;var t=A(),e=C(),n=Se();return se=!t&&!e((function(){return 7!==Object.defineProperty(n("div"),"a",{get:function(){return 7}}).a}))}function Oe(){if(ce)return P;ce=1;var t=A(),e=$(),n=yt(),i=wt(),r=Pt(),o=we(),a=ve(),s=xe(),l=Object.getOwnPropertyDescriptor;return P.f=t?l:function(t,c){if(t=r(t),c=o(c),s)try{return l(t,c)}catch(t){}if(a(t,c))return i(!e(n.f,t,c),t[c])},P}var ke,Te,Pe,Ce,Ae,Ie,$e,Re={};function Ee(){if(Te)return ke;Te=1;var t=A(),e=C();return ke=t&&e((function(){return 42!==Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))}function je(){if(Ce)return Pe;Ce=1;var t=At(),e=String,n=TypeError;return Pe=function(i){if(t(i))return i;throw new n(e(i)+" is not an object")}}function Ne(){if(Ae)return Re;Ae=1;var t=A(),e=xe(),n=Ee(),i=je(),r=we(),o=TypeError,a=Object.defineProperty,s=Object.getOwnPropertyDescriptor,l="enumerable",c="configurable",u="writable";return Re.f=t?n?function(t,e,n){if(i(t),e=r(e),i(n),"function"==typeof t&&"prototype"===e&&"value"in n&&u in n&&!n[u]){var o=s(t,e);o&&o[u]&&(t[e]=n.value,n={configurable:c in n?n[c]:o[c],enumerable:l in n?n[l]:o[l],writable:!1})}return a(t,e,n)}:a:function(t,n,s){if(i(t),n=r(n),i(s),e)try{return a(t,n,s)}catch(t){}if("get"in s||"set"in s)throw new o("Accessors not supported");return"value"in s&&(t[n]=s.value),t},Re}function Fe(){if($e)return Ie;$e=1;var t=A(),e=Ne(),n=wt();return Ie=t?function(t,i,r){return e.f(t,i,n(1,r))}:function(t,e,n){return t[e]=n,t}}var De,Le,_e,Ve,Be,He,Me,Ue,ze,qe,We,Ge,Ke,Je,Ye,Qe={exports:{}};function Xe(){if(Le)return De;Le=1;var t=A(),e=ve(),n=Function.prototype,i=t&&Object.getOwnPropertyDescriptor,r=e(n,"name"),o=r&&"something"===function(){}.name,a=r&&(!t||t&&i(n,"name").configurable);return De={EXISTS:r,PROPER:o,CONFIGURABLE:a}}function Ze(){if(Ve)return _e;Ve=1;var t=St(),e=Ct(),n=de(),i=t(Function.toString);return e(n.inspectSource)||(n.inspectSource=function(t){return i(t)}),_e=n.inspectSource}function tn(){if(Ue)return Me;Ue=1;var t=pe(),e=be(),n=t("keys");return Me=function(t){return n[t]||(n[t]=e(t))}}function en(){return qe?ze:(qe=1,ze={})}function nn(){if(Ge)return We;Ge=1;var t,e,n,i=function(){if(He)return Be;He=1;var t=b(),e=Ct(),n=t.WeakMap;return Be=e(n)&&/native code/.test(String(n))}(),r=b(),o=At(),a=Fe(),s=ve(),l=de(),c=tn(),u=en(),h="Object already initialized",f=r.TypeError,d=r.WeakMap;if(i||l.state){var p=l.state||(l.state=new d);p.get=p.get,p.has=p.has,p.set=p.set,t=function(t,e){if(p.has(t))throw new f(h);return e.facade=t,p.set(t,e),e},e=function(t){return p.get(t)||{}},n=function(t){return p.has(t)}}else{var g=c("state");u[g]=!0,t=function(t,e){if(s(t,g))throw new f(h);return e.facade=t,a(t,g,e),e},e=function(t){return s(t,g)?t[g]:{}},n=function(t){return s(t,g)}}return We={set:t,get:e,has:n,enforce:function(i){return n(i)?e(i):t(i,{})},getterFor:function(t){return function(n){var i;if(!o(n)||(i=e(n)).type!==t)throw new f("Incompatible receiver, "+t+" required");return i}}}}function rn(){if(Ke)return Qe.exports;Ke=1;var t=St(),e=C(),n=Ct(),i=ve(),r=A(),o=Xe().CONFIGURABLE,a=Ze(),s=nn(),l=s.enforce,c=s.get,u=String,h=Object.defineProperty,f=t("".slice),d=t("".replace),p=t([].join),g=r&&!e((function(){return 8!==h((function(){}),"length",{value:8}).length})),v=String(String).split("String"),b=Qe.exports=function(t,e,n){"Symbol("===f(u(e),0,7)&&(e="["+d(u(e),/^Symbol\(([^)]*)\).*$/,"$1")+"]"),n&&n.getter&&(e="get "+e),n&&n.setter&&(e="set "+e),(!i(t,"name")||o&&t.name!==e)&&(r?h(t,"name",{value:e,configurable:!0}):t.name=e),g&&n&&i(n,"arity")&&t.length!==n.arity&&h(t,"length",{value:n.arity});try{n&&i(n,"constructor")&&n.constructor?r&&h(t,"prototype",{writable:!1}):t.prototype&&(t.prototype=void 0)}catch(t){}var a=l(t);return i(a,"source")||(a.source=p(v,"string"==typeof e?e:"")),t};return Function.prototype.toString=b((function(){return n(this)&&c(this).source||a(this)}),"toString"),Qe.exports}function on(){if(Ye)return Je;Ye=1;var t=Ct(),e=Ne(),n=rn(),i=fe();return Je=function(r,o,a,s){s||(s={});var l=s.enumerable,c=void 0!==s.name?s.name:o;if(t(a)&&n(a,c,s),s.global)l?r[o]=a:i(o,a);else{try{s.unsafe?r[o]&&(l=!0):delete r[o]}catch(t){}l?r[o]=a:e.f(r,o,{value:a,enumerable:!1,configurable:!s.nonConfigurable,writable:!s.nonWritable})}return r}}var an,sn,ln,cn,un,hn,fn,dn,pn,gn,vn,bn,mn,yn,wn,Sn,xn,On={};function kn(){if(cn)return ln;cn=1;var t=function(){if(sn)return an;sn=1;var t=Math.ceil,e=Math.floor;return an=Math.trunc||function(n){var i=+n;return(i>0?e:t)(i)}}();return ln=function(e){var n=+e;return n!=n||0===n?0:t(n)}}function Tn(){if(hn)return un;hn=1;var t=kn(),e=Math.max,n=Math.min;return un=function(i,r){var o=t(i);return o<0?e(o+r,0):n(o,r)}}function Pn(){if(dn)return fn;dn=1;var t=kn(),e=Math.min;return fn=function(n){var i=t(n);return i>0?e(i,9007199254740991):0}}function Cn(){if(gn)return pn;gn=1;var t=Pn();return pn=function(e){return t(e.length)}}function An(){if(bn)return vn;bn=1;var t=Pt(),e=Tn(),n=Cn(),i=function(i){return function(r,o,a){var s=t(r),l=n(s);if(0===l)return!i&&-1;var c,u=e(a,l);if(i&&o!=o){for(;l>u;)if((c=s[u++])!=c)return!0}else for(;l>u;u++)if((i||u in s)&&s[u]===o)return i||u||0;return!i&&-1}};return vn={includes:i(!0),indexOf:i(!1)}}function In(){if(yn)return mn;yn=1;var t=St(),e=ve(),n=Pt(),i=An().indexOf,r=en(),o=t([].push);return mn=function(t,a){var s,l=n(t),c=0,u=[];for(s in l)!e(r,s)&&e(l,s)&&o(u,s);for(;a.length>c;)e(l,s=a[c++])&&(~i(u,s)||o(u,s));return u}}function $n(){return Sn?wn:(Sn=1,wn=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"])}function Rn(){if(xn)return On;xn=1;var t=In(),e=$n().concat("length","prototype");return On.f=Object.getOwnPropertyNames||function(n){return t(n,e)},On}var En,jn,Nn,Fn,Dn,Ln,_n,Vn,Bn,Hn,Mn,Un,zn,qn,Wn,Gn,Kn,Jn,Yn,Qn,Xn,Zn,ti,ei,ni,ii,ri,oi,ai={};function si(){return En||(En=1,ai.f=Object.getOwnPropertySymbols),ai}function li(){if(Nn)return jn;Nn=1;var t=It(),e=St(),n=Rn(),i=si(),r=je(),o=e([].concat);return jn=t("Reflect","ownKeys")||function(t){var e=n.f(r(t)),a=i.f;return a?o(e,a(t)):e}}function ci(){if(Dn)return Fn;Dn=1;var t=ve(),e=li(),n=Oe(),i=Ne();return Fn=function(r,o,a){for(var s=e(o),l=i.f,c=n.f,u=0;u9007199254740991)throw t("Maximum allowed index exceeded");return e}}function pi(){if(Wn)return qn;Wn=1;var t=A(),e=Ne(),n=wt();return qn=function(i,r,o){t?e.f(i,r,n(0,o)):i[r]=o}}function gi(){if(Kn)return Gn;Kn=1;var t={};return t[me()("toStringTag")]="z",Gn="[object z]"===String(t)}function vi(){if(Yn)return Jn;Yn=1;var t=gi(),e=Ct(),n=xt(),i=me()("toStringTag"),r=Object,o="Arguments"===n(function(){return arguments}());return Jn=t?n:function(t){var a,s,l;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(s=function(t,e){try{return t[e]}catch(t){}}(a=r(t),i))?s:o?n(a):"Object"===(l=n(a))&&e(a.callee)?"Arguments":l}}function bi(){if(Xn)return Qn;Xn=1;var t=St(),e=C(),n=Ct(),i=vi(),r=It(),o=Ze(),a=function(){},s=r("Reflect","construct"),l=/^\s*(?:class|function)\b/,c=t(l.exec),u=!l.test(a),h=function(t){if(!n(t))return!1;try{return s(a,[],t),!0}catch(t){return!1}},f=function(t){if(!n(t))return!1;switch(i(t)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return u||!!c(l,o(t))}catch(t){return!0}};return f.sham=!0,Qn=!s||e((function(){var t;return h(h.call)||!h(Object)||!h((function(){t=!0}))||t}))?f:h}function mi(){if(ti)return Zn;ti=1;var t=fi(),e=bi(),n=At(),i=me()("species"),r=Array;return Zn=function(o){var a;return t(o)&&(a=o.constructor,(e(a)&&(a===r||t(a.prototype))||n(a)&&null===(a=a[i]))&&(a=void 0)),void 0===a?r:a}}function yi(){if(ni)return ei;ni=1;var t=mi();return ei=function(e,n){return new(t(e))(0===n?0:n)}}function wi(){if(ri)return ii;ri=1;var t=C(),e=me(),n=Et(),i=e("species");return ii=function(e){return n>=51||!t((function(){var t=[];return(t.constructor={})[i]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}}!function(){if(oi)return v;oi=1;var t=hi(),e=C(),n=fi(),i=At(),r=ge(),o=Cn(),a=di(),s=pi(),l=yi(),c=wi(),u=me(),h=Et(),f=u("isConcatSpreadable"),d=h>=51||!e((function(){var t=[];return t[f]=!1,t.concat()[0]!==t})),p=function(t){if(!i(t))return!1;var e=t[f];return void 0!==e?!!e:n(t)};t({target:"Array",proto:!0,arity:1,forced:!d||!c("concat")},{concat:function(t){var e,n,i,c,u,h=r(this),f=l(h,0),d=0;for(e=-1,i=arguments.length;ek;k++)if((d||k in S)&&(y=O(m=S[k],k,w),e))if(s)P[k]=y;else if(y)switch(e){case 3:return!0;case 5:return m;case 6:return k;case 2:a(P,m)}else switch(e){case 4:return!1;case 7:a(P,m)}return h?-1:c||u?u:P}};return Ti={forEach:s(0),map:s(1),filter:s(2),some:s(3),every:s(4),find:s(5),findIndex:s(6),filterReject:s(7)}}!function(){if(Ci)return Ai;Ci=1;var t=hi(),e=Ri().filter;t({target:"Array",proto:!0,forced:!wi()("filter")},{filter:function(t){return e(this,t,arguments.length>1?arguments[1]:void 0)}})}();var Ei,ji,Ni,Fi,Di,Li,_i,Vi,Bi,Hi,Mi={},Ui={};function zi(){if(ji)return Ei;ji=1;var t=In(),e=$n();return Ei=Object.keys||function(n){return t(n,e)}}function qi(){if(Di)return Fi;Di=1;var t=It();return Fi=t("document","documentElement")}function Wi(){if(_i)return Li;_i=1;var t,e=je(),n=function(){if(Ni)return Ui;Ni=1;var t=A(),e=Ee(),n=Ne(),i=je(),r=Pt(),o=zi();return Ui.f=t&&!e?Object.defineProperties:function(t,e){i(t);for(var a,s=r(e),l=o(e),c=l.length,u=0;c>u;)n.f(t,a=l[u++],s[a]);return t},Ui}(),i=$n(),r=en(),o=qi(),a=Se(),s=tn(),l="prototype",c="script",u=s("IE_PROTO"),h=function(){},f=function(t){return"<"+c+">"+t+""},d=function(t){t.write(f("")),t.close();var e=t.parentWindow.Object;return t=null,e},p=function(){try{t=new ActiveXObject("htmlfile")}catch(t){}var e,n,r;p="undefined"!=typeof document?document.domain&&t?d(t):(n=a("iframe"),r="java"+c+":",n.style.display="none",o.appendChild(n),n.src=String(r),(e=n.contentWindow.document).open(),e.write(f("document.F=Object")),e.close(),e.F):d(t);for(var s=i.length;s--;)delete p[l][i[s]];return p()};return r[u]=!0,Li=Object.create||function(t,i){var r;return null!==t?(h[l]=e(t),r=new h,h[l]=null,r[u]=t):r=p(),void 0===i?r:n.f(r,i)}}function Gi(){if(Bi)return Vi;Bi=1;var t=me(),e=Wi(),n=Ne().f,i=t("unscopables"),r=Array.prototype;return void 0===r[i]&&n(r,i,{configurable:!0,value:e(null)}),Vi=function(t){r[i][t]=!0}}!function(){if(Hi)return Mi;Hi=1;var t=hi(),e=Ri().find,n=Gi(),i="find",r=!0;i in[]&&Array(1)[i]((function(){r=!1})),t({target:"Array",proto:!0,forced:r},{find:function(t){return e(this,t,arguments.length>1?arguments[1]:void 0)}}),n(i)}();var Ki,Ji={};!function(){if(Ki)return Ji;Ki=1;var t=hi(),e=Ri().findIndex,n=Gi(),i="findIndex",r=!0;i in[]&&Array(1)[i]((function(){r=!1})),t({target:"Array",proto:!0,forced:r},{findIndex:function(t){return e(this,t,arguments.length>1?arguments[1]:void 0)}}),n(i)}();var Yi,Qi={};!function(){if(Yi)return Qi;Yi=1;var t=hi(),e=An().includes,n=C(),i=Gi();t({target:"Array",proto:!0,forced:n((function(){return!Array(1).includes()}))},{includes:function(t){return e(this,t,arguments.length>1?arguments[1]:void 0)}}),i("includes")}();var Xi,Zi,tr,er,nr,ir,rr,or,ar,sr,lr,cr,ur,hr,fr,dr,pr,gr,vr,br,mr,yr,wr,Sr,xr,Or,kr,Tr,Pr,Cr={};function Ar(){if(Zi)return Xi;Zi=1;var t=C();return Xi=function(e,n){var i=[][e];return!!i&&t((function(){i.call(null,n||function(){return 1},1)}))}}function Ir(){return nr?er:(nr=1,er={})}function $r(){if(rr)return ir;rr=1;var t=C();return ir=!t((function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype}))}function Rr(){if(ar)return or;ar=1;var t=ve(),e=Ct(),n=ge(),i=tn(),r=$r(),o=i("IE_PROTO"),a=Object,s=a.prototype;return or=r?a.getPrototypeOf:function(i){var r=n(i);if(t(r,o))return r[o];var l=r.constructor;return e(l)&&r instanceof l?l.prototype:r instanceof a?s:null}}function Er(){if(lr)return sr;lr=1;var t,e,n,i=C(),r=Ct(),o=At(),a=Wi(),s=Rr(),l=on(),c=me(),u=he(),h=c("iterator"),f=!1;return[].keys&&("next"in(n=[].keys())?(e=s(s(n)))!==Object.prototype&&(t=e):f=!0),!o(t)||i((function(){var e={};return t[h].call(e)!==e}))?t={}:u&&(t=a(t)),r(t[h])||l(t,h,(function(){return this})),sr={IteratorPrototype:t,BUGGY_SAFARI_ITERATORS:f}}function jr(){if(ur)return cr;ur=1;var t=Ne().f,e=ve(),n=me()("toStringTag");return cr=function(i,r,o){i&&!o&&(i=i.prototype),i&&!e(i,n)&&t(i,n,{configurable:!0,value:r})}}function Nr(){if(fr)return hr;fr=1;var t=Er().IteratorPrototype,e=Wi(),n=wt(),i=jr(),r=Ir(),o=function(){return this};return hr=function(a,s,l,c){var u=s+" Iterator";return a.prototype=e(t,{next:n(+!c,l)}),i(a,u,!1,!0),r[u]=o,a}}function Fr(){if(vr)return gr;vr=1;var t=At();return gr=function(e){return t(e)||null===e}}function Dr(){if(mr)return br;mr=1;var t=Fr(),e=String,n=TypeError;return br=function(i){if(t(i))return i;throw new n("Can't set "+e(i)+" as a prototype")}}function Lr(){if(wr)return yr;wr=1;var t=function(){if(pr)return dr;pr=1;var t=St(),e=Lt();return dr=function(n,i,r){try{return t(e(Object.getOwnPropertyDescriptor(n,i)[r]))}catch(t){}}}(),e=At(),n=Tt(),i=Dr();return yr=Object.setPrototypeOf||("__proto__"in{}?function(){var r,o=!1,a={};try{(r=t(Object.prototype,"__proto__","set"))(a,[]),o=a instanceof Array}catch(t){}return function(t,a){return n(t),i(a),e(t)?(o?r(t,a):t.__proto__=a,t):t}}():void 0)}function _r(){if(xr)return Sr;xr=1;var t=hi(),e=$(),n=he(),i=Xe(),r=Ct(),o=Nr(),a=Rr(),s=Lr(),l=jr(),c=Fe(),u=on(),h=me(),f=Ir(),d=Er(),p=i.PROPER,g=i.CONFIGURABLE,v=d.IteratorPrototype,b=d.BUGGY_SAFARI_ITERATORS,m=h("iterator"),y="keys",w="values",S="entries",x=function(){return this};return Sr=function(i,h,d,O,k,T,P){o(d,h,O);var C,A,I,$=function(t){if(t===k&&F)return F;if(!b&&t&&t in j)return j[t];switch(t){case y:case w:case S:return function(){return new d(this,t)}}return function(){return new d(this)}},R=h+" Iterator",E=!1,j=i.prototype,N=j[m]||j["@@iterator"]||k&&j[k],F=!b&&N||$(k),D="Array"===h&&j.entries||N;if(D&&(C=a(D.call(new i)))!==Object.prototype&&C.next&&(n||a(C)===v||(s?s(C,v):r(C[m])||u(C,m,x)),l(C,R,!0,!0),n&&(f[R]=x)),p&&k===w&&N&&N.name!==w&&(!n&&g?c(j,"name",w):(E=!0,F=function(){return e(N,this)})),k)if(A={values:$(w),keys:T?F:$(y),entries:$(S)},P)for(I in A)(b||E||!(I in j))&&u(j,I,A[I]);else t({target:h,proto:!0,forced:b||E},A);return n&&!P||j[m]===F||u(j,m,F,{name:k}),f[h]=F,A}}function Vr(){return kr?Or:(kr=1,Or=function(t,e){return{value:t,done:e}})}function Br(){if(Pr)return Tr;Pr=1;var t=Pt(),e=Gi(),n=Ir(),i=nn(),r=Ne().f,o=_r(),a=Vr(),s=he(),l=A(),c="Array Iterator",u=i.set,h=i.getterFor(c);Tr=o(Array,"Array",(function(e,n){u(this,{type:c,target:t(e),index:0,kind:n})}),(function(){var t=h(this),e=t.target,n=t.index++;if(!e||n>=e.length)return t.target=null,a(void 0,!0);switch(t.kind){case"keys":return a(n,!1);case"values":return a(e[n],!1)}return a([n,e[n]],!1)}),"values");var f=n.Arguments=n.Array;if(e("keys"),e("values"),e("entries"),!s&&l&&"values"!==f.name)try{r(f,"name",{value:"values"})}catch(t){}return Tr}!function(){if(tr)return Cr;tr=1;var t=hi(),e=Ii(),n=An().indexOf,i=Ar(),r=e([].indexOf),o=!!r&&1/r([1],1,-0)<0;t({target:"Array",proto:!0,forced:o||!i("indexOf")},{indexOf:function(t){var e=arguments.length>1?arguments[1]:void 0;return o?r(this,t,e)||0:n(this,t,e)}})}(),Br();var Hr,Mr={};!function(){if(Hr)return Mr;Hr=1;var t=hi(),e=St(),n=Ot(),i=Pt(),r=Ar(),o=e([].join);t({target:"Array",proto:!0,forced:n!==Object||!r("join",",")},{join:function(t){return o(i(this),void 0===t?",":t)}})}();var Ur,zr={};!function(){if(Ur)return zr;Ur=1;var t=hi(),e=Ri().map;t({target:"Array",proto:!0,forced:!wi()("map")},{map:function(t){return e(this,t,arguments.length>1?arguments[1]:void 0)}})}();var qr,Wr={};!function(){if(qr)return Wr;qr=1;var t=hi(),e=St(),n=fi(),i=e([].reverse),r=[1,2];t({target:"Array",proto:!0,forced:String(r)===String(r.reverse())},{reverse:function(){return n(this)&&(this.length=this.length),i(this)}})}();var Gr,Kr,Jr,Yr={};function Qr(){if(Kr)return Gr;Kr=1;var t=St();return Gr=t([].slice)}!function(){if(Jr)return Yr;Jr=1;var t=hi(),e=fi(),n=bi(),i=At(),r=Tn(),o=Cn(),a=Pt(),s=pi(),l=me(),c=wi(),u=Qr(),h=c("slice"),f=l("species"),d=Array,p=Math.max;t({target:"Array",proto:!0,forced:!h},{slice:function(t,l){var c,h,g,v=a(this),b=o(v),m=r(t,b),y=r(void 0===l?b:l,b);if(e(v)&&(c=v.constructor,(n(c)&&(c===d||e(c.prototype))||i(c)&&null===(c=c[f]))&&(c=void 0),c===d||void 0===c))return u(v,m,y);for(h=new(void 0===c?d:c)(p(y-m,0)),g=0;m0;)i[s]=i[--s];s!==l++&&(i[s]=a)}else for(var c=e(o/2),u=n(t(i,0,c),r),h=n(t(i,c),r),f=u.length,d=h.length,p=0,g=0;p3)){if(h)return!0;if(d)return d<603;var t,e,n,i,r="";for(t=65;t<76;t++){switch(e=String.fromCharCode(t),t){case 66:case 69:case 70:case 72:n=3;break;case 68:case 71:n=4;break;default:n=2}for(i=0;i<47;i++)p.push({k:e+i,v:n})}for(p.sort((function(t,e){return e.v-t.v})),i=0;ia(n)?1:-1}}(t)),s=r(u),c=0;cw-p+d;v--)c(y,v-1)}else if(d>p)for(v=w-p;v>S;v--)m=v+d-1,(b=v+p-1)in y?y[m]=y[b]:c(y,m);for(v=0;v2)if(c=m(c),43===(e=T(c,0))||45===e){if(88===(n=T(c,2))||120===n)return NaN}else if(48===e){switch(T(c,1)){case 66:case 98:i=2,r=49;break;case 79:case 111:i=8,r=55;break;default:return+c}for(a=(o=k(c,2)).length,s=0;sr)return NaN;return parseInt(o,i)}return+c},I=a(y,!w(" 0o1")||!w("0b1")||w("+0x1")),$=function(t){var e,n=arguments.length<1?0:w(function(t){var e=h(t,"number");return"bigint"==typeof e?e:P(e)}(t));return c(x,e=this)&&f((function(){v(e)}))?l(Object(n),this,$):n};$.prototype=x,I&&!e&&(x.constructor=$),t({global:!0,constructor:!0,wrap:!0,forced:I},{Number:$});var R=function(t,e){for(var i,r=n?d(e):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,isFinite,isInteger,isNaN,isSafeInteger,parseFloat,parseInt,fromString,range".split(","),o=0;r.length>o;o++)s(e,i=r[o])&&!s(t,i)&&g(t,i,p(e,i))};e&&S&&R(r[y],S),(I||e)&&R(r[y],w)}();var Vo,Bo,Ho,Mo={};!function(){if(Ho)return Mo;Ho=1;var t=hi(),e=function(){if(Bo)return Vo;Bo=1;var t=A(),e=St(),n=$(),i=C(),r=zi(),o=si(),a=yt(),s=ge(),l=Ot(),c=Object.assign,u=Object.defineProperty,h=e([].concat);return Vo=!c||i((function(){if(t&&1!==c({b:1},c(u({},"a",{enumerable:!0,get:function(){u(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},n={},i=Symbol("assign detection"),o="abcdefghijklmnopqrst";return e[i]=7,o.split("").forEach((function(t){n[t]=t})),7!==c({},e)[i]||r(c({},n)).join("")!==o}))?function(e,i){for(var c=s(e),u=arguments.length,f=1,d=o.f,p=a.f;u>f;)for(var g,v=l(arguments[f++]),b=d?h(r(v),d(v)):r(v),m=b.length,y=0;m>y;)g=b[y++],t&&!n(p,v,g)||(c[g]=v[g]);return c}:c,Vo}();t({target:"Object",stat:!0,arity:2,forced:Object.assign!==e},{assign:e})}();var Uo,zo,qo,Wo={};!function(){if(qo)return Wo;qo=1;var t=hi(),e=function(){if(zo)return Uo;zo=1;var t=A(),e=C(),n=St(),i=Rr(),r=zi(),o=Pt(),a=n(yt().f),s=n([].push),l=t&&e((function(){var t=Object.create(null);return t[2]=2,!a(t,2)})),c=function(e){return function(n){for(var c,u=o(n),h=r(u),f=l&&null===i(u),d=h.length,p=0,g=[];d>p;)c=h[p++],t&&!(f?c in u:a(u,c))||s(g,e?[c,u[c]]:u[c]);return g}};return Uo={entries:c(!0),values:c(!1)}}().entries;t({target:"Object",stat:!0},{entries:function(t){return e(t)}})}();var Go,Ko={};!function(){if(Go)return Ko;Go=1;var t=hi(),e=ge(),n=zi();t({target:"Object",stat:!0,forced:C()((function(){n(1)}))},{keys:function(t){return n(e(t))}})}();var Jo,Yo,Qo,Xo={};!function(){if(Qo)return Xo;Qo=1;var t=gi(),e=on(),n=function(){if(Yo)return Jo;Yo=1;var t=gi(),e=vi();return Jo=t?{}.toString:function(){return"[object "+e(this)+"]"}}();t||e(Object.prototype,"toString",n,{unsafe:!0})}();var Zo,ta,ea,na={};!function(){if(ea)return na;ea=1;var t=hi(),e=function(){if(ta)return Zo;ta=1;var t=b(),e=C(),n=St(),i=po(),r=_o().trim,o=Lo(),a=n("".charAt),s=t.parseFloat,l=t.Symbol,c=l&&l.iterator,u=1/s(o+"-0")!=-1/0||c&&!e((function(){s(Object(c))}));return Zo=u?function(t){var e=r(i(t)),n=s(e);return 0===n&&"-"===a(e,0)?-0:n}:s}();t({global:!0,forced:parseFloat!==e},{parseFloat:e})}();var ia,ra,oa,aa={};!function(){if(oa)return aa;oa=1;var t=hi(),e=function(){if(ra)return ia;ra=1;var t=b(),e=C(),n=St(),i=po(),r=_o().trim,o=Lo(),a=t.parseInt,s=t.Symbol,l=s&&s.iterator,c=/^[+-]?0x/i,u=n(c.exec),h=8!==a(o+"08")||22!==a(o+"0x16")||l&&!e((function(){a(Object(l))}));return ia=h?function(t,e){var n=r(i(t));return a(n,e>>>0||(u(c,n)?16:10))}:a}();t({global:!0,forced:parseInt!==e},{parseInt:e})}();var sa,la,ca,ua,ha,fa,da,pa,ga,va,ba,ma,ya,wa,Sa,xa,Oa,ka,Ta,Pa={};function Ca(){if(la)return sa;la=1;var t=At(),e=xt(),n=me()("match");return sa=function(i){var r;return t(i)&&(void 0!==(r=i[n])?!!r:"RegExp"===e(i))}}function Aa(){if(ua)return ca;ua=1;var t=je();return ca=function(){var e=t(this),n="";return e.hasIndices&&(n+="d"),e.global&&(n+="g"),e.ignoreCase&&(n+="i"),e.multiline&&(n+="m"),e.dotAll&&(n+="s"),e.unicode&&(n+="u"),e.unicodeSets&&(n+="v"),e.sticky&&(n+="y"),n}}function Ia(){if(fa)return ha;fa=1;var t=$(),e=ve(),n=$t(),i=Aa(),r=RegExp.prototype;return ha=function(o){var a=o.flags;return void 0!==a||"flags"in r||e(o,"flags")||!n(r,o)?a:t(i,o)}}function $a(){if(pa)return da;pa=1;var t=C(),e=b().RegExp,n=t((function(){var t=e("a","y");return t.lastIndex=2,null!==t.exec("abcd")})),i=n||t((function(){return!e("a","y").sticky})),r=n||t((function(){var t=e("^r","gy");return t.lastIndex=2,null!==t.exec("str")}));return da={BROKEN_CARET:r,MISSED_STICKY:i,UNSUPPORTED_Y:n}}function Ra(){if(va)return ga;va=1;var t=Ne().f;return ga=function(e,n,i){i in e||t(e,i,{configurable:!0,get:function(){return n[i]},set:function(t){n[i]=t}})}}function Ea(){if(ma)return ba;ma=1;var t=rn(),e=Ne();return ba=function(n,i,r){return r.get&&t(r.get,i,{getter:!0}),r.set&&t(r.set,i,{setter:!0}),e.f(n,i,r)}}function ja(){if(wa)return ya;wa=1;var t=It(),e=Ea(),n=me(),i=A(),r=n("species");return ya=function(n){var o=t(n);i&&o&&!o[r]&&e(o,r,{configurable:!0,get:function(){return this}})}}function Na(){if(xa)return Sa;xa=1;var t=C(),e=b().RegExp;return Sa=t((function(){var t=e(".","s");return!(t.dotAll&&t.test("\n")&&"s"===t.flags)}))}function Fa(){if(ka)return Oa;ka=1;var t=C(),e=b().RegExp;return Oa=t((function(){var t=e("(?b)","g");return"b"!==t.exec("b").groups.a||"bc"!=="b".replace(t,"$c")}))}!function(){if(Ta)return Pa;Ta=1;var t=A(),e=b(),n=St(),i=ui(),r=Fo(),o=Fe(),a=Wi(),s=Rn().f,l=$t(),c=Ca(),u=po(),h=Ia(),f=$a(),d=Ra(),p=on(),g=C(),v=ve(),m=nn().enforce,y=ja(),w=me(),S=Na(),x=Fa(),O=w("match"),k=e.RegExp,T=k.prototype,P=e.SyntaxError,I=n(T.exec),$=n("".charAt),R=n("".replace),E=n("".indexOf),j=n("".slice),N=/^\?<[^\s\d!#%&*+<=>@^][^\s!#%&*+<=>@^]*>/,F=/a/g,D=/a/g,L=new k(F)!==F,_=f.MISSED_STICKY,V=f.UNSUPPORTED_Y,B=t&&(!L||_||S||x||g((function(){return D[O]=!1,k(F)!==F||k(D)===D||"/a/i"!==String(k(F,"i"))})));if(i("RegExp",B)){for(var H=function(t,e){var n,i,s,f,d,p,g=l(T,this),b=c(t),y=void 0===e,w=[],O=t;if(!g&&b&&y&&t.constructor===H)return t;if((b||l(T,t))&&(t=t.source,y&&(e=h(O))),t=void 0===t?"":u(t),e=void 0===e?"":u(e),O=t,S&&"dotAll"in F&&(i=!!e&&E(e,"s")>-1)&&(e=R(e,/s/g,"")),n=e,_&&"sticky"in F&&(s=!!e&&E(e,"y")>-1)&&V&&(e=R(e,/y/g,"")),x&&(f=function(t){for(var e,n=t.length,i=0,r="",o=[],s=a(null),l=!1,c=!1,u=0,h="";i<=n;i++){if("\\"===(e=$(t,i)))e+=$(t,++i);else if("]"===e)l=!1;else if(!l)switch(!0){case"["===e:l=!0;break;case"("===e:if(r+=e,"?:"===j(t,i+1,i+3))continue;I(N,j(t,i+1))&&(i+=2,c=!0),u++;continue;case">"===e&&c:if(""===h||v(s,h))throw new P("Invalid capture group name");s[h]=!0,o[o.length]=[h,u],c=!1,h="";continue}c?h+=e:r+=e}return[r,o]}(t),t=f[0],w=f[1]),d=r(k(t,e),g?this:T,H),(i||s||w.length)&&(p=m(d),i&&(p.dotAll=!0,p.raw=H(function(t){for(var e,n=t.length,i=0,r="",o=!1;i<=n;i++)"\\"!==(e=$(t,i))?o||"."!==e?("["===e?o=!0:"]"===e&&(o=!1),r+=e):r+="[\\s\\S]":r+=e+$(t,++i);return r}(t),n)),s&&(p.sticky=!0),w.length&&(p.groups=w)),t!==O)try{o(d,"source",""===O?"(?:)":O)}catch(t){}return d},M=s(k),U=0;M.length>U;)d(H,k,M[U++]);T.constructor=H,H.prototype=T,p(e,"RegExp",H,{constructor:!0})}y("RegExp")}();var Da,La,_a,Va={};function Ba(){if(La)return Da;La=1;var t,e,n=$(),i=St(),r=po(),o=Aa(),a=$a(),s=pe(),l=Wi(),c=nn().get,u=Na(),h=Fa(),f=s("native-string-replace",String.prototype.replace),d=RegExp.prototype.exec,p=d,g=i("".charAt),v=i("".indexOf),b=i("".replace),m=i("".slice),y=(e=/b*/g,n(d,t=/a/,"a"),n(d,e,"a"),0!==t.lastIndex||0!==e.lastIndex),w=a.BROKEN_CARET,S=void 0!==/()??/.exec("")[1];return(y||S||w||u||h)&&(p=function(t){var e,i,a,s,u,h,x,O=this,k=c(O),T=r(t),P=k.raw;if(P)return P.lastIndex=O.lastIndex,e=n(p,P,T),O.lastIndex=P.lastIndex,e;var C=k.groups,A=w&&O.sticky,I=n(o,O),$=O.source,R=0,E=T;if(A&&(I=b(I,"y",""),-1===v(I,"g")&&(I+="g"),E=m(T,O.lastIndex),O.lastIndex>0&&(!O.multiline||O.multiline&&"\n"!==g(T,O.lastIndex-1))&&($="(?: "+$+")",E=" "+E,R++),i=new RegExp("^(?:"+$+")",I)),S&&(i=new RegExp("^"+$+"$(?!\\s)",I)),y&&(a=O.lastIndex),s=n(d,A?i:O,E),A?s?(s.input=m(s.input,R),s[0]=m(s[0],R),s.index=O.lastIndex,O.lastIndex+=s[0].length):O.lastIndex=0:y&&s&&(O.lastIndex=O.global?s.index+s[0].length:a),S&&s&&s.length>1&&n(f,s[0],i,(function(){for(u=1;u1?arguments[1]:void 0)}})}();var Xa,Za,ts,es,ns,is,rs,os,as,ss,ls,cs,us,hs={};function fs(){if(es)return ts;es=1,Ha();var t=$(),e=on(),n=Ba(),i=C(),r=me(),o=Fe(),a=r("species"),s=RegExp.prototype;return ts=function(l,c,u,h){var f=r(l),d=!i((function(){var t={};return t[f]=function(){return 7},7!==""[l](t)})),p=d&&!i((function(){var t=!1,e=/a/;return"split"===l&&((e={}).constructor={},e.constructor[a]=function(){return e},e.flags="",e[f]=/./[f]),e.exec=function(){return t=!0,null},e[f](""),!t}));if(!d||!p||u){var g=/./[f],v=c(f,""[l],(function(e,i,r,o,a){var l=i.exec;return l===n||l===s.exec?d&&!a?{done:!0,value:t(g,i,r,o)}:{done:!0,value:t(e,r,i,o)}:{done:!1}}));e(String.prototype,l,v[0]),e(s,f,v[1])}h&&o(s[f],"sham",!0)}}function ds(){if(is)return ns;is=1;var t=St(),e=kn(),n=po(),i=Tt(),r=t("".charAt),o=t("".charCodeAt),a=t("".slice),s=function(t){return function(s,l){var c,u,h=n(i(s)),f=e(l),d=h.length;return f<0||f>=d?t?"":void 0:(c=o(h,f))<55296||c>56319||f+1===d||(u=o(h,f+1))<56320||u>57343?t?r(h,f):c:t?a(h,f,f+2):u-56320+(c-55296<<10)+65536}};return ns={codeAt:s(!1),charAt:s(!0)}}function ps(){if(os)return rs;os=1;var t=ds().charAt;return rs=function(e,n,i){return n+(i?t(e,n).length:1)}}function gs(){if(ss)return as;ss=1;var t=St(),e=ge(),n=Math.floor,i=t("".charAt),r=t("".replace),o=t("".slice),a=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,s=/\$([$&'`]|\d{1,2})/g;return as=function(t,l,c,u,h,f){var d=c+t.length,p=u.length,g=s;return void 0!==h&&(h=e(h),g=a),r(f,g,(function(e,r){var a;switch(i(r,0)){case"$":return"$";case"&":return t;case"`":return o(l,0,c);case"'":return o(l,d);case"<":a=h[o(r,1,-1)];break;default:var s=+r;if(0===s)return e;if(s>p){var f=n(s/10);return 0===f?e:f<=p?void 0===u[f-1]?i(r,1):u[f-1]+i(r,1):e}a=u[s-1]}return void 0===a?"":a}))}}function vs(){if(cs)return ls;cs=1;var t=$(),e=je(),n=Ct(),i=xt(),r=Ba(),o=TypeError;return ls=function(a,s){var l=a.exec;if(n(l)){var c=t(l,a,s);return null!==c&&e(c),c}if("RegExp"===i(a))return t(r,a,s);throw new o("RegExp#exec called on incompatible receiver")}}!function(){if(us)return hs;us=1;var t=function(){if(Za)return Xa;Za=1;var t=I(),e=Function.prototype,n=e.apply,i=e.call;return Xa="object"==typeof Reflect&&Reflect.apply||(t?i.bind(n):function(){return i.apply(n,arguments)}),Xa}(),e=$(),n=St(),i=fs(),r=C(),o=je(),a=Ct(),s=kt(),l=kn(),c=Pn(),u=po(),h=Tt(),f=ps(),d=_t(),p=gs(),g=vs(),v=me()("replace"),b=Math.max,m=Math.min,y=n([].concat),w=n([].push),S=n("".indexOf),x=n("".slice),O="$0"==="a".replace(/./,"$0"),k=!!/./[v]&&""===/./[v]("a","$0");i("replace",(function(n,i,r){var O=k?"$":"$0";return[function(t,n){var r=h(this),o=s(t)?void 0:d(t,v);return o?e(o,t,r,n):e(i,u(r),t,n)},function(e,n){var s=o(this),h=u(e);if("string"==typeof n&&-1===S(n,O)&&-1===S(n,"$<")){var d=r(i,s,h,n);if(d.done)return d.value}var v=a(n);v||(n=u(n));var k,T=s.global;T&&(k=s.unicode,s.lastIndex=0);for(var P,C=[];null!==(P=g(s,h))&&(w(C,P),T);){""===u(P[0])&&(s.lastIndex=f(h,c(s.lastIndex),k))}for(var A,I="",$=0,R=0;R=$&&(I+=x(h,$,N)+E,$=N+j.length)}return I+x(h,$)}]}),!!r((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")}))||!O||k)}();var bs,ms,ys,ws={};function Ss(){return ms?bs:(ms=1,bs=Object.is||function(t,e){return t===e?0!==t||1/t==1/e:t!=t&&e!=e})}!function(){if(ys)return ws;ys=1;var t=$(),e=fs(),n=je(),i=kt(),r=Tt(),o=Ss(),a=po(),s=_t(),l=vs();e("search",(function(e,c,u){return[function(n){var o=r(this),l=i(n)?void 0:s(n,e);return l?t(l,n,o):new RegExp(n)[e](a(o))},function(t){var e=n(this),i=a(t),r=u(c,e,i);if(r.done)return r.value;var s=e.lastIndex;o(s,0)||(e.lastIndex=0);var h=l(e,i);return o(e.lastIndex,s)||(e.lastIndex=s),null===h?-1:h.index}]}))}();var xs,Os,ks,Ts,Ps,Cs={};function As(){if(Os)return xs;Os=1;var t=bi(),e=Dt(),n=TypeError;return xs=function(i){if(t(i))return i;throw new n(e(i)+" is not a constructor")}}function Is(){if(Ts)return ks;Ts=1;var t=je(),e=As(),n=kt(),i=me()("species");return ks=function(r,o){var a,s=t(r).constructor;return void 0===s||n(a=t(s)[i])?o:e(a)}}!function(){if(Ps)return Cs;Ps=1;var t=$(),e=St(),n=fs(),i=je(),r=kt(),o=Tt(),a=Is(),s=ps(),l=Pn(),c=po(),u=_t(),h=vs(),f=$a(),d=C(),p=f.UNSUPPORTED_Y,g=Math.min,v=e([].push),b=e("".slice),m=!d((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]})),y="c"==="abbc".split(/(b)*/)[1]||4!=="test".split(/(?:)/,-1).length||2!=="ab".split(/(?:ab)*/).length||4!==".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length;n("split",(function(e,n,f){var d="0".split(void 0,0).length?function(e,i){return void 0===e&&0===i?[]:t(n,this,e,i)}:n;return[function(n,i){var a=o(this),s=r(n)?void 0:u(n,e);return s?t(s,n,a,i):t(d,c(a),n,i)},function(t,e){var r=i(this),o=c(t);if(!y){var u=f(d,r,o,e,d!==n);if(u.done)return u.value}var m=a(r,RegExp),w=r.unicode,S=(r.ignoreCase?"i":"")+(r.multiline?"m":"")+(r.unicode?"u":"")+(p?"g":"y"),x=new m(p?"^(?:"+r.source+")":r,S),O=void 0===e?4294967295:e>>>0;if(0===O)return[];if(0===o.length)return null===h(x,o)?[o]:[];for(var k=0,T=0,P=[];T1?arguments[1]:void 0)},_s}(),r=Fe(),o=function(t){if(t&&t.forEach!==i)try{r(t,"forEach",i)}catch(e){t.forEach=i}};for(var a in e)e[a]&&o(t[a]&&t[a].prototype);o(n)}();var zs,qs={};!function(){if(zs)return qs;zs=1;var t=b(),e=Ms(),n=Us(),i=Br(),r=Fe(),o=jr(),a=me()("iterator"),s=i.values,l=function(t,n){if(t){if(t[a]!==s)try{r(t,a,s)}catch(e){t[a]=s}if(o(t,n,!0),e[n])for(var l in i)if(t[l]!==i[l])try{r(t,l,i[l])}catch(e){t[l]=i[l]}}};for(var c in e)l(t[c]&&t[c].prototype,c);l(n,"DOMTokenList")}();var Ws,Gs={};!function(){if(Ws)return Gs;Ws=1;var t=hi(),e=C(),n=ge(),i=Rr(),r=$r();t({target:"Object",stat:!0,forced:e((function(){i(1)})),sham:!r},{getPrototypeOf:function(t){return i(n(t))}})}();var Ks,Js={};!function(){if(Ks)return Js;Ks=1;var t,e=hi(),n=Ii(),i=Oe().f,r=Pn(),o=po(),a=Ya(),s=Tt(),l=Qa(),c=he(),u=n("".slice),h=Math.min,f=l("endsWith");e({target:"String",proto:!0,forced:!!(c||f||(t=i(String.prototype,"endsWith"),!t||t.writable))&&!f},{endsWith:function(t){var e=o(s(this));a(t);var n=arguments.length>1?arguments[1]:void 0,i=e.length,l=void 0===n?i:h(r(n),i),c=o(t);return u(e,l-c.length,l)===c}})}();var Ys,Qs={};!function(){if(Ys)return Qs;Ys=1;var t=ds().charAt,e=po(),n=nn(),i=_r(),r=Vr(),o="String Iterator",a=n.set,s=n.getterFor(o);i(String,"String",(function(t){a(this,{type:o,string:e(t),index:0})}),(function(){var e,n=s(this),i=n.string,o=n.index;return o>=i.length?r(void 0,!0):(e=t(i,o),n.index+=e.length,r(e,!1))}))}();var Xs,Zs={};!function(){if(Xs)return Zs;Xs=1;var t=$(),e=fs(),n=je(),i=kt(),r=Pn(),o=po(),a=Tt(),s=_t(),l=ps(),c=vs();e("match",(function(e,u,h){return[function(n){var r=a(this),l=i(n)?void 0:s(n,e);return l?t(l,n,r):new RegExp(n)[e](o(r))},function(t){var e=n(this),i=o(t),a=h(u,e,i);if(a.done)return a.value;if(!e.global)return c(e,i);var s=e.unicode;e.lastIndex=0;for(var f,d=[],p=0;null!==(f=c(e,i));){var g=o(f[0]);d[p]=g,""===g&&(e.lastIndex=l(i,r(e.lastIndex),s)),p++}return 0===p?null:d}]}))}();var tl,el={};!function(){if(tl)return el;tl=1;var t,e=hi(),n=Ii(),i=Oe().f,r=Pn(),o=po(),a=Ya(),s=Tt(),l=Qa(),c=he(),u=n("".slice),h=Math.min,f=l("startsWith");e({target:"String",proto:!0,forced:!!(c||f||(t=i(String.prototype,"startsWith"),!t||t.writable))&&!f},{startsWith:function(t){var e=o(s(this));a(t);var n=r(h(arguments.length>1?arguments[1]:void 0,e.length)),i=o(t);return u(e,n,n+i.length)===i}})}();var nl,il,rl,ol,al,sl,ll,cl,ul,hl,fl,dl,pl,gl,vl,bl,ml,yl,wl={};function Sl(){if(rl)return il;rl=1;var t=b(),e=A(),n=Object.getOwnPropertyDescriptor;return il=function(i){if(!e)return t[i];var r=n(t,i);return r&&r.value}}function xl(){if(ll)return sl;ll=1;var t=on();return sl=function(e,n,i){for(var r in n)t(e,r,n[r],i);return e}}function Ol(){if(ul)return cl;ul=1;var t=$t(),e=TypeError;return cl=function(n,i){if(t(i,n))return n;throw new e("Incorrect invocation")}}function kl(){if(fl)return hl;fl=1;var t=vi(),e=_t(),n=kt(),i=Ir(),r=me()("iterator");return hl=function(o){if(!n(o))return e(o,r)||e(o,"@@iterator")||i[t(o)]}}function Tl(){if(pl)return dl;pl=1;var t=$(),e=Lt(),n=je(),i=Dt(),r=kl(),o=TypeError;return dl=function(a,s){var l=arguments.length<2?r(a):s;if(e(l))return n(t(l,a));throw new o(i(a)+" is not iterable")},dl}function Pl(){if(vl)return gl;vl=1;var t=TypeError;return gl=function(e,n){if(el;){if(e=+arguments[l++],n(e,1114111)!==e)throw new i(e+" is not a valid code point");o[l]=e<65536?r(e):r(55296+((e-=65536)>>10),e%1024+56320)}return a(o,"")}})}();var t=hi(),e=b(),n=Sl(),i=It(),r=$(),o=St(),a=A(),s=function(){if(al)return ol;al=1;var t=C(),e=me(),n=A(),i=he(),r=e("iterator");return ol=!t((function(){var t=new URL("b?a=1&b=2&c=3","https://a"),e=t.searchParams,o=new URLSearchParams("a=1&a=2&b=3"),a="";return t.pathname="c%20d",e.forEach((function(t,n){e.delete("b"),a+=n+t})),o.delete("a",2),o.delete("b",void 0),i&&(!t.toJSON||!o.has("a",1)||o.has("a",2)||!o.has("a",void 0)||o.has("b"))||!e.size&&(i||!n)||!e.sort||"https://a/c%20d?a=1&c=3"!==t.href||"3"!==e.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!e[r]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("https://тест").host||"#%D0%B1"!==new URL("https://a#б").hash||"a1c3"!==a||"x"!==new URL("https://x",void 0).host}))}(),l=on(),c=Ea(),u=xl(),h=jr(),f=Nr(),d=nn(),p=Ol(),g=Ct(),v=ve(),m=$i(),y=vi(),w=je(),S=At(),x=po(),O=Wi(),k=wt(),T=Tl(),P=kl(),I=Vr(),R=Pl(),E=me(),j=go(),N=E("iterator"),F="URLSearchParams",D=F+"Iterator",L=d.set,_=d.getterFor(F),V=d.getterFor(D),B=n("fetch"),H=n("Request"),M=n("Headers"),U=H&&H.prototype,z=M&&M.prototype,q=e.TypeError,W=e.encodeURIComponent,G=String.fromCharCode,K=i("String","fromCodePoint"),J=parseInt,Y=o("".charAt),Q=o([].join),X=o([].push),Z=o("".replace),tt=o([].shift),et=o([].splice),nt=o("".split),it=o("".slice),rt=o(/./.exec),ot=/\+/g,at=/^[0-9a-f]+$/i,st=function(t,e){var n=it(t,e,e+2);return rt(at,n)?J(n,16):NaN},lt=function(t){for(var e=0,n=128;n>0&&t&n;n>>=1)e++;return e},ct=function(t){var e=null;switch(t.length){case 1:e=t[0];break;case 2:e=(31&t[0])<<6|63&t[1];break;case 3:e=(15&t[0])<<12|(63&t[1])<<6|63&t[2];break;case 4:e=(7&t[0])<<18|(63&t[1])<<12|(63&t[2])<<6|63&t[3]}return e>1114111?null:e},ut=function(t){for(var e=(t=Z(t,ot," ")).length,n="",i=0;ie){n+="%",i++;continue}var o=st(t,i+1);if(o!=o){n+=r,i++;continue}i+=2;var a=lt(o);if(0===a)r=G(o);else{if(1===a||a>4){n+="�",i++;continue}for(var s=[o],l=1;le||"%"!==Y(t,i));){var c=st(t,i+1);if(c!=c){i+=3;break}if(c>191||c<128)break;X(s,c),i+=2,l++}if(s.length!==a){n+="�";continue}var u=ct(s);null===u?n+="�":r=K(u)}}n+=r,i++}return n},ht=/[!'()~]|%20/g,ft={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"},dt=function(t){return ft[t]},pt=function(t){return Z(W(t),ht,dt)},gt=f((function(t,e){L(this,{type:D,target:_(t).entries,index:0,kind:e})}),F,(function(){var t=V(this),e=t.target,n=t.index++;if(!e||n>=e.length)return t.target=null,I(void 0,!0);var i=e[n];switch(t.kind){case"keys":return I(i.key,!1);case"values":return I(i.value,!1)}return I([i.key,i.value],!1)}),!0),vt=function(t){this.entries=[],this.url=null,void 0!==t&&(S(t)?this.parseObject(t):this.parseQuery("string"==typeof t?"?"===Y(t,0)?it(t,1):t:x(t)))};vt.prototype={type:F,bindURL:function(t){this.url=t,this.update()},parseObject:function(t){var e,n,i,o,a,s,l,c=this.entries,u=P(t);if(u)for(n=(e=T(t,u)).next;!(i=r(n,e)).done;){if(a=(o=T(w(i.value))).next,(s=r(a,o)).done||(l=r(a,o)).done||!r(a,o).done)throw new q("Expected sequence with length 2");X(c,{key:x(s.value),value:x(l.value)})}else for(var h in t)v(t,h)&&X(c,{key:h,value:x(t[h])})},parseQuery:function(t){if(t)for(var e,n,i=this.entries,r=nt(t,"&"),o=0;o0?arguments[0]:void 0));a||(this.size=t.entries.length)},mt=bt.prototype;if(u(mt,{append:function(t,e){var n=_(this);R(arguments.length,2),X(n.entries,{key:x(t),value:x(e)}),a||this.length++,n.updateURL()},delete:function(t){for(var e=_(this),n=R(arguments.length,1),i=e.entries,r=x(t),o=n<2?void 0:arguments[1],s=void 0===o?o:x(o),l=0;le.key?1:-1})),t.updateURL()},forEach:function(t){for(var e,n=_(this).entries,i=m(t,arguments.length>1?arguments[1]:void 0),r=0;r1?Ot(arguments[1]):{})}}),g(H)){var kt=function(t){return p(this,U),new H(t,arguments.length>1?Ot(arguments[1]):{})};U.constructor=kt,kt.prototype=U,t({global:!0,constructor:!0,dontCallGetSet:!0,forced:!0},{Request:kt})}}return bl={URLSearchParams:bt,getState:_}}yl||(yl=1,Cl());var Al={getBootstrapVersion:function(){var e=5;try{var n=t.fn.dropdown.Constructor.VERSION;void 0!==n&&(e=parseInt(n,10))}catch(t){}try{var i=bootstrap.Tooltip.VERSION;void 0!==i&&(e=parseInt(i,10))}catch(t){}return e},getIconsPrefix:function(t){return{bootstrap3:"glyphicon",bootstrap4:"fa",bootstrap5:"bi","bootstrap-table":"icon",bulma:"fa",foundation:"fa",materialize:"material-icons",semantic:"fa"}[t]||"fa"},getIcons:function(t){return{glyphicon:{clearSearch:"glyphicon-trash",columns:"glyphicon-th icon-th",detailClose:"glyphicon-minus icon-minus",detailOpen:"glyphicon-plus icon-plus",fullscreen:"glyphicon-fullscreen",paginationSwitchDown:"glyphicon-collapse-down icon-chevron-down",paginationSwitchUp:"glyphicon-collapse-up icon-chevron-up",refresh:"glyphicon-refresh icon-refresh",search:"glyphicon-search",toggleOff:"glyphicon-list-alt icon-list-alt",toggleOn:"glyphicon-list-alt icon-list-alt"},fa:{clearSearch:"fa-trash",columns:"fa-th-list",detailClose:"fa-minus",detailOpen:"fa-plus",fullscreen:"fa-arrows-alt",paginationSwitchDown:"fa-caret-square-down",paginationSwitchUp:"fa-caret-square-up",refresh:"fa-sync",search:"fa-search",toggleOff:"fa-toggle-off",toggleOn:"fa-toggle-on"},bi:{clearSearch:"bi-trash",columns:"bi-list-ul",detailClose:"bi-dash",detailOpen:"bi-plus",fullscreen:"bi-arrows-move",paginationSwitchDown:"bi-caret-down-square",paginationSwitchUp:"bi-caret-up-square",refresh:"bi-arrow-clockwise",search:"bi-search",toggleOff:"bi-toggle-off",toggleOn:"bi-toggle-on"},icon:{clearSearch:"icon-trash-2",columns:"icon-list",detailClose:"icon-minus",detailOpen:"icon-plus",fullscreen:"icon-maximize",paginationSwitchDown:"icon-arrow-up-circle",paginationSwitchUp:"icon-arrow-down-circle",refresh:"icon-refresh-cw",search:"icon-search",toggleOff:"icon-toggle-right",toggleOn:"icon-toggle-right"},"material-icons":{clearSearch:"delete",columns:"view_list",detailClose:"remove",detailOpen:"add",fullscreen:"fullscreen",paginationSwitchDown:"grid_on",paginationSwitchUp:"grid_off",refresh:"refresh",search:"search",sort:"sort",toggleOff:"tablet",toggleOn:"tablet_android"}}[t]||{}},getSearchInput:function(e){return"string"==typeof e.options.searchSelector?t(e.options.searchSelector):e.$toolbar.find(".search input")},extend:function(){for(var t=this,e=arguments.length,n=new Array(e),i=0;i1?e-1:0),i=1;i0&&void 0!==arguments[0]?arguments[0]:{};return 0===Object.entries(t).length&&t.constructor===Object},isNumeric:function(t){return!isNaN(parseFloat(t))&&isFinite(t)},getFieldTitle:function(t,e){var n,i=r(t);try{for(i.s();!(n=i.n()).done;){var o=n.value;if(o.field===e)return o.title}}catch(t){i.e(t)}finally{i.f()}return""},setFieldIndex:function(t){var e,n=0,i=[],o=r(t[0]);try{for(o.s();!(e=o.n()).done;){n+=+e.value.colspan||1}}catch(t){o.e(t)}finally{o.f()}for(var a=0;a1){for(var h=0,f=function(t){var e=o.filter((function(e){return e.fieldIndex===t})),n=e[e.length-1];if(e.length>1)for(var i=0;i0}}}catch(t){l.e(t)}finally{l.f()}}}catch(t){a.e(t)}finally{a.f()}if(!(t.length<2)){var p,g=r(e);try{var v=function(){var t=p.value,e=o.filter((function(e){return e.fieldIndex===t.fieldIndex}));if(e.length>1){var n,i=r(e);try{for(i.s();!(n=i.n()).done;){n.value.visible=t.visible}}catch(t){i.e(t)}finally{i.f()}}};for(g.s();!(p=g.n()).done;)v()}catch(t){g.e(t)}finally{g.f()}}},getScrollBarWidth:function(){if(void 0===this.cachedWidth){var e=t("
").addClass("fixed-table-scroll-inner"),n=t("
").addClass("fixed-table-scroll-outer");n.append(e),t("body").append(n);var i=e[0].offsetWidth;n.css("overflow","scroll");var r=e[0].offsetWidth;i===r&&(r=n[0].clientWidth),n.remove(),this.cachedWidth=i-r}return this.cachedWidth},calculateObjectValue:function(t,e,n,i){var o=e;if("string"==typeof e){var a=e.split(".");if(a.length>1){o=window;var s,l=r(a);try{for(l.s();!(s=l.n()).done;){o=o[s.value]}}catch(t){l.e(t)}finally{l.f()}}else o=window[e]}return null!==o&&"object"===h(o)?o:"function"==typeof o?o.apply(t,n||[]):!o&&"string"==typeof e&&n&&this.sprintf.apply(this,[e].concat(c(n)))?this.sprintf.apply(this,[e].concat(c(n))):i},compareObjects:function(t,e,n){var i=Object.keys(t),r=Object.keys(e);if(n&&i.length!==r.length)return!1;for(var o=0,a=i;o/g,">").replace(/"/g,""").replace(/'/g,"'"):t},unescapeHTML:function(t){return"string"==typeof t&&t?t.toString().replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'"):t},removeHTML:function(t){return t?t.toString().replace(/(<([^>]+)>)/gi,"").replace(/&[#A-Za-z0-9]+;/gi,"").trim():t},getRealDataAttr:function(t){for(var e=0,n=Object.entries(t);e3&&void 0!==arguments[3]?arguments[3]:void 0,o=t;if(void 0!==i&&(n=i),"string"!=typeof e||t.hasOwnProperty(e))return n?this.escapeHTML(t[e]):t[e];var a,s=r(e.split("."));try{for(s.s();!(a=s.n()).done;){var l=a.value;o=o&&o[l]}}catch(t){s.e(t)}finally{s.f()}return n?this.escapeHTML(o):o},isIEBrowser:function(){return navigator.userAgent.includes("MSIE ")||/Trident.*rv:11\./.test(navigator.userAgent)},findIndex:function(t,e){var n,i=r(t);try{for(i.s();!(n=i.n()).done;){var o=n.value;if(JSON.stringify(o)===JSON.stringify(e))return t.indexOf(o)}}catch(t){i.e(t)}finally{i.f()}return-1},trToData:function(e,n){var i=this,r=[],o=[];return n.each((function(n,a){var s=t(a),l={};l._id=s.attr("id"),l._class=s.attr("class"),l._data=i.getRealDataAttr(s.data()),l._style=s.attr("style"),s.find(">td,>th").each((function(r,a){for(var s=t(a),c=+s.attr("colspan")||1,u=+s.attr("rowspan")||1,h=r;o[n]&&o[n][h];h++);for(var f=h;fe?n:0;if(i.sortEmptyLast){if(""===t)return 1;if(""===e)return-1}return t===e?0:("string"!=typeof t&&(t=t.toString()),-1===t.localeCompare(e)?-1*n:n)},getEventName:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=e||"".concat(+new Date).concat(~~(1e6*Math.random())),"".concat(t,"-").concat(e)},hasDetailViewIcon:function(t){return t.detailView&&t.detailViewIcon&&!t.cardView},getDetailViewIndexOffset:function(t){return this.hasDetailViewIcon(t)&&"right"!==t.detailViewAlign?1:0},checkAutoMergeCells:function(t){var e,n=r(t);try{for(n.s();!(e=n.n()).done;)for(var i=e.value,o=0,a=Object.keys(i);o0){var i=e.substring(0,n).trim(),r=e.substring(n+1).trim();t.style.setProperty(i,r)}}));else if(Array.isArray(e)){var n,i=r(e);try{for(i.s();!(n=i.n()).done;){var o=n.value;this.parseStyle(t,o)}}catch(t){i.e(t)}finally{i.f()}}else if("object"===h(e))for(var a=0,s=Object.entries(e);a',icon:'',inputGroup:'
%s%s
',pageDropdown:['"],pageDropdownItem:'
',pagination:['
    ',"
"],paginationItem:'
  • %s
  • ',searchButton:'',searchClearButton:'',searchInput:'',toolbarDropdown:['"],toolbarDropdownItem:'',toolbarDropdownSeparator:'
  • '}},4:{classes:{buttonActive:"active",buttons:"secondary",buttonsDropdown:"btn-group",buttonsGroup:"btn-group",buttonsPrefix:"btn",dropdownActive:"active",dropup:"dropup",input:"form-control",inputGroup:"btn-group",inputPrefix:"form-control-",paginationActive:"active",paginationDropdown:"btn-group dropdown",pull:"float",select:"form-control"},html:{dropdownCaret:'',icon:'',inputGroup:'
    %s
    %s
    ',pageDropdown:['"],pageDropdownItem:'%s',pagination:['
      ',"
    "],paginationItem:'
  • %s
  • ',searchButton:'',searchClearButton:'',searchInput:'',toolbarDropdown:['"],toolbarDropdownItem:'',toolbarDropdownSeparator:''}},5:{classes:{buttonActive:"active",buttons:"secondary",buttonsDropdown:"btn-group",buttonsGroup:"btn-group",buttonsPrefix:"btn",dropdownActive:"active",dropup:"dropup",input:"form-control",inputGroup:"btn-group",inputPrefix:"form-control-",paginationActive:"active",paginationDropdown:"btn-group dropdown",pull:"float",select:"form-select"},html:{dataToggle:"data-bs-toggle",dropdownCaret:'',icon:'',inputGroup:'
    %s%s
    ',pageDropdown:['"],pageDropdownItem:'%s',pagination:['
      ',"
    "],paginationItem:'
  • %s
  • ',searchButton:'',searchClearButton:'',searchInput:'',toolbarDropdown:['"],toolbarDropdownItem:'',toolbarDropdownSeparator:''}}}[Il],Rl={ajax:void 0,ajaxOptions:{},buttons:{},buttonsAlign:"right",buttonsAttributeTitle:"title",buttonsClass:$l.classes.buttons,buttonsOrder:["paginationSwitch","refresh","toggle","fullscreen","columns"],buttonsPrefix:$l.classes.buttonsPrefix,buttonsToolbar:void 0,cache:!0,cardView:!1,checkboxHeader:!0,classes:"table table-bordered table-hover",clickToSelect:!1,columns:[[]],contentType:"application/json",customSearch:void 0,customSort:void 0,data:[],dataField:"rows",dataType:"json",detailFilter:function(t,e){return!0},detailFormatter:function(t,e){return""},detailView:!1,detailViewAlign:"left",detailViewByClick:!1,detailViewIcon:!0,escape:!1,escapeTitle:!0,filterOptions:{filterAlgorithm:"and"},fixedScroll:!1,footerField:"footer",footerStyle:function(t){return{}},headerStyle:function(t){return{}},height:void 0,icons:{},iconSize:void 0,iconsPrefix:void 0,idField:void 0,ignoreClickToSelectOn:function(t){var e=t.tagName;return["A","BUTTON"].includes(e)},loadingFontSize:"auto",loadingTemplate:function(t){return'\n '.concat(t,'\n \n \n ')},locale:void 0,maintainMetaData:!1,method:"get",minimumCountColumns:1,multipleSelectRow:!1,pageList:[10,25,50,100],pageNumber:1,pageSize:10,pagination:!1,paginationDetailHAlign:"left",paginationHAlign:"right",paginationLoadMore:!1,paginationLoop:!0,paginationNextText:"›",paginationPagesBySide:1,paginationParts:["pageInfo","pageSize","pageList"],paginationPreText:"‹",paginationSuccessivelySize:5,paginationUseIntermediate:!1,paginationVAlign:"bottom",queryParams:function(t){return t},queryParamsType:"limit",regexSearch:!1,rememberOrder:!1,responseHandler:function(t){return t},rowAttributes:function(t,e){return{}},rowStyle:function(t,e){return{}},search:!1,searchable:!1,searchAccentNeutralise:!1,searchAlign:"right",searchHighlight:!1,searchOnEnterKey:!1,searchSelector:!1,searchText:"",searchTimeOut:500,selectItemName:"btSelectItem",serverSort:!0,showButtonIcons:!0,showButtonText:!1,showColumns:!1,showColumnsSearch:!1,showColumnsToggleAll:!1,showExtendedPagination:!1,showFooter:!1,showFullscreen:!1,showHeader:!0,showPaginationSwitch:!1,showRefresh:!1,showSearchButton:!1,showSearchClearButton:!1,showToggle:!1,sidePagination:"client",silentSort:!0,singleSelect:!1,smartDisplay:!0,sortable:!0,sortClass:void 0,sortEmptyLast:!1,sortName:void 0,sortOrder:void 0,sortReset:!1,sortResetPage:!1,sortStable:!1,strictSearch:!1,theadClasses:"",toolbar:void 0,toolbarAlign:"left",totalField:"total",totalNotFiltered:0,totalNotFilteredField:"totalNotFiltered",totalRows:0,trimOnSearch:!0,undefinedText:"-",uniqueId:void 0,url:void 0,virtualScroll:!1,virtualScrollItemHeight:void 0,visibleSearch:!1,onAll:function(t,e){return!1},onCheck:function(t){return!1},onCheckAll:function(t){return!1},onCheckSome:function(t){return!1},onClickCell:function(t,e,n,i){return!1},onClickRow:function(t,e){return!1},onCollapseRow:function(t,e){return!1},onColumnSwitch:function(t,e){return!1},onColumnSwitchAll:function(t){return!1},onDblClickCell:function(t,e,n,i){return!1},onDblClickRow:function(t,e){return!1},onExpandRow:function(t,e,n){return!1},onLoadError:function(t){return!1},onLoadSuccess:function(t){return!1},onPageChange:function(t,e){return!1},onPostBody:function(){return!1},onPostFooter:function(){return!1},onPostHeader:function(){return!1},onPreBody:function(t){return!1},onRefresh:function(t){return!1},onRefreshOptions:function(t){return!1},onResetView:function(){return!1},onScrollBody:function(){return!1},onSearch:function(t){return!1},onSort:function(t,e){return!1},onToggle:function(t){return!1},onTogglePagination:function(t){return!1},onUncheck:function(t){return!1},onUncheckAll:function(t){return!1},onUncheckSome:function(t){return!1},onVirtualScroll:function(t,e){return!1}},El={formatLoadingMessage:function(){return"Loading, please wait"},formatRecordsPerPage:function(t){return"".concat(t," rows per page")},formatShowingRows:function(t,e,n,i){return void 0!==i&&i>0&&i>n?"Showing ".concat(t," to ").concat(e," of ").concat(n," rows (filtered from ").concat(i," total rows)"):"Showing ".concat(t," to ").concat(e," of ").concat(n," rows")},formatSRPaginationPreText:function(){return"previous page"},formatSRPaginationPageText:function(t){return"to page ".concat(t)},formatSRPaginationNextText:function(){return"next page"},formatDetailPagination:function(t){return"Showing ".concat(t," rows")},formatSearch:function(){return"Search"},formatClearSearch:function(){return"Clear Search"},formatNoMatches:function(){return"No matching records found"},formatPaginationSwitch:function(){return"Hide/Show pagination"},formatPaginationSwitchDown:function(){return"Show pagination"},formatPaginationSwitchUp:function(){return"Hide pagination"},formatRefresh:function(){return"Refresh"},formatToggleOn:function(){return"Show card view"},formatToggleOff:function(){return"Hide card view"},formatColumns:function(){return"Columns"},formatColumnsToggleAll:function(){return"Toggle all"},formatFullscreen:function(){return"Fullscreen"},formatAllRows:function(){return"All"}},jl={align:void 0,cardVisible:!0,cellStyle:void 0,checkbox:!1,checkboxEnabled:!0,class:void 0,clickToSelect:!0,colspan:void 0,detailFormatter:void 0,escape:void 0,events:void 0,falign:void 0,field:void 0,footerFormatter:void 0,footerStyle:void 0,formatter:void 0,halign:void 0,order:"asc",radio:!1,rowspan:void 0,searchable:!0,searchFormatter:!0,searchHighlightFormatter:!1,showSelectTitle:!1,sortable:!1,sorter:void 0,sortName:void 0,switchable:!0,switchableLabel:void 0,title:void 0,titleTooltip:void 0,valign:void 0,visible:!0,width:void 0,widthUnit:"px"};Object.assign(Rl,El);var Nl={COLUMN_DEFAULTS:jl,CONSTANTS:$l,DEFAULTS:Rl,EVENTS:{"all.bs.table":"onAll","check-all.bs.table":"onCheckAll","check-some.bs.table":"onCheckSome","check.bs.table":"onCheck","click-cell.bs.table":"onClickCell","click-row.bs.table":"onClickRow","collapse-row.bs.table":"onCollapseRow","column-switch-all.bs.table":"onColumnSwitchAll","column-switch.bs.table":"onColumnSwitch","dbl-click-cell.bs.table":"onDblClickCell","dbl-click-row.bs.table":"onDblClickRow","expand-row.bs.table":"onExpandRow","load-error.bs.table":"onLoadError","load-success.bs.table":"onLoadSuccess","page-change.bs.table":"onPageChange","post-body.bs.table":"onPostBody","post-footer.bs.table":"onPostFooter","post-header.bs.table":"onPostHeader","pre-body.bs.table":"onPreBody","refresh-options.bs.table":"onRefreshOptions","refresh.bs.table":"onRefresh","reset-view.bs.table":"onResetView","scroll-body.bs.table":"onScrollBody","search.bs.table":"onSearch","sort.bs.table":"onSort","toggle-pagination.bs.table":"onTogglePagination","toggle.bs.table":"onToggle","uncheck-all.bs.table":"onUncheckAll","uncheck-some.bs.table":"onUncheckSome","uncheck.bs.table":"onUncheck","virtual-scroll.bs.table":"onVirtualScroll"},LOCALES:{en:El,"en-US":El},METHODS:["getOptions","refreshOptions","getData","getFooterData","getSelections","load","append","prepend","remove","removeAll","insertRow","updateRow","getRowByUniqueId","updateByUniqueId","removeByUniqueId","updateCell","updateCellByUniqueId","showRow","hideRow","getHiddenRows","showColumn","hideColumn","getVisibleColumns","getHiddenColumns","showAllColumns","hideAllColumns","mergeCells","checkAll","uncheckAll","checkInvert","check","uncheck","checkBy","uncheckBy","refresh","destroy","resetView","showLoading","hideLoading","togglePagination","toggleFullscreen","toggleView","resetSearch","filterBy","sortBy","sortReset","scrollTo","getScrollPosition","selectPage","prevPage","nextPage","toggleDetailView","expandRow","collapseRow","expandRowByUniqueId","collapseRowByUniqueId","expandAllRows","collapseAllRows","updateColumnTitle","updateFormatText"],THEME:"bootstrap".concat(Il),VERSION:"1.23.5"},Fl=function(){return i((function t(e){var i=this;n(this,t),this.rows=e.rows,this.scrollEl=e.scrollEl,this.contentEl=e.contentEl,this.callback=e.callback,this.itemHeight=e.itemHeight,this.cache={},this.scrollTop=this.scrollEl.scrollTop,this.initDOM(this.rows,e.fixedScroll),this.scrollEl.scrollTop=this.scrollTop,this.lastCluster=0;var r=function(){i.lastCluster!==(i.lastCluster=i.getNum())&&(i.initDOM(i.rows),i.callback(i.startIndex,i.endIndex))};this.scrollEl.addEventListener("scroll",r,!1),this.destroy=function(){i.contentEl.innerHtml="",i.scrollEl.removeEventListener("scroll",r,!1)}}),[{key:"initDOM",value:function(t,e){void 0===this.clusterHeight?(this.cache.scrollTop=this.scrollEl.scrollTop,this.cache.data=this.contentEl.innerHTML=t[0]+t[0]+t[0],this.getRowsHeight(t)):0===this.blockHeight&&this.getRowsHeight(t);var n=this.initData(t,this.getNum(e)),i=n.rows.join(""),r=this.checkChanges("data",i),o=this.checkChanges("top",n.topOffset),a=this.checkChanges("bottom",n.bottomOffset),s=[];r&&o?(n.topOffset&&s.push(this.getExtra("top",n.topOffset)),s.push(i),n.bottomOffset&&s.push(this.getExtra("bottom",n.bottomOffset)),this.startIndex=n.start,this.endIndex=n.end,this.contentEl.innerHTML=s.join(""),e&&(this.contentEl.scrollTop=this.cache.scrollTop)):a&&(this.contentEl.lastChild.style.height="".concat(n.bottomOffset,"px"))}},{key:"getRowsHeight",value:function(){if(void 0===this.itemHeight||0===this.itemHeight){var t=this.contentEl.children,e=t[Math.floor(t.length/2)];this.itemHeight=e.offsetHeight}this.blockHeight=50*this.itemHeight,this.clusterRows=200,this.clusterHeight=4*this.blockHeight}},{key:"getNum",value:function(t){return this.scrollTop=t?this.cache.scrollTop:this.scrollEl.scrollTop,Math.floor(this.scrollTop/(this.clusterHeight-this.blockHeight))||0}},{key:"initData",value:function(t,e){if(t.length<50)return{topOffset:0,bottomOffset:0,rowsAbove:0,rows:t};var n=Math.max((this.clusterRows-50)*e,0),i=n+this.clusterRows,r=Math.max(n*this.itemHeight,0),o=Math.max((t.length-i)*this.itemHeight,0),a=[],s=n;r<1&&s++;for(var l=n;l
    ':"",n=["bottom","both"].includes(this.options.paginationVAlign)?'
    ':"",i=Al.calculateObjectValue(this.options,this.options.loadingTemplate,[this.options.formatLoadingMessage()]);this.$container=t('\n
    \n
    \n ').concat(e,'\n
    \n
    \n
    \n
    \n ').concat(i,'\n
    \n
    \n \n
    \n ').concat(n,"\n
    \n ")),this.$container.insertAfter(this.$el),this.$tableContainer=this.$container.find(".fixed-table-container"),this.$tableHeader=this.$container.find(".fixed-table-header"),this.$tableBody=this.$container.find(".fixed-table-body"),this.$tableLoading=this.$container.find(".fixed-table-loading"),this.$tableFooter=this.$el.find("tfoot"),this.options.buttonsToolbar?this.$toolbar=t("body").find(this.options.buttonsToolbar):this.$toolbar=this.$container.find(".fixed-table-toolbar"),this.$pagination=this.$container.find(".fixed-table-pagination"),this.$tableBody.append(this.$el),this.$container.after('
    '),this.$el.addClass(this.options.classes),this.$tableLoading.addClass(this.options.classes),this.options.height&&(this.$tableContainer.addClass("fixed-height"),this.options.showFooter&&this.$tableContainer.addClass("has-footer"),this.options.classes.split(" ").includes("table-bordered")&&(this.$tableBody.append('
    '),this.$tableBorder=this.$tableBody.find(".fixed-table-border"),this.$tableLoading.addClass("fixed-table-border")),this.$tableFooter=this.$container.find(".fixed-table-footer"))}},{key:"initTable",value:function(){var n=this,i=[];if(this.$header=this.$el.find(">thead"),this.$header.length?this.options.theadClasses&&this.$header.addClass(this.options.theadClasses):this.$header=t('')).appendTo(this.$el),this._headerTrClasses=[],this._headerTrStyles=[],this.$header.find("tr").each((function(e,r){var o=t(r),a=[];o.find("th").each((function(e,n){var i=t(n);void 0!==i.data("field")&&i.data("field","".concat(i.data("field")));var r=Object.assign({},i.data());for(var o in r)t.fn.bootstrapTable.columnDefaults.hasOwnProperty(o)&&delete r[o];a.push(Al.extend({},{_data:Al.getRealDataAttr(r),title:i.html(),class:i.attr("class"),titleTooltip:i.attr("title"),rowspan:i.attr("rowspan")?+i.attr("rowspan"):void 0,colspan:i.attr("colspan")?+i.attr("colspan"):void 0},i.data()))})),i.push(a),o.attr("class")&&n._headerTrClasses.push(o.attr("class")),o.attr("style")&&n._headerTrStyles.push(o.attr("style"))})),Array.isArray(this.options.columns[0])||(this.options.columns=[this.options.columns]),this.options.columns=Al.extend(!0,[],i,this.options.columns),this.columns=[],this.fieldsColumnsIndex=[],Al.setFieldIndex(this.options.columns),this.options.columns.forEach((function(t,i){t.forEach((function(t,r){var o=Al.extend({},e.COLUMN_DEFAULTS,t,{passed:t});void 0!==o.fieldIndex&&(n.columns[o.fieldIndex]=o,n.fieldsColumnsIndex[o.field]=o.fieldIndex),n.options.columns[i][r]=o}))})),!this.options.data.length){var r=Al.trToData(this.columns,this.$el.find(">tbody>tr"));r.length&&(this.options.data=r,this.fromHtml=!0)}this.options.pagination&&"server"!==this.options.sidePagination||(this.footerData=Al.trToData(this.columns,this.$el.find(">tfoot>tr"))),this.footerData&&this.$el.find("tfoot").html(""),!this.options.showFooter||this.options.cardView?this.$tableFooter.hide():this.$tableFooter.show()}},{key:"initHeader",value:function(){var e=this,n={},i=[];this.header={fields:[],styles:[],classes:[],formatters:[],detailFormatters:[],events:[],sorters:[],sortNames:[],cellStyles:[],searchables:[]},Al.updateFieldGroup(this.options.columns,this.columns),this.options.columns.forEach((function(t,r){var o=[];o.push(""));var a="";if(0===r&&Al.hasDetailViewIcon(e.options)){var s=e.options.columns.length>1?' rowspan="'.concat(e.options.columns.length,'"'):"";a='\n
    \n ')}a&&"right"!==e.options.detailViewAlign&&o.push(a),t.forEach((function(t,i){var a=Al.sprintf(' class="%s"',t.class),s=t.widthUnit,c=parseFloat(t.width),u=t.halign?t.halign:t.align,f=Al.sprintf("text-align: %s; ",u),d=Al.sprintf("text-align: %s; ",t.align),p=Al.sprintf("vertical-align: %s; ",t.valign);if(p+=Al.sprintf("width: %s; ",!t.checkbox&&!t.radio||c?c?c+s:void 0:t.showSelectTitle?void 0:"36px"),void 0!==t.fieldIndex||t.visible){var g=Al.calculateObjectValue(null,e.options.headerStyle,[t]),v=[],b=[],m="";if(g&&g.css)for(var y=0,w=Object.entries(g.css);y0)for(var k=0,T=Object.entries(t._data);k0?" data-not-first-th":"",b.length>0?b.join(" "):"",">"),o.push(Al.sprintf('
    ',e.options.sortable&&t.sortable?"sortable".concat("center"===u?" sortable-center":""," both"):""));var I=e.options.escape&&e.options.escapeTitle?Al.escapeHTML(t.title):t.title,$=I;t.checkbox&&(I="",!e.options.singleSelect&&e.options.checkboxHeader&&(I=''),e.header.stateField=t.field),t.radio&&(I="",e.header.stateField=t.field),!I&&t.showSelectTitle&&(I+=$),o.push(I),o.push("
    "),o.push('
    '),o.push("
    "),o.push("")}})),a&&"right"===e.options.detailViewAlign&&o.push(a),o.push(""),o.length>3&&i.push(o.join(""))})),this.$header.html(i.join("")),this.$header.find("th[data-field]").each((function(e,i){t(i).data(n[t(i).data("field")])})),this.$container.off("click",".th-inner").on("click",".th-inner",(function(n){var i=t(n.currentTarget);if(e.options.detailView&&!i.parent().hasClass("bs-checkbox")&&i.closest(".bootstrap-table")[0]!==e.$container[0])return!1;e.options.sortable&&i.parent().data().sortable&&e.onSort(n)}));var r=Al.getEventName("resize.bootstrap-table",this.$el.attr("id"));t(window).off(r),!this.options.showHeader||this.options.cardView?(this.$header.hide(),this.$tableHeader.hide(),this.$tableLoading.css("top",0)):(this.$header.show(),this.$tableHeader.show(),this.$tableLoading.css("top",this.$header.outerHeight()+1),this.getCaret(),t(window).on(r,(function(){return e.resetView()}))),this.$selectAll=this.$header.find('[name="btSelectAll"]'),this.$selectAll.off("click").on("click",(function(n){n.stopPropagation();var i=t(n.currentTarget).prop("checked");e[i?"checkAll":"uncheckAll"](),e.updateSelected()}))}},{key:"initData",value:function(t,e){"append"===e?this.options.data=this.options.data.concat(t):"prepend"===e?this.options.data=[].concat(t).concat(this.options.data):(t=t||Al.deepCopy(this.options.data),this.options.data=Array.isArray(t)?t:t[this.options.dataField]),this.data=c(this.options.data),this.options.sortReset&&(this.unsortedData=c(this.data)),"server"!==this.options.sidePagination&&this.initSort()}},{key:"initSort",value:function(){var t=this,e=this.options.sortName,n="desc"===this.options.sortOrder?-1:1,i=this.header.fields.indexOf(this.options.sortName),r=0;-1!==i?(this.options.sortStable&&this.data.forEach((function(t,e){t.hasOwnProperty("_position")||(t._position=e)})),this.options.customSort?Al.calculateObjectValue(this.options,this.options.customSort,[this.options.sortName,this.options.sortOrder,this.data]):this.data.sort((function(r,o){t.header.sortNames[i]&&(e=t.header.sortNames[i]);var a=Al.getItemField(r,e,t.options.escape),s=Al.getItemField(o,e,t.options.escape),l=Al.calculateObjectValue(t.header,t.header.sorters[i],[a,s,r,o]);return void 0!==l?t.options.sortStable&&0===l?n*(r._position-o._position):n*l:Al.sort(a,s,n,t.options,r._position,o._position)})),void 0!==this.options.sortClass&&(clearTimeout(r),r=setTimeout((function(){t.$el.removeClass(t.options.sortClass);var e=t.$header.find('[data-field="'.concat(t.options.sortName,'"]')).index();t.$el.find("tr td:nth-child(".concat(e+1,")")).addClass(t.options.sortClass)}),250))):this.options.sortReset&&(this.data=c(this.unsortedData))}},{key:"sortReset",value:function(){this.options.sortName=void 0,this.options.sortOrder=void 0,this._sort()}},{key:"sortBy",value:function(t){this.options.sortName=t.field,this.options.sortOrder=t.hasOwnProperty("sortOrder")?t.sortOrder:"asc",this._sort()}},{key:"onSort",value:function(e){var n=e.type,i=e.currentTarget,r="keypress"===n?t(i):t(i).parent(),o=this.$header.find("th").eq(r.index());if(this.$header.add(this.$header_).find("span.order").remove(),this.options.sortName===r.data("field")){var a=this.options.sortOrder,s=this.columns[this.fieldsColumnsIndex[r.data("field")]].sortOrder||this.columns[this.fieldsColumnsIndex[r.data("field")]].order;void 0===a?this.options.sortOrder="asc":"asc"===a?this.options.sortOrder=this.options.sortReset?"asc"===s?"desc":void 0:"desc":"desc"===this.options.sortOrder&&(this.options.sortOrder=this.options.sortReset?"desc"===s?"asc":void 0:"asc"),void 0===this.options.sortOrder&&(this.options.sortName=void 0)}else this.options.sortName=r.data("field"),this.options.rememberOrder?this.options.sortOrder="asc"===r.data("order")?"desc":"asc":this.options.sortOrder=this.columns[this.fieldsColumnsIndex[r.data("field")]].sortOrder||this.columns[this.fieldsColumnsIndex[r.data("field")]].order;r.add(o).data("order",this.options.sortOrder),this.getCaret(),this._sort()}},{key:"_sort",value:function(){if("server"===this.options.sidePagination&&this.options.serverSort)return this.options.pageNumber=1,this.trigger("sort",this.options.sortName,this.options.sortOrder),void this.initServer(this.options.silentSort);this.options.pagination&&this.options.sortResetPage&&(this.options.pageNumber=1,this.initPagination()),this.trigger("sort",this.options.sortName,this.options.sortOrder),this.initSort(),this.initBody()}},{key:"initToolbar",value:function(){var e,n=this,i=this.options,o=[],a=0,s=0;this.$toolbar.find(".bs-bars").children().length&&t("body").append(t(i.toolbar)),this.$toolbar.html(""),"string"!=typeof i.toolbar&&"object"!==h(i.toolbar)||t(Al.sprintf('
    ',this.constants.classes.pull,i.toolbarAlign)).appendTo(this.$toolbar).append(t(i.toolbar)),o=['
    ')],"string"==typeof i.buttonsOrder&&(i.buttonsOrder=i.buttonsOrder.replace(/\[|\]| |'/g,"").split(",")),this.buttons=Object.assign(this.buttons,{paginationSwitch:{text:i.pagination?i.formatPaginationSwitchUp():i.formatPaginationSwitchDown(),icon:i.pagination?i.icons.paginationSwitchDown:i.icons.paginationSwitchUp,render:!1,event:this.togglePagination,attributes:{"aria-label":i.formatPaginationSwitch(),title:i.formatPaginationSwitch()}},refresh:{text:i.formatRefresh(),icon:i.icons.refresh,render:!1,event:this.refresh,attributes:{"aria-label":i.formatRefresh(),title:i.formatRefresh()}},toggle:{text:i.formatToggleOn(),icon:i.icons.toggleOff,render:!1,event:this.toggleView,attributes:{"aria-label":i.formatToggleOn(),title:i.formatToggleOn()}},fullscreen:{text:i.formatFullscreen(),icon:i.icons.fullscreen,render:!1,event:this.toggleFullscreen,attributes:{"aria-label":i.formatFullscreen(),title:i.formatFullscreen()}},columns:{render:!1,html:function(){var t=[];if(t.push('
    \n \n ").concat(n.constants.html.toolbarDropdown[0])),i.showColumnsSearch&&(t.push(Al.sprintf(n.constants.html.toolbarDropdownItem,Al.sprintf('',n.constants.classes.input,i.formatSearch()))),t.push(n.constants.html.toolbarDropdownSeparator)),i.showColumnsToggleAll){var e=n.getVisibleColumns().length===n.columns.filter((function(t){return!n.isSelectionColumn(t)})).length;t.push(Al.sprintf(n.constants.html.toolbarDropdownItem,Al.sprintf(' %s',e?'checked="checked"':"",i.formatColumnsToggleAll()))),t.push(n.constants.html.toolbarDropdownSeparator)}var r=0;return n.columns.forEach((function(t){t.visible&&r++})),n.columns.forEach((function(e,o){if(!n.isSelectionColumn(e)&&(!i.cardView||e.cardVisible)){var a=e.visible?' checked="checked"':"",l=r<=i.minimumCountColumns&&a?' disabled="disabled"':"";e.switchable&&(t.push(Al.sprintf(n.constants.html.toolbarDropdownItem,Al.sprintf(' %s',e.field,o,a,l,e.switchableLabel||e.title))),s++)}})),t.push(n.constants.html.toolbarDropdown[1],"
    "),t.join("")}}});for(var c={},u=0,f=Object.entries(this.buttons);u"}c[p]=v;var k="show".concat(p.charAt(0).toUpperCase()).concat(p.substring(1)),T=i[k];!(!g.hasOwnProperty("render")||g.hasOwnProperty("render")&&g.render)||void 0!==T&&!0!==T||(i[k]=!0),i.buttonsOrder.includes(p)||i.buttonsOrder.push(p)}var P,C=r(i.buttonsOrder);try{for(C.s();!(P=C.n()).done;){var A=P.value;i["show".concat(A.charAt(0).toUpperCase()).concat(A.substring(1))]&&o.push(c[A])}}catch(t){C.e(t)}finally{C.f()}o.push("
    "),(this.showToolbar||o.length>2)&&this.$toolbar.append(o.join(""));for(var I=function(){var t=l(R[$],2),e=t[0],i=t[1];if(i.hasOwnProperty("event")){if("function"==typeof i.event||"string"==typeof i.event){var r="string"==typeof i.event?window[i.event]:i.event;return n.$toolbar.find('button[name="'.concat(e,'"]')).off("click").on("click",(function(){return r.call(n)})),1}for(var o=function(){var t=l(s[a],2),i=t[0],r=t[1],o="string"==typeof r?window[r]:r;n.$toolbar.find('button[name="'.concat(e,'"]')).off(i).on(i,(function(){return o.call(n)}))},a=0,s=Object.entries(i.event);a'),B=V;if(i.showSearchButton||i.showSearchClearButton){var H=(i.showSearchButton?L:"")+(i.showSearchClearButton?_:"");B=i.search?Al.sprintf(this.constants.html.inputGroup,V,H):H}o.push(Al.sprintf('\n
    \n %s\n
    \n '),B)),this.$toolbar.append(o.join(""));var M=Al.getSearchInput(this);i.showSearchButton?(this.$toolbar.find(".search button[name=search]").off("click").on("click",(function(){clearTimeout(a),a=setTimeout((function(){n.onSearch({currentTarget:M})}),i.searchTimeOut)})),i.searchOnEnterKey&&D(M)):D(M),i.showSearchClearButton&&this.$toolbar.find(".search button[name=clearSearch]").click((function(){n.resetSearch()}))}else"string"==typeof i.searchSelector&&D(Al.getSearchInput(this))}},{key:"onSearch",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.currentTarget,i=e.firedByInitSearchText,r=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(void 0!==n&&t(n).length&&r){var o=t(n).val().trim();if(this.options.trimOnSearch&&t(n).val()!==o&&t(n).val(o),this.searchText===o)return;var a=Al.getSearchInput(this),s=n instanceof jQuery?n:t(n);(s.is(a)||s.hasClass("search-input"))&&(this.searchText=o,this.options.searchText=o)}i||(this.options.pageNumber=1),this.initSearch(),i?"client"===this.options.sidePagination&&this.updatePagination():this.updatePagination(),this.trigger("search",this.searchText)}},{key:"initSearch",value:function(){var e=this;if(this.filterOptions=this.filterOptions||this.options.filterOptions,"server"!==this.options.sidePagination){if(this.options.customSearch)return this.data=Al.calculateObjectValue(this.options,this.options.customSearch,[this.options.data,this.searchText,this.filterColumns]),this.options.sortReset&&(this.unsortedData=c(this.data)),void this.initSort();var n=this.searchText&&(this.fromHtml?Al.escapeHTML(this.searchText):this.searchText),i=n?n.toLowerCase():"",r=Al.isEmptyObject(this.filterColumns)?null:this.filterColumns;this.options.searchAccentNeutralise&&(i=Al.normalizeAccent(i)),"function"==typeof this.filterOptions.filterAlgorithm?this.data=this.options.data.filter((function(t){return e.filterOptions.filterAlgorithm.apply(null,[t,r])})):"string"==typeof this.filterOptions.filterAlgorithm&&(this.data=r?this.options.data.filter((function(t){var n=e.filterOptions.filterAlgorithm;if("and"===n){for(var i in r)if(Array.isArray(r[i])&&!r[i].includes(t[i])||!Array.isArray(r[i])&&t[i]!==r[i])return!1}else if("or"===n){var o=!1;for(var a in r)(Array.isArray(r[a])&&r[a].includes(t[a])||!Array.isArray(r[a])&&t[a]===r[a])&&(o=!0);return o}return!0})):c(this.options.data));var o=this.getVisibleFields();this.data=i?this.data.filter((function(r,a){for(var s=0;s").html(u).text())),"string"==typeof u||"number"==typeof u){if(e.options.strictSearch&&"".concat(u).toLowerCase()===i||e.options.regexSearch&&Al.regexCompare(u,n))return!0;var d=/(?:(<=|=>|=<|>=|>|<)(?:\s+)?(-?\d+)?|(-?\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm.exec(e.searchText),p=!1;if(d){var g=d[1]||"".concat(d[5],"l"),v=d[2]||d[3],b=parseInt(u,10),m=parseInt(v,10);switch(g){case">":case"m;break;case"<":case">l":p=b=l":case"=>l":p=b<=m;break;case">=":case"=>":case"<=l":case"==m}}if(p||"".concat(u).toLowerCase().includes(i))return!0}}return!1})):this.data,this.options.sortReset&&(this.unsortedData=c(this.data)),this.initSort()}}},{key:"initPagination",value:function(){var t=this,e=this.options;if(e.pagination){this.$pagination.show();var n,i,r,o,a,s,l,c=[],u=!1,h=this.getData({includeHiddenRows:!1}),f=e.pageList;if("string"==typeof f&&(f=f.replace(/\[|\]| /g,"").toLowerCase().split(",")),f=f.map((function(t){return"string"==typeof t?t.toLowerCase()===e.formatAllRows().toLowerCase()||["all","unlimited"].includes(t.toLowerCase())?e.formatAllRows():+t:t})),this.paginationParts=e.paginationParts,"string"==typeof this.paginationParts&&(this.paginationParts=this.paginationParts.replace(/\[|\]| |'/g,"").split(",")),"server"!==e.sidePagination&&(e.totalRows=h.length),this.totalPages=0,e.totalRows&&(e.pageSize===e.formatAllRows()&&(e.pageSize=e.totalRows,u=!0),this.totalPages=1+~~((e.totalRows-1)/e.pageSize),e.totalPages=this.totalPages),this.totalPages>0&&e.pageNumber>this.totalPages&&(e.pageNumber=this.totalPages),this.pageFrom=(e.pageNumber-1)*e.pageSize+1,this.pageTo=e.pageNumber*e.pageSize,this.pageTo>e.totalRows&&(this.pageTo=e.totalRows),this.options.pagination&&"server"!==this.options.sidePagination&&(this.options.totalNotFiltered=this.options.data.length),this.options.showExtendedPagination||(this.options.totalNotFiltered=void 0),(this.paginationParts.includes("pageInfo")||this.paginationParts.includes("pageInfoShort")||this.paginationParts.includes("pageSize"))&&c.push('
    ')),this.paginationParts.includes("pageInfo")||this.paginationParts.includes("pageInfoShort")){var d=this.options.totalRows+("client"===this.options.sidePagination&&this.options.paginationLoadMore&&!this._paginationLoaded?" +":""),p=this.paginationParts.includes("pageInfoShort")?e.formatDetailPagination(d):e.formatShowingRows(this.pageFrom,this.pageTo,d,e.totalNotFiltered);c.push('\n '.concat(p,"\n "))}if(this.paginationParts.includes("pageSize")){c.push('
    ');var g=['
    \n \n ").concat(this.constants.html.pageDropdown[0])];f.forEach((function(n,i){var r;(!e.smartDisplay||0===i||f[i-1]")),c.push(e.formatRecordsPerPage(g.join("")))}if((this.paginationParts.includes("pageInfo")||this.paginationParts.includes("pageInfoShort")||this.paginationParts.includes("pageSize"))&&c.push("
    "),this.paginationParts.includes("pageList")){c.push('
    '),Al.sprintf(this.constants.html.pagination[0],Al.sprintf(" pagination-%s",e.iconSize)),Al.sprintf(this.constants.html.paginationItem," page-pre",e.formatSRPaginationPreText(),e.paginationPreText)),this.totalPagesthis.totalPages-i&&(i=i-(e.paginationSuccessivelySize-(this.totalPages-i))+1),i<1&&(i=1),r>this.totalPages&&(r=this.totalPages);var v=Math.round(e.paginationPagesBySide/2),b=function(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return Al.sprintf(t.constants.html.paginationItem,i+(n===e.pageNumber?" ".concat(t.constants.classes.paginationActive):""),e.formatSRPaginationPageText(n),n)};if(i>1){var m=e.paginationPagesBySide;for(m>=i&&(m=i-1),n=1;n<=m;n++)c.push(b(n));i-1===m+1?(n=i-1,c.push(b(n))):i-1>m&&(i-2*e.paginationPagesBySide>e.paginationPagesBySide&&e.paginationUseIntermediate?(n=Math.round((i-v)/2+v),c.push(b(n," page-intermediate"))):c.push(Al.sprintf(this.constants.html.paginationItem," page-first-separator disabled","","...")))}for(n=i;n<=r;n++)c.push(b(n));if(this.totalPages>r){var y=this.totalPages-(e.paginationPagesBySide-1);for(r>=y&&(y=r+1),r+1===y-1?(n=r+1,c.push(b(n))):y>r+1&&(this.totalPages-r>2*e.paginationPagesBySide&&e.paginationUseIntermediate?(n=Math.round((this.totalPages-v-r)/2+r),c.push(b(n," page-intermediate"))):c.push(Al.sprintf(this.constants.html.paginationItem," page-last-separator disabled","","..."))),n=y;n<=this.totalPages;n++)c.push(b(n))}c.push(Al.sprintf(this.constants.html.paginationItem," page-next",e.formatSRPaginationNextText(),e.paginationNextText)),c.push(this.constants.html.pagination[1],"
    ")}this.$pagination.html(c.join(""));var w=["bottom","both"].includes(e.paginationVAlign)?" ".concat(this.constants.classes.dropup):"";this.$pagination.last().find(".page-list > div").addClass(w),e.onlyInfoPagination||(o=this.$pagination.find(".page-list a"),a=this.$pagination.find(".page-pre"),s=this.$pagination.find(".page-next"),l=this.$pagination.find(".page-item").not(".page-next, .page-pre, .page-last-separator, .page-first-separator"),this.totalPages<=1&&this.$pagination.find("div.pagination").hide(),e.smartDisplay&&(f.length<2||e.totalRows<=f[0])&&this.$pagination.find("div.page-list").hide(),this.$pagination[this.getData().length?"show":"hide"](),e.paginationLoop||(1===e.pageNumber&&a.addClass("disabled"),e.pageNumber===this.totalPages&&s.addClass("disabled")),u&&(e.pageSize=e.formatAllRows()),o.off("click").on("click",(function(e){return t.onPageListChange(e)})),a.off("click").on("click",(function(e){return t.onPagePre(e)})),s.off("click").on("click",(function(e){return t.onPageNext(e)})),l.off("click").on("click",(function(e){return t.onPageNumber(e)})))}else this.$pagination.hide()}},{key:"updatePagination",value:function(e){e&&t(e.currentTarget).hasClass("disabled")||(this.options.maintainMetaData||this.resetRows(),this.initPagination(),this.trigger("page-change",this.options.pageNumber,this.options.pageSize),"server"===this.options.sidePagination||"client"===this.options.sidePagination&&this.options.paginationLoadMore&&!this._paginationLoaded&&this.options.pageNumber===this.totalPages?this.initServer():this.initBody())}},{key:"onPageListChange",value:function(e){e.preventDefault();var n=t(e.currentTarget);return n.parent().addClass(this.constants.classes.dropdownActive).siblings().removeClass(this.constants.classes.dropdownActive),this.options.pageSize=n.text().toUpperCase()===this.options.formatAllRows().toUpperCase()?this.options.formatAllRows():+n.text(),this.$toolbar.find(".page-size").text(this.options.pageSize),this.updatePagination(e),!1}},{key:"onPagePre",value:function(e){if(!t(e.target).hasClass("disabled"))return e.preventDefault(),this.options.pageNumber-1==0?this.options.pageNumber=this.options.totalPages:this.options.pageNumber--,this.updatePagination(e),!1}},{key:"onPageNext",value:function(e){if(!t(e.target).hasClass("disabled"))return e.preventDefault(),this.options.pageNumber+1>this.options.totalPages?this.options.pageNumber=1:this.options.pageNumber++,this.updatePagination(e),!1}},{key:"onPageNumber",value:function(e){if(e.preventDefault(),this.options.pageNumber!==+t(e.currentTarget).text())return this.options.pageNumber=+t(e.currentTarget).text(),this.updatePagination(e),!1}},{key:"initRow",value:function(e,n,i,r){var o=this;if(!(Al.findIndex(this.hiddenRows,e)>-1)){var a=Al.calculateObjectValue(this.options,this.options.rowStyle,[e,n],{}),u=Al.calculateObjectValue(this.options,this.options.rowAttributes,[e,n],{}),f={};if(e._data&&!Al.isEmptyObject(e._data))for(var d=0,p=Object.entries(e._data);dtbody"),this.$body.length||(this.$body=t("").appendTo(this.$el)),this.options.pagination&&"server"!==this.options.sidePagination||(this.pageFrom=1,this.pageTo=r.length);var o=[],a=t(document.createDocumentFragment()),s=!1,l=[];this.autoMergeCells=Al.checkAutoMergeCells(r.slice(this.pageFrom-1,this.pageTo));for(var c=this.pageFrom-1;c tr[data-uniqueid="%s"][data-has-detail-view]',p)).next();g.is("tr.detail-view")&&(l.push(c),n&&p===n||d.push(g[0]))}this.options.virtualScroll?o.push(t("
    ").html(d).html()):a.append(d)}}this.$el.removeAttr("role"),s?this.options.virtualScroll?(this.virtualScroll&&this.virtualScroll.destroy(),this.virtualScroll=new Fl({rows:o,fixedScroll:e,scrollEl:this.$tableBody[0],contentEl:this.$body[0],itemHeight:this.options.virtualScrollItemHeight,callback:function(t,e){i.fitHeader(),i.initBodyEvent(),i.trigger("virtual-scroll",t,e)}})):this.$body.html(a):(this.$body.html(''.concat(Al.sprintf('%s',this.getVisibleFields().length+Al.getDetailViewIndexOffset(this.options),this.options.formatNoMatches()),"")),this.$el.attr("role","presentation")),l.forEach((function(t){i.expandRow(t)})),e||this.scrollTo(0),this.initBodyEvent(),this.initFooter(),this.resetView(),this.updateSelected(),"server"!==this.options.sidePagination&&(this.options.totalRows=r.length),this.trigger("post-body",r)}},{key:"initBodyEvent",value:function(){var e=this;this.$body.find("> tr[data-index] > td").off("click dblclick").on("click dblclick",(function(n){var i=t(n.currentTarget);if(!(i.find(".detail-icon").length||i.index()-Al.getDetailViewIndexOffset(e.options)<0)){var r=i.parent(),o=t(n.target).parents(".card-views").children(),a=t(n.target).parents(".card-view"),s=r.data("index"),l=e.data[s],c=e.options.cardView?o.index(a):i[0].cellIndex,u=e.getVisibleFields()[c-Al.getDetailViewIndexOffset(e.options)],h=e.columns[e.fieldsColumnsIndex[u]],f=Al.getItemField(l,u,e.options.escape,h.escape);if(e.trigger("click"===n.type?"click-cell":"dbl-click-cell",u,f,l,i),e.trigger("click"===n.type?"click-row":"dbl-click-row",l,r,u),"click"===n.type&&e.options.clickToSelect&&h.clickToSelect&&!Al.calculateObjectValue(e.options,e.options.ignoreClickToSelectOn,[n.target])){var d=r.find(Al.sprintf('[name="%s"]',e.options.selectItemName));d.length&&d[0].click()}"click"===n.type&&e.options.detailViewByClick&&e.toggleDetailView(s,e.header.detailFormatters[e.fieldsColumnsIndex[u]])}})).off("mousedown").on("mousedown",(function(t){e.multipleSelectRowCtrlKey=t.ctrlKey||t.metaKey,e.multipleSelectRowShiftKey=t.shiftKey})),this.$body.find("> tr[data-index] > td > .detail-icon").off("click").on("click",(function(n){return n.preventDefault(),e.toggleDetailView(t(n.currentTarget).parent().parent().data("index")),!1})),this.$selectItem=this.$body.find(Al.sprintf('[name="%s"]',this.options.selectItemName)),this.$selectItem.off("click").on("click",(function(n){n.stopImmediatePropagation();var i=t(n.currentTarget);e._toggleCheck(i.prop("checked"),i.data("index"))})),this.header.events.forEach((function(n,i){var r=n;if(r){if("string"==typeof r&&(r=Al.calculateObjectValue(null,r)),!r)throw new Error("Unknown event in the scope: ".concat(n));var o=e.header.fields[i],a=e.getVisibleFields().indexOf(o);if(-1!==a){a+=Al.getDetailViewIndexOffset(e.options);var s=function(n){if(!r.hasOwnProperty(n))return 1;var i=r[n];e.$body.find(">tr:not(.no-records-found)").each((function(r,s){var l=t(s),c=l.find(e.options.cardView?".card-views>.card-view":">td").eq(a),u=n.indexOf(" "),h=n.substring(0,u),f=n.substring(u+1);c.find(f).off(h).on(h,(function(t){var n=l.data("index"),r=e.data[n],a=r[o];i.apply(e,[t,a,r,n])}))}))};for(var l in r)s(l)}}}))}},{key:"initServer",value:function(e,n){var i=this,o={},a=this.header.fields.indexOf(this.options.sortName),s={searchText:this.searchText,sortName:this.options.sortName,sortOrder:this.options.sortOrder};if(this.header.sortNames[a]&&(s.sortName=this.header.sortNames[a]),this.options.pagination&&"server"===this.options.sidePagination&&(s.pageSize=this.options.pageSize===this.options.formatAllRows()?this.options.totalRows:this.options.pageSize,s.pageNumber=this.options.pageNumber),this.options.url||this.options.ajax){if("limit"===this.options.queryParamsType&&(s={search:s.searchText,sort:s.sortName,order:s.sortOrder},this.options.pagination&&"server"===this.options.sidePagination&&(s.offset=this.options.pageSize===this.options.formatAllRows()?0:this.options.pageSize*(this.options.pageNumber-1),s.limit=this.options.pageSize,0!==s.limit&&this.options.pageSize!==this.options.formatAllRows()||delete s.limit)),this.options.search&&"server"===this.options.sidePagination&&this.options.searchable&&this.columns.filter((function(t){return t.searchable})).length){s.searchable=[];var l,c=r(this.columns);try{for(c.s();!(l=c.n()).done;){var u=l.value;!u.checkbox&&u.searchable&&(this.options.visibleSearch&&u.visible||!this.options.visibleSearch)&&s.searchable.push(u.field)}}catch(t){c.e(t)}finally{c.f()}}if(Al.isEmptyObject(this.filterColumnsPartial)||(s.filter=JSON.stringify(this.filterColumnsPartial,null)),Al.extend(s,n||{}),!1!==(o=Al.calculateObjectValue(this.options,this.options.queryParams,[s],o))){e||this.showLoading();var h=Al.extend({},Al.calculateObjectValue(null,this.options.ajaxOptions),{type:this.options.method,url:this.options.url,data:"application/json"===this.options.contentType&&"post"===this.options.method?JSON.stringify(o):o,cache:this.options.cache,contentType:this.options.contentType,dataType:this.options.dataType,success:function(t,n,r){var o=Al.calculateObjectValue(i.options,i.options.responseHandler,[t,r],t);"client"===i.options.sidePagination&&i.options.paginationLoadMore&&(i._paginationLoaded=i.data.length===o.length),i.load(o),i.trigger("load-success",o,r&&r.status,r),e||i.hideLoading(),"server"===i.options.sidePagination&&i.options.pageNumber>1&&o[i.options.totalField]>0&&!o[i.options.dataField].length&&i.updatePagination()},error:function(t){if(t&&0===t.status&&i._xhrAbort)i._xhrAbort=!1;else{var n=[];"server"===i.options.sidePagination&&((n={})[i.options.totalField]=0,n[i.options.dataField]=[]),i.load(n),i.trigger("load-error",t&&t.status,t),e||i.hideLoading()}}});return this.options.ajax?Al.calculateObjectValue(this,this.options.ajax,[h],null):(this._xhr&&4!==this._xhr.readyState&&(this._xhrAbort=!0,this._xhr.abort()),this._xhr=t.ajax(h)),o}}}},{key:"initSearchText",value:function(){if(this.options.search&&(this.searchText="",""!==this.options.searchText)){var t=Al.getSearchInput(this);t.val(this.options.searchText),this.onSearch({currentTarget:t,firedByInitSearchText:!0})}}},{key:"getCaret",value:function(){var e=this;this.$header.find("th").each((function(n,i){t(i).find(".sortable").removeClass("desc asc").addClass(t(i).data("field")===e.options.sortName?e.options.sortOrder:"both")}))}},{key:"updateSelected",value:function(){var e=this.$selectItem.filter(":enabled").length&&this.$selectItem.filter(":enabled").length===this.$selectItem.filter(":enabled").filter(":checked").length;this.$selectAll.add(this.$selectAll_).prop("checked",e),this.$selectItem.each((function(e,n){t(n).closest("tr")[t(n).prop("checked")?"addClass":"removeClass"]("selected")}))}},{key:"updateRows",value:function(){var e=this;this.$selectItem.each((function(n,i){e.data[t(i).data("index")][e.header.stateField]=t(i).prop("checked")}))}},{key:"resetRows",value:function(){var t,e=r(this.data);try{for(e.s();!(t=e.n()).done;){var n=t.value;this.$selectAll.prop("checked",!1),this.$selectItem.prop("checked",!1),this.header.stateField&&(n[this.header.stateField]=!1)}}catch(t){e.e(t)}finally{e.f()}this.initHiddenRows()}},{key:"trigger",value:function(n){for(var i,r,o="".concat(n,".bs.table"),a=arguments.length,s=new Array(a>1?a-1:0),l=1;ln.clientHeight+this.$header.outerHeight()?Al.getScrollBarWidth():0;this.$el.css("margin-top",-this.$header.outerHeight());var r=this.$tableHeader.find(":focus");if(r.length>0){var o=r.parents("th");if(o.length>0){var a=o.attr("data-field");if(void 0!==a){var s=this.$header.find("[data-field='".concat(a,"']"));s.length>0&&s.find(":input").addClass("focus-temp")}}}this.$header_=this.$header.clone(!0,!0),this.$selectAll_=this.$header_.find('[name="btSelectAll"]'),this.$tableHeader.css("margin-right",i).find("table").css("width",this.$el.outerWidth()).html("").attr("class",this.$el.attr("class")).append(this.$header_),this.$tableLoading.css("width",this.$el.outerWidth());var l=t(".focus-temp:visible:eq(0)");l.length>0&&(l.focus(),this.$header.find(".focus-temp").removeClass("focus-temp")),this.$header.find("th[data-field]").each((function(n,i){e.$header_.find(Al.sprintf('th[data-field="%s"]',t(i).data("field"))).data(t(i).data())}));for(var c=this.getVisibleFields(),u=this.$header_.find("th"),h=this.$body.find(">tr:not(.no-records-found,.virtual-scroll-top)").eq(0);h.length&&h.find('>td[colspan]:not([colspan="1"])').length;)h=h.next();var f=h.find("> *").length;h.find("> *").each((function(n,i){var r=t(i);if(Al.hasDetailViewIcon(e.options)&&(0===n&&"right"!==e.options.detailViewAlign||n===f-1&&"right"===e.options.detailViewAlign)){var o=u.filter(".detail"),a=o.innerWidth()-o.find(".fht-cell").width();o.find(".fht-cell").width(r.innerWidth()-a)}else{var s=n-Al.getDetailViewIndexOffset(e.options),l=e.$header_.find(Al.sprintf('th[data-field="%s"]',c[s]));l.length>1&&(l=t(u[r[0].cellIndex]));var h=l.innerWidth()-l.find(".fht-cell").width();l.find(".fht-cell").width(r.innerWidth()-h)}})),this.horizontalScroll(),this.trigger("post-header")}}},{key:"initFooter",value:function(){if(this.options.showFooter&&!this.options.cardView){var t=this.getData(),e=[],n="";Al.hasDetailViewIcon(this.options)&&(n=Al.h("th",{class:"detail"},[Al.h("div",{class:"th-inner"}),Al.h("div",{class:"fht-cell"})])),n&&"right"!==this.options.detailViewAlign&&e.push(n);var i,o=r(this.columns);try{for(o.s();!(i=o.n()).done;){var a=i.value,l=this.footerData&&this.footerData.length>0;if(a.visible&&(!l||a.field in this.footerData[0])){if(this.options.cardView&&!a.cardVisible)return;var u=Al.calculateObjectValue(null,a.footerStyle||this.options.footerStyle,[a]),h=u&&u.css||{},f=l&&this.footerData[0]["_".concat(a.field,"_colspan")]||0,d=l&&this.footerData[0][a.field]||"";d=Al.calculateObjectValue(a,a.footerFormatter,[t,d],d),e.push(Al.h("th",{class:[a.class,u&&u.classes],style:s({"text-align":a.falign?a.falign:a.align,"vertical-align":a.valign},h),colspan:f||void 0},[Al.h("div",{class:"th-inner"},c(Al.htmlToNodes(d))),Al.h("div",{class:"fht-cell"})]))}}}catch(t){o.e(t)}finally{o.f()}n&&"right"===this.options.detailViewAlign&&e.push(n),this.options.height||this.$tableFooter.length||(this.$el.append(""),this.$tableFooter=this.$el.find("tfoot")),this.$tableFooter.find("tr").length||this.$tableFooter.html("
    "),this.$tableFooter.find("tr").html(e),this.trigger("post-footer",this.$tableFooter)}}},{key:"fitFooter",value:function(){var e=this;if(this.$el.is(":hidden"))setTimeout((function(){return e.fitFooter()}),100);else{var n=this.$tableBody.get(0),i=this.hasScrollBar&&n.scrollHeight>n.clientHeight+this.$header.outerHeight()?Al.getScrollBarWidth():0;this.$tableFooter.css("margin-right",i).find("table").css("width",this.$el.outerWidth()).attr("class",this.$el.attr("class"));var r=this.$tableFooter.find("th"),o=this.$body.find(">tr:first-child:not(.no-records-found)");for(r.find(".fht-cell").width("auto");o.length&&o.find('>td[colspan]:not([colspan="1"])').length;)o=o.next();var a=o.find("> *").length;o.find("> *").each((function(n,i){var o=t(i);if(Al.hasDetailViewIcon(e.options)&&(0===n&&"left"===e.options.detailViewAlign||n===a-1&&"right"===e.options.detailViewAlign)){var s=r.filter(".detail"),l=s.innerWidth()-s.find(".fht-cell").width();s.find(".fht-cell").width(o.innerWidth()-l)}else{var c=r.eq(n),u=c.innerWidth()-c.find(".fht-cell").width();c.find(".fht-cell").width(o.innerWidth()-u)}})),this.horizontalScroll()}}},{key:"horizontalScroll",value:function(){var t=this;this.$tableBody.off("scroll").on("scroll",(function(){var e=t.$tableBody.scrollLeft();t.options.showHeader&&t.options.height&&t.$tableHeader.scrollLeft(e),t.options.showFooter&&!t.options.cardView&&t.$tableFooter.scrollLeft(e),t.trigger("scroll-body",t.$tableBody)}))}},{key:"getVisibleFields",value:function(){var t,e=[],n=r(this.header.fields);try{for(n.s();!(t=n.n()).done;){var i=t.value,o=this.columns[this.fieldsColumnsIndex[i]];o&&o.visible&&(!this.options.cardView||o.cardVisible)&&e.push(i)}}catch(t){n.e(t)}finally{n.f()}return e}},{key:"initHiddenRows",value:function(){this.hiddenRows=[]}},{key:"getOptions",value:function(){var t=Al.extend({},this.options);return delete t.data,Al.extend(!0,{},t)}},{key:"refreshOptions",value:function(t){Al.compareObjects(this.options,t,!0)||(this.options=Al.extend(this.options,t),this.trigger("refresh-options",this.options),this.destroy(),this.init())}},{key:"getData",value:function(t){var e=this,n=this.options.data;if(!(this.searchText||this.options.customSearch||void 0!==this.options.sortName||this.enableCustomSort)&&Al.isEmptyObject(this.filterColumns)&&"function"!=typeof this.options.filterOptions.filterAlgorithm&&Al.isEmptyObject(this.filterColumnsPartial)||t&&t.unfiltered||(n=this.data),t&&!t.includeHiddenRows){var i=this.getHiddenRows();n=n.filter((function(t){return-1===Al.findIndex(i,t)}))}return t&&t.useCurrentPage&&(n=n.slice(this.pageFrom-1,this.pageTo)),t&&t.formatted?n.map((function(t){for(var n={},i=0,r=Object.entries(t);i=0;n--){var i=this.options.data[n],r=Al.getItemField(i,t.field,this.options.escape,i.escape);void 0===r&&"$index"!==t.field||(!i.hasOwnProperty(t.field)&&"$index"===t.field&&t.values.includes(n)||t.values.includes(r))&&(e++,this.options.data.splice(n,1))}e&&("server"===this.options.sidePagination&&(this.options.totalRows-=e,this.data=c(this.options.data)),this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0))}},{key:"removeAll",value:function(){this.options.data.length>0&&(this.data.splice(0,this.data.length),this.options.data.splice(0,this.options.data.length),this.initSearch(),this.initPagination(),this.initBody(!0))}},{key:"insertRow",value:function(t){if(t.hasOwnProperty("index")&&t.hasOwnProperty("row")){var e=this.data[t.index],n=this.options.data.indexOf(e);this.data.splice(t.index,0,t.row),this.options.data.splice(n,0,t.row),this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0)}}},{key:"updateRow",value:function(t){var e,n=r(Array.isArray(t)?t:[t]);try{for(n.s();!(e=n.n()).done;){var i=e.value;if(i.hasOwnProperty("index")&&i.hasOwnProperty("row")){var o=this.data[i.index],a=this.options.data.indexOf(o);i.hasOwnProperty("replace")&&i.replace?(this.data[i.index]=i.row,this.options.data[a]=i.row):(Al.extend(this.data[i.index],i.row),Al.extend(this.options.data[a],i.row))}}}catch(t){n.e(t)}finally{n.f()}this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0)}},{key:"getRowByUniqueId",value:function(t){var e,n,i=this.options.uniqueId,r=t,o=null;for(e=this.options.data.length-1;e>=0;e--){n=this.options.data[e];var a=Al.getItemField(n,i,this.options.escape,n.escape);if(void 0!==a&&("string"==typeof a?r=t.toString():"number"==typeof a&&(Number(a)===a&&a%1==0?r=parseInt(t,10):a===Number(a)&&0!==a&&(r=parseFloat(t))),a===r)){o=n;break}}return o}},{key:"updateByUniqueId",value:function(t){var e,n=null,i=r(Array.isArray(t)?t:[t]);try{for(i.s();!(e=i.n()).done;){var o=e.value;if(o.hasOwnProperty("id")&&o.hasOwnProperty("row")){var a=this.options.data.indexOf(this.getRowByUniqueId(o.id));-1!==a&&(o.hasOwnProperty("replace")&&o.replace?this.options.data[a]=o.row:Al.extend(this.options.data[a],o.row),n=o.id)}}}catch(t){i.e(t)}finally{i.f()}this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0,n)}},{key:"removeByUniqueId",value:function(t){var e=this.options.data.length,n=this.getRowByUniqueId(t);n&&this.options.data.splice(this.options.data.indexOf(n),1),e!==this.options.data.length&&("server"===this.options.sidePagination&&(this.options.totalRows-=1,this.data=c(this.options.data)),this.initSearch(),this.initPagination(),this.initBody(!0))}},{key:"_updateCellOnly",value:function(e,n){var i=this.initRow(this.data[n],n),r=this.getVisibleFields().indexOf(e);-1!==r&&(r+=Al.getDetailViewIndexOffset(this.options),this.$body.find(">tr[data-index=".concat(n,"]")).find(">td:eq(".concat(r,")")).replaceWith(t(i).find(">td:eq(".concat(r,")"))),this.initBodyEvent(),this.initFooter(),this.resetView(),this.updateSelected())}},{key:"updateCell",value:function(t){if(t.hasOwnProperty("index")&&t.hasOwnProperty("field")&&t.hasOwnProperty("value")){var e=this.data[t.index],n=this.options.data.indexOf(e);this.data[t.index][t.field]=t.value,this.options.data[n][t.field]=t.value,!1!==t.reinit?(this.initSort(),this.initBody(!0)):this._updateCellOnly(t.field,t.index)}}},{key:"updateCellByUniqueId",value:function(t){var e=this;(Array.isArray(t)?t:[t]).forEach((function(t){var n=t.id,i=t.field,r=t.value,o=e.options.data.indexOf(e.getRowByUniqueId(n));-1!==o&&(e.options.data[o][i]=r)})),!1!==t.reinit?(this.initSort(),this.initBody(!0)):this._updateCellOnly(t.field,this.options.data.indexOf(this.getRowByUniqueId(t.id)))}},{key:"showRow",value:function(t){this._toggleRow(t,!0)}},{key:"hideRow",value:function(t){this._toggleRow(t,!1)}},{key:"_toggleRow",value:function(t,e){var n;if(t.hasOwnProperty("index")?n=this.getData()[t.index]:t.hasOwnProperty("uniqueId")&&(n=this.getRowByUniqueId(t.uniqueId)),n){var i=Al.findIndex(this.hiddenRows,n);e||-1!==i?e&&i>-1&&this.hiddenRows.splice(i,1):this.hiddenRows.push(n),this.initBody(!0),this.initPagination()}}},{key:"getHiddenRows",value:function(t){if(t)return this.initHiddenRows(),this.initBody(!0),void this.initPagination();var e,n=[],i=r(this.getData());try{for(i.s();!(e=i.n()).done;){var o=e.value;this.hiddenRows.includes(o)&&n.push(o)}}catch(t){i.e(t)}finally{i.f()}return this.hiddenRows=n,n}},{key:"showColumn",value:function(t){var e=this;(Array.isArray(t)?t:[t]).forEach((function(t){e._toggleColumn(e.fieldsColumnsIndex[t],!0,!0)}))}},{key:"hideColumn",value:function(t){var e=this;(Array.isArray(t)?t:[t]).forEach((function(t){e._toggleColumn(e.fieldsColumnsIndex[t],!1,!0)}))}},{key:"_toggleColumn",value:function(t,e,n){if(void 0!==t&&this.columns[t].visible!==e&&(this.columns[t].visible=e,this.initHeader(),this.initSearch(),this.initPagination(),this.initBody(),this.options.showColumns)){var i=this.$toolbar.find('.keep-open input:not(".toggle-all")').prop("disabled",!1);n&&i.filter(Al.sprintf('[value="%s"]',t)).prop("checked",e),i.filter(":checked").length<=this.options.minimumCountColumns&&i.filter(":checked").prop("disabled",!0)}}},{key:"getVisibleColumns",value:function(){var t=this;return this.columns.filter((function(e){return e.visible&&!t.isSelectionColumn(e)}))}},{key:"getHiddenColumns",value:function(){return this.columns.filter((function(t){return!t.visible}))}},{key:"isSelectionColumn",value:function(t){return t.radio||t.checkbox}},{key:"showAllColumns",value:function(){this._toggleAllColumns(!0)}},{key:"hideAllColumns",value:function(){this._toggleAllColumns(!1)}},{key:"_toggleAllColumns",value:function(e){var n,i=this,o=r(this.columns.slice().reverse());try{for(o.s();!(n=o.n()).done;){var a=n.value;if(a.switchable){if(!e&&this.options.showColumns&&this.getVisibleColumns().filter((function(t){return t.switchable})).length===this.options.minimumCountColumns)continue;a.visible=e}}}catch(t){o.e(t)}finally{o.f()}if(this.initHeader(),this.initSearch(),this.initPagination(),this.initBody(),this.options.showColumns){var s=this.$toolbar.find('.keep-open input[type="checkbox"]:not(".toggle-all")').prop("disabled",!1);e?s.prop("checked",e):s.get().reverse().forEach((function(n){s.filter(":checked").length>i.options.minimumCountColumns&&t(n).prop("checked",e)})),s.filter(":checked").length<=this.options.minimumCountColumns&&s.filter(":checked").prop("disabled",!0)}}},{key:"mergeCells",value:function(t){var e,n,i=t.index,r=this.getVisibleFields().indexOf(t.field),o=+t.rowspan||1,a=+t.colspan||1,s=this.$body.find(">tr[data-index]");r+=Al.getDetailViewIndexOffset(this.options);var l=s.eq(i).find(">td").eq(r);if(!(i<0||r<0||i>=this.data.length)){for(e=i;etd").eq(n).hide();l.attr("rowspan",o).attr("colspan",a).show()}}},{key:"checkAll",value:function(){this._toggleCheckAll(!0)}},{key:"uncheckAll",value:function(){this._toggleCheckAll(!1)}},{key:"_toggleCheckAll",value:function(t){var e=this.getSelections();this.$selectAll.add(this.$selectAll_).prop("checked",t),this.$selectItem.filter(":enabled").prop("checked",t),this.updateRows(),this.updateSelected();var n=this.getSelections();t?this.trigger("check-all",n,e):this.trigger("uncheck-all",n,e)}},{key:"checkInvert",value:function(){var e=this.$selectItem.filter(":enabled"),n=e.filter(":checked");e.each((function(e,n){t(n).prop("checked",!t(n).prop("checked"))})),this.updateRows(),this.updateSelected(),this.trigger("uncheck-some",n),n=this.getSelections(),this.trigger("check-some",n)}},{key:"check",value:function(t){this._toggleCheck(!0,t)}},{key:"uncheck",value:function(t){this._toggleCheck(!1,t)}},{key:"_toggleCheck",value:function(t,e){var n=this.$selectItem.filter('[data-index="'.concat(e,'"]')),i=this.data[e];if(n.is(":radio")||this.options.singleSelect||this.options.multipleSelectRow&&!this.multipleSelectRowCtrlKey&&!this.multipleSelectRowShiftKey){var o,a=r(this.options.data);try{for(a.s();!(o=a.n()).done;){o.value[this.header.stateField]=!1}}catch(t){a.e(t)}finally{a.f()}this.$selectItem.filter(":checked").not(n).prop("checked",!1)}if(i[this.header.stateField]=t,this.options.multipleSelectRow){if(this.multipleSelectRowShiftKey&&this.multipleSelectRowLastSelectedIndex>=0)for(var s=l(this.multipleSelectRowLastSelectedIndexn.clientWidth}if(!this.options.cardView&&this.options.showHeader&&this.options.height?(this.$tableHeader.show(),this.resetHeader(),e+=this.$header.outerHeight(!0)+1):(this.$tableHeader.hide(),this.trigger("post-header")),!this.options.cardView&&this.options.showFooter&&(this.$tableFooter.show(),this.fitFooter(),this.options.height&&(e+=this.$tableFooter.outerHeight(!0))),this.$container.hasClass("fullscreen"))this.$tableContainer.css("height",""),this.$tableContainer.css("width","");else if(this.options.height){this.$tableBorder&&(this.$tableBorder.css("width",""),this.$tableBorder.css("height",""));var i=this.$toolbar.outerHeight(!0),r=this.$pagination.outerHeight(!0),o=this.options.height-i-r,a=this.$tableBody.find(">table"),s=a.outerHeight();if(this.$tableContainer.css("height","".concat(o,"px")),this.$tableBorder&&a.is(":visible")){var l=o-s-2;this.hasScrollBar&&(l-=Al.getScrollBarWidth()),this.$tableBorder.css("width","".concat(a.outerWidth(),"px")),this.$tableBorder.css("height","".concat(l,"px"))}}this.options.cardView?(this.$el.css("margin-top","0"),this.$tableContainer.css("padding-bottom","0"),this.$tableFooter.hide()):(this.getCaret(),this.$tableContainer.css("padding-bottom","".concat(e,"px"))),this.trigger("reset-view")}},{key:"showLoading",value:function(){this.$tableLoading.toggleClass("open",!0);var t=this.options.loadingFontSize;"auto"===this.options.loadingFontSize&&(t=.04*this.$tableLoading.width(),t=Math.max(12,t),t=Math.min(32,t),t="".concat(t,"px")),this.$tableLoading.find(".loading-text").css("font-size",t)}},{key:"hideLoading",value:function(){this.$tableLoading.toggleClass("open",!1)}},{key:"togglePagination",value:function(){this.options.pagination=!this.options.pagination;var t=this.options.showButtonIcons?this.options.pagination?this.options.icons.paginationSwitchDown:this.options.icons.paginationSwitchUp:"",e=this.options.showButtonText?this.options.pagination?this.options.formatPaginationSwitchUp():this.options.formatPaginationSwitchDown():"";this.$toolbar.find('button[name="paginationSwitch"]').html("".concat(Al.sprintf(this.constants.html.icon,this.options.iconsPrefix,t)," ").concat(e)),this.updatePagination(),this.trigger("toggle-pagination",this.options.pagination)}},{key:"toggleFullscreen",value:function(){this.$el.closest(".bootstrap-table").toggleClass("fullscreen"),this.resetView()}},{key:"toggleView",value:function(){this.options.cardView=!this.options.cardView,this.initHeader();var t=this.options.showButtonIcons?this.options.cardView?this.options.icons.toggleOn:this.options.icons.toggleOff:"",e=this.options.cardView?this.options.formatToggleOff():this.options.formatToggleOn();this.$toolbar.find('button[name="toggle"]').html("".concat(Al.sprintf(this.constants.html.icon,this.options.iconsPrefix,t)," ").concat(this.options.showButtonText?e:"")).attr("aria-label",e).attr(this.options.buttonsAttributeTitle,e),this.initBody(),this.trigger("toggle",this.options.cardView)}},{key:"resetSearch",value:function(t){var e=Al.getSearchInput(this),n=t||"";e.val(n),this.searchText=n,this.onSearch({currentTarget:e},!1)}},{key:"filterBy",value:function(t,e){this.filterOptions=Al.isEmptyObject(e)?this.options.filterOptions:Al.extend(this.options.filterOptions,e),this.filterColumns=Al.isEmptyObject(t)?{}:t,this.options.pageNumber=1,this.initSearch(),this.updatePagination()}},{key:"scrollTo",value:function(e){var n={unit:"px",value:0};"object"===h(e)?n=Object.assign(n,e):"string"==typeof e&&"bottom"===e?n.value=this.$tableBody[0].scrollHeight:"string"!=typeof e&&"number"!=typeof e||(n.value=e);var i=n.value;"rows"===n.unit&&(i=0,this.$body.find("> tr:lt(".concat(n.value,")")).each((function(e,n){i+=t(n).outerHeight(!0)}))),this.$tableBody.scrollTop(i)}},{key:"getScrollPosition",value:function(){return this.$tableBody.scrollTop()}},{key:"selectPage",value:function(t){t>0&&t<=this.options.totalPages&&(this.options.pageNumber=t,this.updatePagination())}},{key:"prevPage",value:function(){this.options.pageNumber>1&&(this.options.pageNumber--,this.updatePagination())}},{key:"nextPage",value:function(){this.options.pageNumber tr[data-index="%s"]',t)).next().is("tr.detail-view")?this.collapseRow(t):this.expandRow(t,e),this.resetView()}},{key:"expandRow",value:function(t,e){var n=this.data[t],i=this.$body.find(Al.sprintf('> tr[data-index="%s"][data-has-detail-view]',t));if(this.options.detailViewIcon&&i.find("a.detail-icon").html(Al.sprintf(this.constants.html.icon,this.options.iconsPrefix,this.options.icons.detailClose)),!i.next().is("tr.detail-view")){i.after(Al.sprintf('',i.children("td").length));var r=i.next().find("td"),o=e||this.options.detailFormatter,a=Al.calculateObjectValue(this.options,o,[t,n,r],"");1===r.length&&r.append(a),this.trigger("expand-row",t,n,r)}}},{key:"expandRowByUniqueId",value:function(t){var e=this.getRowByUniqueId(t);e&&this.expandRow(this.data.indexOf(e))}},{key:"collapseRow",value:function(t){var e=this.data[t],n=this.$body.find(Al.sprintf('> tr[data-index="%s"][data-has-detail-view]',t));n.next().is("tr.detail-view")&&(this.options.detailViewIcon&&n.find("a.detail-icon").html(Al.sprintf(this.constants.html.icon,this.options.iconsPrefix,this.options.icons.detailOpen)),this.trigger("collapse-row",t,e,n.next()),n.next().remove())}},{key:"collapseRowByUniqueId",value:function(t){var e=this.getRowByUniqueId(t);e&&this.collapseRow(this.data.indexOf(e))}},{key:"expandAllRows",value:function(){for(var e=this.$body.find("> tr[data-index][data-has-detail-view]"),n=0;n tr[data-index][data-has-detail-view]"),n=0;n1?n-1:0),r=1;rt.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
    "},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); + diff --git a/streetup/common/static/common/bootstrap.min.css b/streetup/common/static/common/bootstrap.min.css new file mode 100644 index 0000000..635c8fb --- /dev/null +++ b/streetup/common/static/common/bootstrap.min.css @@ -0,0 +1,6 @@ +@charset "UTF-8";/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} + diff --git a/streetup/common/static/common/bxl_mobilite_fr.png b/streetup/common/static/common/bxl_mobilite_fr.png new file mode 100644 index 0000000..fe0b6ad Binary files /dev/null and b/streetup/common/static/common/bxl_mobilite_fr.png differ diff --git a/streetup/common/static/common/clearable_inputs.js b/streetup/common/static/common/clearable_inputs.js new file mode 100644 index 0000000..1405fb7 --- /dev/null +++ b/streetup/common/static/common/clearable_inputs.js @@ -0,0 +1,67 @@ +// Clearable inputs – JS // + +(function () { + // Quels champs rendre effaçables + const SELECTOR = [ + 'input.form-control[type="text"]', + 'input.form-control[type="search"]', + 'input.form-control[type="email"]', + 'input.form-control[type="tel"]', + 'input.form-control[type="url"]', + 'textarea.form-control' + ].join(','); + + function addClearBtn(field) { + if (field.dataset.clearableInit === '1' || field.readOnly || field.disabled) return; + field.dataset.clearableInit = '1'; + + // Cherche une input-group parente, sinon en crée une + let group = field.closest('.input-group'); + if (!group) { + group = document.createElement('div'); + group.className = 'input-group'; + field.parentNode.insertBefore(group, field); + group.appendChild(field); + } + + // Évite le doublon si déjà présent + if (group.querySelector('.js-clear-btn')) return; + + // Bouton effacer, toujours visible + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'btn btn-outline-secondary js-clear-btn'; + btn.title = 'Effacer'; + btn.setAttribute('aria-label', 'Effacer le texte'); + btn.innerHTML = ''; + + btn.addEventListener('click', () => { + field.value = ''; + // Déclenche l’événement input pour tes previews/autocomplete + field.dispatchEvent(new Event('input', { bubbles: true })); + field.focus({ preventScroll: true }); + }); + + group.appendChild(btn); + } + + function enhance(root = document) { + root.querySelectorAll(SELECTOR).forEach(addClearBtn); + } + + document.addEventListener('DOMContentLoaded', () => { + enhance(); + + // Gère les champs ajoutés dynamiquement (modales, formulaires injectés, etc.) + const mo = new MutationObserver(muts => { + for (const m of muts) { + for (const node of m.addedNodes) { + if (!(node instanceof HTMLElement)) continue; + if (node.matches?.(SELECTOR)) addClearBtn(node); + node.querySelectorAll?.(SELECTOR).forEach(addClearBtn); + } + } + }); + mo.observe(document.body, { childList: true, subtree: true }); + }); +})(); \ No newline at end of file diff --git a/streetup/common/static/common/controls.webmanifest b/streetup/common/static/common/controls.webmanifest new file mode 100644 index 0000000..d602b4b --- /dev/null +++ b/streetup/common/static/common/controls.webmanifest @@ -0,0 +1,23 @@ +{ + "name": "Streetup Controls App", + "short_name": "Controls", + "version": "1.1.2", + "scope": "/", + "start_url": "/controls/?v=mobile", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#3f51b5", + "icons": [ + { + "src": "/static/controls/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/controls/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] + } + \ No newline at end of file diff --git a/streetup/common/static/common/custom-draw-toolbar.css b/streetup/common/static/common/custom-draw-toolbar.css new file mode 100644 index 0000000..349a4b8 --- /dev/null +++ b/streetup/common/static/common/custom-draw-toolbar.css @@ -0,0 +1,429 @@ +/** + * Custom Draw Toolbar for Maplibre GL - CSS Styles + * + * Styles for the reusable drawing toolbar that integrates with CustomDraw. + * Includes styling for all toolbar buttons, groups, and states. + * + * @version 1.0.0 + */ + +/* ===== Toolbar Container ===== */ +.draw-toolbar-container { + display: flex; + flex-direction: column; + gap: 4px; +} + +/* ===== Toolbar Groups ===== */ +.draw-toolbar-group { + background: #fff; + border-radius: 4px; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +/* ===== Base Button Styles ===== */ +.draw-toolbar-btn { + width: 29px; + height: 29px; + display: block; + padding: 0; + outline: none; + border: 0; + box-sizing: border-box; + background-color: transparent; + cursor: pointer; + background-repeat: no-repeat; + background-position: center; + background-size: 18px 18px; + border-bottom: 1px solid rgba(0, 0, 0, 0.15); +} + +.draw-toolbar-btn:last-child { + border-bottom: none; +} + +/* Button Hover State */ +.draw-toolbar-btn:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +/* Button Active State */ +.draw-toolbar-btn.active { + background-color: #e7f3ff; +} + +.draw-toolbar-btn.active:hover { + background-color: #cce5ff; +} + +/* Button Disabled State */ +.draw-toolbar-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.draw-toolbar-btn:disabled:hover { + background-color: transparent; +} + +/* ===== Separator ===== */ +.draw-toolbar-separator { + height: 1px; + background: rgba(0, 0, 0, 0.1); + margin: 0; +} + +/* ===== Maplibre Control Group Integration ===== */ +/* These styles integrate with MapLibre GL's default control styling */ + +.maplibregl-ctrl-group button.draw-mode-btn, +.maplibregl-ctrl-group button.add-polygon-btn, +.maplibregl-ctrl-group button.delete-btn, +.maplibregl-ctrl-group button.snap-mode-btn, +.maplibregl-ctrl-group button.split-mode-btn, +.maplibregl-ctrl-group button.merge-mode-btn { + width: 29px; + height: 29px; + display: block; + padding: 0; + outline: none; + border: 0; + box-sizing: border-box; + background-color: transparent; + cursor: pointer; + background-repeat: no-repeat; + background-position: center; + background-size: 18px 18px; + border-bottom: 1px solid rgba(0, 0, 0, 0.15); +} + +.maplibregl-ctrl-group button.draw-mode-btn:last-child, +.maplibregl-ctrl-group button.add-polygon-btn:last-child, +.maplibregl-ctrl-group button.delete-btn:last-child, +.maplibregl-ctrl-group button.snap-mode-btn:last-child, +.maplibregl-ctrl-group button.split-mode-btn:last-child, +.maplibregl-ctrl-group button.merge-mode-btn:last-child { + border-bottom: none; +} + +/* ===== Icon Definitions ===== */ + +/* Draw Mode - Pencil Icon */ +.maplibregl-ctrl-group button.draw-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z'/%3E%3C/svg%3E"); +} + +/* Add Polygon Icon - Centré */ +.maplibregl-ctrl-group button.add-polygon-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M2 2v12h12V2H2zm11 11H3V3h10v10z'/%3E%3Cpath d='M2 5v2h-2v2h2v2h2v-2h2v-2h-2v-2h-2z'/%3E%3C/svg%3E"); +} + +/* Delete - Trash Icon */ +.maplibregl-ctrl-group button.delete-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z'/%3E%3Cpath fill-rule='evenodd' d='M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z'/%3E%3C/svg%3E"); +} + +/* Snap Icon - Magnet */ +.maplibregl-ctrl-group button.snap-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a7 7 0 0 0-7 7h2a5 5 0 0 1 10 0h2a7 7 0 0 0-7-7zm0 3a4 4 0 0 0-4 4h2a2 2 0 0 1 4 0h2a4 4 0 0 0-4-4z'/%3E%3Crect x='7' y='8' width='2' height='6' rx='1'/%3E%3C/svg%3E"); +} + +/* Split Icon - Square crossed by line */ +.maplibregl-ctrl-group button.split-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M2 2v12h12V2H2zm11 11H3V3h10v10z'/%3E%3Cpath d='M3.5 3.5l9 9' stroke='%23333' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); +} + +/* Merge Icon - Two Arrows Converging */ +.maplibregl-ctrl-group button.merge-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23333' viewBox='0 0 16 16'%3E%3Cpath d='M1 8a.5.5 0 0 1 .5-.5h4.793L4.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L6.293 8.5H1.5A.5.5 0 0 1 1 8zm14 0a.5.5 0 0 1-.5.5h-4.793l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708.708L9.707 7.5h4.793a.5.5 0 0 1 .5.5z'/%3E%3C/svg%3E"); +} + +/* ===== Button States ===== */ + +/* Active State */ +.maplibregl-ctrl-group button.draw-mode-btn.active, +.maplibregl-ctrl-group button.add-polygon-btn.active, +.maplibregl-ctrl-group button.delete-btn.active, +.maplibregl-ctrl-group button.snap-mode-btn.active, +.maplibregl-ctrl-group button.split-mode-btn.active, +.maplibregl-ctrl-group button.merge-mode-btn.active { + background-color: #e7f3ff; +} + +/* Hover State */ +.maplibregl-ctrl-group button.draw-mode-btn:hover, +.maplibregl-ctrl-group button.add-polygon-btn:hover, +.maplibregl-ctrl-group button.delete-btn:hover, +.maplibregl-ctrl-group button.snap-mode-btn:hover, +.maplibregl-ctrl-group button.split-mode-btn:hover, +.maplibregl-ctrl-group button.merge-mode-btn:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +/* Active + Hover State */ +.maplibregl-ctrl-group button.draw-mode-btn.active:hover, +.maplibregl-ctrl-group button.add-polygon-btn.active:hover, +.maplibregl-ctrl-group button.delete-btn.active:hover, +.maplibregl-ctrl-group button.snap-mode-btn.active:hover, +.maplibregl-ctrl-group button.split-mode-btn.active:hover, +.maplibregl-ctrl-group button.merge-mode-btn.active:hover { + background-color: #cce5ff; +} + +/* Disabled State */ +.maplibregl-ctrl-group button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.maplibregl-ctrl-group button:disabled:hover { + background-color: transparent; +} + +/* ===== Tools Group - Hidden by default ===== */ +.draw-tools-group { + display: none; +} + +.draw-tools-group.active { + display: block; +} + +/* ===== Floating Action Buttons (Save/Undo) ===== */ +.draw-floating-actions { + position: absolute; + bottom: 20px; + right: 20px; + z-index: 1001; + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-end; +} + +.draw-floating-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + transition: all 0.2s ease; +} + +.draw-floating-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.35); +} + +.draw-floating-btn-save { + background-color: #198754; + color: white; +} + +.draw-floating-btn-save:hover { + background-color: #157347; +} + +.draw-floating-btn-save:disabled { + background-color: #6c757d; + cursor: not-allowed; + transform: none; +} + +.draw-floating-btn-undo { + background-color: #ffc107; + color: #212529; + border-radius: 50%; + width: 45px; + height: 45px; + padding: 0; +} + +.draw-floating-btn-undo:hover { + background-color: #e0a800; +} + +.draw-floating-btn .badge { + margin-left: 8px; + background-color: rgba(255, 255, 255, 0.9); + color: #212529; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; +} + +.draw-floating-btn svg { + width: 18px; + height: 18px; + fill: currentColor; +} + +.draw-floating-btn .label { + margin-left: 6px; +} + +/* ===== Help Message Toast ===== */ +.draw-help-message { + position: absolute; + bottom: 20px; + left: 20px; + z-index: 1000; + max-width: 350px; + padding: 0.75rem 1rem; + border-radius: 4px; + font-size: 0.875rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s ease, transform 0.3s ease; + pointer-events: none; +} + +.draw-help-message.visible { + opacity: 1; + transform: translateY(0); +} + +.draw-help-message.info { + background-color: #cff4fc; + color: #055160; + border-left: 4px solid #0dcaf0; +} + +.draw-help-message.success { + background-color: #d1e7dd; + color: #0a3622; + border-left: 4px solid #198754; +} + +.draw-help-message.warning { + background-color: #fff3cd; + color: #664d03; + border-left: 4px solid #ffc107; +} + +.draw-help-message.danger { + background-color: #f8d7da; + color: #58151c; + border-left: 4px solid #dc3545; +} + +/* ===== Danger State (for delete button hover) ===== */ +.maplibregl-ctrl-group button.delete-btn:hover:not(:disabled) { + background-color: rgba(220, 53, 69, 0.1); +} + +.maplibregl-ctrl-group button.delete-btn.active:hover { + background-color: rgba(220, 53, 69, 0.2); +} + +/* ===== Touch-Friendly Sizing for Mobile ===== */ +@media (max-width: 768px), (pointer: coarse) { + .maplibregl-ctrl-group button.draw-mode-btn, + .maplibregl-ctrl-group button.add-polygon-btn, + .maplibregl-ctrl-group button.delete-btn, + .maplibregl-ctrl-group button.snap-mode-btn, + .maplibregl-ctrl-group button.split-mode-btn, + .maplibregl-ctrl-group button.merge-mode-btn, + .draw-toolbar-btn { + width: 30px; + height: 30px; + background-size: 19px 19px; + } + + .draw-floating-actions { + bottom: 15px; + right: 15px; + } + + .draw-help-message { + bottom: 15px; + left: 15px; + right: 80px; + max-width: none; + } + + .draw-floating-btn-save { + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + } + + .draw-floating-btn-undo { + width: 40px; + height: 40px; + } +} + +/* ===== High Contrast / Accessibility ===== */ +@media (prefers-contrast: high) { + .draw-toolbar-group, + .maplibregl-ctrl-group { + box-shadow: 0 0 0 2px #000; + } + + .maplibregl-ctrl-group button.draw-mode-btn.active, + .maplibregl-ctrl-group button.add-polygon-btn.active, + .maplibregl-ctrl-group button.delete-btn.active, + .maplibregl-ctrl-group button.snap-mode-btn.active, + .maplibregl-ctrl-group button.split-mode-btn.active, + .maplibregl-ctrl-group button.merge-mode-btn.active, + .draw-toolbar-btn.active { + background-color: #0066ff; + outline: 2px solid #000; + } +} + +/* ===== Dark Mode Support ===== */ +@media (prefers-color-scheme: dark) { + .draw-toolbar-group { + background: #2d2d2d; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); + } + + .draw-toolbar-btn { + border-bottom-color: rgba(255, 255, 255, 0.15); + } + + .draw-toolbar-btn:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + .draw-toolbar-btn.active { + background-color: #1a3c5c; + } + + .draw-toolbar-separator { + background: rgba(255, 255, 255, 0.15); + } + + /* Update icon colors for dark mode */ + .maplibregl-ctrl-group button.draw-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23e0e0e0' viewBox='0 0 16 16'%3E%3Cpath d='M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z'/%3E%3C/svg%3E"); + } + + .maplibregl-ctrl-group button.add-polygon-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23e0e0e0' viewBox='0 0 16 16'%3E%3Cpath d='M2 2v12h12V2H2zm11 11H3V3h10v10z'/%3E%3Cpath d='M8 5.5v2H6v1h2v2h1v-2h2v-1H9v-2H8z'/%3E%3C/svg%3E"); + } + + .maplibregl-ctrl-group button.delete-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23e0e0e0' viewBox='0 0 16 16'%3E%3Cpath d='M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z'/%3E%3Cpath fill-rule='evenodd' d='M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z'/%3E%3C/svg%3E"); + } + + .maplibregl-ctrl-group button.snap-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23e0e0e0' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a7 7 0 0 0-7 7h2a5 5 0 0 1 10 0h2a7 7 0 0 0-7-7zm0 3a4 4 0 0 0-4 4h2a2 2 0 0 1 4 0h2a4 4 0 0 0-4-4z'/%3E%3Crect x='7' y='8' width='2' height='6' rx='1'/%3E%3C/svg%3E"); + } + + .maplibregl-ctrl-group button.split-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23e0e0e0' viewBox='0 0 16 16'%3E%3Cpath d='M2 2v12h12V2H2zm11 11H3V3h10v10z'/%3E%3Cpath d='M3.5 3.5l9 9' stroke='%23e0e0e0' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); + } + + .maplibregl-ctrl-group button.merge-mode-btn { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23e0e0e0' viewBox='0 0 16 16'%3E%3Cpath d='M1 8a.5.5 0 0 1 .5-.5h4.793L4.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L6.293 8.5H1.5A.5.5 0 0 1 1 8zm14 0a.5.5 0 0 1-.5.5h-4.793l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708.708L9.707 7.5h4.793a.5.5 0 0 1 .5.5z'/%3E%3C/svg%3E"); + } +} diff --git a/streetup/common/static/common/custom-draw-toolbar.js b/streetup/common/static/common/custom-draw-toolbar.js new file mode 100644 index 0000000..7193177 --- /dev/null +++ b/streetup/common/static/common/custom-draw-toolbar.js @@ -0,0 +1,3443 @@ +/** + * Custom Draw Toolbar for Maplibre GL + * + * A reusable, configurable drawing toolbar that integrates with CustomDraw. + * Provides drawing mode toggle, polygon tools (split, merge), snapping, + * and geometry add/delete operations. + * + * Integrated features: + * - Save/Undo floating buttons (bottom-right of map) + * - Help messages display (bottom-left of map) + * - Internationalization via gettext + * + * @version 2.0.0 + * @requires maplibre-gl + * @requires custom-draw.js + * @requires turf.js + * @requires Django gettext (optional, falls back to labels) + */ + +(function(window) { + 'use strict'; + + /** + * Translation helper - uses Django gettext if available + * @param {string} text - Text to translate + * @returns {string} - Translated text or original if gettext unavailable + */ + function _(text) { + if (typeof gettext === 'function') { + return gettext(text); + } + return text; + } + + // Default SVG icons + const ICONS = { + pencil: ``, + + addPolygon: ``, + + trash: ``, + + snap: ``, + + split: ``, + + // New merge icon: two arrows pointing together (more intuitive) + merge: ``, + + polygon: `` + }; + + // CSS styles for the toolbar + const TOOLBAR_STYLES = ` + .draw-toolbar-container { + display: flex; + flex-direction: column; + gap: 4px; + } + + .draw-toolbar-group { + background: #fff; + border-radius: 4px; + box-shadow: 0 0 0 2px rgba(0,0,0,.1); + overflow: hidden; + } + + .draw-toolbar-btn { + width: 29px; + height: 29px; + display: block; + padding: 0; + outline: none; + border: 0; + box-sizing: border-box; + background-color: transparent; + cursor: pointer; + background-repeat: no-repeat; + background-position: center; + background-size: 18px 18px; + border-bottom: 1px solid rgba(0,0,0,.1); + } + + .draw-toolbar-btn:last-child { + border-bottom: none; + } + + .draw-toolbar-btn:hover { + background-color: rgba(0, 0, 0, 0.05); + } + + .draw-toolbar-btn.active { + background-color: #e7f3ff; + } + + .draw-toolbar-btn.active:hover { + background-color: #cce5ff; + } + + .draw-toolbar-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + .draw-toolbar-btn:disabled:hover { + background-color: transparent; + } + + .draw-toolbar-separator { + height: 1px; + background: rgba(0,0,0,.1); + margin: 0; + } + + /* Floating action buttons (save/undo) */ + .draw-floating-actions { + position: absolute; + bottom: 20px; + right: 20px; + z-index: 1001; + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-end; + } + + .draw-floating-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + transition: all 0.2s ease; + } + + .draw-floating-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 12px rgba(0,0,0,0.35); + } + + .draw-floating-btn-save { + background-color: #198754; + color: white; + } + + .draw-floating-btn-save:hover { + background-color: #157347; + } + + .draw-floating-btn-undo { + background-color: #ffc107; + color: #212529; + border-radius: 50%; + width: 45px; + height: 45px; + padding: 0; + } + + .draw-floating-btn-undo:hover { + background-color: #e0a800; + } + + .draw-floating-btn .badge { + margin-left: 8px; + background-color: rgba(255,255,255,0.9); + color: #212529; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + } + + .draw-floating-btn svg { + width: 18px; + height: 18px; + fill: currentColor; + } + + /* Help message toast */ + .draw-help-message { + position: absolute; + bottom: 20px; + left: 20px; + z-index: 1000; + max-width: 350px; + padding: 0.75rem 1rem; + border-radius: 4px; + font-size: 0.875rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s ease, transform 0.3s ease; + pointer-events: none; + } + + .draw-help-message.visible { + opacity: 1; + transform: translateY(0); + } + + .draw-help-message.info { + background-color: #cff4fc; + color: #055160; + border-left: 4px solid #0dcaf0; + } + + .draw-help-message.success { + background-color: #d1e7dd; + color: #0a3622; + border-left: 4px solid #198754; + } + + .draw-help-message.warning { + background-color: #fff3cd; + color: #664d03; + border-left: 4px solid #ffc107; + } + + .draw-help-message.danger { + background-color: #f8d7da; + color: #58151c; + border-left: 4px solid #dc3545; + } + + /* Responsive adjustments */ + @media (max-width: 768px) { + .draw-floating-actions { + bottom: 15px; + right: 15px; + } + + .draw-help-message { + bottom: 15px; + left: 15px; + right: 15px; + max-width: none; + } + + .draw-floating-btn-save { + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + } + + .draw-floating-btn-undo { + width: 40px; + height: 40px; + } + } + `; + + /** + * CustomDrawToolbar class + * + * A reusable toolbar control for Maplibre GL that provides drawing tools + */ + class CustomDrawToolbar { + /** + * Create a new CustomDrawToolbar + * @param {Object} options - Configuration options + * @param {Object} options.draw - The CustomDraw instance + * @param {Object} options.layerConfig - Configuration for the layer(s) to edit + * @param {string} options.layerConfig.sourceId - The MapLibre source ID for the features + * @param {string[]} options.layerConfig.layerIds - The MapLibre layer IDs to filter + * @param {string} options.layerConfig.assetType - The asset type identifier (e.g., 'naturegreensurface') + * @param {Object} options.api - API endpoints configuration + * @param {string} options.api.loadUrl - URL to load assets (POST with filter_geom) + * @param {string} options.api.saveUrl - URL to save geometry changes (POST) + * @param {string} options.api.deleteUrl - URL to delete assets (POST with asset_id) + * @param {Function} options.api.getCsrfToken - Function that returns CSRF token + * @param {Object} options.callbacks - Callback functions + * @param {Function} options.callbacks.onMessage - Callback for showing messages (text, type) + * @param {Function} options.callbacks.onDrawModeChange - Callback when draw mode changes (isActive) + * @param {Function} options.callbacks.onModifiedCountChange - Callback when modified count changes (count) + * @param {Function} options.callbacks.onSelectionChange - Callback when selection changes (selectedFeatures, selectedIds) + * @param {Function} options.callbacks.onFeatureEdit - Callback when a feature enters edit mode (feature with properties) + * @param {Function} options.callbacks.onGeometryCreate - Callback when geometry is created (feature) + * @param {Function} options.callbacks.onGeometryUpdate - Callback when geometry is updated (feature) + * @param {Function} options.callbacks.onGeometryDelete - Callback when geometry is deleted (featureId) + * @param {Object} options.labels - UI labels (for i18n) + * @param {string} options.labels.drawMode - "Draw Mode" + * @param {string} options.labels.addPolygon - "Add Polygon" + * @param {string} options.labels.delete - "Delete" + * @param {string} options.labels.snap - "Snap" + * @param {string} options.labels.split - "Split Polygon" + * @param {string} options.labels.merge - "Merge Polygons" + */ + constructor(options = {}) { + this.options = options; + this.draw = options.draw; + this.map = null; + this.container = null; + + // Layer configuration + this.layerConfig = options.layerConfig || { + sourceId: 'assets', + layerIds: ['green-surfaces', 'green-surfaces-outline'], + assetType: 'asset' + }; + + // API configuration + this.api = options.api || {}; + + // Callbacks + this.callbacks = options.callbacks || {}; + + // Help message configuration + this.helpMessageDuration = options.helpMessageDuration || 2000; // 2 seconds default + + // Labels with defaults using gettext + this.labels = Object.assign({ + drawMode: _('Draw Mode'), + addPolygon: _('Add Polygon'), + delete: _('Delete'), + snap: _('Snap'), + snapEnabled: _('Snap enabled'), + snapDisabled: _('Snap disabled'), + split: _('Split Polygon'), + splitCancelled: _('Split cancelled'), + splitSuccess: _('Split successful'), + merge: _('Merge Polygons'), + mergeSuccess: _('Merge successful'), + mergeError: _('Error merging polygons'), + polygonRemoved: _('Polygon removed from edit mode'), + polygonAdded: _('Polygon added to edit mode'), + drawModeEnabled: _('Draw mode enabled - Click on a geometry to edit'), + drawModeDisabled: _('Draw mode disabled'), + saveGeometries: _('Save geometries'), + undo: _('Undo'), + noFeaturesToDelete: _('No features to delete'), + geometriesDeleted: _('Geometry(ies) deleted'), + addPolygonHelp: _('Click to add points. Double-click or click the first point to finish.'), + splitHelp: _('Draw a line across the polygon to split it.') + }, options.labels || {}); + + // State + this.isDrawMode = false; + this.isSplitDrawing = false; + this.splitLineFeatureId = null; + this.loadedAssets = new Map(); + this.modifiedGeometries = new Map(); + this.hiddenFeatures = new Set(); + this.originalGeometries = new Map(); // Store original geometries (after normalization by draw) + this.currentEditingAsset = null; + this.geometryHistory = []; + this.lastSelectedFeatureIds = []; + + // Button references + this.buttons = {}; + + // UI elements + this.floatingActionsContainer = null; + this.saveBtn = null; + this.undoBtn = null; + this.helpMessageEl = null; + this.helpMessageTimeout = null; + + // Inject styles + this._injectStyles(); + } + + /** + * Inject toolbar CSS styles + */ + _injectStyles() { + if (!document.getElementById('draw-toolbar-styles')) { + const style = document.createElement('style'); + style.id = 'draw-toolbar-styles'; + style.textContent = TOOLBAR_STYLES; + document.head.appendChild(style); + } + } + + /** + * Create icon URL from SVG string + */ + _iconUrl(svgString) { + return `url("data:image/svg+xml,${encodeURIComponent(svgString.replace(/%23333/g, '#333'))}")`; + } + + /** + * Add the control to the map (Maplibre IControl interface) + */ + onAdd(map) { + this.map = map; + + // Create main container + this.container = document.createElement('div'); + this.container.className = 'maplibregl-ctrl draw-toolbar-container'; + + // Create main toolbar group + this._createMainToolbar(); + + // Create tools group (hidden by default) + this._createToolsGroup(); + + // Create floating action buttons (save/undo) + this._createFloatingActions(); + + // Create help message element + this._createHelpMessage(); + + // Setup draw event listeners + this._setupDrawEventListeners(); + + return this.container; + } + + /** + * Remove the control from the map + */ + onRemove() { + // Remove asset click handlers from attached layers + if (this.map && this._boundAssetClickHandler && this._attachedLayers) { + this._attachedLayers.forEach(layerId => { + if (this.map.getLayer(layerId)) { + this.map.off('click', layerId, this._boundAssetClickHandler); + this.map.off('mouseenter', layerId, this._boundAssetMouseEnterHandler); + this.map.off('mouseleave', layerId, this._boundAssetMouseLeaveHandler); + } + }); + this._attachedLayers.clear(); + } + + // Remove empty click handler + if (this.map && this._boundEmptyClickHandler) { + this.map.off('click', this._boundEmptyClickHandler); + } + + if (this.container && this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + + // Remove floating actions + if (this.floatingActionsContainer && this.floatingActionsContainer.parentNode) { + this.floatingActionsContainer.parentNode.removeChild(this.floatingActionsContainer); + } + + // Remove help message + if (this.helpMessageEl && this.helpMessageEl.parentNode) { + this.helpMessageEl.parentNode.removeChild(this.helpMessageEl); + } + + // Clear timeout + if (this.helpMessageTimeout) { + clearTimeout(this.helpMessageTimeout); + } + + this.map = null; + } + + /** + * Create the main toolbar with draw mode toggle + */ + _createMainToolbar() { + const group = document.createElement('div'); + group.className = 'maplibregl-ctrl-group draw-toolbar-group'; + + // Draw mode toggle button (pencil icon) + this.buttons.drawMode = this._createButton( + ICONS.pencil, + this.labels.drawMode, + () => this.toggleDrawMode() + ); + group.appendChild(this.buttons.drawMode); + + this.container.appendChild(group); + } + + /** + * Create the tools group (snap, add, delete, split, merge) + */ + _createToolsGroup() { + this.toolsGroup = document.createElement('div'); + this.toolsGroup.className = 'maplibregl-ctrl-group draw-toolbar-group'; + this.toolsGroup.style.display = 'none'; + + // Add polygon button + this.buttons.addPolygon = this._createButton( + ICONS.addPolygon, + this.labels.addPolygon, + () => this.startDrawPolygon(), + true + ); + this.toolsGroup.appendChild(this.buttons.addPolygon); + + // Delete button + this.buttons.delete = this._createButton( + ICONS.trash, + this.labels.delete, + () => this.deleteSelected(), + true + ); + this.toolsGroup.appendChild(this.buttons.delete); + + // Separator + const sep1 = document.createElement('div'); + sep1.className = 'draw-toolbar-separator'; + this.toolsGroup.appendChild(sep1); + + // Snap button + this.buttons.snap = this._createButton( + ICONS.snap, + this.labels.snap, + () => this.toggleSnap() + ); + this.buttons.snap.classList.add('active'); // Snap enabled by default + this.toolsGroup.appendChild(this.buttons.snap); + + // Split button + this.buttons.split = this._createButton( + ICONS.split, + this.labels.split, + () => this.toggleSplit(), + true + ); + this.toolsGroup.appendChild(this.buttons.split); + + // Merge button + this.buttons.merge = this._createButton( + ICONS.merge, + this.labels.merge, + () => this.performMerge(), + true + ); + this.toolsGroup.appendChild(this.buttons.merge); + + this.container.appendChild(this.toolsGroup); + } + + /** + * Create floating action buttons (save/undo) + */ + _createFloatingActions() { + // Container for floating actions + this.floatingActionsContainer = document.createElement('div'); + this.floatingActionsContainer.className = 'draw-floating-actions'; + this.floatingActionsContainer.style.display = 'none'; + + // Undo button (circular) + this.undoBtn = document.createElement('button'); + this.undoBtn.type = 'button'; + this.undoBtn.className = 'draw-floating-btn draw-floating-btn-undo'; + this.undoBtn.title = this.labels.undo; + this.undoBtn.innerHTML = ``; + this.undoBtn.addEventListener('click', (e) => { + e.preventDefault(); + this.undoLastAction(); + }); + + // Save button + this.saveBtn = document.createElement('button'); + this.saveBtn.type = 'button'; + this.saveBtn.className = 'draw-floating-btn draw-floating-btn-save'; + this.saveBtn.title = this.labels.saveGeometries; + this.saveBtn.innerHTML = ` + + ${this.labels.saveGeometries} + 0 + `; + this.saveBtn.addEventListener('click', (e) => { + e.preventDefault(); + this._handleSave(); + }); + + this.floatingActionsContainer.appendChild(this.undoBtn); + this.floatingActionsContainer.appendChild(this.saveBtn); + + // Append to map container + const mapContainer = this.map.getContainer(); + mapContainer.appendChild(this.floatingActionsContainer); + } + + /** + * Create help message element + */ + _createHelpMessage() { + this.helpMessageEl = document.createElement('div'); + this.helpMessageEl.className = 'draw-help-message'; + + // Append to map container + const mapContainer = this.map.getContainer(); + mapContainer.appendChild(this.helpMessageEl); + } + + /** + * Show help message + * @param {string} message - Message text + * @param {string} type - Message type: 'info', 'success', 'warning', 'danger' + * @param {number} duration - Duration in ms (default: helpMessageDuration) + */ + showHelp(message, type = 'info', duration = null) { + // Clear any pending timeout + if (this.helpMessageTimeout) { + clearTimeout(this.helpMessageTimeout); + } + + // Set message content and type + this.helpMessageEl.textContent = message; + this.helpMessageEl.className = 'draw-help-message ' + type; + + // Show message + requestAnimationFrame(() => { + this.helpMessageEl.classList.add('visible'); + }); + + // Hide after duration + const displayDuration = duration !== null ? duration : this.helpMessageDuration; + if (displayDuration > 0) { + this.helpMessageTimeout = setTimeout(() => { + this.helpMessageEl.classList.remove('visible'); + }, displayDuration); + } + } + + /** + * Hide help message immediately + */ + hideHelp() { + if (this.helpMessageTimeout) { + clearTimeout(this.helpMessageTimeout); + } + this.helpMessageEl.classList.remove('visible'); + } + + /** + * Update floating buttons visibility + */ + _updateFloatingButtons() { + // Use callbacks if provided, otherwise use internal state + const modCount = this.callbacks.getModifiedCount + ? this.callbacks.getModifiedCount() + : this.modifiedGeometries.size; + const drawMode = this.callbacks.getIsDrawMode + ? this.callbacks.getIsDrawMode() + : this.isDrawMode; + const historyCount = this.geometryHistory.length; + + // Show/hide container based on draw mode and modifications + const shouldShow = drawMode && (modCount > 0 || historyCount > 0); + this.floatingActionsContainer.style.display = shouldShow ? 'flex' : 'none'; + + // Update save button badge + const badge = this.saveBtn.querySelector('.badge'); + if (badge) { + badge.textContent = modCount.toString(); + } + + // Enable/disable buttons + this.saveBtn.disabled = modCount === 0; + this.saveBtn.style.opacity = modCount === 0 ? '0.5' : '1'; + + this.undoBtn.style.display = historyCount > 0 ? 'flex' : 'none'; + } + + /** + * Public method to refresh floating buttons (for external state updates) + */ + refreshFloatingButtons() { + this._updateFloatingButtons(); + } + + /** + * Handle save button click + */ + _handleSave() { + if (this.callbacks.onSave) { + this.callbacks.onSave(this.modifiedGeometries); + } + } + + /** + * Create a toolbar button + */ + _createButton(iconSvg, title, onClick, disabled = false) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'draw-toolbar-btn'; + btn.title = title; + btn.style.backgroundImage = this._iconUrl(iconSvg); + btn.disabled = disabled; + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!btn.disabled) { + onClick(); + } + }); + return btn; + } + + /** + * Setup draw event listeners + */ + _setupDrawEventListeners() { + if (!this.draw || !this.map) return; + + // Draw create event + this.map.on('draw.create', (e) => this._handleDrawCreate(e)); + + // Draw update event + this.map.on('draw.update', (e) => this._handleDrawUpdate(e)); + + // Draw delete event + this.map.on('draw.delete', (e) => this._handleDrawDelete(e)); + + // Draw selection change + this.map.on('draw.selectionchange', (e) => this._handleSelectionChange(e)); + + // Draw mode change + this.map.on('draw.modechange', (e) => this._handleModeChange(e)); + + // Draw feature tap event (for touch deselection) + this.map.on('draw.featureTap', (e) => this._handleFeatureTap(e)); + + // Setup click handlers on configured layers for asset selection + this._setupAssetClickHandlers(); + } + + /** + * Setup click handlers on asset layers + * This allows clicking on polygons to add them to edit mode + */ + _setupAssetClickHandlers() { + if (!this.layerConfig.layerIds || this.layerConfig.layerIds.length === 0) { + return; + } + + // Bind the handler so we can reference it later + this._boundAssetClickHandler = this._handleAssetClick.bind(this); + this._boundAssetMouseEnterHandler = this._handleAssetMouseEnter.bind(this); + this._boundAssetMouseLeaveHandler = this._handleAssetMouseLeave.bind(this); + + // Track which layers have handlers attached + this._attachedLayers = new Set(); + + // Function to attach handlers to available layers + const attachHandlers = () => { + let allAttached = true; + this.layerConfig.layerIds.forEach(layerId => { + // Skip if already attached + if (this._attachedLayers.has(layerId)) { + return; + } + + if (this.map.getLayer(layerId)) { + this.map.on('click', layerId, this._boundAssetClickHandler); + this.map.on('mouseenter', layerId, this._boundAssetMouseEnterHandler); + this.map.on('mouseleave', layerId, this._boundAssetMouseLeaveHandler); + this._attachedLayers.add(layerId); + } else { + allAttached = false; + } + }); + return allAttached; + }; + + // Try to attach immediately + if (!attachHandlers()) { + // Listen for layer additions via styledata event + const onStyleData = (e) => { + if (attachHandlers()) { + this.map.off('styledata', onStyleData); + this.map.off('sourcedata', onSourceData); + } + }; + + // Also listen for sourcedata in case layers are added after source + const onSourceData = (e) => { + if (e.sourceId === this.layerConfig.sourceId && e.isSourceLoaded) { + // Give a small delay for layers to be added after source + setTimeout(() => { + if (attachHandlers()) { + this.map.off('styledata', onStyleData); + this.map.off('sourcedata', onSourceData); + } + }, 100); + } + }; + + this.map.on('styledata', onStyleData); + this.map.on('sourcedata', onSourceData); + + // Also try with a delay as fallback + setTimeout(() => { + if (attachHandlers()) { + this.map.off('styledata', onStyleData); + this.map.off('sourcedata', onSourceData); + } + }, 500); + } + + // Also setup handler for clicks on empty areas (to deselect all) + this._boundEmptyClickHandler = this._handleEmptyClick.bind(this); + this.map.on('click', this._boundEmptyClickHandler); + } + + /** + * Handle click on empty area (no features) + * Deselects all features but keeps modifications + */ + _handleEmptyClick(e) { + + // Only process if in draw mode + if (!this.isDrawMode) { + return; + } + + // If currently drawing a line or polygon, don't handle empty clicks + if (this.draw && typeof this.draw.getMode === 'function') { + const mode = this.draw.getMode(); + if (mode === 'draw_line_string' || mode === 'draw_polygon') { + return; + } + } + + // Skip if we just handled a feature tap (for touch events) + if (this._justHandledFeatureTap) { + return; + } + + // Check if CustomDraw is currently dragging - if so, don't process click + if (this.draw && (this.draw.isDragging || this.draw.isDraggingFeature)) { + return; + } + + // Check if we should ignore this click because an actual drag just ended + if (this.draw && this.draw._ignoringClickAfterDrag) { + this.draw._ignoringClickAfterDrag = false; // Clear the flag + return; + } + + // Check if we clicked on any of our asset layers + const layerIds = this.layerConfig.layerIds || []; + const features = this.map.queryRenderedFeatures(e.point, { layers: layerIds }); + + // If we clicked on a feature that is NOT hidden, let the layer click handler handle it + // If the feature IS hidden (in edit mode), we should process deselection + if (features && features.length > 0) { + // Check if the feature is hidden (being edited) + const clickedId = features[0].properties?.id || features[0].properties?.asset_id || features[0].id; + if (!this.hiddenFeatures.has(clickedId) && !this.hiddenFeatures.has(String(clickedId))) { + return; + } + } + + // Check if we clicked on a CustomDraw element (vertex, midpoint, edge) + // Layer IDs used by CustomDraw (custom-draw.js) + const drawInteractiveLayers = [ + 'vertex', + 'vertex-halo', + 'midpoint', + 'polygon-fill', + 'polygon-outline', + 'line', + 'point' + ]; + + // Query for draw elements at click point + const existingLayers = drawInteractiveLayers.filter(id => this.map.getLayer(id)); + if (existingLayers.length > 0) { + const drawElements = this.map.queryRenderedFeatures(e.point, { layers: existingLayers }); + if (drawElements && drawElements.length > 0) { + // Clicked on a draw element (vertex/midpoint) - let CustomDraw handle it + return; + } + } + + // Check if we clicked on a draw feature (CustomDraw's own features) + const drawFeatures = this.draw.getAll().features; + if (drawFeatures.length === 0) { + // No features in draw, nothing to deselect + return; + } + + // Check if click is on a draw feature by checking if any draw feature contains the clicked point + const clickedPoint = [e.lngLat.lng, e.lngLat.lat]; + let clickedDrawFeature = null; + + for (const feature of drawFeatures) { + if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { + try { + if (typeof turf !== 'undefined' && turf.booleanPointInPolygon) { + if (turf.booleanPointInPolygon(clickedPoint, feature)) { + clickedDrawFeature = feature; + break; + } + } + } catch (err) { + // Ignore turf errors + } + } + } + + if (clickedDrawFeature) { + // Clicked on a draw feature - deselect it (remove from edit mode, keep modifications) + this._removeFeatureFromEditKeepModifications(clickedDrawFeature.id); + return; + } + + this._deselectAllKeepModifications(); + } + + /** + * Handle click on asset layer + */ + _handleAssetClick(e) { + // Ignore clicks when comment repositioning is active + if (window.commentsComponent?.isRepositioning) { + return; + } + + // Only process if in draw mode + if (!this.isDrawMode) { + return; + } + + // Check if CustomDraw is currently dragging or just finished dragging + if (this.draw && (this.draw.isDragging || this.draw.isDraggingFeature)) { + return; + } + if (this.draw && this.draw._lastDragEndTime && (Date.now() - this.draw._lastDragEndTime < 100)) { + return; + } + + // Check if we clicked on a CustomDraw vertex or midpoint - if so, let CustomDraw handle it + const drawInteractiveLayers = ['vertex', 'vertex-halo', 'midpoint']; + const existingLayers = drawInteractiveLayers.filter(id => this.map.getLayer(id)); + if (existingLayers.length > 0) { + const drawElements = this.map.queryRenderedFeatures(e.point, { layers: existingLayers }); + if (drawElements && drawElements.length > 0) { + // Clicked on a vertex/midpoint - let CustomDraw handle it + return; + } + } + + // Check if we have features + if (!e.features || e.features.length === 0) { + return; + } + + const clickedFeature = e.features[0]; + + // Try to get the real feature ID from multiple sources: + // IMPORTANT: Check properties FIRST because MapLibre's generateId creates + // sequential IDs that don't match the real feature IDs from the API. + // The template stores the real ID in properties.id and properties.asset_id + let featureId = clickedFeature.properties?.id || + clickedFeature.properties?.asset_id || + clickedFeature.properties?.pk; + + // Fall back to feature.id only if properties don't have it + if (featureId === undefined || featureId === null) { + featureId = clickedFeature.id; + } + + // If still no ID, try to find it in loadedAssets by geometry matching + if ((featureId === undefined || featureId === null) && this.loadedAssets.size > 0) { + const clickedGeomStr = JSON.stringify(clickedFeature.geometry); + + for (const [assetId, asset] of this.loadedAssets) { + if (JSON.stringify(asset.geometry) === clickedGeomStr) { + featureId = assetId; + break; + } + } + } + + if (featureId === undefined || featureId === null) { + return; + } + + // FIRST CHECK: If feature is hidden (in edit mode), check if we should deselect it + // This handles the case where MapLibre hasn't yet updated the rendered features + if (this.hiddenFeatures.has(featureId)) { + const existingFeature = this.draw.get(featureId); + if (existingFeature) { + this._removeFeatureFromEditKeepModifications(featureId); + } + return; + } + + // Check if this feature is already in draw mode - if so, REMOVE it (deselect) + const existingFeature = this.draw.get(featureId); + if (existingFeature) { + this._removeFeatureFromEditKeepModifications(featureId); + return; + } + + // Create a feature object with the correct ID + const feature = this.loadedAssets.get(featureId) || { + id: featureId, + type: 'Feature', + geometry: clickedFeature.geometry, + properties: clickedFeature.properties || {} + }; + + // If callback provided, use it + if (this.callbacks.onAssetClick) { + this.callbacks.onAssetClick(feature); + return; + } + + // Internal handling: add feature to edit mode + this.addFeatureToEdit(feature); + + // Show message + this._showMessage(this.labels.polygonAdded || 'Polygon added to edit mode', 'success'); + } + + /** + * Handle mouse enter on asset layer + */ + _handleAssetMouseEnter(e) { + if (this.isDrawMode) { + this.map.getCanvas().style.cursor = 'pointer'; + } + } + + /** + * Handle mouse leave on asset layer + */ + _handleAssetMouseLeave(e) { + if (this.isDrawMode) { + this.map.getCanvas().style.cursor = ''; + } + } + + /** + * Toggle draw mode on/off + */ + toggleDrawMode() { + this.isDrawMode = !this.isDrawMode; + + // Update button state + this.buttons.drawMode.classList.toggle('active', this.isDrawMode); + + // Show/hide tools group + this.toolsGroup.style.display = this.isDrawMode ? 'block' : 'none'; + + // Enable/disable tools + this.buttons.addPolygon.disabled = !this.isDrawMode; + this.buttons.delete.disabled = !this.isDrawMode; + + // Update split/merge state + this._updateSplitMergeState(); + + // Update floating buttons + this._updateFloatingButtons(); + + // Fire callback - let the page handle draw mode logic + if (this.callbacks.onDrawModeChange) { + this.callbacks.onDrawModeChange(this.isDrawMode); + } else { + // Internal handling (no callback provided) + this._showMessage( + this.isDrawMode ? this.labels.drawModeEnabled : this.labels.drawModeDisabled, + 'info' + ); + + // If turning off draw mode, clear draw control + if (!this.isDrawMode) { + this._exitDrawMode(); + } + } + } + + /** + * Exit draw mode and restore features (discards all modifications) + */ + _exitDrawMode() { + // Get all features from draw + const drawFeatures = this.draw.getAll().features; + const shouldDiscardChanges = this.modifiedGeometries.size > 0; + + // Restore hidden features on the map + drawFeatures.forEach(feature => { + const featureId = feature.id; + if (!String(featureId).startsWith('new_')) { + this.hiddenFeatures.delete(featureId); + + // Restore original geometry (discard edits) OR keep current draw geometry + if (this.loadedAssets.has(featureId)) { + const asset = this.loadedAssets.get(featureId); + + if (shouldDiscardChanges) { + if (this.originalGeometries.has(featureId)) { + const originalGeometry = this.originalGeometries.get(featureId); + asset.geometry = JSON.parse(JSON.stringify(originalGeometry)); + } + } else { + asset.geometry = JSON.parse(JSON.stringify(feature.geometry)); + } + } + } + }); + + // Clear draw + this.draw.deleteAll(); + this.draw.changeMode('edit'); + + // Clear tracked originals; discard modifications only if needed + if (shouldDiscardChanges) { + this.modifiedGeometries.clear(); + } + this.originalGeometries.clear(); + this.geometryHistory = []; + + // Update layer filters + this._updateLayerFilters(); + + // Update map source with original data + this._updateMapSource(); + + // Update floating buttons (should hide them since no modifications) + this._updateFloatingButtons(); + + // Reset state + this.currentEditingAsset = null; + this.lastSelectedFeatureIds = []; + this.isSplitDrawing = false; + + // Cancel any pending split + if (this.splitLineFeatureId) { + this.splitLineFeatureId = null; + } + + if (shouldDiscardChanges) { + this._showMessage(this.labels.drawModeDisabled || 'Draw mode disabled - modifications discarded', 'warning'); + } else { + this._showMessage(this.labels.drawModeDisabled || 'Draw mode disabled', 'info'); + } + } + + /** + * Remove a feature from edit mode but keep its modifications + * The modified geometry is stored and will be shown on the map + */ + _removeFeatureFromEditKeepModifications(featureId) { + // Get the current geometry from draw (which may have been modified) + const drawFeature = this.draw.get(featureId); + + if (!drawFeature) { + // Try with string/numeric conversion + const stringId = String(featureId); + const numericId = Number(featureId); + if (!this.draw.get(stringId) && !this.draw.get(numericId)) { + return; + } + } + + // Check if geometry was modified + // IMPORTANT: Compare with the NORMALIZED original geometry (stored after draw.add) + const originalGeometry = this.originalGeometries.get(featureId); + const currentGeometry = drawFeature.geometry; + + // Use a robust geometry comparison + const geometryChanged = this._geometriesAreDifferent(originalGeometry, currentGeometry); + + if (geometryChanged) { + // Track the modification + this.modifiedGeometries.set(featureId, { + assetId: featureId, + assetType: drawFeature.properties?.asset_type || this.layerConfig.assetType, + originalGeometry: originalGeometry, + currentGeometry: currentGeometry, + isDeleted: false + }); + + // Update the loaded asset with the new geometry for display + const originalAsset = this.loadedAssets.get(featureId); + if (originalAsset) { + originalAsset.geometry = JSON.parse(JSON.stringify(currentGeometry)); + } + } + + // Remove from draw + this.draw.delete(featureId); + + // Clear the stored original geometry + this.originalGeometries.delete(featureId); + + // IMPORTANT: Reset draw mode to refresh the display (clear vertices/outline) + const remainingFeatures = this.draw.getAll().features; + const remainingIds = remainingFeatures.map(f => f.id); + + // Reset to edit mode to clear the visual state + if (remainingIds.length > 0) { + this.draw.changeMode('edit', { featureIds: remainingIds }); + } else { + this.draw.changeMode('edit'); + } + + // Show on map again + this.hiddenFeatures.delete(featureId); + this._updateLayerFilters(); + + // Update the map source to show the (possibly modified) geometry + this._updateMapSource(); + + // Update state + if (this.currentEditingAsset?.id === featureId) { + this.currentEditingAsset = null; + } + + this._updateSplitMergeState(); + this._updateFloatingButtons(); + + this._showMessage(this.labels.polygonRemoved || 'Polygon removed from edit mode', 'info'); + } + + /** + * Remove all features from edit mode but keep modifications + * Called when clicking on empty area + */ + _deselectAllKeepModifications() { + const drawFeatures = this.draw.getAll().features; + const featuresToRemove = []; + + drawFeatures.forEach(feature => { + const featureId = feature.id; + + // Skip new features (they should stay in draw) + if (String(featureId).startsWith('new_')) { + return; + } + + featuresToRemove.push(featureId); + + // Check if geometry was modified + const originalAsset = this.loadedAssets.get(featureId); + const originalGeometry = originalAsset?.geometry; + const currentGeometry = feature.geometry; + + const geometryChanged = JSON.stringify(originalGeometry) !== JSON.stringify(currentGeometry); + + if (geometryChanged) { + // Track the modification + this.modifiedGeometries.set(featureId, { + assetId: featureId, + assetType: feature.properties?.asset_type || this.layerConfig.assetType, + originalGeometry: originalGeometry, + currentGeometry: currentGeometry, + isDeleted: false + }); + + // Update the loaded asset with the new geometry + if (originalAsset) { + originalAsset.geometry = currentGeometry; + } + } + + // Mark for showing on map + this.hiddenFeatures.delete(featureId); + }); + + // Remove all features from draw at once + if (featuresToRemove.length > 0) { + this.draw.delete(featuresToRemove); + } + + // IMPORTANT: Reset draw mode to clear visual state (vertices/outline) + this.draw.changeMode('edit'); + + // Update map + this._updateLayerFilters(); + this._updateMapSource(); + + // Reset state + this.currentEditingAsset = null; + this.lastSelectedFeatureIds = []; + + this._updateSplitMergeState(); + this._updateFloatingButtons(); + } + + /** + * Start drawing a new polygon + */ + startDrawPolygon() { + if (!this.isDrawMode) return; + + // If callback provided, let the page handle it + if (this.callbacks.onAddPolygon) { + this.callbacks.onAddPolygon(); + // Show help message + this.showHelp(this.labels.addPolygonHelp, 'info'); + } else { + // Internal handling + this.draw.changeMode('draw_polygon'); + // Show help message + this._showMessage(this.labels.addPolygonHelp, 'info'); + } + } + + /** + * Delete selected features + */ + deleteSelected() { + if (!this.isDrawMode) return; + + // If callback provided, let the page handle it + if (this.callbacks.onDeleteSelected) { + const deleted = this.callbacks.onDeleteSelected(); + if (deleted) { + this.showHelp(this.labels.geometriesDeleted, 'info'); + } else { + this.showHelp(this.labels.noFeaturesToDelete, 'warning'); + } + return; + } + + // Internal handling + const selectedFeatures = this.draw.getAll().features; + if (selectedFeatures.length === 0) { + this._showMessage(this.labels.noFeaturesToDelete, 'warning'); + return; + } + + selectedFeatures.forEach(feature => { + const featureId = feature.id; + + // Track deletion in history + this._addToHistory({ + type: 'delete', + featureId: featureId, + geometry: feature.geometry, + properties: feature.properties + }); + + // Remove from draw + this.draw.delete(featureId); + + // If it's an existing asset, track for deletion on save + if (!String(featureId).startsWith('new_')) { + this.modifiedGeometries.set(featureId, { + assetId: featureId, + assetType: this.layerConfig.assetType, + originalGeometry: this.loadedAssets.get(featureId)?.geometry || null, + currentGeometry: null, // null = deleted + isDeleted: true + }); + } else { + // Remove new feature from tracking + this.modifiedGeometries.delete(featureId); + this.loadedAssets.delete(featureId); + } + + // Show feature on map if it was hidden + this.hiddenFeatures.delete(featureId); + }); + + this._updateLayerFilters(); + this._updateMapSource(); + this._updateModifiedCount(); + + // Show confirmation + this._showMessage(this.labels.geometriesDeleted, 'info'); + + if (this.callbacks.onGeometryDelete) { + this.callbacks.onGeometryDelete(selectedFeatures.map(f => f.id)); + } + } + + /** + * Toggle snapping + */ + toggleSnap() { + const isActive = this.buttons.snap.classList.contains('active'); + + this.buttons.snap.classList.toggle('active', !isActive); + this.draw.options.snap = !isActive; + + this._showMessage( + isActive ? this.labels.snapDisabled : this.labels.snapEnabled, + 'info' + ); + } + + /** + * Toggle split mode + */ + toggleSplit() { + // If callback provided, let the page handle split logic + if (this.callbacks.onSplitToggle) { + const newState = !this.buttons.split.classList.contains('active'); + this.buttons.split.classList.toggle('active', newState); + this.callbacks.onSplitToggle(newState); + if (newState) { + this.showHelp(this.labels.splitHelp, 'info'); + } + } else { + // Internal handling + if (this.isSplitDrawing) { + this._cancelSplit(); + } else { + this._startSplit(); + } + } + } + + /** + * Start split drawing + */ + _startSplit() { + this.isSplitDrawing = true; + this.buttons.split.classList.add('active'); + + // Switch to line drawing mode + this.draw.changeMode('draw_line_string'); + + // Show help message + this._showMessage(this.labels.splitHelp, 'info'); + } + + /** + * Cancel split drawing + */ + _cancelSplit() { + // Clean up temp line + if (this.splitLineFeatureId && this.draw.get(this.splitLineFeatureId)) { + try { + this.draw.delete(this.splitLineFeatureId); + } catch (err) {} + } + + this.isSplitDrawing = false; + this.splitLineFeatureId = null; + this.buttons.split.classList.remove('active'); + + // Return to edit mode + const drawFeatures = this.draw.getAll().features; + if (drawFeatures.length > 0) { + this.draw.changeMode('edit', { featureIds: drawFeatures.map(f => f.id) }); + } else { + this.draw.changeMode('edit'); + } + + this._showMessage(this.labels.splitCancelled, 'info'); + } + + /** + * Perform merge of selected polygons + */ + performMerge() { + // If callback provided, let the page handle merge logic + if (this.callbacks.onMerge) { + this.callbacks.onMerge(); + return; + } + + // Internal handling + const drawFeatures = this.draw.getAll().features; + const polygons = drawFeatures.filter(f => + f.geometry && (f.geometry.type === 'Polygon' || f.geometry.type === 'MultiPolygon') + ); + + if (polygons.length < 2) { + this._showMessage('Select at least 2 polygons to merge', 'warning'); + return; + } + + try { + // Use turf to merge polygons + let merged = polygons[0]; + for (let i = 1; i < polygons.length; i++) { + merged = turf.union( + turf.feature(merged.geometry), + turf.feature(polygons[i].geometry) + ); + } + + if (!merged || !merged.geometry) { + throw new Error('Union failed'); + } + + // Use the first polygon's ID and properties + const primaryId = polygons[0].id; + const primaryProps = polygons[0].properties || {}; + + // Track deletion for all other polygons + for (let i = 1; i < polygons.length; i++) { + const polyId = polygons[i].id; + + this._addToHistory({ + type: 'merge_delete', + featureId: polyId, + geometry: polygons[i].geometry, + properties: polygons[i].properties + }); + + // Mark for deletion + this.modifiedGeometries.set(polyId, { + assetId: polyId, + assetType: this.layerConfig.assetType, + originalGeometry: this.loadedAssets.get(polyId)?.geometry || polygons[i].geometry, + currentGeometry: null, + isDeleted: true + }); + + // Remove from draw + this.draw.delete(polyId); + + // Remove from loadedAssets + this.loadedAssets.delete(polyId); + } + + // Update primary polygon with merged geometry + this._addToHistory({ + type: 'merge_update', + featureId: primaryId, + geometry: polygons[0].geometry, + newGeometry: merged.geometry + }); + + // Delete and re-add with new geometry + this.draw.delete(primaryId); + this.draw.add({ + id: primaryId, + type: 'Feature', + geometry: merged.geometry, + properties: primaryProps + }); + + // Update loadedAssets + const primaryAsset = this.loadedAssets.get(primaryId); + if (primaryAsset) { + primaryAsset.geometry = merged.geometry; + } + + // Track modification + this.modifiedGeometries.set(primaryId, { + assetId: primaryId, + assetType: this.layerConfig.assetType, + originalGeometry: primaryAsset?.geometry || polygons[0].geometry, + currentGeometry: merged.geometry, + modelId: primaryProps.model_id || primaryProps.modelId || null + }); + + this._updateMapSource(); + this._updateModifiedCount(); + this._updateSplitMergeState(); + + this._showMessage(`${this.labels.mergeSuccess}: ${polygons.length}`, 'success'); + + } catch (error) { + this._showMessage(this.labels.mergeError, 'danger'); + } + } + + /** + * Update split/merge button states + */ + _updateSplitMergeState() { + if (!this.isDrawMode && !(this.callbacks.getIsDrawMode && this.callbacks.getIsDrawMode())) { + this.buttons.split.disabled = true; + this.buttons.merge.disabled = true; + return; + } + + // Use callback to get features if provided + let drawFeatures; + if (this.callbacks.getDrawFeatures) { + drawFeatures = this.callbacks.getDrawFeatures(); + } else { + drawFeatures = this.draw.getAll().features; + } + + const polygonCount = drawFeatures.filter(f => + f.geometry && (f.geometry.type === 'Polygon' || f.geometry.type === 'MultiPolygon') + ).length; + + // Update delete button + this.buttons.delete.disabled = polygonCount < 1; + + // Split needs draw mode to be active + const isSplitDrawing = this.callbacks.getIsSplitDrawing + ? this.callbacks.getIsSplitDrawing() + : this.isSplitDrawing; + this.buttons.split.disabled = false; + + // Merge needs at least 2 polygons + this.buttons.merge.disabled = polygonCount < 2; + } + + /** + * Update delete button state + * @param {boolean} enabled - Whether the button should be enabled + */ + updateDeleteState(enabled) { + if (this.buttons.delete) { + this.buttons.delete.disabled = !enabled; + } + } + + /** + * Update split button state + * @param {boolean} enabled - Whether the button should be enabled + */ + updateSplitState(enabled) { + if (this.buttons.split) { + this.buttons.split.disabled = !enabled; + // Also update active class based on split drawing state + const isSplitDrawing = this.callbacks.getIsSplitDrawing + ? this.callbacks.getIsSplitDrawing() + : this.isSplitDrawing; + this.buttons.split.classList.toggle('active', isSplitDrawing); + } + } + + /** + * Update merge button state + * @param {boolean} enabled - Whether the button should be enabled + */ + updateMergeState(enabled) { + if (this.buttons.merge) { + this.buttons.merge.disabled = !enabled; + } + } + + /** + * Handle draw create event + */ + _handleDrawCreate(e) { + const features = e.features || []; + + // Check if we're in split drawing mode (use callback if provided, otherwise internal state) + const isSplitDrawing = this.callbacks.getIsSplitDrawing + ? this.callbacks.getIsSplitDrawing() + : this.isSplitDrawing; + + // Check if this is a split line + const splitLine = features.find(f => + isSplitDrawing && f.geometry && f.geometry.type === 'LineString' + ); + + if (splitLine) { + // If onSplitToggle callback is provided, the page handles split logic itself + // via its own draw.create handler, so we don't duplicate the split here + if (this.callbacks.onSplitToggle) { + // Page handles split - don't do anything here + return; + } + this._performSplit(splitLine); + return; + } + + // Process new polygon(s) + features.forEach(feature => { + if (feature.geometry.type !== 'Polygon' && feature.geometry.type !== 'MultiPolygon') { + return; + } + + const tempId = 'new_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + const originalId = feature.id; + + // Replace with new ID + this.draw.delete(originalId); + this.draw.add({ + id: tempId, + type: 'Feature', + geometry: feature.geometry, + properties: { + isNew: true, + asset_type: this.layerConfig.assetType + } + }); + + // Track as new geometry + this.modifiedGeometries.set(tempId, { + assetId: tempId, + assetType: this.layerConfig.assetType, + originalGeometry: null, + currentGeometry: feature.geometry, + isNew: true + }); + + // Add to loaded assets + this.loadedAssets.set(tempId, { + id: tempId, + type: 'Feature', + geometry: feature.geometry, + properties: { isNew: true, asset_type: this.layerConfig.assetType } + }); + + // Track in history + this._addToHistory({ + type: 'create', + featureId: tempId, + geometry: feature.geometry + }); + + this.currentEditingAsset = { + id: tempId, + assetType: this.layerConfig.assetType + }; + + // Switch to edit mode + setTimeout(() => { + this.draw.changeMode('edit', { featureIds: [tempId] }); + }, 10); + }); + + this._updateMapSource(); + this._updateModifiedCount(); + + if (this.callbacks.onGeometryCreate) { + this.callbacks.onGeometryCreate(features); + } + } + + /** + * Handle draw update event + */ + _handleDrawUpdate(e) { + const features = e.features || []; + + features.forEach(feature => { + const featureId = feature.id; + + // Get the original geometry (normalized by draw when added) + const originalGeometry = this.originalGeometries.get(featureId); + const currentGeometry = feature.geometry; + + // Check if geometry actually changed + const geometryChanged = this._geometriesAreDifferent(originalGeometry, currentGeometry); + + if (!geometryChanged) { + // No actual change, skip this feature + return; + } + + const existingMod = this.modifiedGeometries.get(featureId); + + // Track in history + this._addToHistory({ + type: 'update', + featureId: featureId, + geometry: existingMod?.currentGeometry || originalGeometry || feature.geometry, + newGeometry: feature.geometry + }); + + // Update modification tracking + this.modifiedGeometries.set(featureId, { + assetId: featureId, + assetType: this.layerConfig.assetType, + originalGeometry: originalGeometry || existingMod?.originalGeometry || null, + currentGeometry: feature.geometry, + isNew: String(featureId).startsWith('new_') + }); + + // Update loaded assets + const asset = this.loadedAssets.get(featureId); + if (asset) { + asset.geometry = feature.geometry; + } + }); + + this._updateMapSource(); + this._updateModifiedCount(); + + if (this.callbacks.onGeometryUpdate) { + this.callbacks.onGeometryUpdate(features); + } + } + + /** + * Handle draw delete event + */ + _handleDrawDelete(e) { + const features = e.features || []; + + features.forEach(feature => { + // Callback already handled in deleteSelected + }); + } + + /** + * Handle selection change event + */ + _handleSelectionChange(e) { + const selectedIds = e.features ? e.features.map(f => f.id) : []; + + this.lastSelectedFeatureIds = selectedIds; + this._updateSplitMergeState(); + + if (this.callbacks.onSelectionChange) { + // Pass full features with properties, not just IDs + const selectedFeatures = selectedIds.map(id => { + // Try to get from draw first (has current geometry) + const drawFeature = this.draw.get(id); + // Merge with loadedAssets to get original properties + const loadedAsset = this.loadedAssets.get(id); + + if (drawFeature) { + return { + id: id, + type: 'Feature', + geometry: drawFeature.geometry, + properties: { + ...(loadedAsset?.properties || {}), + ...(drawFeature.properties || {}) + } + }; + } else if (loadedAsset) { + return loadedAsset; + } + return { id: id, type: 'Feature', geometry: null, properties: {} }; + }).filter(f => f.geometry); + + this.callbacks.onSelectionChange(selectedFeatures, selectedIds); + } + } + + /** + * Handle mode change event + */ + _handleModeChange(e) { + // Cancel split if mode changed away from line drawing + if (this.isSplitDrawing && e.mode !== 'draw_line_string') { + this.isSplitDrawing = false; + this.splitLineFeatureId = null; + this.buttons.split.classList.remove('active'); + } + + this._updateSplitMergeState(); + } + + /** + * Handle tap on a feature in CustomDraw (for touch deselection) + */ + _handleFeatureTap(e) { + + if (!this.isDrawMode) { + return; + } + + // Set flag to prevent click handler from also processing this + this._justHandledFeatureTap = true; + setTimeout(() => { + this._justHandledFeatureTap = false; + }, 300); // Clear after a short delay + + // Deselect the tapped feature + if (e.featureId) { + this._removeFeatureFromEditKeepModifications(e.featureId); + } + } + + /** + * Perform polygon split with a line + */ + _performSplit(lineFeature) { + const drawFeatures = this.draw.getAll().features; + + const polygonsById = new Map(); + + // Polygons currently in draw (editing) + drawFeatures.forEach(f => { + if (f.id === lineFeature.id) return; + if (!f.geometry) return; + if (f.geometry.type !== 'Polygon' && f.geometry.type !== 'MultiPolygon') return; + polygonsById.set(f.id, { + feature: f, + inDraw: true + }); + }); + + // Polygons from loaded assets (visible source) + this.loadedAssets.forEach((asset, assetId) => { + if (!asset?.geometry) return; + if (asset.geometry.type !== 'Polygon' && asset.geometry.type !== 'MultiPolygon') return; + if (!polygonsById.has(assetId)) { + polygonsById.set(assetId, { + feature: asset, + inDraw: false + }); + } + }); + + const polygons = Array.from(polygonsById.values()); + + let splitCount = 0; + + polygons.forEach(entry => { + const polygon = entry.feature; + const inDraw = entry.inDraw; + + // Check if line intersects polygon + const intersects = this._geometryIntersectsLine(polygon.geometry, lineFeature.geometry.coordinates); + if (!intersects) { + return; + } + + // Perform split + const result = this._splitPolygonWithLine(polygon.geometry, lineFeature.geometry); + + if (result && result.length >= 2) { + // Track original in history + this._addToHistory({ + type: 'split', + featureId: polygon.id, + geometry: polygon.geometry, + splitResults: result + }); + + // Delete original from draw if present + if (inDraw) { + this.draw.delete(polygon.id); + } + + // Always add first split result to draw control with original ID + // so user can see and edit the split result + this.draw.add({ + id: polygon.id, + type: 'Feature', + geometry: result[0], + properties: polygon.properties || {} + }); + + // Update tracking for original + this.modifiedGeometries.set(polygon.id, { + assetId: polygon.id, + assetType: this.layerConfig.assetType, + originalGeometry: this.loadedAssets.get(polygon.id)?.geometry || polygon.geometry, + currentGeometry: result[0] + }); + + // Update loaded assets for original + const origAsset = this.loadedAssets.get(polygon.id); + if (origAsset) { + origAsset.geometry = result[0]; + } else { + // If not already in loadedAssets, add it so map updates + this.loadedAssets.set(polygon.id, { + id: polygon.id, + type: 'Feature', + geometry: result[0], + properties: polygon.properties || {} + }); + } + + // Hide original feature on the map layer since it's now in draw + if (!inDraw) { + this.hiddenFeatures.add(polygon.id); + } + + // Add remaining split results with new IDs + // Always add to draw control so user can see and edit them + for (let i = 1; i < result.length; i++) { + const newId = 'new_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + // Copy all properties from original polygon (including color, model_id, etc.) + const newProperties = { + ...polygon.properties, + isNew: true, + id: newId // Update the id property + }; + + // Always add to draw control + this.draw.add({ + id: newId, + type: 'Feature', + geometry: result[i], + properties: newProperties + }); + + this.modifiedGeometries.set(newId, { + assetId: newId, + assetType: this.layerConfig.assetType, + originalGeometry: null, + currentGeometry: result[i], + modelId: polygon.properties?.model_id || null, + properties: newProperties, + isNew: true + }); + + this.loadedAssets.set(newId, { + id: newId, + type: 'Feature', + geometry: result[i], + properties: newProperties // Use the same properties with all attributes + }); + + // Hide from map source since it's now in draw control + this.hiddenFeatures.add(newId); + } + + splitCount++; + } + }); + + if (splitCount > 0) { + // Delete the split line after successful split + this.draw.delete(lineFeature.id); + + // Reset split mode + this.isSplitDrawing = false; + this.buttons.split.classList.remove('active'); + + // Get all features after split + const allFeatures = this.draw.getAll().features; + + if (allFeatures.length > 0) { + // Use 'edit' mode which allows editing multiple features with all vertices visible + // This is the mode that shows all polygons with their vertices + this.draw.changeMode('edit', { featureIds: allFeatures.map(f => f.id) }); + } + + this._updateMapSource(); + this._updateModifiedCount(); + this._updateSplitMergeState(); + + this._showMessage(`${this.labels.splitSuccess}: ${splitCount}`, 'success'); + } else { + // No split performed - remove line and keep split mode active for retry + this.draw.delete(lineFeature.id); + this._showMessage(this.labels.splitCancelled || 'No polygons were split', 'warning'); + } + } + + /** + * Check if geometry intersects a line (requires at least 2 intersection points for split) + */ + _geometryIntersectsLine(geometry, lineCoords) { + if (window.turf && window.turf.polygonToLine && window.turf.lineIntersect) { + try { + // Handle MultiPolygon by checking each polygon + if (geometry.type === 'MultiPolygon') { + for (const polyCoords of geometry.coordinates) { + const polygon = turf.polygon(polyCoords); + const line = turf.lineString(lineCoords); + const boundary = turf.polygonToLine(polygon); + const intersections = turf.lineIntersect(boundary, line); + + // Need at least 2 intersections to split + if (intersections && intersections.features && intersections.features.length >= 2) { + return true; + } + + // If line is inside polygon but doesn't cross boundary, extend it + if (turf.booleanContains && turf.booleanContains(polygon, line)) { + return true; + } + } + return false; + } + + // Handle Polygon + const polygon = turf.feature(geometry); + const line = turf.lineString(lineCoords); + const boundary = turf.polygonToLine(polygon); + const intersections = turf.lineIntersect(boundary, line); + + // Need at least 2 intersections to split + if (intersections && intersections.features && intersections.features.length >= 2) { + return true; + } + + // If line is inside polygon but doesn't cross boundary, extend it + if (turf.booleanContains && turf.booleanContains(polygon, line)) { + // Line is entirely inside - we need to extend it + return true; // Will be handled by extending the line in split function + } + + return false; + } catch (err) { + // Fall back to manual checks + } + } + + if (geometry.type === 'Polygon') { + return this._polygonIntersectsLine(geometry.coordinates[0], lineCoords); + } else if (geometry.type === 'MultiPolygon') { + return geometry.coordinates.some(poly => + this._polygonIntersectsLine(poly[0], lineCoords) + ); + } + return false; + } + + /** + * Check if polygon ring intersects line + */ + _polygonIntersectsLine(ring, lineCoords) { + for (let i = 0; i < lineCoords.length - 1; i++) { + for (let j = 0; j < ring.length - 1; j++) { + if (this._lineSegmentIntersection( + lineCoords[i], lineCoords[i + 1], + ring[j], ring[j + 1] + )) { + return true; + } + } + } + return false; + } + + /** + * Line segment intersection + */ + _lineSegmentIntersection(p1, p2, p3, p4) { + const x1 = p1[0], y1 = p1[1]; + const x2 = p2[0], y2 = p2[1]; + const x3 = p3[0], y3 = p3[1]; + const x4 = p4[0], y4 = p4[1]; + + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (Math.abs(denom) < 1e-10) return null; + + const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; + const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; + + if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { + return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)]; + } + return null; + } + + /** + * Split polygon with line (simplified algorithm) + */ + _splitPolygonWithLine(polygonGeometry, lineGeometry) { + + // Handle MultiPolygon by splitting each polygon separately + if (polygonGeometry.type === 'MultiPolygon') { + // For now, just try to split the first polygon that intersects the line + // TODO: Handle all polygons in the MultiPolygon + for (let i = 0; i < polygonGeometry.coordinates.length; i++) { + const singlePolygon = { type: 'Polygon', coordinates: polygonGeometry.coordinates[i] }; + const result = this._splitPolygonWithLine(singlePolygon, lineGeometry); + if (result && result.length >= 2) { + return result; + } + } + return null; + } + + let lineCoords = lineGeometry.coordinates; + + // Check if line needs to be extended (if it's inside the polygon) + if (window.turf) { + try { + const polygon = turf.feature(polygonGeometry); + const line = turf.lineString(lineCoords); + const boundary = turf.polygonToLine(polygon); + const intersections = turf.lineIntersect(boundary, line); + + // If less than 2 intersections, extend the line + if (intersections.features.length < 2) { + // Calculate extension distance based on polygon size + const bbox = turf.bbox(polygon); + const diag = Math.sqrt( + Math.pow(bbox[2] - bbox[0], 2) + + Math.pow(bbox[3] - bbox[1], 2) + ); + const extension = diag * 2; // Extend by twice the diagonal + + lineCoords = this._extendLine(lineCoords, extension); + + // Check intersections again + const extendedLine = turf.lineString(lineCoords); + const newIntersections = turf.lineIntersect(boundary, extendedLine); + + if (newIntersections.features.length < 2) { + return null; + } + } + } catch (e) { + // Silent fail + } + } + + // Update lineGeometry with potentially extended coordinates + const extendedLineGeometry = { type: 'LineString', coordinates: lineCoords }; + + // DISABLED: turf.polygonSplit includes all line vertices, not just intermediate ones + // We use our custom algorithm instead to respect the rule: + // "For line ABCDE with intersections at B and D, only use segment BCD, not A or E" + /* + if (window.turf && window.turf.polygonSplit) { + try { + const polygon = turf.feature(polygonGeometry); + const line = turf.feature(extendedLineGeometry); + const split = turf.polygonSplit(polygon, line); + + if (split && split.features && split.features.length > 1) { + return split.features.map(f => f.geometry); + } + } catch (e) { + // Silent fail + } + } + */ + + // DISABLED: The polygonize/lineSplit approach is too complex and doesn't build polygons correctly + // We use our robust manual algorithm instead + /* + // Fallback using polygonize: split boundary with line, polygonize, then filter + if (window.turf && window.turf.polygonToLine && window.turf.polygonize) { + try { + const polygon = turf.feature(polygonGeometry); + const line = turf.feature(lineGeometry); + + // Extend the line beyond the polygon to ensure it crosses completely + const extendedLine = this._extendLine(lineGeometry.coordinates, 0.001); + const extLine = turf.lineString(extendedLine); + + const boundary = turf.polygonToLine(polygon); + + // Use lineSplit to split the boundary with the extended line + if (window.turf.lineSplit) { + const splitBoundary = turf.lineSplit(boundary, extLine); + + if (splitBoundary.features.length >= 2) { + // Find intersection points + const intersections = turf.lineIntersect(boundary, extLine); + + if (intersections.features.length >= 2) { + // Build polygons manually from the segments + const result = this._buildPolygonsFromSplit( + splitBoundary.features, + intersections.features, + lineCoords, + polygon + ); + if (result && result.length >= 2) { + return result; + } + } + } + } + + const lines = turf.featureCollection([boundary, extLine]); + const polys = turf.polygonize(lines); + + if (polys && polys.features && polys.features.length > 1) { + const results = polys.features.filter(f => { + try { + const center = turf.centroid(f); + return turf.booleanPointInPolygon(center, polygon); + } catch (err) { + return false; + } + }); + + if (results.length > 1) { + return results.map(f => f.geometry); + } + } + } catch (e) { + } + } + */ + + // Manual split approach for simple cases + const ring = polygonGeometry.coordinates[0]; + // Use the potentially extended line coordinates + const manualLineCoords = lineCoords; + + // Use turf.lineIntersect if available for more robust intersection detection + let intersections = []; + if (window.turf && window.turf.lineIntersect) { + try { + const polygonBoundary = turf.lineString([...ring]); + const splitLine = turf.lineString(manualLineCoords); + const intersectResult = turf.lineIntersect(polygonBoundary, splitLine); + + if (intersectResult.features.length >= 2) { + // Calculate cumulative distances along the line for each vertex + const cumulativeDistances = [0]; + for (let i = 1; i < manualLineCoords.length; i++) { + const dx = manualLineCoords[i][0] - manualLineCoords[i-1][0]; + const dy = manualLineCoords[i][1] - manualLineCoords[i-1][1]; + const segmentLength = Math.sqrt(dx * dx + dy * dy); + cumulativeDistances.push(cumulativeDistances[i-1] + segmentLength); + } + + // Convert turf results to our format + intersections = intersectResult.features.map((f, i) => { + const pt = f.geometry.coordinates; + // Find which edge this intersection is on (polygon edge) + let edgeIndex = 0; + let minDist = Infinity; + for (let j = 0; j < ring.length - 1; j++) { + const dist = this._distanceToSegment(pt, ring[j], ring[j + 1]); + if (dist < minDist) { + minDist = dist; + edgeIndex = j; + } + } + + // Find which segment of the line this intersection is on and calculate distance along line + let lineIndex = 0; + let distanceAlongLine = 0; + minDist = Infinity; + for (let k = 0; k < manualLineCoords.length - 1; k++) { + const dist = this._distanceToSegment(pt, manualLineCoords[k], manualLineCoords[k + 1]); + if (dist < minDist) { + minDist = dist; + lineIndex = k; + // Calculate distance from start of this segment to the intersection + const dx = pt[0] - manualLineCoords[k][0]; + const dy = pt[1] - manualLineCoords[k][1]; + const distFromSegmentStart = Math.sqrt(dx * dx + dy * dy); + distanceAlongLine = cumulativeDistances[k] + distFromSegmentStart; + } + } + + return { + coords: pt, + edgeIndex: edgeIndex, + lineIndex: lineIndex, + distance: distanceAlongLine + }; + }); + intersections.sort((a, b) => a.distance - b.distance); + } + } catch (e) { + } + } + + // Fallback to manual intersection detection + if (intersections.length < 2) { + intersections = this._findIntersectionPoints(ring, manualLineCoords); + } + + if (intersections.length > 0) { + } + if (intersections.length > 1) { + } + + if (intersections.length < 2) { + return null; + } + + // Build split polygons using original line coordinates (not extended) for better geometry + const result = this._buildSplitPolygons(ring, intersections, manualLineCoords); + return result; + } + + /** + * Calculate distance from point to line segment + */ + _distanceToSegment(p, a, b) { + const dx = b[0] - a[0]; + const dy = b[1] - a[1]; + const length2 = dx * dx + dy * dy; + + if (length2 === 0) { + return Math.sqrt(Math.pow(p[0] - a[0], 2) + Math.pow(p[1] - a[1], 2)); + } + + let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length2; + t = Math.max(0, Math.min(1, t)); + + const closestX = a[0] + t * dx; + const closestY = a[1] + t * dy; + + return Math.sqrt(Math.pow(p[0] - closestX, 2) + Math.pow(p[1] - closestY, 2)); + } + + /** + * Find intersection points between polygon ring and line + */ + _findIntersectionPoints(ring, lineCoords) { + const intersections = []; + + // First, calculate cumulative distances along the line for each vertex + const cumulativeDistances = [0]; + for (let i = 1; i < lineCoords.length; i++) { + const dx = lineCoords[i][0] - lineCoords[i-1][0]; + const dy = lineCoords[i][1] - lineCoords[i-1][1]; + const segmentLength = Math.sqrt(dx * dx + dy * dy); + cumulativeDistances.push(cumulativeDistances[i-1] + segmentLength); + } + + for (let i = 0; i < lineCoords.length - 1; i++) { + for (let j = 0; j < ring.length - 1; j++) { + const intersection = this._lineSegmentIntersection( + lineCoords[i], lineCoords[i + 1], + ring[j], ring[j + 1] + ); + + if (intersection) { + // Calculate distance along the line to this intersection point + // = cumulative distance to segment start + distance from segment start to intersection + const dx = intersection[0] - lineCoords[i][0]; + const dy = intersection[1] - lineCoords[i][1]; + const distFromSegmentStart = Math.sqrt(dx * dx + dy * dy); + const distanceAlongLine = cumulativeDistances[i] + distFromSegmentStart; + + intersections.push({ + coords: intersection, + edgeIndex: j, + lineIndex: i, + distance: distanceAlongLine + }); + } + } + } + + // Sort by distance along line + intersections.sort((a, b) => a.distance - b.distance); + + // Remove duplicates + const unique = []; + for (const int of intersections) { + const isDupe = unique.some(u => + Math.abs(u.coords[0] - int.coords[0]) < 1e-9 && + Math.abs(u.coords[1] - int.coords[1]) < 1e-9 + ); + if (!isDupe) unique.push(int); + } + + return unique; + } + + /** + * Build split polygons from intersection data + */ + _buildSplitPolygons(ring, intersections, lineCoords) { + if (intersections.length < 2) return null; + + const entry = intersections[0]; + const exit = intersections[intersections.length - 1]; + + // Insert intersection points into ring + const augmentedRing = []; + const insertions = new Map(); + + // Track insertions by edge + for (const int of [entry, exit]) { + if (!insertions.has(int.edgeIndex)) { + insertions.set(int.edgeIndex, []); + } + insertions.get(int.edgeIndex).push(int); + } + + // Build augmented ring + for (let i = 0; i < ring.length - 1; i++) { + augmentedRing.push(ring[i]); + + if (insertions.has(i)) { + const edgeInserts = insertions.get(i); + // Sort by distance from edge start + edgeInserts.sort((a, b) => { + const distA = Math.sqrt( + Math.pow(a.coords[0] - ring[i][0], 2) + + Math.pow(a.coords[1] - ring[i][1], 2) + ); + const distB = Math.sqrt( + Math.pow(b.coords[0] - ring[i][0], 2) + + Math.pow(b.coords[1] - ring[i][1], 2) + ); + return distA - distB; + }); + + edgeInserts.forEach(ins => augmentedRing.push(ins.coords)); + } + } + augmentedRing.push(ring[ring.length - 1]); + + // Find indices in augmented ring with larger tolerance + const tolerance = 1e-6; // Increased tolerance + let entryIdx = -1, exitIdx = -1; + for (let i = 0; i < augmentedRing.length; i++) { + if (entryIdx === -1 && + Math.abs(augmentedRing[i][0] - entry.coords[0]) < tolerance && + Math.abs(augmentedRing[i][1] - entry.coords[1]) < tolerance) { + entryIdx = i; + } + if (exitIdx === -1 && + Math.abs(augmentedRing[i][0] - exit.coords[0]) < tolerance && + Math.abs(augmentedRing[i][1] - exit.coords[1]) < tolerance) { + exitIdx = i; + } + } + + if (entryIdx === -1 || exitIdx === -1 || entryIdx === exitIdx) { + // Fallback: insert intersection points directly based on edge index + return this._buildSplitPolygonsFallback(ring, entry, exit, lineCoords); + } + + // Normalize order - track if we swapped + let idx1 = entryIdx, idx2 = exitIdx; + let swapped = false; + if (idx1 > idx2) { + [idx1, idx2] = [idx2, idx1]; + swapped = true; + } + + // Extract only the line segment between the two intersection points + // The intersections array contains entry and exit points sorted by distance along the line + // We need to find which points from lineCoords are actually between these intersections + const lineSegment = this._extractLineSegmentBetweenIntersections(lineCoords, entry, exit); + + // Build first polygon: from idx1 to idx2 along the ring + const poly1 = []; + for (let i = idx1; i <= idx2; i++) { + poly1.push(augmentedRing[i]); + } + // Add line segment to close - direction depends on whether we swapped + // If not swapped: idx1=entry, idx2=exit, so line goes entry->segment->exit + // To go from idx2 back to idx1, traverse segment in reverse + // If swapped: idx1=exit, idx2=entry, so line goes exit<-segment<-entry + // To go from idx2 back to idx1, traverse segment forward + if (!swapped) { + for (let i = lineSegment.length - 1; i >= 0; i--) { + poly1.push(lineSegment[i]); + } + } else { + for (const pt of lineSegment) { + poly1.push(pt); + } + } + if (poly1[0][0] !== poly1[poly1.length - 1][0] || + poly1[0][1] !== poly1[poly1.length - 1][1]) { + poly1.push(poly1[0]); + } + + // Build second polygon: from idx2 around the ring back to idx1 + const poly2 = []; + for (let i = idx2; i < augmentedRing.length; i++) { + poly2.push(augmentedRing[i]); + } + for (let i = 0; i <= idx1; i++) { + poly2.push(augmentedRing[i]); + } + // Add line segment to close - opposite direction from poly1 + if (!swapped) { + for (const pt of lineSegment) { + poly2.push(pt); + } + } else { + for (let i = lineSegment.length - 1; i >= 0; i--) { + poly2.push(lineSegment[i]); + } + } + if (poly2[0][0] !== poly2[poly2.length - 1][0] || + poly2[0][1] !== poly2[poly2.length - 1][1]) { + poly2.push(poly2[0]); + } + + if (poly1.length < 4 || poly2.length < 4) { + return this._buildSplitPolygonsFallback(ring, entry, exit, lineCoords); + } + + return [ + { type: 'Polygon', coordinates: [poly1] }, + { type: 'Polygon', coordinates: [poly2] } + ]; + } + + /** + * Fallback method to build split polygons directly from edge indices + */ + _buildSplitPolygonsFallback(ring, entry, exit, lineCoords) { + + // Get edge indices sorted + let edge1 = entry.edgeIndex; + let edge2 = exit.edgeIndex; + let int1 = entry.coords; + let int2 = exit.coords; + let actualEntry = entry; + let actualExit = exit; + + // Make sure edge1 < edge2 (swap if needed) + if (edge1 > edge2) { + [edge1, edge2] = [edge2, edge1]; + [int1, int2] = [int2, int1]; + [actualEntry, actualExit] = [actualExit, actualEntry]; + } + + // Extract only the intermediate points between the two intersection points + // Use the original entry/exit (not swapped) so we get correct distance-based extraction + const lineSegment = this._extractLineSegmentBetweenIntersections(lineCoords, entry, exit); + + // Build first polygon: from int1, through edges edge1+1 to edge2, to int2, then line back + const poly1 = [int1]; + for (let i = edge1 + 1; i <= edge2; i++) { + poly1.push(ring[i]); + } + poly1.push(int2); + + // Add intermediate line points in correct order based on which intersection comes first along the line + // If int1 corresponds to entry (first intersection along line), we need to add lineSegment in reverse + // If int1 corresponds to exit (second intersection along line), we add lineSegment forward + if (actualEntry === entry) { + // int1 = entry, int2 = exit, so line goes entry -> lineSegment -> exit + // To go from int2 back to int1, we traverse: lineSegment in reverse + for (let i = lineSegment.length - 1; i >= 0; i--) { + poly1.push(lineSegment[i]); + } + } else { + // int1 = exit, int2 = entry, so line goes exit <- lineSegment <- entry + // To go from int2 back to int1, we traverse: lineSegment forward + for (const pt of lineSegment) { + poly1.push(pt); + } + } + poly1.push(int1); // close + + // Build second polygon: from int2, through remaining edges back to int1, then line + const poly2 = [int2]; + for (let i = edge2 + 1; i < ring.length - 1; i++) { + poly2.push(ring[i]); + } + for (let i = 0; i <= edge1; i++) { + poly2.push(ring[i]); + } + poly2.push(int1); + + // Add intermediate line points in the opposite order from poly1 + if (actualEntry === entry) { + // int1 = entry, int2 = exit + // To go from int1 back to int2, we traverse: lineSegment forward + for (const pt of lineSegment) { + poly2.push(pt); + } + } else { + // int1 = exit, int2 = entry + // To go from int1 back to int2, we traverse: lineSegment in reverse + for (let i = lineSegment.length - 1; i >= 0; i--) { + poly2.push(lineSegment[i]); + } + } + poly2.push(int2); // close + + if (poly1.length < 4 || poly2.length < 4) { + return null; + } + + // Validate the polygons with turf if available + if (window.turf) { + try { + const p1 = turf.polygon([poly1]); + const p2 = turf.polygon([poly2]); + return [p1.geometry, p2.geometry]; + } catch (e) { + // Try to clean up the polygons + try { + // Clean up any self-intersections using turf.cleanCoords + const cleanPoly1 = turf.cleanCoords(turf.lineString(poly1)).geometry.coordinates; + const cleanPoly2 = turf.cleanCoords(turf.lineString(poly2)).geometry.coordinates; + + // Close the rings + if (cleanPoly1[0][0] !== cleanPoly1[cleanPoly1.length-1][0] || + cleanPoly1[0][1] !== cleanPoly1[cleanPoly1.length-1][1]) { + cleanPoly1.push(cleanPoly1[0]); + } + if (cleanPoly2[0][0] !== cleanPoly2[cleanPoly2.length-1][0] || + cleanPoly2[0][1] !== cleanPoly2[cleanPoly2.length-1][1]) { + cleanPoly2.push(cleanPoly2[0]); + } + + if (cleanPoly1.length >= 4 && cleanPoly2.length >= 4) { + return [ + { type: 'Polygon', coordinates: [cleanPoly1] }, + { type: 'Polygon', coordinates: [cleanPoly2] } + ]; + } + } catch (e2) { + } + } + } + + // Return raw polygons as last resort + return [ + { type: 'Polygon', coordinates: [poly1] }, + { type: 'Polygon', coordinates: [poly2] } + ]; + } + + /** + * Extract the portion of the line that is between the two intersection points. + * Given a line ABCDE where B and D are intersection points with the polygon, + * this returns only the points [C] (points strictly between B and D). + * The intersection points themselves are already added as int1/int2. + * + * @param {Array} lineCoords - Full line coordinates + * @param {Object} entry - First intersection {coords, distance, lineIndex} + * @param {Object} exit - Last intersection {coords, distance, lineIndex} + * @returns {Array} - Array of coordinates between the two intersections + */ + _extractLineSegmentBetweenIntersections(lineCoords, entry, exit) { + if (!lineCoords || lineCoords.length < 2) return []; + + // Special case: if line has only 2 points, there are no intermediate points + if (lineCoords.length === 2) { + return []; + } + + // The intersections have a 'distance' property (distance from line start) + // and possibly a 'lineIndex' property (which segment the intersection is on) + const entryDist = entry.distance !== undefined ? entry.distance : 0; + const exitDist = exit.distance !== undefined ? exit.distance : 0; + + // If distances are not set or equal, fall back to using lineIndex + if (entryDist === 0 && exitDist === 0) { + // Use lineIndex to determine which points are between intersections + const entryIdx = entry.lineIndex !== undefined ? entry.lineIndex : 0; + const exitIdx = exit.lineIndex !== undefined ? exit.lineIndex : lineCoords.length - 1; + + let startIdx = Math.min(entryIdx, exitIdx); + let endIdx = Math.max(entryIdx, exitIdx); + + // If both intersections are on the same segment (e.g., both have lineIndex=0), + // there are no intermediate points + if (startIdx === endIdx) { + return []; + } + + // Return only vertices strictly between the two intersection segments + // For a line A-B-C-D-E with intersections on segments 1 (B-C) and 3 (D-E), + // we want to include only C and D, not B or E + const intermediatePoints = []; + for (let i = startIdx + 1; i < endIdx + 1; i++) { + if (i < lineCoords.length) { + intermediatePoints.push(lineCoords[i]); + } + } + return intermediatePoints; + } + + // Make sure we have the correct order (entry should have smaller distance) + let startDist = entryDist; + let endDist = exitDist; + let startCoords = entry.coords; + let endCoords = exit.coords; + + if (startDist > endDist) { + [startDist, endDist] = [endDist, startDist]; + [startCoords, endCoords] = [endCoords, startCoords]; + } + + // Calculate cumulative distances along the line for each vertex + const cumulativeDistances = [0]; + for (let i = 1; i < lineCoords.length; i++) { + const dx = lineCoords[i][0] - lineCoords[i-1][0]; + const dy = lineCoords[i][1] - lineCoords[i-1][1]; + const segmentLength = Math.sqrt(dx * dx + dy * dy); + cumulativeDistances.push(cumulativeDistances[i-1] + segmentLength); + } + + // Find all line vertices that fall between the two intersection points + // A vertex is "between" if its cumulative distance is > startDist and < endDist + const intermediatePoints = []; + + for (let i = 0; i < lineCoords.length; i++) { + const vertexDist = cumulativeDistances[i]; + + // Use a small tolerance for comparisons + const tolerance = 1e-9; + + if (vertexDist > startDist + tolerance && vertexDist < endDist - tolerance) { + intermediatePoints.push(lineCoords[i]); + } + } + + return intermediatePoints; + } + + /** + * Extend a line by a given amount at both ends + */ + _extendLine(coords, extension) { + if (coords.length < 2) return coords; + + const result = [...coords]; + + // Extend start + const dx1 = coords[0][0] - coords[1][0]; + const dy1 = coords[0][1] - coords[1][1]; + const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); + if (len1 > 0) { + result[0] = [ + coords[0][0] + (dx1 / len1) * extension, + coords[0][1] + (dy1 / len1) * extension + ]; + } + + // Extend end + const last = coords.length - 1; + const dx2 = coords[last][0] - coords[last - 1][0]; + const dy2 = coords[last][1] - coords[last - 1][1]; + const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); + if (len2 > 0) { + result[last] = [ + coords[last][0] + (dx2 / len2) * extension, + coords[last][1] + (dy2 / len2) * extension + ]; + } + + return result; + } + + /** + * Build polygons from split boundary segments and intersection points + */ + _buildPolygonsFromSplit(segments, intersections, lineCoords, originalPolygon) { + try { + // Get intersection coordinates + const intPoints = intersections.map(f => f.geometry.coordinates); + + if (intPoints.length < 2) return null; + + // Extract only intermediate points between intersections + // For a line ABCDE with intersections at B and D, we only want C + const entry = { + coords: intPoints[0], + distance: 0, // Will be calculated + lineIndex: 0 + }; + const exit = { + coords: intPoints[intPoints.length - 1], + distance: 0, // Will be calculated + lineIndex: lineCoords.length - 1 + }; + + // Calculate distances for entry and exit + for (let i = 0; i < lineCoords.length; i++) { + const dx = lineCoords[i][0] - entry.coords[0]; + const dy = lineCoords[i][1] - entry.coords[1]; + const distEntry = Math.sqrt(dx * dx + dy * dy); + if (distEntry < 1e-6) { + entry.lineIndex = i; + break; + } + } + for (let i = 0; i < lineCoords.length; i++) { + const dx = lineCoords[i][0] - exit.coords[0]; + const dy = lineCoords[i][1] - exit.coords[1]; + const distExit = Math.sqrt(dx * dx + dy * dy); + if (distExit < 1e-6) { + exit.lineIndex = i; + break; + } + } + + // Calculate cumulative distances + const cumulativeDistances = [0]; + for (let i = 1; i < lineCoords.length; i++) { + const dx = lineCoords[i][0] - lineCoords[i-1][0]; + const dy = lineCoords[i][1] - lineCoords[i-1][1]; + const segmentLength = Math.sqrt(dx * dx + dy * dy); + cumulativeDistances.push(cumulativeDistances[i-1] + segmentLength); + } + entry.distance = cumulativeDistances[entry.lineIndex]; + exit.distance = cumulativeDistances[exit.lineIndex]; + + // Extract intermediate points + const intermediatePoints = this._extractLineSegmentBetweenIntersections(lineCoords, entry, exit); + + // Sort segments by starting point + const sortedSegments = segments.map(s => ({ + coords: s.geometry.coordinates, + feature: s + })); + + // Build segment including only: [entry] + intermediatePoints + [exit] + const lineSegmentForward = [entry.coords, ...intermediatePoints, exit.coords]; + const lineSegmentReverse = [...lineSegmentForward].reverse(); + + // Try to build valid polygons + const poly1Coords = []; + const poly2Coords = []; + + // Helper function to remove duplicate consecutive points + const removeDuplicates = (coords) => { + const result = []; + for (let i = 0; i < coords.length; i++) { + if (i === 0 || + Math.abs(coords[i][0] - coords[i-1][0]) > 1e-9 || + Math.abs(coords[i][1] - coords[i-1][1]) > 1e-9) { + result.push(coords[i]); + } + } + return result; + }; + + // Simple approach: find which segments are on which side + for (let i = 0; i < sortedSegments.length; i++) { + const seg = sortedSegments[i].coords; + + // Just alternate for now (this is a simplified approach) + if (i % 2 === 0) { + // Add all points from segment except the last (to avoid duplication with next segment) + for (let j = 0; j < seg.length - 1; j++) { + poly1Coords.push(seg[j]); + } + } else { + for (let j = 0; j < seg.length - 1; j++) { + poly2Coords.push(seg[j]); + } + } + } + + // For the line segment, only add the intermediate points (intersections are already in segments) + // Actually, we need to add the intersections + intermediate points to close the polygon + // But we need to be careful not to duplicate points that are already in the segments + + // Add line segment to close each polygon (only intermediate points + intersections) + // Skip first and last point of lineSegment if they're already in the coords + const addLineSegment = (targetCoords, lineSegment) => { + for (const pt of lineSegment) { + // Check if this point is already at the end of targetCoords + const lastPt = targetCoords.length > 0 ? targetCoords[targetCoords.length - 1] : null; + if (!lastPt || + Math.abs(pt[0] - lastPt[0]) > 1e-9 || + Math.abs(pt[1] - lastPt[1]) > 1e-9) { + targetCoords.push(pt); + } + } + }; + + addLineSegment(poly1Coords, lineSegmentForward); + addLineSegment(poly2Coords, lineSegmentReverse); + + // Remove any remaining duplicates + const cleanPoly1 = removeDuplicates(poly1Coords); + const cleanPoly2 = removeDuplicates(poly2Coords); + + // Close the rings if not already closed + if (cleanPoly1.length >= 3) { + if (Math.abs(cleanPoly1[0][0] - cleanPoly1[cleanPoly1.length-1][0]) > 1e-9 || + Math.abs(cleanPoly1[0][1] - cleanPoly1[cleanPoly1.length-1][1]) > 1e-9) { + cleanPoly1.push(cleanPoly1[0]); + } + } + if (cleanPoly2.length >= 3) { + if (Math.abs(cleanPoly2[0][0] - cleanPoly2[cleanPoly2.length-1][0]) > 1e-9 || + Math.abs(cleanPoly2[0][1] - cleanPoly2[cleanPoly2.length-1][1]) > 1e-9) { + cleanPoly2.push(cleanPoly2[0]); + } + } + + // Validate with turf + if (cleanPoly1.length >= 4 && cleanPoly2.length >= 4) { + try { + const p1 = turf.polygon([cleanPoly1]); + const p2 = turf.polygon([cleanPoly2]); + + // Check if both centroids are inside original + const c1 = turf.centroid(p1); + const c2 = turf.centroid(p2); + + if (turf.booleanPointInPolygon(c1, originalPolygon) && + turf.booleanPointInPolygon(c2, originalPolygon)) { + return [p1.geometry, p2.geometry]; + } + } catch (e) { + } + } + + return null; + } catch (e) { + return null; + } + } + + /** + * Add action to history for undo + */ + _addToHistory(action) { + this.geometryHistory.push(action); + + // Limit history size + if (this.geometryHistory.length > 50) { + this.geometryHistory.shift(); + } + } + + /** + * Update layer filters to hide features being edited + * Note: This is now supplementary - main hiding is done in _updateMapSource + */ + _updateLayerFilters() { + // We no longer rely on filters since they have type matching issues + // Instead, we remove hidden features directly from the source data in _updateMapSource + + // Clear any existing filters + this.layerConfig.layerIds.forEach(layerId => { + if (this.map.getLayer(layerId)) { + this.map.setFilter(layerId, null); + } + }); + + // Update the source data to exclude hidden features + this._updateMapSource(); + } + + /** + * Update map source with current data (excluding hidden and deleted features) + */ + _updateMapSource() { + const source = this.map.getSource(this.layerConfig.sourceId); + + if (source) { + // Filter out hidden features and deleted features from the source data + const visibleFeatures = Array.from(this.loadedAssets.values()).filter(feature => { + // Check both number and string versions of ID + const isHiddenByNumber = this.hiddenFeatures.has(feature.id); + const isHiddenByString = this.hiddenFeatures.has(String(feature.id)); + const isHiddenByParsed = typeof feature.id === 'string' && + this.hiddenFeatures.has(parseInt(feature.id, 10)); + const isHidden = isHiddenByNumber || isHiddenByString || isHiddenByParsed; + + // Check if feature is marked as deleted in modifiedGeometries + const modByNumber = this.modifiedGeometries.get(feature.id); + const modByString = this.modifiedGeometries.get(String(feature.id)); + const modByParsed = typeof feature.id === 'string' ? + this.modifiedGeometries.get(parseInt(feature.id, 10)) : null; + const modification = modByNumber || modByString || modByParsed; + const isDeleted = modification && modification.isDeleted; + + return !isHidden && !isDeleted; + }); + + source.setData({ + type: 'FeatureCollection', + features: visibleFeatures + }); + } else { + + } + } + + /** + * Compare two geometries to check if they are different + * Uses coordinate comparison with tolerance to avoid false positives + * from floating point rounding differences + */ + _geometriesAreDifferent(geom1, geom2) { + if (!geom1 || !geom2) { + return geom1 !== geom2; + } + + if (geom1.type !== geom2.type) { + return true; + } + + // Compare coordinates arrays + const coords1 = JSON.stringify(geom1.coordinates); + const coords2 = JSON.stringify(geom2.coordinates); + + if (coords1 === coords2) { + return false; + } + + // If JSON differs, compare coordinate by coordinate with tolerance + const TOLERANCE = 1e-9; // Very small tolerance for floating point comparison + + const compareCoords = (c1, c2) => { + if (Array.isArray(c1) && Array.isArray(c2)) { + if (c1.length !== c2.length) return false; + + // Check if this is a coordinate pair [lng, lat] + if (c1.length >= 2 && typeof c1[0] === 'number' && typeof c1[1] === 'number') { + return Math.abs(c1[0] - c2[0]) < TOLERANCE && + Math.abs(c1[1] - c2[1]) < TOLERANCE; + } + + // Otherwise it's a nested array, compare recursively + return c1.every((coord, i) => compareCoords(coord, c2[i])); + } + return c1 === c2; + }; + + return !compareCoords(geom1.coordinates, geom2.coordinates); + } + + /** + * Update modified count and trigger callback + */ + _updateModifiedCount() { + const count = this.modifiedGeometries.size; + + // Update floating buttons + this._updateFloatingButtons(); + + if (this.callbacks.onModifiedCountChange) { + this.callbacks.onModifiedCountChange(count, this.modifiedGeometries); + } + } + + /** + * Show a message to the user + */ + _showMessage(text, type) { + // Show via integrated help message + this.showHelp(text, type); + + // Also call callback if defined + if (this.callbacks.onMessage) { + this.callbacks.onMessage(text, type); + } + } + + // ========== Public API ========== + + /** + * Get loaded assets map + */ + getLoadedAssets() { + return this.loadedAssets; + } + + /** + * Set loaded assets + */ + setLoadedAssets(assets) { + this.loadedAssets = assets; + } + + /** + * Get modified geometries + */ + getModifiedGeometries() { + return this.modifiedGeometries; + } + + /** + * Clear modified geometries + */ + clearModifiedGeometries() { + this.modifiedGeometries.clear(); + this._updateModifiedCount(); + } + + /** + * Get hidden features set + */ + getHiddenFeatures() { + return this.hiddenFeatures; + } + + /** + * Hide a feature + */ + hideFeature(featureId) { + this.hiddenFeatures.add(featureId); + this._updateLayerFilters(); + } + + /** + * Show a feature + */ + showFeature(featureId) { + this.hiddenFeatures.delete(featureId); + this._updateLayerFilters(); + } + + /** + * Check if draw mode is active + */ + isInDrawMode() { + return this.isDrawMode; + } + + /** + * Add feature to draw for editing + */ + addFeatureToEdit(feature) { + if (!this.isDrawMode) { + this.toggleDrawMode(); + } + + const featureId = feature.id; + + if (!featureId) { + return; + } + + if (!feature.geometry) { + return; + } + + // Store in loadedAssets if not already present + if (!this.loadedAssets.has(featureId)) { + this.loadedAssets.set(featureId, { + id: featureId, + type: 'Feature', + geometry: JSON.parse(JSON.stringify(feature.geometry)), // Deep clone + properties: feature.properties || {} + }); + } + + // Hide on map + this.hideFeature(featureId); + + // Add to draw + const addedIds = this.draw.add({ + id: featureId, + type: 'Feature', + geometry: feature.geometry, + properties: feature.properties || {} + }); + + // Verify feature was added + const addedFeature = this.draw.get(featureId); + + // Store the NORMALIZED geometry (after draw processing) as the original + // This is important because draw may simplify/round coordinates + if (addedFeature && !this.originalGeometries.has(featureId)) { + this.originalGeometries.set(featureId, JSON.parse(JSON.stringify(addedFeature.geometry))); + } + + // Enter edit mode with a small delay to ensure feature is rendered + setTimeout(() => { + const allFeatures = this.draw.getAll(); + const allIds = allFeatures.features.map(f => f.id); + this.draw.changeMode('edit', { featureIds: allIds }); + this.lastSelectedFeatureIds = allIds; + }, 50); + + this.currentEditingAsset = { + id: featureId, + assetType: feature.properties?.asset_type || this.layerConfig.assetType + }; + + this._updateSplitMergeState(); + + // Notify that a feature has entered edit mode + if (this.callbacks.onFeatureEdit) { + const fullFeature = this.loadedAssets.get(featureId) || feature; + this.callbacks.onFeatureEdit(fullFeature); + } + } + + /** + * Remove feature from draw editing + */ + removeFeatureFromEdit(featureId) { + // Remove from draw + this.draw.delete(featureId); + + // Show on map + this.showFeature(featureId); + + if (this.currentEditingAsset?.id === featureId) { + this.currentEditingAsset = null; + } + + this._updateMapSource(); + this._updateSplitMergeState(); + } + + /** + * Update the properties of a feature (for attribute editing) + * This updates both the draw feature and loadedAssets, and tracks the modification + * + * @param {string|number} featureId - The ID of the feature to update + * @param {Object} properties - The properties to update (will be merged with existing) + * @returns {boolean} - True if update was successful + */ + updateFeatureProperties(featureId, properties) { + if (!featureId || !properties) { + return false; + } + + // Update in draw if present + const drawFeature = this.draw.get(featureId); + if (drawFeature) { + const updatedFeature = { + ...drawFeature, + properties: { + ...drawFeature.properties, + ...properties + } + }; + this.draw.add(updatedFeature); + } + + // Update in loadedAssets + const loadedAsset = this.loadedAssets.get(featureId); + if (loadedAsset) { + loadedAsset.properties = { + ...loadedAsset.properties, + ...properties + }; + } + + // Track modification if not already tracked + let mod = this.modifiedGeometries.get(featureId); + if (mod) { + // Update properties in existing modification + mod.properties = { + ...(mod.properties || {}), + ...properties + }; + } else { + // Create new modification entry for property change only + const currentGeom = drawFeature?.geometry || loadedAsset?.geometry; + const originalGeom = this.originalGeometries.get(featureId) || loadedAsset?.geometry; + + this.modifiedGeometries.set(featureId, { + assetId: featureId, + assetType: loadedAsset?.properties?.asset_type || this.layerConfig.assetType, + originalGeometry: originalGeom, + currentGeometry: currentGeom, + originalProperties: loadedAsset?.properties || {}, + properties: properties, + isDeleted: false + }); + } + + this._updateModifiedCount(); + + return true; + } + + /** + * Get the current properties of a feature + * + * @param {string|number} featureId - The ID of the feature + * @returns {Object|null} - The properties or null if not found + */ + getFeatureProperties(featureId) { + // Check draw first + const drawFeature = this.draw.get(featureId); + if (drawFeature) { + return drawFeature.properties || {}; + } + + // Check loadedAssets + const loadedAsset = this.loadedAssets.get(featureId); + if (loadedAsset) { + return loadedAsset.properties || {}; + } + + return null; + } + + /** + * Get geometry history for undo + */ + getGeometryHistory() { + return this.geometryHistory; + } + + /** + * Undo last geometry action + */ + undoLastAction() { + if (this.geometryHistory.length === 0) { + return false; + } + + const action = this.geometryHistory.pop(); + + switch (action.type) { + case 'create': + // Delete the created feature + this.draw.delete(action.featureId); + this.modifiedGeometries.delete(action.featureId); + this.loadedAssets.delete(action.featureId); + break; + + case 'update': + // Restore previous geometry + const feature = this.draw.get(action.featureId); + if (feature) { + this.draw.delete(action.featureId); + this.draw.add({ + id: action.featureId, + type: 'Feature', + geometry: action.geometry, + properties: feature.properties + }); + } + + // Update tracking + const mod = this.modifiedGeometries.get(action.featureId); + if (mod) { + mod.currentGeometry = action.geometry; + } + + // Update loaded assets + const asset = this.loadedAssets.get(action.featureId); + if (asset) { + asset.geometry = action.geometry; + } + break; + + case 'delete': + // Restore deleted feature + this.draw.add({ + id: action.featureId, + type: 'Feature', + geometry: action.geometry, + properties: action.properties + }); + + // Remove deletion tracking + this.modifiedGeometries.delete(action.featureId); + + // Re-add to loaded assets + this.loadedAssets.set(action.featureId, { + id: action.featureId, + type: 'Feature', + geometry: action.geometry, + properties: action.properties + }); + + // Hide from map layer + this.hideFeature(action.featureId); + break; + } + + this._updateMapSource(); + this._updateModifiedCount(); + this._updateSplitMergeState(); + + return true; + } + + /** + * Check if draw mode is active + * @returns {boolean} True if draw mode is active + */ + isActive() { + return this.callbacks.getIsDrawMode + ? this.callbacks.getIsDrawMode() + : this.isDrawMode; + } + + /** + * Check if currently in draw mode (alias for isActive) + * @returns {boolean} True if draw mode is active + */ + isInDrawMode() { + return this.isActive(); + } + } + + // Export to global scope + window.CustomDrawToolbar = CustomDrawToolbar; + +})(window); diff --git a/streetup/common/static/common/custom-draw.js b/streetup/common/static/common/custom-draw.js new file mode 100644 index 0000000..7620c11 --- /dev/null +++ b/streetup/common/static/common/custom-draw.js @@ -0,0 +1,2039 @@ +/** + * Custom Drawing Module for Maplibre GL + * + * A replacement for MapboxDraw that provides: + * - Point, Line, and Polygon drawing + * - Vertex snapping functionality + * - Single 'edit' mode for vertex editing and feature dragging + * - Compatible with Maplibre GL + * - Uses only turf.js for geometry operations + * + * @version 2.0.0 + * @requires maplibre-gl + * @requires turf.js + */ + +(function(window) { + 'use strict'; + + // Constants + const SNAP_DISTANCE_PX = 15; + const SNAP_VERTEX_PRIORITY_DISTANCE = 0.0005; // ~50m in degrees + const VERTEX_CLICK_TOLERANCE_PX = 20; // Increased for touch support + const DRAW_LAYER_PREFIX = 'custom-draw-'; + + /** + * Main CustomDraw class + */ + class CustomDraw { + constructor(options = {}) { + this.options = { + displayControlsDefault: options.displayControlsDefault !== false, + controls: options.controls || {}, + defaultMode: options.defaultMode || 'edit', + clickTolerance: options.clickTolerance || 2, + touchBuffer: options.touchBuffer || 0, + snap: options.snap !== false, + snapOptions: options.snapOptions || {}, + styles: options.styles || this._getDefaultStyles(), + userProperties: options.userProperties !== false + }; + + this.map = null; + this.mode = this.options.defaultMode; + this.features = new Map(); // Store all features by ID + this.selectedFeatureIds = new Set(); + this.currentFeature = null; // Feature being drawn + this.handlers = {}; // Event handlers + this.eventListeners = {}; // User event listeners + + // Snapping configuration + this.snapOptions = { + snapPx: this.options.snapOptions.snapPx || SNAP_DISTANCE_PX, + snapToMidPoints: this.options.snapOptions.snapToMidPoints !== false, + snapToSegments: this.options.snapOptions.snapToSegments !== false, + snapVertexPriorityDistance: this.options.snapOptions.snapVertexPriorityDistance || SNAP_VERTEX_PRIORITY_DISTANCE, + snapIncludeMapFeatures: this.options.snapOptions.snapIncludeMapFeatures !== false, + snapGetFeatures: this.options.snapOptions.snapGetFeatures || null + }; + + // State for edit mode + this.draggedVertex = null; + this.draggedFeature = null; // For dragging entire features + this.dragStartPoint = null; // Starting point for feature drag + this.hoveredVertex = null; + this.isDragging = false; + this.isDraggingFeature = false; // True when dragging whole feature + this._lastDragEndTime = 0; // Timestamp when drag ended (to prevent click after drag) + + // Snapping cache + this._snapFeaturesCache = null; + this._snapFeaturesCacheTime = 0; + this._snapFeaturesCacheTTL = 100; // Cache for 100ms + + // Mode-specific snap setting (can override global snap) + this._modeSnap = this.options.snap; + + // Double-click detection (fallback since dblclick event is unreliable) + this._lastClickTime = 0; + this._lastClickPoint = null; + this._doubleClickThreshold = 300; // ms + this._doubleClickDistanceThreshold = 10; // pixels + + this._setupEventHandlers(); + } + + /** + * Add the draw control to a map + */ + onAdd(map) { + this.map = map; + this._addLayers(); + this._attachMapHandlers(); + this._updateLayers(); + + return document.createElement('div'); // Dummy container for IControl + } + + /** + * Remove the draw control from the map + */ + onRemove() { + this._detachMapHandlers(); + this._removeLayers(); + this.map = null; + } + + /** + * Get all features as a GeoJSON FeatureCollection + */ + getAll() { + return { + type: 'FeatureCollection', + features: Array.from(this.features.values()).map(f => ({ + type: 'Feature', + id: f.id, + geometry: f.geometry, + properties: f.properties || {} + })) + }; + } + + /** + * Add a feature or features + */ + add(featureOrCollection) { + const features = featureOrCollection.type === 'FeatureCollection' + ? featureOrCollection.features + : [featureOrCollection]; + + features.forEach(feature => { + const id = feature.id || this._generateId(); + this.features.set(id, { + id: id, + type: 'Feature', + geometry: feature.geometry, + properties: feature.properties || {} + }); + }); + + this._updateLayers(); + return features.map(f => f.id); + } + + /** + * Get a feature by ID + */ + get(id) { + return this.features.get(id) || null; + } + + /** + * Get selected features + */ + getSelected() { + return { + type: 'FeatureCollection', + features: Array.from(this.selectedFeatureIds) + .map(id => this.features.get(id)) + .filter(f => f) + }; + } + + /** + * Get selected feature IDs + */ + getSelectedIds() { + return Array.from(this.selectedFeatureIds); + } + + /** + * Delete a feature or features by ID(s) + */ + delete(ids) { + const idArray = Array.isArray(ids) ? ids : [ids]; + idArray.forEach(id => { + this.features.delete(id); + this.selectedFeatureIds.delete(id); + }); + this._updateLayers(); + return this; + } + + /** + * Delete all features + */ + deleteAll() { + this.features.clear(); + this.selectedFeatureIds.clear(); + this._updateLayers(); + return this; + } + + /** + * Set features (replace all) + */ + set(featureCollection) { + this.features.clear(); + this.selectedFeatureIds.clear(); + this.add(featureCollection); + return this; + } + + /** + * Change the drawing mode + */ + changeMode(mode, options = {}) { + const oldMode = this.mode; + this.mode = mode; + + // Clear current drawing state + this.currentFeature = null; + this.draggedVertex = null; + this.isDragging = false; + + // Store mode-specific snap option + if (options.snap !== undefined) { + this._modeSnap = options.snap; + } else { + // Use global snap setting as default + this._modeSnap = this.options.snap; + } + + // Update snapOptions if provided in mode options + if (options.snapOptions) { + this.snapOptions = { + ...this.snapOptions, + ...options.snapOptions + }; + } + + // Handle mode-specific setup + // Map old mode names to new 'edit' mode for compatibility + if (mode === 'simple_select' || mode === 'direct_select') { + this.mode = 'edit'; + } + + if ((mode === 'edit' || mode === 'simple_select' || mode === 'direct_select') && options.featureIds) { + this.selectedFeatureIds.clear(); + options.featureIds.forEach(id => this.selectedFeatureIds.add(id)); + } else if (options.featureId) { + // Support direct_select style single featureId option + this.selectedFeatureIds.clear(); + this.selectedFeatureIds.add(options.featureId); + } else { + // console.log('[CustomDraw.changeMode] No featureIds in options, selectedFeatureIds:', Array.from(this.selectedFeatureIds)); + } + + this._updateLayers(); + this._updateMapCursor(); + + this._fireEvent('mode.change', { mode: mode }); + this._fireEvent('draw.modechange', { mode: mode }); // Alias for compatibility + + return this; + } + + /** + * Get the current mode + */ + getMode() { + return this.mode; + } + + /** + * Trash/delete selected features + */ + trash() { + const ids = Array.from(this.selectedFeatureIds); + if (ids.length > 0) { + this.delete(ids); + this._fireEvent('draw.delete', { + features: ids.map(id => this.features.get(id)).filter(f => f) + }); + } + return this; + } + + /** + * Add an event listener + */ + on(event, handler) { + if (!this.eventListeners[event]) { + this.eventListeners[event] = []; + } + this.eventListeners[event].push(handler); + return this; + } + + /** + * Remove an event listener + */ + off(event, handler) { + if (!this.eventListeners[event]) return this; + + if (handler) { + this.eventListeners[event] = this.eventListeners[event] + .filter(h => h !== handler); + } else { + this.eventListeners[event] = []; + } + return this; + } + + // ========== Private Methods ========== + + /** + * Fire an event to all listeners + */ + _fireEvent(eventName, data = {}) { + const listeners = this.eventListeners[eventName] || []; + listeners.forEach(handler => { + try { + handler(data); + } catch (err) { + console.error(`Error in ${eventName} handler:`, err); + } + }); + + // Also fire on the map for compatibility with existing code + if (this.map) { + this.map.fire(eventName, data); + } + } + + /** + * Generate a unique ID for a feature + */ + _generateId() { + // More robust UUID-like generation to avoid collisions + const timestamp = Date.now().toString(36); + const randomPart = Math.random().toString(36).substr(2, 9); + const counterPart = (this._idCounter = (this._idCounter || 0) + 1).toString(36); + return 'feature_' + timestamp + '_' + randomPart + '_' + counterPart; + } + + /** + * Setup event handler methods + */ + _setupEventHandlers() { + this.handlers.click = this._handleClick.bind(this); + this.handlers.mousemove = this._handleMouseMove.bind(this); + this.handlers.mousedown = this._handleMouseDown.bind(this); + this.handlers.mouseup = this._handleMouseUp.bind(this); + this.handlers.dblclick = this._handleDoubleClick.bind(this); + this.handlers.contextmenu = this._handleContextMenu.bind(this); + // Touch events are handled separately via canvas event listeners with passive: false + } + + /** + * Attach map event handlers + */ + _attachMapHandlers() { + if (!this.map) return; + + this.map.on('click', this.handlers.click); + this.map.on('mousemove', this.handlers.mousemove); + this.map.on('mousedown', this.handlers.mousedown); + this.map.on('mouseup', this.handlers.mouseup); + this.map.on('dblclick', this.handlers.dblclick); + this.map.on('contextmenu', this.handlers.contextmenu); + + // Touch events - attach directly to canvas with passive: false to allow preventDefault + const canvas = this.map.getCanvas(); + if (canvas) { + this._canvasTouchHandlers = { + touchstart: this._handleCanvasTouchStart.bind(this), + touchmove: this._handleCanvasTouchMove.bind(this), + touchend: this._handleCanvasTouchEnd.bind(this) + }; + canvas.addEventListener('touchstart', this._canvasTouchHandlers.touchstart, { passive: false }); + canvas.addEventListener('touchmove', this._canvasTouchHandlers.touchmove, { passive: false }); + canvas.addEventListener('touchend', this._canvasTouchHandlers.touchend, { passive: false }); + } + } + + /** + * Detach map event handlers + */ + _detachMapHandlers() { + if (!this.map) return; + + this.map.off('click', this.handlers.click); + this.map.off('mousemove', this.handlers.mousemove); + this.map.off('mousedown', this.handlers.mousedown); + this.map.off('mouseup', this.handlers.mouseup); + this.map.off('dblclick', this.handlers.dblclick); + this.map.off('contextmenu', this.handlers.contextmenu); + + // Remove canvas touch handlers + const canvas = this.map.getCanvas(); + if (canvas && this._canvasTouchHandlers) { + canvas.removeEventListener('touchstart', this._canvasTouchHandlers.touchstart); + canvas.removeEventListener('touchmove', this._canvasTouchHandlers.touchmove); + canvas.removeEventListener('touchend', this._canvasTouchHandlers.touchend); + } + } + + /** + * Convert touch event to MapLibre-like event object + */ + _touchEventToMapEvent(e) { + if (!e.touches || e.touches.length === 0) { + // For touchend, use changedTouches + if (!e.changedTouches || e.changedTouches.length === 0) return null; + const touch = e.changedTouches[0]; + const canvas = this.map.getCanvas(); + const rect = canvas.getBoundingClientRect(); + const point = { x: touch.clientX - rect.left, y: touch.clientY - rect.top }; + const lngLat = this.map.unproject(point); + return { point, lngLat, originalEvent: e }; + } + const touch = e.touches[0]; + const canvas = this.map.getCanvas(); + const rect = canvas.getBoundingClientRect(); + const point = { x: touch.clientX - rect.left, y: touch.clientY - rect.top }; + const lngLat = this.map.unproject(point); + return { point, lngLat, originalEvent: e }; + } + + /** + * Handle touch start on canvas + */ + _handleCanvasTouchStart(e) { + const mapEvent = this._touchEventToMapEvent(e); + if (!mapEvent) return; + + + // Check if we're touching a vertex or feature in edit mode + if (this.mode === 'edit') { + // Log the vertices of selected features for debugging + for (const featureId of this.selectedFeatureIds) { + const feature = this.features.get(featureId); + if (feature && feature.geometry) { + if (feature.geometry.type === 'Polygon') { + const ring = feature.geometry.coordinates[0]; + for (let i = 0; i < Math.min(3, ring.length); i++) { + const projected = this.map.project(ring[i]); + } + } else if (feature.geometry.type === 'MultiPolygon') { + const ring = feature.geometry.coordinates[0][0]; + for (let i = 0; i < Math.min(3, ring.length); i++) { + const projected = this.map.project(ring[i]); + } + } + } + } + + const vertex = this._getVertexAtPoint(mapEvent.point); + if (vertex) { + this.isDragging = true; + this.draggedVertex = vertex; + // Disable map drag pan to prevent interference + if (this.map.dragPan) this.map.dragPan.disable(); + e.preventDefault(); // Prevent map panning + return; + } + + const midpoint = this._getMidpointAtPoint(mapEvent.point); + if (midpoint) { + const newVertex = this._insertVertexAtMidpoint(midpoint); + if (newVertex) { + this.isDragging = true; + this.draggedVertex = newVertex; + // Disable map drag pan to prevent interference + if (this.map.dragPan) this.map.dragPan.disable(); + e.preventDefault(); + return; + } + } + + const clickedFeatures = this._getFeatureAtPoint(mapEvent.point); + if (clickedFeatures.length > 0) { + // Don't start drag immediately - wait for movement + // Store potential drag info for touchmove to decide + this._potentialFeatureDrag = { + feature: clickedFeatures[0], + startPoint: mapEvent.lngLat, + startScreenPoint: mapEvent.point + }; + // Don't call preventDefault here - allow click event for deselection + // e.preventDefault(); + return; + } + } + } + + /** + * Handle touch move on canvas + */ + _handleCanvasTouchMove(e) { + const mapEvent = this._touchEventToMapEvent(e); + if (!mapEvent) return; + + // Check if we have a potential feature drag that should now become a real drag + if (this._potentialFeatureDrag && !this.isDraggingFeature) { + const dx = mapEvent.point.x - this._potentialFeatureDrag.startScreenPoint.x; + const dy = mapEvent.point.y - this._potentialFeatureDrag.startScreenPoint.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Only start drag if moved more than 5 pixels + if (distance > 5) { + this.isDraggingFeature = true; + this.draggedFeature = this._potentialFeatureDrag.feature; + this.dragStartPoint = this._potentialFeatureDrag.startPoint; + if (this.map.dragPan) this.map.dragPan.disable(); + } + } + + if (!this.isDragging && !this.isDraggingFeature) return; + + + e.preventDefault(); // Prevent map panning during drag + + if (this.isDragging && this.draggedVertex) { + this._handleVertexDrag(mapEvent); + } else if (this.isDraggingFeature && this.draggedFeature) { + this._handleFeatureDrag(mapEvent); + } + } + + /** + * Handle touch end on canvas + */ + _handleCanvasTouchEnd(e) { + + // Re-enable map drag pan + if (this.map && this.map.dragPan) { + this.map.dragPan.enable(); + } + + // Check if this was a tap on a feature (potentialDrag that never became a real drag) + // If so, fire a custom event for deselection + if (this._potentialFeatureDrag && !this.isDraggingFeature) { + const tappedFeature = this._potentialFeatureDrag.feature; + this._potentialFeatureDrag = null; + + // Fire a custom event that the toolbar can listen to + this._fireEvent('draw.featureTap', { + feature: tappedFeature, + featureId: tappedFeature.id + }); + return; + } + + // Clear potential drag + this._potentialFeatureDrag = null; + + if (this.isDragging || this.isDraggingFeature) { + const mapEvent = this._touchEventToMapEvent(e); + this._handleEditMouseUp(mapEvent || { point: { x: 0, y: 0 }, lngLat: { lng: 0, lat: 0 } }); + } + } + + /** + * Handle click events + */ + _handleClick(e) { + const mode = this.mode; + const now = Date.now(); + + + // Double-click detection for drawing modes + if (mode === 'draw_line_string' || mode === 'draw_polygon') { + const timeDiff = now - this._lastClickTime; + const isSamePoint = this._lastClickPoint && + Math.abs(e.point.x - this._lastClickPoint.x) < this._doubleClickDistanceThreshold && + Math.abs(e.point.y - this._lastClickPoint.y) < this._doubleClickDistanceThreshold; + + + if (timeDiff < this._doubleClickThreshold && isSamePoint) { + this._lastClickTime = 0; + this._lastClickPoint = null; + this._finishDrawing(); + return; + } + + this._lastClickTime = now; + this._lastClickPoint = { x: e.point.x, y: e.point.y }; + } + + // In edit mode, check if we clicked on a vertex or draw feature + // If not, let the event propagate to allow other layers to handle it + if (mode === 'edit') { + const vertex = this._getVertexAtPoint(e.point); + const clickedFeatures = this._getFeatureAtPoint(e.point); + + if (!vertex && clickedFeatures.length === 0) { + // No vertex or draw feature clicked - let the event propagate + return; + } + // Don't handle click in edit mode - dragging is handled by mousedown/mouseup + return; + } + + // For drawing modes, prevent default (if cancelable) + if (mode === 'draw_point' || mode === 'draw_line_string' || mode === 'draw_polygon') { + if (e.preventDefault && (!e.originalEvent || e.originalEvent.cancelable !== false)) { + e.preventDefault(); + } + } + + if (mode === 'draw_point') { + this._handleDrawPointClick(e); + } else if (mode === 'draw_line_string') { + this._handleDrawLineClick(e); + } else if (mode === 'draw_polygon') { + this._handleDrawPolygonClick(e); + } + } + + /** + * Handle mouse move events + */ + _handleMouseMove(e) { + const mode = this.mode; + + if (mode === 'draw_line_string' || mode === 'draw_polygon') { + this._updateCurrentFeatureLastVertex(e); + } else if (mode === 'edit') { + // Handle vertex dragging + if (this.isDragging && this.draggedVertex) { + this._handleVertexDrag(e); + } + // Handle feature dragging + else if (this.isDraggingFeature && this.draggedFeature) { + this._handleFeatureDrag(e); + } + } + + this._updateMapCursor(e); + } + + /** + * Handle mouse down events + */ + _handleMouseDown(e) { + if (this.mode === 'edit') { + this._handleEditMouseDown(e); + } + } + + /** + * Handle mouse up events + */ + _handleMouseUp(e) { + if (this.mode === 'edit') { + this._handleEditMouseUp(e); + } + } + + /** + * Handle edit mode mouse down - start vertex or feature dragging + */ + _handleEditMouseDown(e) { + + // First check if we clicked on a vertex + const vertex = this._getVertexAtPoint(e.point); + + if (vertex) { + this.isDragging = true; + this.draggedVertex = vertex; + this.map.getCanvas().style.cursor = 'grabbing'; + + // Prevent default map behavior (panning) + // Check if event is cancelable to avoid passive listener warnings + if (e.originalEvent && e.originalEvent.cancelable !== false) { + e.originalEvent.stopPropagation(); + e.originalEvent.preventDefault(); + } + if (e.preventDefault) e.preventDefault(); + return; + } + + // Check if we clicked on a midpoint (to add a new vertex) + const midpoint = this._getMidpointAtPoint(e.point); + + if (midpoint) { + // Insert the new vertex into the feature + const newVertex = this._insertVertexAtMidpoint(midpoint); + if (newVertex) { + // Start dragging the newly created vertex + this.isDragging = true; + this.draggedVertex = newVertex; + this.map.getCanvas().style.cursor = 'grabbing'; + + // Prevent default map behavior (panning) + // Check if event is cancelable to avoid passive listener warnings + if (e.originalEvent && e.originalEvent.cancelable !== false) { + e.originalEvent.stopPropagation(); + e.originalEvent.preventDefault(); + } + if (e.preventDefault) e.preventDefault(); + } + return; + } + + // Check if we clicked on a feature (for dragging the whole feature) + const clickedFeatures = this._getFeatureAtPoint(e.point); + + if (clickedFeatures.length > 0) { + const feature = clickedFeatures[0]; + this.isDraggingFeature = true; + this.draggedFeature = feature; + this.dragStartPoint = e.lngLat; + this.map.getCanvas().style.cursor = 'grabbing'; + + // Prevent default map behavior (panning) + // Check if event is cancelable to avoid passive listener warnings + if (e.originalEvent && e.originalEvent.cancelable !== false) { + e.originalEvent.stopPropagation(); + e.originalEvent.preventDefault(); + } + if (e.preventDefault) e.preventDefault(); + return; + } + } + + /** + * Handle edit mode mouse up - end vertex or feature dragging + */ + _handleEditMouseUp(e) { + if (this.isDragging && this.draggedVertex) { + this.isDragging = false; + this._lastDragEndTime = Date.now(); // Track when drag ended + this._ignoringClickAfterDrag = true; // Flag to ignore next click event + + const feature = this.features.get(this.draggedVertex.featureId); + if (feature) { + // Include synced features in the update event + const allModifiedFeatures = [feature]; + if (this._lastSyncedFeatures && this._lastSyncedFeatures.length > 0) { + allModifiedFeatures.push(...this._lastSyncedFeatures); + } + + this._fireEvent('draw.update', { + features: allModifiedFeatures, + action: 'move' + }); + + // Clear synced features after firing event + this._lastSyncedFeatures = null; + } + + this.draggedVertex = null; + this.map.getCanvas().style.cursor = ''; + } + + if (this.isDraggingFeature && this.draggedFeature) { + this.isDraggingFeature = false; + this._lastDragEndTime = Date.now(); // Track when drag ended + this._ignoringClickAfterDrag = true; // Flag to ignore next click event + + const feature = this.features.get(this.draggedFeature.id); + if (feature) { + this._fireEvent('draw.update', { + features: [feature], + action: 'move' + }); + } + + this.draggedFeature = null; + this.dragStartPoint = null; + this.map.getCanvas().style.cursor = ''; + } + } + + /** + * Handle feature dragging (move entire geometry) + */ + _handleFeatureDrag(e) { + if (!this.draggedFeature || !this.dragStartPoint) return; + + const feature = this.features.get(this.draggedFeature.id); + if (!feature) return; + + // Calculate the delta + const dx = e.lngLat.lng - this.dragStartPoint.lng; + const dy = e.lngLat.lat - this.dragStartPoint.lat; + + // Update all coordinates by the delta + this._translateGeometry(feature.geometry, dx, dy); + + // Update the start point for next move + this.dragStartPoint = e.lngLat; + + this._updateLayers(); + } + + /** + * Translate all coordinates in a geometry by dx, dy + */ + _translateGeometry(geometry, dx, dy) { + if (geometry.type === 'Point') { + geometry.coordinates[0] += dx; + geometry.coordinates[1] += dy; + } else if (geometry.type === 'LineString') { + geometry.coordinates.forEach(coord => { + coord[0] += dx; + coord[1] += dy; + }); + } else if (geometry.type === 'Polygon') { + geometry.coordinates.forEach(ring => { + ring.forEach(coord => { + coord[0] += dx; + coord[1] += dy; + }); + }); + } else if (geometry.type === 'MultiPolygon') { + geometry.coordinates.forEach(polygon => { + polygon.forEach(ring => { + ring.forEach(coord => { + coord[0] += dx; + coord[1] += dy; + }); + }); + }); + } + } + + /** + * Handle double click events + */ + _handleDoubleClick(e) { + if (e.preventDefault && (!e.originalEvent || e.originalEvent.cancelable !== false)) { + e.preventDefault(); + } + + if (this.mode === 'draw_line_string' || this.mode === 'draw_polygon') { + this._finishDrawing(); + } + } + + /** + * Handle right click / context menu + */ + _handleContextMenu(e) { + if (e.preventDefault && (!e.originalEvent || e.originalEvent.cancelable !== false)) { + e.preventDefault(); + } + + if (this.mode === 'draw_line_string' || this.mode === 'draw_polygon') { + this._finishDrawing(); + } + } + + /** + * Handle point drawing click + */ + _handleDrawPointClick(e) { + const coords = this._getSnappedCoordinates(e); + + const feature = { + id: this._generateId(), + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coords + }, + properties: {} + }; + + this.features.set(feature.id, feature); + this._updateLayers(); + + this._fireEvent('draw.create', { + features: [feature] + }); + + // Stay in draw_point mode for multiple points + } + + /** + * Handle line drawing click + */ + _handleDrawLineClick(e) { + const coords = this._getSnappedCoordinates(e); + + if (!this.currentFeature) { + // Start new line + this.currentFeature = { + id: this._generateId(), + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [coords, coords] // Start with duplicate for preview + }, + properties: {} + }; + } else { + // If clicking near the last fixed point, finish the line + const coordinates = this.currentFeature.geometry.coordinates; + if (coordinates.length >= 3) { + const lastFixed = coordinates[coordinates.length - 2]; + const lastPoint = this.map.project(lastFixed); + const distance = Math.sqrt( + Math.pow(e.point.x - lastPoint.x, 2) + + Math.pow(e.point.y - lastPoint.y, 2) + ); + + if (distance < VERTEX_CLICK_TOLERANCE_PX) { + this._finishDrawing(); + return; + } + } + + // Add point to line + coordinates[coordinates.length - 1] = coords; + coordinates.push(coords); // Add new preview point + } + + this._updateLayers(); + } + + /** + * Handle polygon drawing click + */ + _handleDrawPolygonClick(e) { + const coords = this._getSnappedCoordinates(e); + + if (!this.currentFeature) { + // Start new polygon + this.currentFeature = { + id: this._generateId(), + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[coords, coords, coords, coords]] // Start with 4 duplicates for preview + }, + properties: {} + }; + } else { + // Add point to polygon + const ring = this.currentFeature.geometry.coordinates[0]; + + // Check if clicking near the first point to close the polygon + // We need at least 3 points (excluding preview) to close + if (ring.length >= 4) { // At least 3 real points + 1 preview + const firstPoint = ring[0]; + const point = this.map.project([firstPoint[0], firstPoint[1]]); + const distance = Math.sqrt( + Math.pow(e.point.x - point.x, 2) + + Math.pow(e.point.y - point.y, 2) + ); + + // If close to first point (within 15px), finish the polygon + if (distance < 15) { + this._finishDrawing(); + return; + } + } + + ring[ring.length - 2] = coords; // Update the second-to-last point + ring.push(coords); // Add new preview point + } + + this._updateLayers(); + } + + /** + * Handle vertex dragging + */ + _handleVertexDrag(e) { + if (!this.draggedVertex) return; + + const coords = this._getSnappedCoordinates(e); + const feature = this.features.get(this.draggedVertex.featureId); + + if (!feature) return; + + // Get the OLD coordinates before updating (for shared vertex detection) + const { coordPath } = this.draggedVertex; + const oldCoords = this._getCoordinateAtPath(feature.geometry, coordPath); + + // Update the vertex coordinates + this._setCoordinateAtPath(feature.geometry, coordPath, coords); + + // For polygons, if we moved the first or last vertex, update both (they must be identical to close the polygon) + if (feature.geometry.type === 'Polygon' && coordPath.length === 3) { + const ringIdx = coordPath[1]; + const vertexIdx = coordPath[2]; + const ring = feature.geometry.coordinates[ringIdx]; + + // If it's the first vertex (index 0), also update the last vertex (index length-1) + if (vertexIdx === 0) { + ring[ring.length - 1] = [...coords]; + } + // If it's the last vertex, also update the first vertex + else if (vertexIdx === ring.length - 1) { + ring[0] = [...coords]; + } + } + // For MultiPolygon, coordPath is [coordinates, polyIdx, ringIdx, vertexIdx] + else if (feature.geometry.type === 'MultiPolygon' && coordPath.length === 4) { + const polyIdx = coordPath[1]; + const ringIdx = coordPath[2]; + const vertexIdx = coordPath[3]; + const ring = feature.geometry.coordinates[polyIdx][ringIdx]; + + // If it's the first vertex (index 0), also update the last vertex (index length-1) + if (vertexIdx === 0) { + ring[ring.length - 1] = [...coords]; + } + // If it's the last vertex, also update the first vertex + else if (vertexIdx === ring.length - 1) { + ring[0] = [...coords]; + } + } + + // Sync shared vertices across all selected features + if (oldCoords && this.selectedFeatureIds.size > 1) { + const syncedFeatures = this._syncSharedVertices(this.draggedVertex.featureId, oldCoords, coords); + // Store synced features for inclusion in draw.update event + this._lastSyncedFeatures = syncedFeatures; + } + + this._updateLayers(); + } + + /** + * Sync shared vertices: when a vertex is moved, update matching vertices in other selected features + * Returns array of modified features (excluding source) + */ + _syncSharedVertices(sourceFeatureId, oldCoords, newCoords) { + const TOLERANCE = 1e-9; // Very tight tolerance for exact matches + const modifiedFeatures = []; + + this.selectedFeatureIds.forEach(featureId => { + if (featureId === sourceFeatureId) return; + + const feature = this.features.get(featureId); + if (!feature || feature.geometry.type !== 'Polygon') return; + + let modified = false; + + feature.geometry.coordinates.forEach((ring, ringIdx) => { + ring.forEach((vertex, vertexIdx) => { + // Check if this vertex matches the OLD position of the dragged vertex + if (Math.abs(vertex[0] - oldCoords[0]) <= TOLERANCE && + Math.abs(vertex[1] - oldCoords[1]) <= TOLERANCE) { + // Update to new position + feature.geometry.coordinates[ringIdx][vertexIdx] = [...newCoords]; + modified = true; + + // If it's first or last vertex of a ring, update the corresponding one too + if (vertexIdx === 0) { + ring[ring.length - 1] = [...newCoords]; + } else if (vertexIdx === ring.length - 1) { + ring[0] = [...newCoords]; + } + } + }); + }); + + if (modified) { + modifiedFeatures.push(feature); + } + }); + + return modifiedFeatures; + } + + /** + * Get coordinate at a nested path + */ + _getCoordinateAtPath(geometry, path) { + let coords = geometry.coordinates; + for (let i = 1; i < path.length; i++) { + coords = coords[path[i]]; + } + return coords ? [...coords] : null; + } + + /** + * Update the last vertex of the current feature being drawn + */ + _updateCurrentFeatureLastVertex(e) { + if (!this.currentFeature) return; + + const coords = this._getSnappedCoordinates(e); + const geometry = this.currentFeature.geometry; + + if (geometry.type === 'LineString') { + const coordinates = geometry.coordinates; + coordinates[coordinates.length - 1] = coords; + } else if (geometry.type === 'Polygon') { + const ring = geometry.coordinates[0]; + ring[ring.length - 1] = coords; + } + + this._updateLayers(); + } + + /** + * Finish drawing the current feature + */ + _finishDrawing() { + if (!this.currentFeature) return; + + const geometry = this.currentFeature.geometry; + let isValid = false; + + if (geometry.type === 'LineString') { + // Remove the preview point + const coords = geometry.coordinates; + coords.pop(); + + // Line needs at least 2 points + isValid = coords.length >= 2; + } else if (geometry.type === 'Polygon') { + // Remove the preview point + const ring = geometry.coordinates[0]; + ring.pop(); + + // Close the ring + if (ring.length >= 3) { + ring.push(ring[0]); + isValid = true; + } + } + + if (isValid) { + this.features.set(this.currentFeature.id, this.currentFeature); + + this._fireEvent('draw.create', { + features: [this.currentFeature] + }); + } else { + } + + this.currentFeature = null; + this._updateLayers(); + } + + /** + * Get coordinates with snapping applied + */ + _getSnappedCoordinates(e) { + const lngLat = e.lngLat; + let coords = [lngLat.lng, lngLat.lat]; + + // Check if snapping is enabled (either globally or for this mode) + const isSnapEnabled = this.options.snap || this._modeSnap; + + if (!isSnapEnabled) { + return coords; + } + + // Get features to snap to + const snapFeatures = this._getSnapFeatures(); + + if (snapFeatures.length === 0) { + return coords; + } + + // Find nearest snap point + const snapPoint = this._findNearestSnapPoint(e.point, e.lngLat, snapFeatures); + + if (snapPoint) { + return snapPoint.coordinates; + } + + return coords; + } + + /** + * Get features to snap to (with caching) + */ + _getSnapFeatures() { + // Use cache if still valid + const now = Date.now(); + if (this._snapFeaturesCache && (now - this._snapFeaturesCacheTime) < this._snapFeaturesCacheTTL) { + return this._snapFeaturesCache; + } + + let snapFeatures = []; + + // Use custom snap function if provided + if (this.snapOptions.snapGetFeatures) { + try { + const customFeatures = this.snapOptions.snapGetFeatures(this.map, this, []); + snapFeatures = customFeatures; + } catch (err) { + console.error('Error getting snap features:', err); + } + } + + // Include rendered map features (excluding draw layers) + if (this.map && this.snapOptions.snapIncludeMapFeatures) { + try { + const rendered = this.map.queryRenderedFeatures(); + const drawSourceId = DRAW_LAYER_PREFIX + 'source'; + + rendered.forEach(mapFeature => { + const layerId = (mapFeature.layer && mapFeature.layer.id) ? mapFeature.layer.id : ''; + if (layerId && layerId.startsWith(DRAW_LAYER_PREFIX)) return; + if (mapFeature.source && mapFeature.source === drawSourceId) return; + if (!mapFeature.geometry) return; + + snapFeatures.push({ + id: mapFeature.id || mapFeature.properties?.id || null, + type: 'Feature', + geometry: mapFeature.geometry, + properties: mapFeature.properties || {} + }); + }); + } catch (err) { + console.error('Error getting rendered map features for snapping:', err); + } + } + + // Also include current drawing features from the draw control + const drawFeatures = Array.from(this.features.values()); + snapFeatures = snapFeatures.concat(drawFeatures); + + // Cache the result + this._snapFeaturesCache = snapFeatures; + this._snapFeaturesCacheTime = now; + + return snapFeatures; + } + + /** + * Find the nearest snap point + */ + _findNearestSnapPoint(screenPoint, lngLat, features) { + const snapPx = this.snapOptions.snapPx; + const vertexPriorityDistance = this.snapOptions.snapVertexPriorityDistance; + let nearestPoint = null; + let nearestDistance = Infinity; + let nearestVertex = null; + let nearestVertexDistance = Infinity; + let nearestVertexGeoDistance = Infinity; + let nearestSegment = null; + let nearestSegmentDistance = Infinity; + + features.forEach(feature => { + if (!feature.geometry) return; + + // Extract all coordinates from the feature + const coordinates = this._extractCoordinates(feature.geometry); + + coordinates.forEach(coord => { + const point = this.map.project(coord); + const distance = Math.sqrt( + Math.pow(point.x - screenPoint.x, 2) + + Math.pow(point.y - screenPoint.y, 2) + ); + + if (distance < snapPx && distance < nearestVertexDistance) { + nearestVertexDistance = distance; + const geoDistance = lngLat + ? Math.sqrt( + Math.pow(coord[0] - lngLat.lng, 2) + + Math.pow(coord[1] - lngLat.lat, 2) + ) + : Infinity; + nearestVertexGeoDistance = geoDistance; + nearestVertex = { + coordinates: coord, + distance: distance, + geoDistance: geoDistance + }; + } + }); + + // Also check midpoints if enabled + if (this.snapOptions.snapToMidPoints) { + const midpoints = this._getMidpoints(feature.geometry); + midpoints.forEach(coord => { + const point = this.map.project(coord); + const distance = Math.sqrt( + Math.pow(point.x - screenPoint.x, 2) + + Math.pow(point.y - screenPoint.y, 2) + ); + + if (distance < snapPx && distance < nearestDistance) { + nearestDistance = distance; + nearestPoint = { + coordinates: coord, + distance: distance + }; + } + }); + } + + // Also check nearest point on segments if enabled + if (this.snapOptions.snapToSegments) { + const segmentPoint = this._findNearestPointOnSegments(screenPoint, feature.geometry); + if (segmentPoint && segmentPoint.distance < snapPx && segmentPoint.distance < nearestSegmentDistance) { + nearestSegmentDistance = segmentPoint.distance; + nearestSegment = { + coordinates: segmentPoint.coordinates, + distance: segmentPoint.distance + }; + } + } + }); + + // Vertex priority within a tolerance (in degrees) + if (nearestVertex && vertexPriorityDistance !== null && vertexPriorityDistance !== undefined) { + if (nearestVertexGeoDistance <= vertexPriorityDistance) { + return nearestVertex; + } + } + + // Otherwise, pick the closest between vertex/segment/midpoint candidates + nearestPoint = nearestPoint || nearestSegment || nearestVertex; + nearestDistance = nearestPoint ? nearestPoint.distance : Infinity; + + if (nearestSegment && nearestSegment.distance < nearestDistance) { + nearestPoint = nearestSegment; + nearestDistance = nearestSegment.distance; + } + + if (nearestVertex && nearestVertex.distance < nearestDistance) { + nearestPoint = nearestVertex; + } + + return nearestPoint; + } + + /** + * Find nearest point on any segment of a geometry in screen space + */ + _findNearestPointOnSegments(screenPoint, geometry) { + if (!this.map || !geometry) return null; + + let nearest = null; + let nearestDistance = Infinity; + + const checkSegment = (coordA, coordB) => { + const a = this.map.project(coordA); + const b = this.map.project(coordB); + + const dx = b.x - a.x; + const dy = b.y - a.y; + const lengthSq = dx * dx + dy * dy; + if (lengthSq === 0) return; + + let t = ((screenPoint.x - a.x) * dx + (screenPoint.y - a.y) * dy) / lengthSq; + t = Math.max(0, Math.min(1, t)); + + const closestScreen = { + x: a.x + t * dx, + y: a.y + t * dy + }; + + const distance = Math.sqrt( + Math.pow(closestScreen.x - screenPoint.x, 2) + + Math.pow(closestScreen.y - screenPoint.y, 2) + ); + + if (distance < nearestDistance) { + nearestDistance = distance; + const lngLat = this.map.unproject(closestScreen); + nearest = { + coordinates: [lngLat.lng, lngLat.lat], + distance: distance + }; + } + }; + + const processLine = (coords) => { + for (let i = 0; i < coords.length - 1; i++) { + checkSegment(coords[i], coords[i + 1]); + } + }; + + if (geometry.type === 'LineString') { + processLine(geometry.coordinates); + } else if (geometry.type === 'Polygon') { + geometry.coordinates.forEach(ring => processLine(ring)); + } else if (geometry.type === 'MultiLineString') { + geometry.coordinates.forEach(line => processLine(line)); + } else if (geometry.type === 'MultiPolygon') { + geometry.coordinates.forEach(polygon => { + polygon.forEach(ring => processLine(ring)); + }); + } + + return nearest; + } + + /** + * Extract all coordinates from a geometry + */ + _extractCoordinates(geometry) { + const coords = []; + + if (geometry.type === 'Point') { + coords.push(geometry.coordinates); + } else if (geometry.type === 'LineString') { + coords.push(...geometry.coordinates); + } else if (geometry.type === 'Polygon') { + geometry.coordinates.forEach(ring => { + coords.push(...ring); + }); + } else if (geometry.type === 'MultiPoint') { + coords.push(...geometry.coordinates); + } else if (geometry.type === 'MultiLineString') { + geometry.coordinates.forEach(line => { + coords.push(...line); + }); + } else if (geometry.type === 'MultiPolygon') { + geometry.coordinates.forEach(polygon => { + polygon.forEach(ring => { + coords.push(...ring); + }); + }); + } + + return coords; + } + + /** + * Get midpoints of line segments in a geometry + */ + _getMidpoints(geometry) { + const midpoints = []; + + const processRing = (coords) => { + for (let i = 0; i < coords.length - 1; i++) { + const midpoint = [ + (coords[i][0] + coords[i + 1][0]) / 2, + (coords[i][1] + coords[i + 1][1]) / 2 + ]; + midpoints.push(midpoint); + } + }; + + if (geometry.type === 'LineString') { + processRing(geometry.coordinates); + } else if (geometry.type === 'Polygon') { + geometry.coordinates.forEach(ring => processRing(ring)); + } else if (geometry.type === 'MultiLineString') { + geometry.coordinates.forEach(line => processRing(line)); + } else if (geometry.type === 'MultiPolygon') { + geometry.coordinates.forEach(polygon => { + polygon.forEach(ring => processRing(ring)); + }); + } + + return midpoints; + } + + /** + * Get feature at a screen point + */ + _getFeatureAtPoint(point) { + if (!this.map) return []; + + const tolerance = this.options.clickTolerance; + const bbox = [ + [point.x - tolerance, point.y - tolerance], + [point.x + tolerance, point.y + tolerance] + ]; + + const layers = this._getDrawLayerIds(); + const mapFeatures = this.map.queryRenderedFeatures(bbox, { layers: layers }); + + // Map back to our feature objects + const features = []; + mapFeatures.forEach(mapFeature => { + const featureId = mapFeature.properties.id || mapFeature.id; + const feature = this.features.get(featureId); + if (feature) { + features.push(feature); + } + }); + + return features; + } + + /** + * Get vertex at a screen point + */ + _getVertexAtPoint(point) { + if (!this.map) return null; + + const tolerance = VERTEX_CLICK_TOLERANCE_PX; + + // Check selected features for vertices + for (const featureId of this.selectedFeatureIds) { + const feature = this.features.get(featureId); + if (!feature) continue; + + const vertex = this._findVertexNearPoint(feature, point, tolerance); + if (vertex) { + return vertex; + } + } + + return null; + } + + /** + * Get midpoint at a screen point + */ + _getMidpointAtPoint(point) { + if (!this.map) return null; + + const tolerance = VERTEX_CLICK_TOLERANCE_PX; + + // Check selected features for midpoints + for (const featureId of this.selectedFeatureIds) { + const feature = this.features.get(featureId); + if (!feature) continue; + if (feature.geometry.type !== 'Polygon' && feature.geometry.type !== 'MultiPolygon') continue; + + const midpoint = this._findMidpointNearPoint(feature, point, tolerance); + if (midpoint) { + return midpoint; + } + } + + return null; + } + + /** + * Find a midpoint near a point + */ + _findMidpointNearPoint(feature, screenPoint, tolerance) { + const geometry = feature.geometry; + + const checkRing = (ring, ringIdx, polyIdx = null) => { + for (let i = 0; i < ring.length - 1; i++) { + const coord1 = ring[i]; + const coord2 = ring[i + 1]; + + // Calculate midpoint + const midCoord = [ + (coord1[0] + coord2[0]) / 2, + (coord1[1] + coord2[1]) / 2 + ]; + + const point = this.map.project(midCoord); + const distance = Math.sqrt( + Math.pow(point.x - screenPoint.x, 2) + + Math.pow(point.y - screenPoint.y, 2) + ); + + if (distance < tolerance) { + return { + featureId: feature.id, + coordinates: midCoord, + polyIdx: polyIdx, + ringIdx: ringIdx, + insertAfterIdx: i, + distance: distance + }; + } + } + return null; + }; + + if (geometry.type === 'Polygon') { + for (let ringIdx = 0; ringIdx < geometry.coordinates.length; ringIdx++) { + const result = checkRing(geometry.coordinates[ringIdx], ringIdx); + if (result) return result; + } + } else if (geometry.type === 'MultiPolygon') { + for (let polyIdx = 0; polyIdx < geometry.coordinates.length; polyIdx++) { + const polygon = geometry.coordinates[polyIdx]; + for (let ringIdx = 0; ringIdx < polygon.length; ringIdx++) { + const result = checkRing(polygon[ringIdx], ringIdx, polyIdx); + if (result) return result; + } + } + } + + return null; + } + + /** + * Insert a vertex at a midpoint position + */ + _insertVertexAtMidpoint(midpoint) { + const feature = this.features.get(midpoint.featureId); + if (!feature) return null; + + let ring; + let coordPath; + + if (feature.geometry.type === 'Polygon') { + ring = feature.geometry.coordinates[midpoint.ringIdx]; + coordPath = ['coordinates', midpoint.ringIdx, midpoint.insertAfterIdx + 1]; + } else if (feature.geometry.type === 'MultiPolygon' && midpoint.polyIdx !== null) { + ring = feature.geometry.coordinates[midpoint.polyIdx][midpoint.ringIdx]; + coordPath = ['coordinates', midpoint.polyIdx, midpoint.ringIdx, midpoint.insertAfterIdx + 1]; + } else { + return null; + } + + // Insert the new vertex after insertAfterIdx + ring.splice(midpoint.insertAfterIdx + 1, 0, midpoint.coordinates); + + // Update layers + this._updateLayers(); + + // Return the new vertex info for dragging + return { + featureId: midpoint.featureId, + coordinates: midpoint.coordinates, + coordPath: coordPath + }; + } + + /** + * Find a vertex near a point + */ + _findVertexNearPoint(feature, screenPoint, tolerance) { + const geometry = feature.geometry; + + const checkCoord = (coord, path) => { + const point = this.map.project(coord); + const distance = Math.sqrt( + Math.pow(point.x - screenPoint.x, 2) + + Math.pow(point.y - screenPoint.y, 2) + ); + + if (distance < tolerance) { + return { + featureId: feature.id, + coordinates: coord, + coordPath: path, + distance: distance + }; + } + return null; + }; + + if (geometry.type === 'Point') { + return checkCoord(geometry.coordinates, ['coordinates']); + } else if (geometry.type === 'LineString') { + for (let i = 0; i < geometry.coordinates.length; i++) { + const vertex = checkCoord(geometry.coordinates[i], ['coordinates', i]); + if (vertex) return vertex; + } + } else if (geometry.type === 'Polygon') { + for (let ringIdx = 0; ringIdx < geometry.coordinates.length; ringIdx++) { + const ring = geometry.coordinates[ringIdx]; + for (let i = 0; i < ring.length; i++) { + const vertex = checkCoord(ring[i], ['coordinates', ringIdx, i]); + if (vertex) return vertex; + } + } + } else if (geometry.type === 'MultiPolygon') { + for (let polyIdx = 0; polyIdx < geometry.coordinates.length; polyIdx++) { + const polygon = geometry.coordinates[polyIdx]; + for (let ringIdx = 0; ringIdx < polygon.length; ringIdx++) { + const ring = polygon[ringIdx]; + for (let i = 0; i < ring.length; i++) { + const vertex = checkCoord(ring[i], ['coordinates', polyIdx, ringIdx, i]); + if (vertex) return vertex; + } + } + } + } + + return null; + } + + /** + * Set coordinate at a nested path + */ + _setCoordinateAtPath(geometry, path, newCoord) { + let obj = geometry; + for (let i = 0; i < path.length - 1; i++) { + obj = obj[path[i]]; + } + obj[path[path.length - 1]] = newCoord; + } + + /** + * Update map cursor based on mode and hover state + */ + _updateMapCursor(e) { + if (!this.map) return; + + const mode = this.mode; + + if (mode === 'draw_point' || mode === 'draw_line_string' || mode === 'draw_polygon') { + this.map.getCanvas().style.cursor = 'crosshair'; + } else if (mode === 'direct_select') { + // Check if hovering over a vertex + if (e) { + const vertex = this._getVertexAtPoint(e.point); + this.map.getCanvas().style.cursor = vertex ? 'move' : 'default'; + } else { + this.map.getCanvas().style.cursor = 'default'; + } + } else { + this.map.getCanvas().style.cursor = ''; + } + } + + /** + * Add map layers for rendering + */ + _addLayers() { + if (!this.map) return; + + // Add source + this.map.addSource(DRAW_LAYER_PREFIX + 'source', { + type: 'geojson', + data: this._getSourceData() + }); + + // Add layers based on styles + const styles = this.options.styles; + styles.forEach(style => { + const layer = { + ...style, + id: DRAW_LAYER_PREFIX + style.id, + source: DRAW_LAYER_PREFIX + 'source' + }; + + try { + this.map.addLayer(layer); + } catch (err) { + console.error('Error adding layer:', err); + } + }); + } + + /** + * Remove map layers + */ + _removeLayers() { + if (!this.map) return; + + const styles = this.options.styles; + styles.forEach(style => { + const layerId = DRAW_LAYER_PREFIX + style.id; + if (this.map.getLayer(layerId)) { + this.map.removeLayer(layerId); + } + }); + + if (this.map.getSource(DRAW_LAYER_PREFIX + 'source')) { + this.map.removeSource(DRAW_LAYER_PREFIX + 'source'); + } + } + + /** + * Update map layers with current data + */ + _updateLayers() { + if (!this.map) return; + + const source = this.map.getSource(DRAW_LAYER_PREFIX + 'source'); + if (source) { + source.setData(this._getSourceData()); + } + + // Invalidate snap features cache when features change + this._snapFeaturesCache = null; + } + + /** + * Get source data as GeoJSON + */ + _getSourceData() { + const features = []; + + // Add all features + this.features.forEach(feature => { + const isActive = this.selectedFeatureIds.has(feature.id); + const isVertex = false; + + features.push({ + type: 'Feature', + id: feature.id, + geometry: feature.geometry, + properties: { + ...feature.properties, + id: feature.id, + active: isActive ? 'true' : 'false', + meta: 'feature' + } + }); + + // Add vertices for selected features in edit mode (includes direct_select) + if (isActive && (this.mode === 'edit' || this.mode === 'direct_select')) { + this._addVertexFeatures(feature, features); + } + }); + + // Add current drawing feature if any + if (this.currentFeature) { + features.push({ + type: 'Feature', + id: this.currentFeature.id, + geometry: this.currentFeature.geometry, + properties: { + id: this.currentFeature.id, + active: 'true', + meta: 'feature' + } + }); + + // Add vertices for the current drawing feature so user can see the points + if (this.mode === 'draw_polygon' || this.mode === 'draw_line_string') { + this._addVertexFeatures(this.currentFeature, features); + } + } + + return { + type: 'FeatureCollection', + features: features + }; + } + + /** + * Add vertex features for a feature + */ + _addVertexFeatures(feature, features) { + const coordinates = this._extractCoordinates(feature.geometry); + + // Add actual vertices + coordinates.forEach((coord, idx) => { + features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coord + }, + properties: { + meta: 'vertex', + parent: feature.id, + coord_path: idx + } + }); + }); + + // Add midpoints for adding new vertices + const midpoints = this._getMidpoints(feature.geometry); + midpoints.forEach((coord, idx) => { + features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coord + }, + properties: { + meta: 'midpoint', + parent: feature.id, + coord_path: idx + } + }); + }); + } + + /** + * Get draw layer IDs + */ + _getDrawLayerIds() { + return this.options.styles + .filter(s => s.type === 'fill' || s.type === 'line' || s.type === 'circle') + .map(s => DRAW_LAYER_PREFIX + s.id); + } + + /** + * Get default styles + */ + _getDefaultStyles() { + return [ + // Polygon fill inactive + { + id: 'polygon-fill-inactive', + type: 'fill', + filter: ['all', + ['==', ['get', 'active'], 'false'], + ['==', ['geometry-type'], 'Polygon'], + ['==', ['get', 'meta'], 'feature'] + ], + paint: { + 'fill-color': '#3bb2d0', + 'fill-opacity': 0.1 + } + }, + // Polygon fill active + { + id: 'polygon-fill-active', + type: 'fill', + filter: ['all', + ['==', ['get', 'active'], 'true'], + ['==', ['geometry-type'], 'Polygon'], + ['==', ['get', 'meta'], 'feature'] + ], + paint: { + 'fill-color': '#fbb03b', + 'fill-opacity': 0.1 + } + }, + // Polygon outline inactive + { + id: 'polygon-stroke-inactive', + type: 'line', + filter: ['all', + ['==', ['get', 'active'], 'false'], + ['==', ['geometry-type'], 'Polygon'], + ['==', ['get', 'meta'], 'feature'] + ], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#3bb2d0', + 'line-width': 2 + } + }, + // Polygon outline active + { + id: 'polygon-stroke-active', + type: 'line', + filter: ['all', + ['==', ['get', 'active'], 'true'], + ['==', ['geometry-type'], 'Polygon'], + ['==', ['get', 'meta'], 'feature'] + ], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#fbb03b', + 'line-width': 2 + } + }, + // Line inactive + { + id: 'line-inactive', + type: 'line', + filter: ['all', + ['==', ['get', 'active'], 'false'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['get', 'meta'], 'feature'] + ], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#3bb2d0', + 'line-width': 2 + } + }, + // Line active + { + id: 'line-active', + type: 'line', + filter: ['all', + ['==', ['get', 'active'], 'true'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['get', 'meta'], 'feature'] + ], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#fbb03b', + 'line-width': 2 + } + }, + // Point inactive + { + id: 'point-inactive', + type: 'circle', + filter: ['all', + ['==', ['get', 'active'], 'false'], + ['==', ['geometry-type'], 'Point'], + ['==', ['get', 'meta'], 'feature'] + ], + paint: { + 'circle-radius': 5, + 'circle-color': '#3bb2d0' + } + }, + // Point active + { + id: 'point-active', + type: 'circle', + filter: ['all', + ['==', ['get', 'active'], 'true'], + ['==', ['geometry-type'], 'Point'], + ['==', ['get', 'meta'], 'feature'] + ], + paint: { + 'circle-radius': 7, + 'circle-color': '#fbb03b' + } + }, + // Vertex points + { + id: 'vertex-halo', + type: 'circle', + filter: ['all', + ['==', ['get', 'meta'], 'vertex'] + ], + paint: { + 'circle-radius': 5, + 'circle-color': '#FFF' + } + }, + { + id: 'vertex', + type: 'circle', + filter: ['all', + ['==', ['get', 'meta'], 'vertex'] + ], + paint: { + 'circle-radius': 3, + 'circle-color': '#fbb03b' + } + }, + // Midpoint points (for adding new vertices) + { + id: 'midpoint', + type: 'circle', + filter: ['all', + ['==', ['get', 'meta'], 'midpoint'] + ], + paint: { + 'circle-radius': 3, + 'circle-color': '#fbb03b', + 'circle-opacity': 0.6 + } + } + ]; + } + } + + // Export to global scope + window.CustomDraw = CustomDraw; + +})(window); diff --git a/streetup/common/static/common/echarts.min.js b/streetup/common/static/common/echarts.min.js new file mode 100644 index 0000000..835966f --- /dev/null +++ b/streetup/common/static/common/echarts.min.js @@ -0,0 +1,45 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).echarts={})}(this,(function(t){"use strict"; +/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},e(t,n)};function n(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var i=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},r=new function(){this.browser=new i,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(r.wxa=!0,r.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?r.worker=!0:"undefined"==typeof navigator||0===navigator.userAgent.indexOf("Node.js")?(r.node=!0,r.svgSupported=!0):function(t,e){var n=e.browser,i=t.match(/Firefox\/([\d.]+)/),r=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),o=t.match(/Edge?\/([\d.]+)/),a=/micromessenger/i.test(t);i&&(n.firefox=!0,n.version=i[1]);r&&(n.ie=!0,n.version=r[1]);o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),e.domSupported="undefined"!=typeof document;var s=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,r);var o="sans-serif",a="12px "+o;var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n=0)o=r*t.length;else for(var c=0;c>1)%2;a.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",r[l]+":0",i[1-s]+":auto",r[1-l]+":auto",""].join("!important;"),t.appendChild(a),n.push(a)}return n}(e,a),l=function(t,e,n){for(var i=n?"invTrans":"trans",r=e[i],o=e.srcCoords,a=[],s=[],l=!0,u=0;u<4;u++){var h=t[u].getBoundingClientRect(),c=2*u,p=h.left,d=h.top;a.push(p,d),l=l&&o&&p===o[c]&&d===o[c+1],s.push(t[u].offsetLeft,t[u].offsetTop)}return l&&r?r:(e.srcCoords=a,e[i]=n?$t(s,a):$t(a,s))}(s,a,o);if(l)return l(t,n,i),!0}return!1}function ee(t){return"CANVAS"===t.nodeName.toUpperCase()}var ne=/([&<>"'])/g,ie={"&":"&","<":"<",">":">",'"':""","'":"'"};function re(t){return null==t?"":(t+"").replace(ne,(function(t,e){return ie[e]}))}var oe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ae=[],se=r.browser.firefox&&+r.browser.version.split(".")[0]<39;function le(t,e,n,i){return n=n||{},i?ue(t,e,n):se&&null!=e.layerX&&e.layerX!==e.offsetX?(n.zrX=e.layerX,n.zrY=e.layerY):null!=e.offsetX?(n.zrX=e.offsetX,n.zrY=e.offsetY):ue(t,e,n),n}function ue(t,e,n){if(r.domSupported&&t.getBoundingClientRect){var i=e.clientX,o=e.clientY;if(ee(t)){var a=t.getBoundingClientRect();return n.zrX=i-a.left,void(n.zrY=o-a.top)}if(te(ae,t,i,o))return n.zrX=ae[0],void(n.zrY=ae[1])}n.zrX=n.zrY=0}function he(t){return t||window.event}function ce(t,e,n){if(null!=(e=he(e)).zrX)return e;var i=e.type;if(i&&i.indexOf("touch")>=0){var r="touchend"!==i?e.targetTouches[0]:e.changedTouches[0];r&&le(t,r,e,n)}else{le(t,e,e,n);var o=function(t){var e=t.wheelDelta;if(e)return e;var n=t.deltaX,i=t.deltaY;if(null==n||null==i)return e;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(e);e.zrDelta=o?o/120:-(e.detail||0)/3}var a=e.button;return null==e.which&&void 0!==a&&oe.test(e.type)&&(e.which=1&a?1:2&a?3:4&a?2:0),e}function pe(t,e,n,i){t.addEventListener(e,n,i)}var de=function(t){t.preventDefault(),t.stopPropagation(),t.cancelBubble=!0};function fe(t){return 2===t.which||3===t.which}var ge=function(){function t(){this._track=[]}return t.prototype.recognize=function(t,e,n){return this._doTrack(t,e,n),this._recognize(t)},t.prototype.clear=function(){return this._track.length=0,this},t.prototype._doTrack=function(t,e,n){var i=t.touches;if(i){for(var r={points:[],touches:[],target:e,event:t},o=0,a=i.length;o1&&r&&r.length>1){var a=ye(r)/ye(o);!isFinite(a)&&(a=1),e.pinchScale=a;var s=[((i=r)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return e.pinchX=s[0],e.pinchY=s[1],{type:"pinch",target:t[0].target,event:e}}}}};function me(){return[1,0,0,1,0,0]}function xe(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t}function _e(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t}function be(t,e,n){var i=e[0]*n[0]+e[2]*n[1],r=e[1]*n[0]+e[3]*n[1],o=e[0]*n[2]+e[2]*n[3],a=e[1]*n[2]+e[3]*n[3],s=e[0]*n[4]+e[2]*n[5]+e[4],l=e[1]*n[4]+e[3]*n[5]+e[5];return t[0]=i,t[1]=r,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t}function we(t,e,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4]+n[0],t[5]=e[5]+n[1],t}function Se(t,e,n,i){void 0===i&&(i=[0,0]);var r=e[0],o=e[2],a=e[4],s=e[1],l=e[3],u=e[5],h=Math.sin(n),c=Math.cos(n);return t[0]=r*c+s*h,t[1]=-r*h+s*c,t[2]=o*c+l*h,t[3]=-o*h+c*l,t[4]=c*(a-i[0])+h*(u-i[1])+i[0],t[5]=c*(u-i[1])-h*(a-i[0])+i[1],t}function Me(t,e,n){var i=n[0],r=n[1];return t[0]=e[0]*i,t[1]=e[1]*r,t[2]=e[2]*i,t[3]=e[3]*r,t[4]=e[4]*i,t[5]=e[5]*r,t}function Ie(t,e){var n=e[0],i=e[2],r=e[4],o=e[1],a=e[3],s=e[5],l=n*a-o*i;return l?(l=1/l,t[0]=a*l,t[1]=-o*l,t[2]=-i*l,t[3]=n*l,t[4]=(i*s-a*r)*l,t[5]=(o*r-n*s)*l,t):null}function Te(t){var e=[1,0,0,1,0,0];return _e(e,t),e}var Ce=Object.freeze({__proto__:null,create:me,identity:xe,copy:_e,mul:be,translate:we,rotate:Se,scale:Me,invert:Ie,clone:Te}),De=function(){function t(t,e){this.x=t||0,this.y=e||0}return t.prototype.copy=function(t){return this.x=t.x,this.y=t.y,this},t.prototype.clone=function(){return new t(this.x,this.y)},t.prototype.set=function(t,e){return this.x=t,this.y=e,this},t.prototype.equal=function(t){return t.x===this.x&&t.y===this.y},t.prototype.add=function(t){return this.x+=t.x,this.y+=t.y,this},t.prototype.scale=function(t){this.x*=t,this.y*=t},t.prototype.scaleAndAdd=function(t,e){this.x+=t.x*e,this.y+=t.y*e},t.prototype.sub=function(t){return this.x-=t.x,this.y-=t.y,this},t.prototype.dot=function(t){return this.x*t.x+this.y*t.y},t.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},t.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},t.prototype.normalize=function(){var t=this.len();return this.x/=t,this.y/=t,this},t.prototype.distance=function(t){var e=this.x-t.x,n=this.y-t.y;return Math.sqrt(e*e+n*n)},t.prototype.distanceSquare=function(t){var e=this.x-t.x,n=this.y-t.y;return e*e+n*n},t.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},t.prototype.transform=function(t){if(t){var e=this.x,n=this.y;return this.x=t[0]*e+t[2]*n+t[4],this.y=t[1]*e+t[3]*n+t[5],this}},t.prototype.toArray=function(t){return t[0]=this.x,t[1]=this.y,t},t.prototype.fromArray=function(t){this.x=t[0],this.y=t[1]},t.set=function(t,e,n){t.x=e,t.y=n},t.copy=function(t,e){t.x=e.x,t.y=e.y},t.len=function(t){return Math.sqrt(t.x*t.x+t.y*t.y)},t.lenSquare=function(t){return t.x*t.x+t.y*t.y},t.dot=function(t,e){return t.x*e.x+t.y*e.y},t.add=function(t,e,n){t.x=e.x+n.x,t.y=e.y+n.y},t.sub=function(t,e,n){t.x=e.x-n.x,t.y=e.y-n.y},t.scale=function(t,e,n){t.x=e.x*n,t.y=e.y*n},t.scaleAndAdd=function(t,e,n,i){t.x=e.x+n.x*i,t.y=e.y+n.y*i},t.lerp=function(t,e,n,i){var r=1-i;t.x=r*e.x+i*n.x,t.y=r*e.y+i*n.y},t}(),Ae=Math.min,ke=Math.max,Le=new De,Pe=new De,Oe=new De,Re=new De,Ne=new De,Ee=new De,ze=function(){function t(t,e,n,i){n<0&&(t+=n,n=-n),i<0&&(e+=i,i=-i),this.x=t,this.y=e,this.width=n,this.height=i}return t.prototype.union=function(t){var e=Ae(t.x,this.x),n=Ae(t.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=ke(t.x+t.width,this.x+this.width)-e:this.width=t.width,isFinite(this.y)&&isFinite(this.height)?this.height=ke(t.y+t.height,this.y+this.height)-n:this.height=t.height,this.x=e,this.y=n},t.prototype.applyTransform=function(e){t.applyTransform(this,this,e)},t.prototype.calculateTransform=function(t){var e=this,n=t.width/e.width,i=t.height/e.height,r=[1,0,0,1,0,0];return we(r,r,[-e.x,-e.y]),Me(r,r,[n,i]),we(r,r,[t.x,t.y]),r},t.prototype.intersect=function(e,n){if(!e)return!1;e instanceof t||(e=t.create(e));var i=this,r=i.x,o=i.x+i.width,a=i.y,s=i.y+i.height,l=e.x,u=e.x+e.width,h=e.y,c=e.y+e.height,p=!(of&&(f=x,gf&&(f=_,v=n.x&&t<=n.x+n.width&&e>=n.y&&e<=n.y+n.height},t.prototype.clone=function(){return new t(this.x,this.y,this.width,this.height)},t.prototype.copy=function(e){t.copy(this,e)},t.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},t.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},t.prototype.isZero=function(){return 0===this.width||0===this.height},t.create=function(e){return new t(e.x,e.y,e.width,e.height)},t.copy=function(t,e){t.x=e.x,t.y=e.y,t.width=e.width,t.height=e.height},t.applyTransform=function(e,n,i){if(i){if(i[1]<1e-5&&i[1]>-1e-5&&i[2]<1e-5&&i[2]>-1e-5){var r=i[0],o=i[3],a=i[4],s=i[5];return e.x=n.x*r+a,e.y=n.y*o+s,e.width=n.width*r,e.height=n.height*o,e.width<0&&(e.x+=e.width,e.width=-e.width),void(e.height<0&&(e.y+=e.height,e.height=-e.height))}Le.x=Oe.x=n.x,Le.y=Re.y=n.y,Pe.x=Re.x=n.x+n.width,Pe.y=Oe.y=n.y+n.height,Le.transform(i),Re.transform(i),Pe.transform(i),Oe.transform(i),e.x=Ae(Le.x,Pe.x,Oe.x,Re.x),e.y=Ae(Le.y,Pe.y,Oe.y,Re.y);var l=ke(Le.x,Pe.x,Oe.x,Re.x),u=ke(Le.y,Pe.y,Oe.y,Re.y);e.width=l-e.x,e.height=u-e.y}else e!==n&&t.copy(e,n)},t}(),Ve="silent";function Be(){de(this.event)}var Fe=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.handler=null,e}return n(e,t),e.prototype.dispose=function(){},e.prototype.setCursor=function(){},e}(jt),Ge=function(t,e){this.x=t,this.y=e},We=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],He=new ze(0,0,0,0),Ye=function(t){function e(e,n,i,r,o){var a=t.call(this)||this;return a._hovered=new Ge(0,0),a.storage=e,a.painter=n,a.painterRoot=r,a._pointerSize=o,i=i||new Fe,a.proxy=null,a.setHandlerProxy(i),a._draggingMgr=new Zt(a),a}return n(e,t),e.prototype.setHandlerProxy=function(t){this.proxy&&this.proxy.dispose(),t&&(E(We,(function(e){t.on&&t.on(e,this[e],this)}),this),t.handler=this),this.proxy=t},e.prototype.mousemove=function(t){var e=t.zrX,n=t.zrY,i=Ze(this,e,n),r=this._hovered,o=r.target;o&&!o.__zr&&(o=(r=this.findHover(r.x,r.y)).target);var a=this._hovered=i?new Ge(e,n):this.findHover(e,n),s=a.target,l=this.proxy;l.setCursor&&l.setCursor(s?s.cursor:"default"),o&&s!==o&&this.dispatchToElement(r,"mouseout",t),this.dispatchToElement(a,"mousemove",t),s&&s!==o&&this.dispatchToElement(a,"mouseover",t)},e.prototype.mouseout=function(t){var e=t.zrEventControl;"only_globalout"!==e&&this.dispatchToElement(this._hovered,"mouseout",t),"no_globalout"!==e&&this.trigger("globalout",{type:"globalout",event:t})},e.prototype.resize=function(){this._hovered=new Ge(0,0)},e.prototype.dispatch=function(t,e){var n=this[t];n&&n.call(this,e)},e.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},e.prototype.setCursorStyle=function(t){var e=this.proxy;e.setCursor&&e.setCursor(t)},e.prototype.dispatchToElement=function(t,e,n){var i=(t=t||{}).target;if(!i||!i.silent){for(var r="on"+e,o=function(t,e,n){return{type:t,event:n,target:e.target,topTarget:e.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which,stop:Be}}(e,t,n);i&&(i[r]&&(o.cancelBubble=!!i[r].call(i,o)),i.trigger(e,o),i=i.__hostTarget?i.__hostTarget:i.parent,!o.cancelBubble););o.cancelBubble||(this.trigger(e,o),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer((function(t){"function"==typeof t[r]&&t[r].call(t,o),t.trigger&&t.trigger(e,o)})))}},e.prototype.findHover=function(t,e,n){var i=this.storage.getDisplayList(),r=new Ge(t,e);if(Ue(i,r,t,e,n),this._pointerSize&&!r.target){for(var o=[],a=this._pointerSize,s=a/2,l=new ze(t-s,e-s,a,a),u=i.length-1;u>=0;u--){var h=i[u];h===n||h.ignore||h.ignoreCoarsePointer||h.parent&&h.parent.ignoreCoarsePointer||(He.copy(h.getBoundingRect()),h.transform&&He.applyTransform(h.transform),He.intersect(l)&&o.push(h))}if(o.length)for(var c=Math.PI/12,p=2*Math.PI,d=0;d=0;o--){var a=t[o],s=void 0;if(a!==r&&!a.ignore&&(s=Xe(a,n,i))&&(!e.topTarget&&(e.topTarget=a),s!==Ve)){e.target=a;break}}}function Ze(t,e,n){var i=t.painter;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}E(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],(function(t){Ye.prototype[t]=function(e){var n,i,r=e.zrX,o=e.zrY,a=Ze(this,r,o);if("mouseup"===t&&a||(i=(n=this.findHover(r,o)).target),"mousedown"===t)this._downEl=i,this._downPoint=[e.zrX,e.zrY],this._upEl=i;else if("mouseup"===t)this._upEl=i;else if("click"===t){if(this._downEl!==this._upEl||!this._downPoint||Vt(this._downPoint,[e.zrX,e.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,t,e)}}));function je(t,e,n,i){var r=e+1;if(r===n)return 1;if(i(t[r++],t[e])<0){for(;r=0;)r++;return r-e}function qe(t,e,n,i,r){for(i===e&&i++;i>>1])<0?l=o:s=o+1;var u=i-s;switch(u){case 3:t[s+3]=t[s+2];case 2:t[s+2]=t[s+1];case 1:t[s+1]=t[s];break;default:for(;u>0;)t[s+u]=t[s+u-1],u--}t[s]=a}}function Ke(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])>0){for(s=i-r;l0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a>>1);o(t,e[n+h])>0?a=h+1:l=h}return l}function $e(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])<0){for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a>>1);o(t,e[n+h])<0?l=h:a=h+1}return l}function Je(t,e){var n,i,r=7,o=0,a=[];function s(s){var l=n[s],u=i[s],h=n[s+1],c=i[s+1];i[s]=u+c,s===o-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),o--;var p=$e(t[h],t,l,u,0,e);l+=p,0!==(u-=p)&&0!==(c=Ke(t[l+u-1],t,h,c,c-1,e))&&(u<=c?function(n,i,o,s){var l=0;for(l=0;l=7||d>=7);if(f)break;g<0&&(g=0),g+=2}if((r=g)<1&&(r=1),1===i){for(l=0;l=0;l--)t[d+l]=t[p+l];return void(t[c]=a[h])}var f=r;for(;;){var g=0,y=0,v=!1;do{if(e(a[h],t[u])<0){if(t[c--]=t[u--],g++,y=0,0==--i){v=!0;break}}else if(t[c--]=a[h--],y++,g=0,1==--s){v=!0;break}}while((g|y)=0;l--)t[d+l]=t[p+l];if(0===i){v=!0;break}}if(t[c--]=a[h--],1==--s){v=!0;break}if(0!==(y=s-Ke(t[u],a,0,s,s-1,e))){for(s-=y,d=(c-=y)+1,p=(h-=y)+1,l=0;l=7||y>=7);if(v)break;f<0&&(f=0),f+=2}(r=f)<1&&(r=1);if(1===s){for(d=(c-=i)+1,p=(u-=i)+1,l=i-1;l>=0;l--)t[d+l]=t[p+l];t[c]=a[h]}else{if(0===s)throw new Error;for(p=c-(s-1),l=0;l1;){var t=o-2;if(t>=1&&i[t-1]<=i[t]+i[t+1]||t>=2&&i[t-2]<=i[t]+i[t-1])i[t-1]i[t+1])break;s(t)}},forceMergeRuns:function(){for(;o>1;){var t=o-2;t>0&&i[t-1]=32;)e|=1&t,t>>=1;return t+e}(r);do{if((o=je(t,n,i,e))s&&(l=s),qe(t,n,n+l,n+o,e),o=l}a.pushRun(n,o),a.mergeRuns(),r-=o,n+=o}while(0!==r);a.forceMergeRuns()}}}var tn=!1;function en(){tn||(tn=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function nn(t,e){return t.zlevel===e.zlevel?t.z===e.z?t.z2-e.z2:t.z-e.z:t.zlevel-e.zlevel}var rn=function(){function t(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=nn}return t.prototype.traverse=function(t,e){for(var n=0;n0&&(u.__clipPaths=[]),isNaN(u.z)&&(en(),u.z=0),isNaN(u.z2)&&(en(),u.z2=0),isNaN(u.zlevel)&&(en(),u.zlevel=0),this._displayList[this._displayListLen++]=u}var h=t.getDecalElement&&t.getDecalElement();h&&this._updateAndAddDisplayable(h,e,n);var c=t.getTextGuideLine();c&&this._updateAndAddDisplayable(c,e,n);var p=t.getTextContent();p&&this._updateAndAddDisplayable(p,e,n)}},t.prototype.addRoot=function(t){t.__zr&&t.__zr.storage===this||this._roots.push(t)},t.prototype.delRoot=function(t){if(t instanceof Array)for(var e=0,n=t.length;e=0&&this._roots.splice(i,1)}},t.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},t.prototype.getRoots=function(){return this._roots},t.prototype.dispose=function(){this._displayList=null,this._roots=null},t}(),on=r.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){return setTimeout(t,16)},an={linear:function(t){return t},quadraticIn:function(t){return t*t},quadraticOut:function(t){return t*(2-t)},quadraticInOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)},cubicIn:function(t){return t*t*t},cubicOut:function(t){return--t*t*t+1},cubicInOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},quarticIn:function(t){return t*t*t*t},quarticOut:function(t){return 1- --t*t*t*t},quarticInOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},quinticIn:function(t){return t*t*t*t*t},quinticOut:function(t){return--t*t*t*t*t+1},quinticInOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},sinusoidalIn:function(t){return 1-Math.cos(t*Math.PI/2)},sinusoidalOut:function(t){return Math.sin(t*Math.PI/2)},sinusoidalInOut:function(t){return.5*(1-Math.cos(Math.PI*t))},exponentialIn:function(t){return 0===t?0:Math.pow(1024,t-1)},exponentialOut:function(t){return 1===t?1:1-Math.pow(2,-10*t)},exponentialInOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))},circularIn:function(t){return 1-Math.sqrt(1-t*t)},circularOut:function(t){return Math.sqrt(1- --t*t)},circularInOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},elasticIn:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4))},elasticOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/.4)+1)},elasticInOut:function(t){var e,n=.1,i=.4;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=i*Math.asin(1/n)/(2*Math.PI),(t*=2)<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},backIn:function(t){var e=1.70158;return t*t*((e+1)*t-e)},backOut:function(t){var e=1.70158;return--t*t*((e+1)*t+e)+1},backInOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)},bounceIn:function(t){return 1-an.bounceOut(1-t)},bounceOut:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},bounceInOut:function(t){return t<.5?.5*an.bounceIn(2*t):.5*an.bounceOut(2*t-1)+.5}},sn=Math.pow,ln=Math.sqrt,un=1e-8,hn=1e-4,cn=ln(3),pn=1/3,dn=Mt(),fn=Mt(),gn=Mt();function yn(t){return t>-1e-8&&tun||t<-1e-8}function mn(t,e,n,i,r){var o=1-r;return o*o*(o*t+3*r*e)+r*r*(r*i+3*o*n)}function xn(t,e,n,i,r){var o=1-r;return 3*(((e-t)*o+2*(n-e)*r)*o+(i-n)*r*r)}function _n(t,e,n,i,r,o){var a=i+3*(e-n)-t,s=3*(n-2*e+t),l=3*(e-t),u=t-r,h=s*s-3*a*l,c=s*l-9*a*u,p=l*l-3*s*u,d=0;if(yn(h)&&yn(c)){if(yn(s))o[0]=0;else(M=-l/s)>=0&&M<=1&&(o[d++]=M)}else{var f=c*c-4*h*p;if(yn(f)){var g=c/h,y=-g/2;(M=-s/a+g)>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y)}else if(f>0){var v=ln(f),m=h*s+1.5*a*(-c+v),x=h*s+1.5*a*(-c-v);(M=(-s-((m=m<0?-sn(-m,pn):sn(m,pn))+(x=x<0?-sn(-x,pn):sn(x,pn))))/(3*a))>=0&&M<=1&&(o[d++]=M)}else{var _=(2*h*s-3*a*c)/(2*ln(h*h*h)),b=Math.acos(_)/3,w=ln(h),S=Math.cos(b),M=(-s-2*w*S)/(3*a),I=(y=(-s+w*(S+cn*Math.sin(b)))/(3*a),(-s+w*(S-cn*Math.sin(b)))/(3*a));M>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y),I>=0&&I<=1&&(o[d++]=I)}}return d}function bn(t,e,n,i,r){var o=6*n-12*e+6*t,a=9*e+3*i-3*t-9*n,s=3*e-3*t,l=0;if(yn(a)){if(vn(o))(h=-s/o)>=0&&h<=1&&(r[l++]=h)}else{var u=o*o-4*a*s;if(yn(u))r[0]=-o/(2*a);else if(u>0){var h,c=ln(u),p=(-o-c)/(2*a);(h=(-o+c)/(2*a))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}function wn(t,e,n,i,r,o){var a=(e-t)*r+t,s=(n-e)*r+e,l=(i-n)*r+n,u=(s-a)*r+a,h=(l-s)*r+s,c=(h-u)*r+u;o[0]=t,o[1]=a,o[2]=u,o[3]=c,o[4]=c,o[5]=h,o[6]=l,o[7]=i}function Sn(t,e,n,i,r,o,a,s,l,u,h){var c,p,d,f,g,y=.005,v=1/0;dn[0]=l,dn[1]=u;for(var m=0;m<1;m+=.05)fn[0]=mn(t,n,r,a,m),fn[1]=mn(e,i,o,s,m),(f=Ft(dn,fn))=0&&f=0&&y=1?1:_n(0,i,o,1,t,s)&&mn(0,r,a,1,s[0])}}}var On=function(){function t(t){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=t.life||1e3,this._delay=t.delay||0,this.loop=t.loop||!1,this.onframe=t.onframe||bt,this.ondestroy=t.ondestroy||bt,this.onrestart=t.onrestart||bt,t.easing&&this.setEasing(t.easing)}return t.prototype.step=function(t,e){if(this._inited||(this._startTime=t+this._delay,this._inited=!0),!this._paused){var n=this._life,i=t-this._startTime-this._pausedTime,r=i/n;r<0&&(r=0),r=Math.min(r,1);var o=this.easingFunc,a=o?o(r):r;if(this.onframe(a),1===r){if(!this.loop)return!0;var s=i%n;this._startTime=t-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=e},t.prototype.pause=function(){this._paused=!0},t.prototype.resume=function(){this._paused=!1},t.prototype.setEasing=function(t){this.easing=t,this.easingFunc=X(t)?t:an[t]||Pn(t)},t}(),Rn=function(t){this.value=t},Nn=function(){function t(){this._len=0}return t.prototype.insert=function(t){var e=new Rn(t);return this.insertEntry(e),e},t.prototype.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},t.prototype.remove=function(t){var e=t.prev,n=t.next;e?e.next=n:this.head=n,n?n.prev=e:this.tail=e,t.next=t.prev=null,this._len--},t.prototype.len=function(){return this._len},t.prototype.clear=function(){this.head=this.tail=null,this._len=0},t}(),En=function(){function t(t){this._list=new Nn,this._maxSize=10,this._map={},this._maxSize=t}return t.prototype.put=function(t,e){var n=this._list,i=this._map,r=null;if(null==i[t]){var o=n.len(),a=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var s=n.head;n.remove(s),delete i[s.key],r=s.value,this._lastRemovedEntry=s}a?a.value=e:a=new Rn(e),a.key=t,n.insertEntry(a),i[t]=a}return r},t.prototype.get=function(t){var e=this._map[t],n=this._list;if(null!=e)return e!==n.tail&&(n.remove(e),n.insertEntry(e)),e.value},t.prototype.clear=function(){this._list.clear(),this._map={}},t.prototype.len=function(){return this._list.len()},t}(),zn={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function Vn(t){return(t=Math.round(t))<0?0:t>255?255:t}function Bn(t){return t<0?0:t>1?1:t}function Fn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?Vn(parseFloat(e)/100*255):Vn(parseInt(e,10))}function Gn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?Bn(parseFloat(e)/100):Bn(parseFloat(e))}function Wn(t,e,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?t+(e-t)*n*6:2*n<1?e:3*n<2?t+(e-t)*(2/3-n)*6:t}function Hn(t,e,n){return t+(e-t)*n}function Yn(t,e,n,i,r){return t[0]=e,t[1]=n,t[2]=i,t[3]=r,t}function Xn(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}var Un=new En(20),Zn=null;function jn(t,e){Zn&&Xn(Zn,e),Zn=Un.put(t,Zn||e.slice())}function qn(t,e){if(t){e=e||[];var n=Un.get(t);if(n)return Xn(e,n);var i=(t+="").replace(/ /g,"").toLowerCase();if(i in zn)return Xn(e,zn[i]),jn(t,e),e;var r,o=i.length;if("#"===i.charAt(0))return 4===o||5===o?(r=parseInt(i.slice(1,4),16))>=0&&r<=4095?(Yn(e,(3840&r)>>4|(3840&r)>>8,240&r|(240&r)>>4,15&r|(15&r)<<4,5===o?parseInt(i.slice(4),16)/15:1),jn(t,e),e):void Yn(e,0,0,0,1):7===o||9===o?(r=parseInt(i.slice(1,7),16))>=0&&r<=16777215?(Yn(e,(16711680&r)>>16,(65280&r)>>8,255&r,9===o?parseInt(i.slice(7),16)/255:1),jn(t,e),e):void Yn(e,0,0,0,1):void 0;var a=i.indexOf("("),s=i.indexOf(")");if(-1!==a&&s+1===o){var l=i.substr(0,a),u=i.substr(a+1,s-(a+1)).split(","),h=1;switch(l){case"rgba":if(4!==u.length)return 3===u.length?Yn(e,+u[0],+u[1],+u[2],1):Yn(e,0,0,0,1);h=Gn(u.pop());case"rgb":return u.length>=3?(Yn(e,Fn(u[0]),Fn(u[1]),Fn(u[2]),3===u.length?h:Gn(u[3])),jn(t,e),e):void Yn(e,0,0,0,1);case"hsla":return 4!==u.length?void Yn(e,0,0,0,1):(u[3]=Gn(u[3]),Kn(u,e),jn(t,e),e);case"hsl":return 3!==u.length?void Yn(e,0,0,0,1):(Kn(u,e),jn(t,e),e);default:return}}Yn(e,0,0,0,1)}}function Kn(t,e){var n=(parseFloat(t[0])%360+360)%360/360,i=Gn(t[1]),r=Gn(t[2]),o=r<=.5?r*(i+1):r+i-r*i,a=2*r-o;return Yn(e=e||[],Vn(255*Wn(a,o,n+1/3)),Vn(255*Wn(a,o,n)),Vn(255*Wn(a,o,n-1/3)),1),4===t.length&&(e[3]=t[3]),e}function $n(t,e){var n=qn(t);if(n){for(var i=0;i<3;i++)n[i]=e<0?n[i]*(1-e)|0:(255-n[i])*e+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return ri(n,4===n.length?"rgba":"rgb")}}function Jn(t,e,n){if(e&&e.length&&t>=0&&t<=1){n=n||[];var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=e[r],s=e[o],l=i-r;return n[0]=Vn(Hn(a[0],s[0],l)),n[1]=Vn(Hn(a[1],s[1],l)),n[2]=Vn(Hn(a[2],s[2],l)),n[3]=Bn(Hn(a[3],s[3],l)),n}}var Qn=Jn;function ti(t,e,n){if(e&&e.length&&t>=0&&t<=1){var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=qn(e[r]),s=qn(e[o]),l=i-r,u=ri([Vn(Hn(a[0],s[0],l)),Vn(Hn(a[1],s[1],l)),Vn(Hn(a[2],s[2],l)),Bn(Hn(a[3],s[3],l))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:i}:u}}var ei=ti;function ni(t,e,n,i){var r=qn(t);if(t)return r=function(t){if(t){var e,n,i=t[0]/255,r=t[1]/255,o=t[2]/255,a=Math.min(i,r,o),s=Math.max(i,r,o),l=s-a,u=(s+a)/2;if(0===l)e=0,n=0;else{n=u<.5?l/(s+a):l/(2-s-a);var h=((s-i)/6+l/2)/l,c=((s-r)/6+l/2)/l,p=((s-o)/6+l/2)/l;i===s?e=p-c:r===s?e=1/3+h-p:o===s&&(e=2/3+c-h),e<0&&(e+=1),e>1&&(e-=1)}var d=[360*e,n,u];return null!=t[3]&&d.push(t[3]),d}}(r),null!=e&&(r[0]=function(t){return(t=Math.round(t))<0?0:t>360?360:t}(e)),null!=n&&(r[1]=Gn(n)),null!=i&&(r[2]=Gn(i)),ri(Kn(r),"rgba")}function ii(t,e){var n=qn(t);if(n&&null!=e)return n[3]=Bn(e),ri(n,"rgba")}function ri(t,e){if(t&&t.length){var n=t[0]+","+t[1]+","+t[2];return"rgba"!==e&&"hsva"!==e&&"hsla"!==e||(n+=","+t[3]),e+"("+n+")"}}function oi(t,e){var n=qn(t);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*e:0}var ai=new En(100);function si(t){if(U(t)){var e=ai.get(t);return e||(e=$n(t,-.1),ai.put(t,e)),e}if(Q(t)){var n=A({},t);return n.colorStops=z(t.colorStops,(function(t){return{offset:t.offset,color:$n(t.color,-.1)}})),n}return t}var li=Object.freeze({__proto__:null,parse:qn,lift:$n,toHex:function(t){var e=qn(t);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)},fastLerp:Jn,fastMapToColor:Qn,lerp:ti,mapToColor:ei,modifyHSL:ni,modifyAlpha:ii,stringify:ri,lum:oi,random:function(){return ri([Math.round(255*Math.random()),Math.round(255*Math.random()),Math.round(255*Math.random())],"rgb")},liftColor:si}),ui=Math.round;function hi(t){var e;if(t&&"transparent"!==t){if("string"==typeof t&&t.indexOf("rgba")>-1){var n=qn(t);n&&(t="rgb("+n[0]+","+n[1]+","+n[2]+")",e=n[3])}}else t="none";return{color:t,opacity:null==e?1:e}}var ci=1e-4;function pi(t){return t-1e-4}function di(t){return ui(1e3*t)/1e3}function fi(t){return ui(1e4*t)/1e4}var gi={left:"start",right:"end",center:"middle",middle:"middle"};function yi(t){return t&&!!t.image}function vi(t){return yi(t)||function(t){return t&&!!t.svgElement}(t)}function mi(t){return"linear"===t.type}function xi(t){return"radial"===t.type}function _i(t){return t&&("linear"===t.type||"radial"===t.type)}function bi(t){return"url(#"+t+")"}function wi(t){var e=t.getGlobalScale(),n=Math.max(e[0],e[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function Si(t){var e=t.x||0,n=t.y||0,i=(t.rotation||0)*wt,r=rt(t.scaleX,1),o=rt(t.scaleY,1),a=t.skewX||0,s=t.skewY||0,l=[];return(e||n)&&l.push("translate("+e+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===r&&1===o||l.push("scale("+r+","+o+")"),(a||s)&&l.push("skew("+ui(a*wt)+"deg, "+ui(s*wt)+"deg)"),l.join(" ")}var Mi=r.hasGlobalWindow&&X(window.btoa)?function(t){return window.btoa(unescape(encodeURIComponent(t)))}:"undefined"!=typeof Buffer?function(t){return Buffer.from(t).toString("base64")}:function(t){return null},Ii=Array.prototype.slice;function Ti(t,e,n){return(e-t)*n+t}function Ci(t,e,n,i){for(var r=e.length,o=0;oi?e:t,o=Math.min(n,i),a=r[o-1]||{color:[0,0,0,0],offset:0},s=o;sa)i.length=a;else for(var s=o;s=1},t.prototype.getAdditiveTrack=function(){return this._additiveTrack},t.prototype.addKeyframe=function(t,e,n){this._needsSort=!0;var i=this.keyframes,r=i.length,o=!1,a=6,s=e;if(N(e)){var l=function(t){return N(t&&t[0])?2:1}(e);a=l,(1===l&&!j(e[0])||2===l&&!j(e[0][0]))&&(o=!0)}else if(j(e)&&!nt(e))a=0;else if(U(e))if(isNaN(+e)){var u=qn(e);u&&(s=u,a=3)}else a=0;else if(Q(e)){var h=A({},s);h.colorStops=z(e.colorStops,(function(t){return{offset:t.offset,color:qn(t.color)}})),mi(e)?a=4:xi(e)&&(a=5),s=h}0===r?this.valType=a:a===this.valType&&6!==a||(o=!0),this.discrete=this.discrete||o;var c={time:t,value:s,rawValue:e,percent:0};return n&&(c.easing=n,c.easingFunc=X(n)?n:an[n]||Pn(n)),i.push(c),c},t.prototype.prepare=function(t,e){var n=this.keyframes;this._needsSort&&n.sort((function(t,e){return t.time-e.time}));for(var i=this.valType,r=n.length,o=n[r-1],a=this.discrete,s=Ni(i),l=Ri(i),u=0;u=0&&!(l[n].percent<=e);n--);n=d(n,u-2)}else{for(n=p;ne);n++);n=d(n-1,u-2)}r=l[n+1],i=l[n]}if(i&&r){this._lastFr=n,this._lastFrP=e;var f=r.percent-i.percent,g=0===f?1:d((e-i.percent)/f,1);r.easingFunc&&(g=r.easingFunc(g));var y=o?this._additiveValue:c?Ei:t[h];if(!Ni(s)&&!c||y||(y=this._additiveValue=[]),this.discrete)t[h]=g<1?i.rawValue:r.rawValue;else if(Ni(s))1===s?Ci(y,i[a],r[a],g):function(t,e,n,i){for(var r=e.length,o=r&&e[0].length,a=0;a0&&s.addKeyframe(0,Pi(l),i),this._trackKeys.push(a)}s.addKeyframe(t,Pi(e[a]),i)}return this._maxTime=Math.max(this._maxTime,t),this},t.prototype.pause=function(){this._clip.pause(),this._paused=!0},t.prototype.resume=function(){this._clip.resume(),this._paused=!1},t.prototype.isPaused=function(){return!!this._paused},t.prototype.duration=function(t){return this._maxTime=t,this._force=!0,this},t.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var t=this._doneCbs;if(t)for(var e=t.length,n=0;n0)){this._started=1;for(var e=this,n=[],i=this._maxTime||0,r=0;r1){var a=o.pop();r.addKeyframe(a.time,t[i]),r.prepare(this._maxTime,r.getAdditiveTrack())}}}},t}();function Bi(){return(new Date).getTime()}var Fi,Gi,Wi=function(t){function e(e){var n=t.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,e=e||{},n.stage=e.stage||{},n}return n(e,t),e.prototype.addClip=function(t){t.animation&&this.removeClip(t),this._head?(this._tail.next=t,t.prev=this._tail,t.next=null,this._tail=t):this._head=this._tail=t,t.animation=this},e.prototype.addAnimator=function(t){t.animation=this;var e=t.getClip();e&&this.addClip(e)},e.prototype.removeClip=function(t){if(t.animation){var e=t.prev,n=t.next;e?e.next=n:this._head=n,n?n.prev=e:this._tail=e,t.next=t.prev=t.animation=null}},e.prototype.removeAnimator=function(t){var e=t.getClip();e&&this.removeClip(e),t.animation=null},e.prototype.update=function(t){for(var e=Bi()-this._pausedTime,n=e-this._time,i=this._head;i;){var r=i.next;i.step(e,n)?(i.ondestroy(),this.removeClip(i),i=r):i=r}this._time=e,t||(this.trigger("frame",n),this.stage.update&&this.stage.update())},e.prototype._startLoop=function(){var t=this;this._running=!0,on((function e(){t._running&&(on(e),!t._paused&&t.update())}))},e.prototype.start=function(){this._running||(this._time=Bi(),this._pausedTime=0,this._startLoop())},e.prototype.stop=function(){this._running=!1},e.prototype.pause=function(){this._paused||(this._pauseStart=Bi(),this._paused=!0)},e.prototype.resume=function(){this._paused&&(this._pausedTime+=Bi()-this._pauseStart,this._paused=!1)},e.prototype.clear=function(){for(var t=this._head;t;){var e=t.next;t.prev=t.next=t.animation=null,t=e}this._head=this._tail=null},e.prototype.isFinished=function(){return null==this._head},e.prototype.animate=function(t,e){e=e||{},this.start();var n=new Vi(t,e.loop);return this.addAnimator(n),n},e}(jt),Hi=r.domSupported,Yi=(Gi={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:Fi=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:z(Fi,(function(t){var e=t.replace("mouse","pointer");return Gi.hasOwnProperty(e)?e:t}))}),Xi=["mousemove","mouseup"],Ui=["pointermove","pointerup"],Zi=!1;function ji(t){var e=t.pointerType;return"pen"===e||"touch"===e}function qi(t){t&&(t.zrByTouch=!0)}function Ki(t,e){for(var n=e,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==e&&n===t.painterRoot);)n=n.parentNode;return i}var $i=function(t,e){this.stopPropagation=bt,this.stopImmediatePropagation=bt,this.preventDefault=bt,this.type=e.type,this.target=this.currentTarget=t.dom,this.pointerType=e.pointerType,this.clientX=e.clientX,this.clientY=e.clientY},Ji={mousedown:function(t){t=ce(this.dom,t),this.__mayPointerCapture=[t.zrX,t.zrY],this.trigger("mousedown",t)},mousemove:function(t){t=ce(this.dom,t);var e=this.__mayPointerCapture;!e||t.zrX===e[0]&&t.zrY===e[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",t)},mouseup:function(t){t=ce(this.dom,t),this.__togglePointerCapture(!1),this.trigger("mouseup",t)},mouseout:function(t){Ki(this,(t=ce(this.dom,t)).toElement||t.relatedTarget)||(this.__pointerCapturing&&(t.zrEventControl="no_globalout"),this.trigger("mouseout",t))},wheel:function(t){Zi=!0,t=ce(this.dom,t),this.trigger("mousewheel",t)},mousewheel:function(t){Zi||(t=ce(this.dom,t),this.trigger("mousewheel",t))},touchstart:function(t){qi(t=ce(this.dom,t)),this.__lastTouchMoment=new Date,this.handler.processGesture(t,"start"),Ji.mousemove.call(this,t),Ji.mousedown.call(this,t)},touchmove:function(t){qi(t=ce(this.dom,t)),this.handler.processGesture(t,"change"),Ji.mousemove.call(this,t)},touchend:function(t){qi(t=ce(this.dom,t)),this.handler.processGesture(t,"end"),Ji.mouseup.call(this,t),+new Date-+this.__lastTouchMoment<300&&Ji.click.call(this,t)},pointerdown:function(t){Ji.mousedown.call(this,t)},pointermove:function(t){ji(t)||Ji.mousemove.call(this,t)},pointerup:function(t){Ji.mouseup.call(this,t)},pointerout:function(t){ji(t)||Ji.mouseout.call(this,t)}};E(["click","dblclick","contextmenu"],(function(t){Ji[t]=function(e){e=ce(this.dom,e),this.trigger(t,e)}}));var Qi={pointermove:function(t){ji(t)||Qi.mousemove.call(this,t)},pointerup:function(t){Qi.mouseup.call(this,t)},mousemove:function(t){this.trigger("mousemove",t)},mouseup:function(t){var e=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",t),e&&(t.zrEventControl="only_globalout",this.trigger("mouseout",t))}};function tr(t,e){var n=e.domHandlers;r.pointerEventsSupported?E(Yi.pointer,(function(i){nr(e,i,(function(e){n[i].call(t,e)}))})):(r.touchEventsSupported&&E(Yi.touch,(function(i){nr(e,i,(function(r){n[i].call(t,r),function(t){t.touching=!0,null!=t.touchTimer&&(clearTimeout(t.touchTimer),t.touchTimer=null),t.touchTimer=setTimeout((function(){t.touching=!1,t.touchTimer=null}),700)}(e)}))})),E(Yi.mouse,(function(i){nr(e,i,(function(r){r=he(r),e.touching||n[i].call(t,r)}))})))}function er(t,e){function n(n){nr(e,n,(function(i){i=he(i),Ki(t,i.target)||(i=function(t,e){return ce(t.dom,new $i(t,e),!0)}(t,i),e.domHandlers[n].call(t,i))}),{capture:!0})}r.pointerEventsSupported?E(Ui,n):r.touchEventsSupported||E(Xi,n)}function nr(t,e,n,i){t.mounted[e]=n,t.listenerOpts[e]=i,pe(t.domTarget,e,n,i)}function ir(t){var e,n,i,r,o=t.mounted;for(var a in o)o.hasOwnProperty(a)&&(e=t.domTarget,n=a,i=o[a],r=t.listenerOpts[a],e.removeEventListener(n,i,r));t.mounted={}}var rr=function(t,e){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=t,this.domHandlers=e},or=function(t){function e(e,n){var i=t.call(this)||this;return i.__pointerCapturing=!1,i.dom=e,i.painterRoot=n,i._localHandlerScope=new rr(e,Ji),Hi&&(i._globalHandlerScope=new rr(document,Qi)),tr(i,i._localHandlerScope),i}return n(e,t),e.prototype.dispose=function(){ir(this._localHandlerScope),Hi&&ir(this._globalHandlerScope)},e.prototype.setCursor=function(t){this.dom.style&&(this.dom.style.cursor=t||"default")},e.prototype.__togglePointerCapture=function(t){if(this.__mayPointerCapture=null,Hi&&+this.__pointerCapturing^+t){this.__pointerCapturing=t;var e=this._globalHandlerScope;t?er(this,e):ir(e)}},e}(jt),ar=1;r.hasGlobalWindow&&(ar=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var sr=ar,lr="#333",ur="#ccc",hr=xe,cr=5e-5;function pr(t){return t>cr||t<-5e-5}var dr=[],fr=[],gr=[1,0,0,1,0,0],yr=Math.abs,vr=function(){function t(){}return t.prototype.getLocalTransform=function(e){return t.getLocalTransform(this,e)},t.prototype.setPosition=function(t){this.x=t[0],this.y=t[1]},t.prototype.setScale=function(t){this.scaleX=t[0],this.scaleY=t[1]},t.prototype.setSkew=function(t){this.skewX=t[0],this.skewY=t[1]},t.prototype.setOrigin=function(t){this.originX=t[0],this.originY=t[1]},t.prototype.needLocalTransform=function(){return pr(this.rotation)||pr(this.x)||pr(this.y)||pr(this.scaleX-1)||pr(this.scaleY-1)||pr(this.skewX)||pr(this.skewY)},t.prototype.updateTransform=function(){var t=this.parent&&this.parent.transform,e=this.needLocalTransform(),n=this.transform;e||t?(n=n||[1,0,0,1,0,0],e?this.getLocalTransform(n):hr(n),t&&(e?be(n,t,n):_e(n,t)),this.transform=n,this._resolveGlobalScaleRatio(n)):n&&(hr(n),this.invTransform=null)},t.prototype._resolveGlobalScaleRatio=function(t){var e=this.globalScaleRatio;if(null!=e&&1!==e){this.getGlobalScale(dr);var n=dr[0]<0?-1:1,i=dr[1]<0?-1:1,r=((dr[0]-n)*e+n)/dr[0]||0,o=((dr[1]-i)*e+i)/dr[1]||0;t[0]*=r,t[1]*=r,t[2]*=o,t[3]*=o}this.invTransform=this.invTransform||[1,0,0,1,0,0],Ie(this.invTransform,t)},t.prototype.getComputedTransform=function(){for(var t=this,e=[];t;)e.push(t),t=t.parent;for(;t=e.pop();)t.updateTransform();return this.transform},t.prototype.setLocalTransform=function(t){if(t){var e=t[0]*t[0]+t[1]*t[1],n=t[2]*t[2]+t[3]*t[3],i=Math.atan2(t[1],t[0]),r=Math.PI/2+i-Math.atan2(t[3],t[2]);n=Math.sqrt(n)*Math.cos(r),e=Math.sqrt(e),this.skewX=r,this.skewY=0,this.rotation=-i,this.x=+t[4],this.y=+t[5],this.scaleX=e,this.scaleY=n,this.originX=0,this.originY=0}},t.prototype.decomposeTransform=function(){if(this.transform){var t=this.parent,e=this.transform;t&&t.transform&&(t.invTransform=t.invTransform||[1,0,0,1,0,0],be(fr,t.invTransform,e),e=fr);var n=this.originX,i=this.originY;(n||i)&&(gr[4]=n,gr[5]=i,be(fr,e,gr),fr[4]-=n,fr[5]-=i,e=fr),this.setLocalTransform(e)}},t.prototype.getGlobalScale=function(t){var e=this.transform;return t=t||[],e?(t[0]=Math.sqrt(e[0]*e[0]+e[1]*e[1]),t[1]=Math.sqrt(e[2]*e[2]+e[3]*e[3]),e[0]<0&&(t[0]=-t[0]),e[3]<0&&(t[1]=-t[1]),t):(t[0]=1,t[1]=1,t)},t.prototype.transformCoordToLocal=function(t,e){var n=[t,e],i=this.invTransform;return i&&Wt(n,n,i),n},t.prototype.transformCoordToGlobal=function(t,e){var n=[t,e],i=this.transform;return i&&Wt(n,n,i),n},t.prototype.getLineScale=function(){var t=this.transform;return t&&yr(t[0]-1)>1e-10&&yr(t[3]-1)>1e-10?Math.sqrt(yr(t[0]*t[3]-t[2]*t[1])):1},t.prototype.copyTransform=function(t){xr(this,t)},t.getLocalTransform=function(t,e){e=e||[];var n=t.originX||0,i=t.originY||0,r=t.scaleX,o=t.scaleY,a=t.anchorX,s=t.anchorY,l=t.rotation||0,u=t.x,h=t.y,c=t.skewX?Math.tan(t.skewX):0,p=t.skewY?Math.tan(-t.skewY):0;if(n||i||a||s){var d=n+a,f=i+s;e[4]=-d*r-c*f*o,e[5]=-f*o-p*d*r}else e[4]=e[5]=0;return e[0]=r,e[3]=o,e[1]=p*r,e[2]=c*o,l&&Se(e,e,l),e[4]+=n+u,e[5]+=i+h,e},t.initDefaultProps=function(){var e=t.prototype;e.scaleX=e.scaleY=e.globalScaleRatio=1,e.x=e.y=e.originX=e.originY=e.skewX=e.skewY=e.rotation=e.anchorX=e.anchorY=0}(),t}(),mr=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function xr(t,e){for(var n=0;n=0?parseFloat(t)/100*e:parseFloat(t):t}function Dr(t,e,n){var i=e.position||"inside",r=null!=e.distance?e.distance:5,o=n.height,a=n.width,s=o/2,l=n.x,u=n.y,h="left",c="top";if(i instanceof Array)l+=Cr(i[0],n.width),u+=Cr(i[1],n.height),h=null,c=null;else switch(i){case"left":l-=r,u+=s,h="right",c="middle";break;case"right":l+=r+a,u+=s,c="middle";break;case"top":l+=a/2,u-=r,h="center",c="bottom";break;case"bottom":l+=a/2,u+=o+r,h="center";break;case"inside":l+=a/2,u+=s,h="center",c="middle";break;case"insideLeft":l+=r,u+=s,c="middle";break;case"insideRight":l+=a-r,u+=s,h="right",c="middle";break;case"insideTop":l+=a/2,u+=r,h="center";break;case"insideBottom":l+=a/2,u+=o-r,h="center",c="bottom";break;case"insideTopLeft":l+=r,u+=r;break;case"insideTopRight":l+=a-r,u+=r,h="right";break;case"insideBottomLeft":l+=r,u+=o-r,c="bottom";break;case"insideBottomRight":l+=a-r,u+=o-r,h="right",c="bottom"}return(t=t||{}).x=l,t.y=u,t.align=h,t.verticalAlign=c,t}var Ar="__zr_normal__",kr=mr.concat(["ignore"]),Lr=V(mr,(function(t,e){return t[e]=!0,t}),{ignore:!1}),Pr={},Or=new ze(0,0,0,0),Rr=function(){function t(t){this.id=M(),this.animators=[],this.currentStates=[],this.states={},this._init(t)}return t.prototype._init=function(t){this.attr(t)},t.prototype.drift=function(t,e,n){switch(this.draggable){case"horizontal":e=0;break;case"vertical":t=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=t,i[5]+=e,this.decomposeTransform(),this.markRedraw()},t.prototype.beforeUpdate=function(){},t.prototype.afterUpdate=function(){},t.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},t.prototype.updateInnerText=function(t){var e=this._textContent;if(e&&(!e.ignore||t)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,r=e.innerTransformable,o=void 0,a=void 0,s=!1;r.parent=i?this:null;var l=!1;if(r.copyTransform(e),null!=n.position){var u=Or;n.layoutRect?u.copy(n.layoutRect):u.copy(this.getBoundingRect()),i||u.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(Pr,n,u):Dr(Pr,n,u),r.x=Pr.x,r.y=Pr.y,o=Pr.align,a=Pr.verticalAlign;var h=n.origin;if(h&&null!=n.rotation){var c=void 0,p=void 0;"center"===h?(c=.5*u.width,p=.5*u.height):(c=Cr(h[0],u.width),p=Cr(h[1],u.height)),l=!0,r.originX=-r.x+c+(i?0:u.x),r.originY=-r.y+p+(i?0:u.y)}}null!=n.rotation&&(r.rotation=n.rotation);var d=n.offset;d&&(r.x+=d[0],r.y+=d[1],l||(r.originX=-d[0],r.originY=-d[1]));var f=null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),y=void 0,v=void 0,m=void 0;f&&this.canBeInsideText()?(y=n.insideFill,v=n.insideStroke,null!=y&&"auto"!==y||(y=this.getInsideTextFill()),null!=v&&"auto"!==v||(v=this.getInsideTextStroke(y),m=!0)):(y=n.outsideFill,v=n.outsideStroke,null!=y&&"auto"!==y||(y=this.getOutsideFill()),null!=v&&"auto"!==v||(v=this.getOutsideStroke(y),m=!0)),(y=y||"#000")===g.fill&&v===g.stroke&&m===g.autoStroke&&o===g.align&&a===g.verticalAlign||(s=!0,g.fill=y,g.stroke=v,g.autoStroke=m,g.align=o,g.verticalAlign=a,e.setDefaultTextStyle(g)),e.__dirty|=1,s&&e.dirtyStyle(!0)}},t.prototype.canBeInsideText=function(){return!0},t.prototype.getInsideTextFill=function(){return"#fff"},t.prototype.getInsideTextStroke=function(t){return"#000"},t.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?ur:lr},t.prototype.getOutsideStroke=function(t){var e=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof e&&qn(e);n||(n=[255,255,255,1]);for(var i=n[3],r=this.__zr.isDarkMode(),o=0;o<3;o++)n[o]=n[o]*i+(r?0:255)*(1-i);return n[3]=1,ri(n,"rgba")},t.prototype.traverse=function(t,e){},t.prototype.attrKV=function(t,e){"textConfig"===t?this.setTextConfig(e):"textContent"===t?this.setTextContent(e):"clipPath"===t?this.setClipPath(e):"extra"===t?(this.extra=this.extra||{},A(this.extra,e)):this[t]=e},t.prototype.hide=function(){this.ignore=!0,this.markRedraw()},t.prototype.show=function(){this.ignore=!1,this.markRedraw()},t.prototype.attr=function(t,e){if("string"==typeof t)this.attrKV(t,e);else if(q(t))for(var n=G(t),i=0;i0},t.prototype.getState=function(t){return this.states[t]},t.prototype.ensureState=function(t){var e=this.states;return e[t]||(e[t]={}),e[t]},t.prototype.clearStates=function(t){this.useState(Ar,!1,t)},t.prototype.useState=function(t,e,n,i){var r=t===Ar;if(this.hasState()||!r){var o=this.currentStates,a=this.stateTransition;if(!(P(o,t)>=0)||!e&&1!==o.length){var s;if(this.stateProxy&&!r&&(s=this.stateProxy(t)),s||(s=this.states&&this.states[t]),s||r){r||this.saveCurrentToNormalState(s);var l=!!(s&&s.hoverLayer||i);l&&this._toggleHoverLayerFlag(!0),this._applyStateObj(t,s,this._normalState,e,!n&&!this.__inHover&&a&&a.duration>0,a);var u=this._textContent,h=this._textGuide;return u&&u.useState(t,e,n,l),h&&h.useState(t,e,n,l),r?(this.currentStates=[],this._normalState={}):e?this.currentStates.push(t):this.currentStates=[t],this._updateAnimationTargets(),this.markRedraw(),!l&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2),s}I("State "+t+" not exists.")}}},t.prototype.useStates=function(t,e,n){if(t.length){var i=[],r=this.currentStates,o=t.length,a=o===r.length;if(a)for(var s=0;s0,d);var f=this._textContent,g=this._textGuide;f&&f.useStates(t,e,c),g&&g.useStates(t,e,c),this._updateAnimationTargets(),this.currentStates=t.slice(),this.markRedraw(),!c&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2)}else this.clearStates()},t.prototype.isSilent=function(){for(var t=this.silent,e=this.parent;!t&&e;){if(e.silent){t=!0;break}e=e.parent}return t},t.prototype._updateAnimationTargets=function(){for(var t=0;t=0){var n=this.currentStates.slice();n.splice(e,1),this.useStates(n)}},t.prototype.replaceState=function(t,e,n){var i=this.currentStates.slice(),r=P(i,t),o=P(i,e)>=0;r>=0?o?i.splice(r,1):i[r]=e:n&&!o&&i.push(e),this.useStates(i)},t.prototype.toggleState=function(t,e){e?this.useState(t,!0):this.removeState(t)},t.prototype._mergeStates=function(t){for(var e,n={},i=0;i=0&&e.splice(n,1)})),this.animators.push(t),n&&n.animation.addAnimator(t),n&&n.wakeUp()},t.prototype.updateDuringAnimation=function(t){this.markRedraw()},t.prototype.stopAnimation=function(t,e){for(var n=this.animators,i=n.length,r=[],o=0;o0&&n.during&&o[0].during((function(t,e){n.during(e)}));for(var p=0;p0||r.force&&!a.length){var w,S=void 0,M=void 0,I=void 0;if(s){M={},p&&(S={});for(_=0;_=0&&(n.splice(i,0,t),this._doAdd(t))}return this},e.prototype.replace=function(t,e){var n=P(this._children,t);return n>=0&&this.replaceAt(e,n),this},e.prototype.replaceAt=function(t,e){var n=this._children,i=n[e];if(t&&t!==this&&t.parent!==this&&t!==i){n[e]=t,i.parent=null;var r=this.__zr;r&&i.removeSelfFromZr(r),this._doAdd(t)}return this},e.prototype._doAdd=function(t){t.parent&&t.parent.remove(t),t.parent=this;var e=this.__zr;e&&e!==t.__zr&&t.addSelfToZr(e),e&&e.refresh()},e.prototype.remove=function(t){var e=this.__zr,n=this._children,i=P(n,t);return i<0||(n.splice(i,1),t.parent=null,e&&t.removeSelfFromZr(e),e&&e.refresh()),this},e.prototype.removeAll=function(){for(var t=this._children,e=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},t.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._disposed||(this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover())},t.prototype.resize=function(t){this._disposed||(t=t||{},this.painter.resize(t.width,t.height),this.handler.resize())},t.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},t.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},t.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},t.prototype.setCursorStyle=function(t){this._disposed||this.handler.setCursorStyle(t)},t.prototype.findHover=function(t,e){if(!this._disposed)return this.handler.findHover(t,e)},t.prototype.on=function(t,e,n){return this._disposed||this.handler.on(t,e,n),this},t.prototype.off=function(t,e){this._disposed||this.handler.off(t,e)},t.prototype.trigger=function(t,e){this._disposed||this.handler.trigger(t,e)},t.prototype.clear=function(){if(!this._disposed){for(var t=this.storage.getRoots(),e=0;e0){if(t<=r)return a;if(t>=o)return s}else{if(t>=r)return a;if(t<=o)return s}else{if(t===r)return a;if(t===o)return s}return(t-r)/l*u+a}function $r(t,e){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return U(t)?(n=t,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(t)/100*e:parseFloat(t):null==t?NaN:+t;var n}function Jr(t,e,n){return null==e&&(e=10),e=Math.min(Math.max(0,e),20),t=(+t).toFixed(e),n?t:+t}function Qr(t){return t.sort((function(t,e){return t-e})),t}function to(t){if(t=+t,isNaN(t))return 0;if(t>1e-14)for(var e=1,n=0;n<15;n++,e*=10)if(Math.round(t*e)/e===t)return n;return eo(t)}function eo(t){var e=t.toString().toLowerCase(),n=e.indexOf("e"),i=n>0?+e.slice(n+1):0,r=n>0?n:e.length,o=e.indexOf("."),a=o<0?0:r-1-o;return Math.max(0,a-i)}function no(t,e){var n=Math.log,i=Math.LN10,r=Math.floor(n(t[1]-t[0])/i),o=Math.round(n(Math.abs(e[1]-e[0]))/i),a=Math.min(Math.max(-r+o,0),20);return isFinite(a)?a:20}function io(t,e){var n=V(t,(function(t,e){return t+(isNaN(e)?0:e)}),0);if(0===n)return[];for(var i=Math.pow(10,e),r=z(t,(function(t){return(isNaN(t)?0:t)/n*i*100})),o=100*i,a=z(r,(function(t){return Math.floor(t)})),s=V(a,(function(t,e){return t+e}),0),l=z(r,(function(t,e){return t-a[e]}));su&&(u=l[c],h=c);++a[h],l[h]=0,++s}return z(a,(function(t){return t/i}))}function ro(t,e){var n=Math.max(to(t),to(e)),i=t+e;return n>20?i:Jr(i,n)}var oo=9007199254740991;function ao(t){var e=2*Math.PI;return(t%e+e)%e}function so(t){return t>-1e-4&&t=10&&e++,e}function po(t,e){var n=co(t),i=Math.pow(10,n),r=t/i;return t=(e?r<1.5?1:r<2.5?2:r<4?3:r<7?5:10:r<1?1:r<2?2:r<3?3:r<5?5:10)*i,n>=-20?+t.toFixed(n<0?-n:0):t}function fo(t,e){var n=(t.length-1)*e+1,i=Math.floor(n),r=+t[i-1],o=n-i;return o?r+o*(t[i]-r):r}function go(t){t.sort((function(t,e){return s(t,e,0)?-1:1}));for(var e=-1/0,n=1,i=0;i=0||r&&P(r,s)<0)){var l=n.getShallow(s,e);null!=l&&(o[t[a][0]]=l)}}return o}}var ra=ia([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),oa=function(){function t(){}return t.prototype.getAreaStyle=function(t,e){return ra(this,t,e)},t}(),aa=new En(50);function sa(t){if("string"==typeof t){var e=aa.get(t);return e&&e.image}return t}function la(t,e,n,i,r){if(t){if("string"==typeof t){if(e&&e.__zrImageSrc===t||!n)return e;var o=aa.get(t),a={hostEl:n,cb:i,cbPayload:r};return o?!ha(e=o.image)&&o.pending.push(a):((e=h.loadImage(t,ua,ua)).__zrImageSrc=t,aa.put(t,e.__cachedImgObj={image:e,pending:[a]})),e}return t}return e}function ua(){var t=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var e=0;e=a;l++)s-=a;var u=br(n,e);return u>s&&(n="",u=0),s=t-u,r.ellipsis=n,r.ellipsisWidth=u,r.contentWidth=s,r.containerWidth=t,r}function fa(t,e){var n=e.containerWidth,i=e.font,r=e.contentWidth;if(!n)return"";var o=br(t,i);if(o<=n)return t;for(var a=0;;a++){if(o<=r||a>=e.maxIterations){t+=e.ellipsis;break}var s=0===a?ga(t,r,e.ascCharWidth,e.cnCharWidth):o>0?Math.floor(t.length*r/o):0;o=br(t=t.substr(0,s),i)}return""===t&&(t=e.placeholder),t}function ga(t,e,n,i){for(var r=0,o=0,a=t.length;o0&&f+i.accumWidth>i.width&&(o=e.split("\n"),c=!0),i.accumWidth=f}else{var g=wa(e,h,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+d,a=g.linesWidths,o=g.lines}}else o=e.split("\n");for(var y=0;y=32&&e<=591||e>=880&&e<=4351||e>=4608&&e<=5119||e>=7680&&e<=8303}(t)||!!_a[t]}function wa(t,e,n,i,r){for(var o=[],a=[],s="",l="",u=0,h=0,c=0;cn:r+h+d>n)?h?(s||l)&&(f?(s||(s=l,l="",h=u=0),o.push(s),a.push(h-u),l+=p,s="",h=u+=d):(l&&(s+=l,l="",u=0),o.push(s),a.push(h),s=p,h=d)):f?(o.push(l),a.push(u),l=p,u=d):(o.push(p),a.push(d)):(h+=d,f?(l+=p,u+=d):(l&&(s+=l,l="",u=0),s+=p))}else l&&(s+=l,h+=u),o.push(s),a.push(h),s="",l="",u=0,h=0}return o.length||s||(s=t,l="",u=0),l&&(s+=l),s&&(o.push(s),a.push(h)),1===o.length&&(h+=r),{accumWidth:h,lines:o,linesWidths:a}}var Sa="__zr_style_"+Math.round(10*Math.random()),Ma={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},Ia={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};Ma[Sa]=!0;var Ta=["z","z2","invisible"],Ca=["invisible"],Da=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype._init=function(e){for(var n=G(e),i=0;i1e-4)return s[0]=t-n,s[1]=e-i,l[0]=t+n,void(l[1]=e+i);if(Ea[0]=Ra(r)*n+t,Ea[1]=Oa(r)*i+e,za[0]=Ra(o)*n+t,za[1]=Oa(o)*i+e,u(s,Ea,za),h(l,Ea,za),(r%=Na)<0&&(r+=Na),(o%=Na)<0&&(o+=Na),r>o&&!a?o+=Na:rr&&(Va[0]=Ra(d)*n+t,Va[1]=Oa(d)*i+e,u(s,Va,s),h(l,Va,l))}var Ua={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},Za=[],ja=[],qa=[],Ka=[],$a=[],Ja=[],Qa=Math.min,ts=Math.max,es=Math.cos,ns=Math.sin,is=Math.abs,rs=Math.PI,os=2*rs,as="undefined"!=typeof Float32Array,ss=[];function ls(t){return Math.round(t/rs*1e8)/1e8%2*rs}function us(t,e){var n=ls(t[0]);n<0&&(n+=os);var i=n-t[0],r=t[1];r+=i,!e&&r-n>=os?r=n+os:e&&n-r>=os?r=n-os:!e&&n>r?r=n+(os-ls(n-r)):e&&n0&&(this._ux=is(n/sr/t)||0,this._uy=is(n/sr/e)||0)},t.prototype.setDPR=function(t){this.dpr=t},t.prototype.setContext=function(t){this._ctx=t},t.prototype.getContext=function(){return this._ctx},t.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},t.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},t.prototype.moveTo=function(t,e){return this._drawPendingPt(),this.addData(Ua.M,t,e),this._ctx&&this._ctx.moveTo(t,e),this._x0=t,this._y0=e,this._xi=t,this._yi=e,this},t.prototype.lineTo=function(t,e){var n=is(t-this._xi),i=is(e-this._yi),r=n>this._ux||i>this._uy;if(this.addData(Ua.L,t,e),this._ctx&&r&&this._ctx.lineTo(t,e),r)this._xi=t,this._yi=e,this._pendingPtDist=0;else{var o=n*n+i*i;o>this._pendingPtDist&&(this._pendingPtX=t,this._pendingPtY=e,this._pendingPtDist=o)}return this},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){return this._drawPendingPt(),this.addData(Ua.C,t,e,n,i,r,o),this._ctx&&this._ctx.bezierCurveTo(t,e,n,i,r,o),this._xi=r,this._yi=o,this},t.prototype.quadraticCurveTo=function(t,e,n,i){return this._drawPendingPt(),this.addData(Ua.Q,t,e,n,i),this._ctx&&this._ctx.quadraticCurveTo(t,e,n,i),this._xi=n,this._yi=i,this},t.prototype.arc=function(t,e,n,i,r,o){this._drawPendingPt(),ss[0]=i,ss[1]=r,us(ss,o),i=ss[0];var a=(r=ss[1])-i;return this.addData(Ua.A,t,e,n,n,i,a,0,o?0:1),this._ctx&&this._ctx.arc(t,e,n,i,r,o),this._xi=es(r)*n+t,this._yi=ns(r)*n+e,this},t.prototype.arcTo=function(t,e,n,i,r){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(t,e,n,i,r),this},t.prototype.rect=function(t,e,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(t,e,n,i),this.addData(Ua.R,t,e,n,i),this},t.prototype.closePath=function(){this._drawPendingPt(),this.addData(Ua.Z);var t=this._ctx,e=this._x0,n=this._y0;return t&&t.closePath(),this._xi=e,this._yi=n,this},t.prototype.fill=function(t){t&&t.fill(),this.toStatic()},t.prototype.stroke=function(t){t&&t.stroke(),this.toStatic()},t.prototype.len=function(){return this._len},t.prototype.setData=function(t){var e=t.length;this.data&&this.data.length===e||!as||(this.data=new Float32Array(e));for(var n=0;nu.length&&(this._expandData(),u=this.data);for(var h=0;h0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},t.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var t=[],e=0;e11&&(this.data=new Float32Array(t)))}},t.prototype.getBoundingRect=function(){qa[0]=qa[1]=$a[0]=$a[1]=Number.MAX_VALUE,Ka[0]=Ka[1]=Ja[0]=Ja[1]=-Number.MAX_VALUE;var t,e=this.data,n=0,i=0,r=0,o=0;for(t=0;tn||is(y)>i||c===e-1)&&(f=Math.sqrt(A*A+y*y),r=g,o=x);break;case Ua.C:var v=t[c++],m=t[c++],x=(g=t[c++],t[c++]),_=t[c++],b=t[c++];f=Mn(r,o,v,m,g,x,_,b,10),r=_,o=b;break;case Ua.Q:f=kn(r,o,v=t[c++],m=t[c++],g=t[c++],x=t[c++],10),r=g,o=x;break;case Ua.A:var w=t[c++],S=t[c++],M=t[c++],I=t[c++],T=t[c++],C=t[c++],D=C+T;c+=1,d&&(a=es(T)*M+w,s=ns(T)*I+S),f=ts(M,I)*Qa(os,Math.abs(C)),r=es(D)*M+w,o=ns(D)*I+S;break;case Ua.R:a=r=t[c++],s=o=t[c++],f=2*t[c++]+2*t[c++];break;case Ua.Z:var A=a-r;y=s-o;f=Math.sqrt(A*A+y*y),r=a,o=s}f>=0&&(l[h++]=f,u+=f)}return this._pathLen=u,u},t.prototype.rebuildPath=function(t,e){var n,i,r,o,a,s,l,u,h,c,p=this.data,d=this._ux,f=this._uy,g=this._len,y=e<1,v=0,m=0,x=0;if(!y||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,u=e*this._pathLen))t:for(var _=0;_0&&(t.lineTo(h,c),x=0),b){case Ua.M:n=r=p[_++],i=o=p[_++],t.moveTo(r,o);break;case Ua.L:a=p[_++],s=p[_++];var S=is(a-r),M=is(s-o);if(S>d||M>f){if(y){if(v+(j=l[m++])>u){var I=(u-v)/j;t.lineTo(r*(1-I)+a*I,o*(1-I)+s*I);break t}v+=j}t.lineTo(a,s),r=a,o=s,x=0}else{var T=S*S+M*M;T>x&&(h=a,c=s,x=T)}break;case Ua.C:var C=p[_++],D=p[_++],A=p[_++],k=p[_++],L=p[_++],P=p[_++];if(y){if(v+(j=l[m++])>u){wn(r,C,A,L,I=(u-v)/j,Za),wn(o,D,k,P,I,ja),t.bezierCurveTo(Za[1],ja[1],Za[2],ja[2],Za[3],ja[3]);break t}v+=j}t.bezierCurveTo(C,D,A,k,L,P),r=L,o=P;break;case Ua.Q:C=p[_++],D=p[_++],A=p[_++],k=p[_++];if(y){if(v+(j=l[m++])>u){Dn(r,C,A,I=(u-v)/j,Za),Dn(o,D,k,I,ja),t.quadraticCurveTo(Za[1],ja[1],Za[2],ja[2]);break t}v+=j}t.quadraticCurveTo(C,D,A,k),r=A,o=k;break;case Ua.A:var O=p[_++],R=p[_++],N=p[_++],E=p[_++],z=p[_++],V=p[_++],B=p[_++],F=!p[_++],G=N>E?N:E,W=is(N-E)>.001,H=z+V,Y=!1;if(y)v+(j=l[m++])>u&&(H=z+V*(u-v)/j,Y=!0),v+=j;if(W&&t.ellipse?t.ellipse(O,R,N,E,B,z,H,F):t.arc(O,R,G,z,H,F),Y)break t;w&&(n=es(z)*N+O,i=ns(z)*E+R),r=es(H)*N+O,o=ns(H)*E+R;break;case Ua.R:n=r=p[_],i=o=p[_+1],a=p[_++],s=p[_++];var X=p[_++],U=p[_++];if(y){if(v+(j=l[m++])>u){var Z=u-v;t.moveTo(a,s),t.lineTo(a+Qa(Z,X),s),(Z-=X)>0&&t.lineTo(a+X,s+Qa(Z,U)),(Z-=U)>0&&t.lineTo(a+ts(X-Z,0),s+U),(Z-=X)>0&&t.lineTo(a,s+ts(U-Z,0));break t}v+=j}t.rect(a,s,X,U);break;case Ua.Z:if(y){var j;if(v+(j=l[m++])>u){I=(u-v)/j;t.lineTo(r*(1-I)+n*I,o*(1-I)+i*I);break t}v+=j}t.closePath(),r=n,o=i}}},t.prototype.clone=function(){var e=new t,n=this.data;return e.data=n.slice?n.slice():Array.prototype.slice.call(n),e._len=this._len,e},t.CMD=Ua,t.initDefaultProps=function(){var e=t.prototype;e._saveData=!0,e._ux=0,e._uy=0,e._pendingPtDist=0,e._version=0}(),t}();function cs(t,e,n,i,r,o,a){if(0===r)return!1;var s=r,l=0;if(a>e+s&&a>i+s||at+s&&o>n+s||oe+c&&h>i+c&&h>o+c&&h>s+c||ht+c&&u>n+c&&u>r+c&&u>a+c||ue+u&&l>i+u&&l>o+u||lt+u&&s>n+u&&s>r+u||sn||h+ur&&(r+=ys);var p=Math.atan2(l,s);return p<0&&(p+=ys),p>=i&&p<=r||p+ys>=i&&p+ys<=r}function ms(t,e,n,i,r,o){if(o>e&&o>i||or?s:0}var xs=hs.CMD,_s=2*Math.PI;var bs=[-1,-1,-1],ws=[-1,-1];function Ss(t,e,n,i,r,o,a,s,l,u){if(u>e&&u>i&&u>o&&u>s||u1&&(h=void 0,h=ws[0],ws[0]=ws[1],ws[1]=h),f=mn(e,i,o,s,ws[0]),d>1&&(g=mn(e,i,o,s,ws[1]))),2===d?ve&&s>i&&s>o||s=0&&h<=1&&(r[l++]=h);else{var u=a*a-4*o*s;if(yn(u))(h=-a/(2*o))>=0&&h<=1&&(r[l++]=h);else if(u>0){var h,c=ln(u),p=(-a-c)/(2*o);(h=(-a+c)/(2*o))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}(e,i,o,s,bs);if(0===l)return 0;var u=Cn(e,i,o);if(u>=0&&u<=1){for(var h=0,c=In(e,i,o,u),p=0;pn||s<-n)return 0;var l=Math.sqrt(n*n-s*s);bs[0]=-l,bs[1]=l;var u=Math.abs(i-r);if(u<1e-4)return 0;if(u>=_s-1e-4){i=0,r=_s;var h=o?1:-1;return a>=bs[0]+t&&a<=bs[1]+t?h:0}if(i>r){var c=i;i=r,r=c}i<0&&(i+=_s,r+=_s);for(var p=0,d=0;d<2;d++){var f=bs[d];if(f+t>a){var g=Math.atan2(s,f);h=o?1:-1;g<0&&(g=_s+g),(g>=i&&g<=r||g+_s>=i&&g+_s<=r)&&(g>Math.PI/2&&g<1.5*Math.PI&&(h=-h),p+=h)}}return p}function Ts(t,e,n,i,r){for(var o,a,s,l,u=t.data,h=t.len(),c=0,p=0,d=0,f=0,g=0,y=0;y1&&(n||(c+=ms(p,d,f,g,i,r))),m&&(f=p=u[y],g=d=u[y+1]),v){case xs.M:p=f=u[y++],d=g=u[y++];break;case xs.L:if(n){if(cs(p,d,u[y],u[y+1],e,i,r))return!0}else c+=ms(p,d,u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case xs.C:if(n){if(ps(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=Ss(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case xs.Q:if(n){if(ds(p,d,u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=Ms(p,d,u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case xs.A:var x=u[y++],_=u[y++],b=u[y++],w=u[y++],S=u[y++],M=u[y++];y+=1;var I=!!(1-u[y++]);o=Math.cos(S)*b+x,a=Math.sin(S)*w+_,m?(f=o,g=a):c+=ms(p,d,o,a,i,r);var T=(i-x)*w/b+x;if(n){if(vs(x,_,w,S,S+M,I,e,T,r))return!0}else c+=Is(x,_,w,S,S+M,I,T,r);p=Math.cos(S+M)*b+x,d=Math.sin(S+M)*w+_;break;case xs.R:if(f=p=u[y++],g=d=u[y++],o=f+u[y++],a=g+u[y++],n){if(cs(f,g,o,g,e,i,r)||cs(o,g,o,a,e,i,r)||cs(o,a,f,a,e,i,r)||cs(f,a,f,g,e,i,r))return!0}else c+=ms(o,g,o,a,i,r),c+=ms(f,a,f,g,i,r);break;case xs.Z:if(n){if(cs(p,d,f,g,e,i,r))return!0}else c+=ms(p,d,f,g,i,r);p=f,d=g}}return n||(s=d,l=g,Math.abs(s-l)<1e-4)||(c+=ms(p,d,f,g,i,r)||0),0!==c}var Cs=k({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},Ma),Ds={style:k({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},Ia.style)},As=mr.concat(["invisible","culling","z","z2","zlevel","parent"]),ks=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype.update=function(){var n=this;t.prototype.update.call(this);var i=this.style;if(i.decal){var r=this._decalEl=this._decalEl||new e;r.buildPath===e.prototype.buildPath&&(r.buildPath=function(t){n.buildPath(t,n.shape)}),r.silent=!0;var o=r.style;for(var a in i)o[a]!==i[a]&&(o[a]=i[a]);o.fill=i.fill?i.decal:null,o.decal=null,o.shadowColor=null,i.strokeFirst&&(o.stroke=null);for(var s=0;s.5?lr:e>.2?"#eee":ur}if(t)return ur}return lr},e.prototype.getInsideTextStroke=function(t){var e=this.style.fill;if(U(e)){var n=this.__zr;if(!(!n||!n.isDarkMode())===oi(t,0)<.4)return e}},e.prototype.buildPath=function(t,e,n){},e.prototype.pathUpdated=function(){this.__dirty&=-5},e.prototype.getUpdatedPathProxy=function(t){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,t),this.path},e.prototype.createPathProxy=function(){this.path=new hs(!1)},e.prototype.hasStroke=function(){var t=this.style,e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.getBoundingRect=function(){var t=this._rect,e=this.style,n=!t;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var r=this.path;(i||4&this.__dirty)&&(r.beginPath(),this.buildPath(r,this.shape,!1),this.pathUpdated()),t=r.getBoundingRect()}if(this._rect=t,this.hasStroke()&&this.path&&this.path.len()>0){var o=this._rectStroke||(this._rectStroke=t.clone());if(this.__dirty||n){o.copy(t);var a=e.strokeNoScale?this.getLineScale():1,s=e.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}a>1e-10&&(o.width+=s/a,o.height+=s/a,o.x-=s/a/2,o.y-=s/a/2)}return o}return t},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect(),r=this.style;if(t=n[0],e=n[1],i.contain(t,e)){var o=this.path;if(this.hasStroke()){var a=r.lineWidth,s=r.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),function(t,e,n,i){return Ts(t,e,!0,n,i)}(o,a/s,t,e)))return!0}if(this.hasFill())return function(t,e,n){return Ts(t,0,!1,e,n)}(o,t,e)}return!1},e.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},e.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},e.prototype.animateShape=function(t){return this.animate("shape",t)},e.prototype.updateDuringAnimation=function(t){"style"===t?this.dirtyStyle():"shape"===t?this.dirtyShape():this.markRedraw()},e.prototype.attrKV=function(e,n){"shape"===e?this.setShape(n):t.prototype.attrKV.call(this,e,n)},e.prototype.setShape=function(t,e){var n=this.shape;return n||(n=this.shape={}),"string"==typeof t?n[t]=e:A(n,t),this.dirtyShape(),this},e.prototype.shapeChanged=function(){return!!(4&this.__dirty)},e.prototype.createStyle=function(t){return mt(Cs,t)},e.prototype._innerSaveToNormal=function(e){t.prototype._innerSaveToNormal.call(this,e);var n=this._normalState;e.shape&&!n.shape&&(n.shape=A({},this.shape))},e.prototype._applyStateObj=function(e,n,i,r,o,a){t.prototype._applyStateObj.call(this,e,n,i,r,o,a);var s,l=!(n&&r);if(n&&n.shape?o?r?s=n.shape:(s=A({},i.shape),A(s,n.shape)):(s=A({},r?this.shape:i.shape),A(s,n.shape)):l&&(s=i.shape),s)if(o){this.shape=A({},this.shape);for(var u={},h=G(s),c=0;c0},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.createStyle=function(t){return mt(Ls,t)},e.prototype.setBoundingRect=function(t){this._rect=t},e.prototype.getBoundingRect=function(){var t=this.style;if(!this._rect){var e=t.text;null!=e?e+="":e="";var n=Sr(e,t.font,t.textAlign,t.textBaseline);if(n.x+=t.x||0,n.y+=t.y||0,this.hasStroke()){var i=t.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},e.initDefaultProps=void(e.prototype.dirtyRectTolerance=10),e}(Da);Ps.prototype.type="tspan";var Os=k({x:0,y:0},Ma),Rs={style:k({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},Ia.style)};var Ns=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.createStyle=function(t){return mt(Os,t)},e.prototype._getSize=function(t){var e=this.style,n=e[t];if(null!=n)return n;var i,r=(i=e.image)&&"string"!=typeof i&&i.width&&i.height?e.image:this.__image;if(!r)return 0;var o="width"===t?"height":"width",a=e[o];return null==a?r[t]:r[t]/r[o]*a},e.prototype.getWidth=function(){return this._getSize("width")},e.prototype.getHeight=function(){return this._getSize("height")},e.prototype.getAnimationStyleProps=function(){return Rs},e.prototype.getBoundingRect=function(){var t=this.style;return this._rect||(this._rect=new ze(t.x||0,t.y||0,this.getWidth(),this.getHeight())),this._rect},e}(Da);Ns.prototype.type="image";var Es=Math.round;function zs(t,e,n){if(e){var i=e.x1,r=e.x2,o=e.y1,a=e.y2;t.x1=i,t.x2=r,t.y1=o,t.y2=a;var s=n&&n.lineWidth;return s?(Es(2*i)===Es(2*r)&&(t.x1=t.x2=Bs(i,s,!0)),Es(2*o)===Es(2*a)&&(t.y1=t.y2=Bs(o,s,!0)),t):t}}function Vs(t,e,n){if(e){var i=e.x,r=e.y,o=e.width,a=e.height;t.x=i,t.y=r,t.width=o,t.height=a;var s=n&&n.lineWidth;return s?(t.x=Bs(i,s,!0),t.y=Bs(r,s,!0),t.width=Math.max(Bs(i+o,s,!1)-t.x,0===o?0:1),t.height=Math.max(Bs(r+a,s,!1)-t.y,0===a?0:1),t):t}}function Bs(t,e,n){if(!e)return t;var i=Es(2*t);return(i+Es(e))%2==0?i/2:(i+(n?1:-1))/2}var Fs=function(){this.x=0,this.y=0,this.width=0,this.height=0},Gs={},Ws=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Fs},e.prototype.buildPath=function(t,e){var n,i,r,o;if(this.subPixelOptimize){var a=Vs(Gs,e,this.style);n=a.x,i=a.y,r=a.width,o=a.height,a.r=e.r,e=a}else n=e.x,i=e.y,r=e.width,o=e.height;e.r?function(t,e){var n,i,r,o,a,s=e.x,l=e.y,u=e.width,h=e.height,c=e.r;u<0&&(s+=u,u=-u),h<0&&(l+=h,h=-h),"number"==typeof c?n=i=r=o=c:c instanceof Array?1===c.length?n=i=r=o=c[0]:2===c.length?(n=r=c[0],i=o=c[1]):3===c.length?(n=c[0],i=o=c[1],r=c[2]):(n=c[0],i=c[1],r=c[2],o=c[3]):n=i=r=o=0,n+i>u&&(n*=u/(a=n+i),i*=u/a),r+o>u&&(r*=u/(a=r+o),o*=u/a),i+r>h&&(i*=h/(a=i+r),r*=h/a),n+o>h&&(n*=h/(a=n+o),o*=h/a),t.moveTo(s+n,l),t.lineTo(s+u-i,l),0!==i&&t.arc(s+u-i,l+i,i,-Math.PI/2,0),t.lineTo(s+u,l+h-r),0!==r&&t.arc(s+u-r,l+h-r,r,0,Math.PI/2),t.lineTo(s+o,l+h),0!==o&&t.arc(s+o,l+h-o,o,Math.PI/2,Math.PI),t.lineTo(s,l+n),0!==n&&t.arc(s+n,l+n,n,Math.PI,1.5*Math.PI)}(t,e):t.rect(n,i,r,o)},e.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},e}(ks);Ws.prototype.type="rect";var Hs={fill:"#000"},Ys={style:k({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},Ia.style)},Xs=function(t){function e(e){var n=t.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=Hs,n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.update=function(){t.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var e=0;ed&&h){var f=Math.floor(d/l);n=n.slice(0,f)}if(t&&a&&null!=c)for(var g=da(c,o,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),y=0;y0,T=null!=t.width&&("truncate"===t.overflow||"break"===t.overflow||"breakAll"===t.overflow),C=i.calculatedLineHeight,D=0;Dl&&xa(n,t.substring(l,u),e,s),xa(n,i[2],e,s,i[1]),l=ca.lastIndex}lo){b>0?(m.tokens=m.tokens.slice(0,b),y(m,_,x),n.lines=n.lines.slice(0,v+1)):n.lines=n.lines.slice(0,v);break t}var C=w.width,D=null==C||"auto"===C;if("string"==typeof C&&"%"===C.charAt(C.length-1))P.percentWidth=C,h.push(P),P.contentWidth=br(P.text,I);else{if(D){var A=w.backgroundColor,k=A&&A.image;k&&ha(k=sa(k))&&(P.width=Math.max(P.width,k.width*T/k.height))}var L=f&&null!=r?r-_:null;null!=L&&L=0&&"right"===(C=x[T]).align;)this._placeToken(C,t,b,f,I,"right",y),w-=C.width,I-=C.width,T--;for(M+=(n-(M-d)-(g-I)-w)/2;S<=T;)C=x[S],this._placeToken(C,t,b,f,M+C.width/2,"center",y),M+=C.width,S++;f+=b}},e.prototype._placeToken=function(t,e,n,i,r,o,s){var l=e.rich[t.styleName]||{};l.text=t.text;var u=t.verticalAlign,h=i+n/2;"top"===u?h=i+t.height/2:"bottom"===u&&(h=i+n-t.height/2),!t.isLineHolder&&il(l)&&this._renderBackground(l,e,"right"===o?r-t.width:"center"===o?r-t.width/2:r,h-t.height/2,t.width,t.height);var c=!!l.backgroundColor,p=t.textPadding;p&&(r=el(r,o,p),h-=t.height/2-p[0]-t.innerHeight/2);var d=this._getOrCreateChild(Ps),f=d.createStyle();d.useStyle(f);var g=this._defaultStyle,y=!1,v=0,m=tl("fill"in l?l.fill:"fill"in e?e.fill:(y=!0,g.fill)),x=Qs("stroke"in l?l.stroke:"stroke"in e?e.stroke:c||s||g.autoStroke&&!y?null:(v=2,g.stroke)),_=l.textShadowBlur>0||e.textShadowBlur>0;f.text=t.text,f.x=r,f.y=h,_&&(f.shadowBlur=l.textShadowBlur||e.textShadowBlur||0,f.shadowColor=l.textShadowColor||e.textShadowColor||"transparent",f.shadowOffsetX=l.textShadowOffsetX||e.textShadowOffsetX||0,f.shadowOffsetY=l.textShadowOffsetY||e.textShadowOffsetY||0),f.textAlign=o,f.textBaseline="middle",f.font=t.font||a,f.opacity=ot(l.opacity,e.opacity,1),Ks(f,l),x&&(f.lineWidth=ot(l.lineWidth,e.lineWidth,v),f.lineDash=rt(l.lineDash,e.lineDash),f.lineDashOffset=e.lineDashOffset||0,f.stroke=x),m&&(f.fill=m);var b=t.contentWidth,w=t.contentHeight;d.setBoundingRect(new ze(Mr(f.x,b,f.textAlign),Ir(f.y,w,f.textBaseline),b,w))},e.prototype._renderBackground=function(t,e,n,i,r,o){var a,s,l,u=t.backgroundColor,h=t.borderWidth,c=t.borderColor,p=u&&u.image,d=u&&!p,f=t.borderRadius,g=this;if(d||t.lineHeight||h&&c){(a=this._getOrCreateChild(Ws)).useStyle(a.createStyle()),a.style.fill=null;var y=a.shape;y.x=n,y.y=i,y.width=r,y.height=o,y.r=f,a.dirtyShape()}if(d)(l=a.style).fill=u||null,l.fillOpacity=rt(t.fillOpacity,1);else if(p){(s=this._getOrCreateChild(Ns)).onload=function(){g.dirtyStyle()};var v=s.style;v.image=u.image,v.x=n,v.y=i,v.width=r,v.height=o}h&&c&&((l=a.style).lineWidth=h,l.stroke=c,l.strokeOpacity=rt(t.strokeOpacity,1),l.lineDash=t.borderDash,l.lineDashOffset=t.borderDashOffset||0,a.strokeContainThreshold=0,a.hasFill()&&a.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var m=(a||s).style;m.shadowBlur=t.shadowBlur||0,m.shadowColor=t.shadowColor||"transparent",m.shadowOffsetX=t.shadowOffsetX||0,m.shadowOffsetY=t.shadowOffsetY||0,m.opacity=ot(t.opacity,e.opacity,1)},e.makeFont=function(t){var e="";return $s(t)&&(e=[t.fontStyle,t.fontWeight,qs(t.fontSize),t.fontFamily||"sans-serif"].join(" ")),e&&ut(e)||t.textFont||t.font},e}(Da),Us={left:!0,right:1,center:1},Zs={top:1,bottom:1,middle:1},js=["fontStyle","fontWeight","fontSize","fontFamily"];function qs(t){return"string"!=typeof t||-1===t.indexOf("px")&&-1===t.indexOf("rem")&&-1===t.indexOf("em")?isNaN(+t)?"12px":t+"px":t}function Ks(t,e){for(var n=0;n=0,o=!1;if(t instanceof ks){var a=ll(t),s=r&&a.selectFill||a.normalFill,l=r&&a.selectStroke||a.normalStroke;if(ml(s)||ml(l)){var u=(i=i||{}).style||{};"inherit"===u.fill?(o=!0,i=A({},i),(u=A({},u)).fill=s):!ml(u.fill)&&ml(s)?(o=!0,i=A({},i),(u=A({},u)).fill=si(s)):!ml(u.stroke)&&ml(l)&&(o||(i=A({},i),u=A({},u)),u.stroke=si(l)),i.style=u}}if(i&&null==i.z2){o||(i=A({},i));var h=t.z2EmphasisLift;i.z2=t.z2+(null!=h?h:pl)}return i}(this,0,e,n);if("blur"===t)return function(t,e,n){var i=P(t.currentStates,e)>=0,r=t.style.opacity,o=i?null:function(t,e,n,i){for(var r=t.style,o={},a=0;a0){var o={dataIndex:r,seriesIndex:t.seriesIndex};null!=i&&(o.dataType=i),e.push(o)}}))})),e}function Ul(t,e,n){Jl(t,!0),Cl(t,kl),jl(t,e,n)}function Zl(t,e,n,i){i?function(t){Jl(t,!1)}(t):Ul(t,e,n)}function jl(t,e,n){var i=rl(t);null!=e?(i.focus=e,i.blurScope=n):i.focus&&(i.focus=null)}var ql=["emphasis","blur","select"],Kl={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function $l(t,e,n,i){n=n||"itemStyle";for(var r=0;r1&&(a*=su(f),s*=su(f));var g=(r===o?-1:1)*su((a*a*(s*s)-a*a*(d*d)-s*s*(p*p))/(a*a*(d*d)+s*s*(p*p)))||0,y=g*a*d/s,v=g*-s*p/a,m=(t+n)/2+uu(c)*y-lu(c)*v,x=(e+i)/2+lu(c)*y+uu(c)*v,_=du([1,0],[(p-y)/a,(d-v)/s]),b=[(p-y)/a,(d-v)/s],w=[(-1*p-y)/a,(-1*d-v)/s],S=du(b,w);if(pu(b,w)<=-1&&(S=hu),pu(b,w)>=1&&(S=0),S<0){var M=Math.round(S/hu*1e6)/1e6;S=2*hu+M%2*hu}h.addData(u,m,x,a,s,_,S,c,o)}var gu=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,yu=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var vu=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.applyTransform=function(t){},e}(ks);function mu(t){return null!=t.setData}function xu(t,e){var n=function(t){var e=new hs;if(!t)return e;var n,i=0,r=0,o=i,a=r,s=hs.CMD,l=t.match(gu);if(!l)return e;for(var u=0;uk*k+L*L&&(M=T,I=C),{cx:M,cy:I,x0:-h,y0:-c,x1:M*(r/b-1),y1:I*(r/b-1)}}function Vu(t,e){var n,i=Ru(e.r,0),r=Ru(e.r0||0,0),o=i>0;if(o||r>0){if(o||(i=r,r=0),r>i){var a=i;i=r,r=a}var s=e.startAngle,l=e.endAngle;if(!isNaN(s)&&!isNaN(l)){var u=e.cx,h=e.cy,c=!!e.clockwise,p=Pu(l-s),d=p>Cu&&p%Cu;if(d>Eu&&(p=d),i>Eu)if(p>Cu-Eu)t.moveTo(u+i*Au(s),h+i*Du(s)),t.arc(u,h,i,s,l,!c),r>Eu&&(t.moveTo(u+r*Au(l),h+r*Du(l)),t.arc(u,h,r,l,s,c));else{var f=void 0,g=void 0,y=void 0,v=void 0,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0,S=void 0,M=void 0,I=void 0,T=void 0,C=void 0,D=void 0,A=void 0,k=i*Au(s),L=i*Du(s),P=r*Au(l),O=r*Du(l),R=p>Eu;if(R){var N=e.cornerRadius;N&&(n=function(t){var e;if(Y(t)){var n=t.length;if(!n)return t;e=1===n?[t[0],t[0],0,0]:2===n?[t[0],t[0],t[1],t[1]]:3===n?t.concat(t[2]):t}else e=[t,t,t,t];return e}(N),f=n[0],g=n[1],y=n[2],v=n[3]);var E=Pu(i-r)/2;if(m=Nu(E,y),x=Nu(E,v),_=Nu(E,f),b=Nu(E,g),M=w=Ru(m,x),I=S=Ru(_,b),(w>Eu||S>Eu)&&(T=i*Au(l),C=i*Du(l),D=r*Au(s),A=r*Du(s),pEu){var X=Nu(y,M),U=Nu(v,M),Z=zu(D,A,k,L,i,X,c),j=zu(T,C,P,O,i,U,c);t.moveTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),M0&&t.arc(u+Z.cx,h+Z.cy,X,Lu(Z.y0,Z.x0),Lu(Z.y1,Z.x1),!c),t.arc(u,h,i,Lu(Z.cy+Z.y1,Z.cx+Z.x1),Lu(j.cy+j.y1,j.cx+j.x1),!c),U>0&&t.arc(u+j.cx,h+j.cy,U,Lu(j.y1,j.x1),Lu(j.y0,j.x0),!c))}else t.moveTo(u+k,h+L),t.arc(u,h,i,s,l,!c);else t.moveTo(u+k,h+L);if(r>Eu&&R)if(I>Eu){X=Nu(f,I),Z=zu(P,O,T,C,r,-(U=Nu(g,I)),c),j=zu(k,L,D,A,r,-X,c);t.lineTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),I0&&t.arc(u+Z.cx,h+Z.cy,U,Lu(Z.y0,Z.x0),Lu(Z.y1,Z.x1),!c),t.arc(u,h,r,Lu(Z.cy+Z.y1,Z.cx+Z.x1),Lu(j.cy+j.y1,j.cx+j.x1),c),X>0&&t.arc(u+j.cx,h+j.cy,X,Lu(j.y1,j.x1),Lu(j.y0,j.x0),!c))}else t.lineTo(u+P,h+O),t.arc(u,h,r,l,s,c);else t.lineTo(u+P,h+O)}else t.moveTo(u,h);t.closePath()}}}var Bu=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},Fu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Bu},e.prototype.buildPath=function(t,e){Vu(t,e)},e.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},e}(ks);Fu.prototype.type="sector";var Gu=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Wu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Gu},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=2*Math.PI;t.moveTo(n+e.r,i),t.arc(n,i,e.r,0,r,!1),t.moveTo(n+e.r0,i),t.arc(n,i,e.r0,0,r,!0)},e}(ks);function Hu(t,e,n){var i=e.smooth,r=e.points;if(r&&r.length>=2){if(i){var o=function(t,e,n,i){var r,o,a,s,l=[],u=[],h=[],c=[];if(i){a=[1/0,1/0],s=[-1/0,-1/0];for(var p=0,d=t.length;plh[1]){if(a=!1,r)return a;var u=Math.abs(lh[0]-sh[1]),h=Math.abs(sh[0]-lh[1]);Math.min(u,h)>i.len()&&(u0){var c={duration:h.duration,delay:h.delay||0,easing:h.easing,done:o,force:!!o||!!a,setToFinal:!u,scope:t,during:a};l?e.animateFrom(n,c):e.animateTo(n,c)}else e.stopAnimation(),!l&&e.attr(n),a&&a(1),o&&o()}function vh(t,e,n,i,r,o){yh("update",t,e,n,i,r,o)}function mh(t,e,n,i,r,o){yh("enter",t,e,n,i,r,o)}function xh(t){if(!t.__zr)return!0;for(var e=0;eMath.abs(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function Wh(t){return!t.isGroup}function Hh(t,e,n){if(t&&e){var i,r=(i={},t.traverse((function(t){Wh(t)&&t.anid&&(i[t.anid]=t)})),i);e.traverse((function(t){if(Wh(t)&&t.anid){var e=r[t.anid];if(e){var i=o(t);t.attr(o(e)),vh(t,i,n,rl(t).dataIndex)}}}))}function o(t){var e={x:t.x,y:t.y,rotation:t.rotation};return function(t){return null!=t.shape}(t)&&(e.shape=A({},t.shape)),e}}function Yh(t,e){return z(t,(function(t){var n=t[0];n=Mh(n,e.x),n=Ih(n,e.x+e.width);var i=t[1];return i=Mh(i,e.y),[n,i=Ih(i,e.y+e.height)]}))}function Xh(t,e){var n=Mh(t.x,e.x),i=Ih(t.x+t.width,e.x+e.width),r=Mh(t.y,e.y),o=Ih(t.y+t.height,e.y+e.height);if(i>=n&&o>=r)return{x:n,y:r,width:i-n,height:o-r}}function Uh(t,e,n){var i=A({rectHover:!0},e),r=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},t)return 0===t.indexOf("image://")?(r.image=t.slice(8),k(r,n),new Ns(i)):Ph(t.replace("path://",""),i,n,"center")}function Zh(t,e,n,i,r){for(var o=0,a=r[r.length-1];o=-1e-6)return!1;var f=t-r,g=e-o,y=qh(f,g,u,h)/d;if(y<0||y>1)return!1;var v=qh(f,g,c,p)/d;return!(v<0||v>1)}function qh(t,e,n,i){return t*i-n*e}function Kh(t){var e=t.itemTooltipOption,n=t.componentModel,i=t.itemName,r=U(e)?{formatter:e}:e,o=n.mainType,a=n.componentIndex,s={componentType:o,name:i,$vars:["name"]};s[o+"Index"]=a;var l=t.formatterParamsExtra;l&&E(G(l),(function(t){_t(s,t)||(s[t]=l[t],s.$vars.push(t))}));var u=rl(t.el);u.componentMainType=o,u.componentIndex=a,u.tooltipConfig={name:i,option:k({content:i,encodeHTMLContent:!0,formatterParams:s},r)}}function $h(t,e){var n;t.isGroup&&(n=e(t)),n||t.traverse(e)}function Jh(t,e){if(t)if(Y(t))for(var n=0;n-1?Lc:Oc;function zc(t,e){t=t.toUpperCase(),Nc[t]=new Cc(e),Rc[t]=e}function Vc(t){return Nc[t]}zc(Pc,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst",custom:"Custom chart",chart:"Chart"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),zc(Lc,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图",custom:"自定义图表",chart:"图表"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var Bc=1e3,Fc=6e4,Gc=36e5,Wc=864e5,Hc=31536e6,Yc={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},Xc="{yyyy}-{MM}-{dd}",Uc={year:"{yyyy}",month:"{yyyy}-{MM}",day:Xc,hour:Xc+" "+Yc.hour,minute:Xc+" "+Yc.minute,second:Xc+" "+Yc.second,millisecond:Yc.none},Zc=["year","month","day","hour","minute","second","millisecond"],jc=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function qc(t,e){return"0000".substr(0,e-(t+="").length)+t}function Kc(t){switch(t){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return t}}function $c(t){return t===Kc(t)}function Jc(t,e,n,i){var r=uo(t),o=r[ep(n)](),a=r[np(n)]()+1,s=Math.floor((a-1)/3)+1,l=r[ip(n)](),u=r["get"+(n?"UTC":"")+"Day"](),h=r[rp(n)](),c=(h-1)%12+1,p=r[op(n)](),d=r[ap(n)](),f=r[sp(n)](),g=h>=12?"pm":"am",y=g.toUpperCase(),v=(i instanceof Cc?i:Vc(i||Ec)||Nc[Oc]).getModel("time"),m=v.get("month"),x=v.get("monthAbbr"),_=v.get("dayOfWeek"),b=v.get("dayOfWeekAbbr");return(e||"").replace(/{a}/g,g+"").replace(/{A}/g,y+"").replace(/{yyyy}/g,o+"").replace(/{yy}/g,qc(o%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,m[a-1]).replace(/{MMM}/g,x[a-1]).replace(/{MM}/g,qc(a,2)).replace(/{M}/g,a+"").replace(/{dd}/g,qc(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,_[u]).replace(/{ee}/g,b[u]).replace(/{e}/g,u+"").replace(/{HH}/g,qc(h,2)).replace(/{H}/g,h+"").replace(/{hh}/g,qc(c+"",2)).replace(/{h}/g,c+"").replace(/{mm}/g,qc(p,2)).replace(/{m}/g,p+"").replace(/{ss}/g,qc(d,2)).replace(/{s}/g,d+"").replace(/{SSS}/g,qc(f,3)).replace(/{S}/g,f+"")}function Qc(t,e){var n=uo(t),i=n[np(e)]()+1,r=n[ip(e)](),o=n[rp(e)](),a=n[op(e)](),s=n[ap(e)](),l=0===n[sp(e)](),u=l&&0===s,h=u&&0===a,c=h&&0===o,p=c&&1===r;return p&&1===i?"year":p?"month":c?"day":h?"hour":u?"minute":l?"second":"millisecond"}function tp(t,e,n){var i=j(t)?uo(t):t;switch(e=e||Qc(t,n)){case"year":return i[ep(n)]();case"half-year":return i[np(n)]()>=6?1:0;case"quarter":return Math.floor((i[np(n)]()+1)/4);case"month":return i[np(n)]();case"day":return i[ip(n)]();case"half-day":return i[rp(n)]()/24;case"hour":return i[rp(n)]();case"minute":return i[op(n)]();case"second":return i[ap(n)]();case"millisecond":return i[sp(n)]()}}function ep(t){return t?"getUTCFullYear":"getFullYear"}function np(t){return t?"getUTCMonth":"getMonth"}function ip(t){return t?"getUTCDate":"getDate"}function rp(t){return t?"getUTCHours":"getHours"}function op(t){return t?"getUTCMinutes":"getMinutes"}function ap(t){return t?"getUTCSeconds":"getSeconds"}function sp(t){return t?"getUTCMilliseconds":"getMilliseconds"}function lp(t){return t?"setUTCFullYear":"setFullYear"}function up(t){return t?"setUTCMonth":"setMonth"}function hp(t){return t?"setUTCDate":"setDate"}function cp(t){return t?"setUTCHours":"setHours"}function pp(t){return t?"setUTCMinutes":"setMinutes"}function dp(t){return t?"setUTCSeconds":"setSeconds"}function fp(t){return t?"setUTCMilliseconds":"setMilliseconds"}function gp(t){if(!vo(t))return U(t)?t:"-";var e=(t+"").split(".");return e[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(e.length>1?"."+e[1]:"")}function yp(t,e){return t=(t||"").toLowerCase().replace(/-(.)/g,(function(t,e){return e.toUpperCase()})),e&&t&&(t=t.charAt(0).toUpperCase()+t.slice(1)),t}var vp=st;function mp(t,e,n){function i(t){return t&&ut(t)?t:"-"}function r(t){return!(null==t||isNaN(t)||!isFinite(t))}var o="time"===e,a=t instanceof Date;if(o||a){var s=o?uo(t):t;if(!isNaN(+s))return Jc(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(a)return"-"}if("ordinal"===e)return Z(t)?i(t):j(t)&&r(t)?t+"":"-";var l=yo(t);return r(l)?gp(l):Z(t)?i(t):"boolean"==typeof t?t+"":"-"}var xp=["a","b","c","d","e","f","g"],_p=function(t,e){return"{"+t+(null==e?"":e)+"}"};function bp(t,e,n){Y(e)||(e=[e]);var i=e.length;if(!i)return"";for(var r=e[0].$vars||[],o=0;o':'':{renderMode:o,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===r?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function Sp(t,e){return e=e||"transparent",U(t)?t:q(t)&&t.colorStops&&(t.colorStops[0]||{}).color||e}function Mp(t,e){if("_blank"===e||"blank"===e){var n=window.open();n.opener=null,n.location.href=t}else window.open(t,e)}var Ip=E,Tp=["left","right","top","bottom","width","height"],Cp=[["width","left","right"],["height","top","bottom"]];function Dp(t,e,n,i,r){var o=0,a=0;null==i&&(i=1/0),null==r&&(r=1/0);var s=0;e.eachChild((function(l,u){var h,c,p=l.getBoundingRect(),d=e.childAt(u+1),f=d&&d.getBoundingRect();if("horizontal"===t){var g=p.width+(f?-f.x+p.x:0);(h=o+g)>i||l.newline?(o=0,h=g,a+=s+n,s=p.height):s=Math.max(s,p.height)}else{var y=p.height+(f?-f.y+p.y:0);(c=a+y)>r||l.newline?(o+=s+n,a=0,c=y,s=p.width):s=Math.max(s,p.width)}l.newline||(l.x=o,l.y=a,l.markRedraw(),"horizontal"===t?o=h+n:a=c+n)}))}var Ap=Dp;H(Dp,"vertical"),H(Dp,"horizontal");function kp(t,e,n){n=vp(n||0);var i=e.width,r=e.height,o=$r(t.left,i),a=$r(t.top,r),s=$r(t.right,i),l=$r(t.bottom,r),u=$r(t.width,i),h=$r(t.height,r),c=n[2]+n[0],p=n[1]+n[3],d=t.aspect;switch(isNaN(u)&&(u=i-s-p-o),isNaN(h)&&(h=r-l-c-a),null!=d&&(isNaN(u)&&isNaN(h)&&(d>i/r?u=.8*i:h=.8*r),isNaN(u)&&(u=d*h),isNaN(h)&&(h=u/d)),isNaN(o)&&(o=i-s-u-p),isNaN(a)&&(a=r-l-h-c),t.left||t.right){case"center":o=i/2-u/2-n[3];break;case"right":o=i-u-p}switch(t.top||t.bottom){case"middle":case"center":a=r/2-h/2-n[0];break;case"bottom":a=r-h-c}o=o||0,a=a||0,isNaN(u)&&(u=i-p-o-(s||0)),isNaN(h)&&(h=r-c-a-(l||0));var f=new ze(o+n[3],a+n[0],u,h);return f.margin=n,f}function Lp(t,e,n,i,r,o){var a,s=!r||!r.hv||r.hv[0],l=!r||!r.hv||r.hv[1],u=r&&r.boundingMode||"all";if((o=o||t).x=t.x,o.y=t.y,!s&&!l)return!1;if("raw"===u)a="group"===t.type?new ze(0,0,+e.width||0,+e.height||0):t.getBoundingRect();else if(a=t.getBoundingRect(),t.needLocalTransform()){var h=t.getLocalTransform();(a=a.clone()).applyTransform(h)}var c=kp(k({width:a.width,height:a.height},e),n,i),p=s?c.x-a.x:0,d=l?c.y-a.y:0;return"raw"===u?(o.x=p,o.y=d):(o.x+=p,o.y+=d),o===t&&t.markRedraw(),!0}function Pp(t){var e=t.layoutMode||t.constructor.layoutMode;return q(e)?e:e?{type:e}:null}function Op(t,e,n){var i=n&&n.ignoreSize;!Y(i)&&(i=[i,i]);var r=a(Cp[0],0),o=a(Cp[1],1);function a(n,r){var o={},a=0,u={},h=0;if(Ip(n,(function(e){u[e]=t[e]})),Ip(n,(function(t){s(e,t)&&(o[t]=u[t]=e[t]),l(o,t)&&a++,l(u,t)&&h++})),i[r])return l(e,n[1])?u[n[2]]=null:l(e,n[2])&&(u[n[1]]=null),u;if(2!==h&&a){if(a>=2)return o;for(var c=0;c=0;a--)o=C(o,n[a],!0);e.defaultOption=o}return e.defaultOption},e.prototype.getReferringComponents=function(t,e){var n=t+"Index",i=t+"Id";return Yo(this.ecModel,t,{index:this.get(n,!0),id:this.get(i,!0)},e)},e.prototype.getBoxLayoutParams=function(){var t=this;return{left:t.get("left"),top:t.get("top"),right:t.get("right"),bottom:t.get("bottom"),width:t.get("width"),height:t.get("height")}},e.prototype.getZLevelKey=function(){return""},e.prototype.setZLevel=function(t){this.option.zlevel=t},e.protoInitialize=function(){var t=e.prototype;t.type="component",t.id="",t.name="",t.mainType="",t.subType="",t.componentIndex=0}(),e}(Cc);Jo(zp,Cc),na(zp),function(t){var e={};t.registerSubTypeDefaulter=function(t,n){var i=Ko(t);e[i.main]=n},t.determineSubType=function(n,i){var r=i.type;if(!r){var o=Ko(n).main;t.hasSubTypes(n)&&e[o]&&(r=e[o](i))}return r}}(zp),function(t,e){function n(t,e){return t[e]||(t[e]={predecessor:[],successor:[]}),t[e]}t.topologicalTravel=function(t,i,r,o){if(t.length){var a=function(t){var i={},r=[];return E(t,(function(o){var a=n(i,o),s=function(t,e){var n=[];return E(t,(function(t){P(e,t)>=0&&n.push(t)})),n}(a.originalDeps=e(o),t);a.entryCount=s.length,0===a.entryCount&&r.push(o),E(s,(function(t){P(a.predecessor,t)<0&&a.predecessor.push(t);var e=n(i,t);P(e.successor,t)<0&&e.successor.push(o)}))})),{graph:i,noEntryList:r}}(i),s=a.graph,l=a.noEntryList,u={};for(E(t,(function(t){u[t]=!0}));l.length;){var h=l.pop(),c=s[h],p=!!u[h];p&&(r.call(o,h,c.originalDeps.slice()),delete u[h]),E(c.successor,p?f:d)}E(u,(function(){var t="";throw new Error(t)}))}function d(t){s[t].entryCount--,0===s[t].entryCount&&l.push(t)}function f(t){u[t]=!0,d(t)}}}(zp,(function(t){var e=[];E(zp.getClassesByMainType(t),(function(t){e=e.concat(t.dependencies||t.prototype.dependencies||[])})),e=z(e,(function(t){return Ko(t).main})),"dataset"!==t&&P(e,"dataset")<=0&&e.unshift("dataset");return e}));var Vp="";"undefined"!=typeof navigator&&(Vp=navigator.platform||"");var Bp="rgba(0, 0, 0, 0.2)",Fp={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:Bp,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Bp,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Bp,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Bp,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Bp,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Bp,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:Vp.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},Gp=yt(["tooltip","label","itemName","itemId","itemGroupId","itemChildGroupId","seriesName"]),Wp="original",Hp="arrayRows",Yp="objectRows",Xp="keyedColumns",Up="typedArray",Zp="unknown",jp="column",qp="row",Kp=1,$p=2,Jp=3,Qp=Vo();function td(t,e,n){var i={},r=nd(e);if(!r||!t)return i;var o,a,s=[],l=[],u=e.ecModel,h=Qp(u).datasetMap,c=r.uid+"_"+n.seriesLayoutBy;E(t=t.slice(),(function(e,n){var r=q(e)?e:t[n]={name:e};"ordinal"===r.type&&null==o&&(o=n,a=f(r)),i[r.name]=[]}));var p=h.get(c)||h.set(c,{categoryWayDim:a,valueWayDim:0});function d(t,e,n){for(var i=0;ie)return t[i];return t[n-1]}(i,a):n;if((h=h||n)&&h.length){var c=h[l];return r&&(u[r]=c),s.paletteIdx=(l+1)%h.length,c}}var fd="\0_ec_inner";var gd=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(t,e,n,i,r,o){i=i||{},this.option=null,this._theme=new Cc(i),this._locale=new Cc(r),this._optionManager=o},e.prototype.setOption=function(t,e,n){var i=md(e);this._optionManager.setOption(t,n,i),this._resetOption(null,i)},e.prototype.resetOption=function(t,e){return this._resetOption(t,md(e))},e.prototype._resetOption=function(t,e){var n=!1,i=this._optionManager;if(!t||"recreate"===t){var r=i.mountOption("recreate"===t);0,this.option&&"recreate"!==t?(this.restoreData(),this._mergeOption(r,e)):ld(this,r),n=!0}if("timeline"!==t&&"media"!==t||this.restoreData(),!t||"recreate"===t||"timeline"===t){var o=i.getTimelineOption(this);o&&(n=!0,this._mergeOption(o,e))}if(!t||"recreate"===t||"media"===t){var a=i.getMediaOption(this);a.length&&E(a,(function(t){n=!0,this._mergeOption(t,e)}),this)}return n},e.prototype.mergeOption=function(t){this._mergeOption(t,null)},e.prototype._mergeOption=function(t,e){var n=this.option,i=this._componentsMap,r=this._componentsCount,o=[],a=yt(),s=e&&e.replaceMergeMainTypeMap;Qp(this).datasetMap=yt(),E(t,(function(t,e){null!=t&&(zp.hasClass(e)?e&&(o.push(e),a.set(e,!0)):n[e]=null==n[e]?T(t):C(n[e],t,!0))})),s&&s.each((function(t,e){zp.hasClass(e)&&!a.get(e)&&(o.push(e),a.set(e,!0))})),zp.topologicalTravel(o,zp.getAllClassMainTypes(),(function(e){var o=function(t,e,n){var i=od.get(e);if(!i)return n;var r=i(t);return r?n.concat(r):n}(this,e,To(t[e])),a=i.get(e),l=a?s&&s.get(e)?"replaceMerge":"normalMerge":"replaceAll",u=Lo(a,o,l);(function(t,e,n){E(t,(function(t){var i=t.newOption;q(i)&&(t.keyInfo.mainType=e,t.keyInfo.subType=function(t,e,n,i){return e.type?e.type:n?n.subType:i.determineSubType(t,e)}(e,i,t.existing,n))}))})(u,e,zp),n[e]=null,i.set(e,null),r.set(e,0);var h,c=[],p=[],d=0;E(u,(function(t,n){var i=t.existing,r=t.newOption;if(r){var o="series"===e,a=zp.getClass(e,t.keyInfo.subType,!o);if(!a)return;if("tooltip"===e){if(h)return void 0;h=!0}if(i&&i.constructor===a)i.name=t.keyInfo.name,i.mergeOption(r,this),i.optionUpdated(r,!1);else{var s=A({componentIndex:n},t.keyInfo);A(i=new a(r,this,this,s),s),t.brandNew&&(i.__requireNewView=!0),i.init(r,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(c.push(i.option),p.push(i),d++):(c.push(void 0),p.push(void 0))}),this),n[e]=c,i.set(e,p),r.set(e,d),"series"===e&&ad(this)}),this),this._seriesIndices||ad(this)},e.prototype.getOption=function(){var t=T(this.option);return E(t,(function(e,n){if(zp.hasClass(n)){for(var i=To(e),r=i.length,o=!1,a=r-1;a>=0;a--)i[a]&&!Eo(i[a])?o=!0:(i[a]=null,!o&&r--);i.length=r,t[n]=i}})),delete t[fd],t},e.prototype.getTheme=function(){return this._theme},e.prototype.getLocaleModel=function(){return this._locale},e.prototype.setUpdatePayload=function(t){this._payload=t},e.prototype.getUpdatePayload=function(){return this._payload},e.prototype.getComponent=function(t,e){var n=this._componentsMap.get(t);if(n){var i=n[e||0];if(i)return i;if(null==e)for(var r=0;r=e:"max"===n?t<=e:t===e})(i[a],t,o)||(r=!1)}})),r}var Td=E,Cd=q,Dd=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function Ad(t){var e=t&&t.itemStyle;if(e)for(var n=0,i=Dd.length;n=0;g--){var y=t[g];if(s||(p=y.data.rawIndexOf(y.stackedByDimension,c)),p>=0){var v=y.data.getByRawIndex(y.stackResultDimension,p);if("all"===l||"positive"===l&&v>0||"negative"===l&&v<0||"samesign"===l&&d>=0&&v>0||"samesign"===l&&d<=0&&v<0){d=ro(d,v),f=v;break}}}return i[0]=d,i[1]=f,i}))}))}var Zd,jd,qd,Kd,$d,Jd=function(t){this.data=t.data||(t.sourceFormat===Xp?{}:[]),this.sourceFormat=t.sourceFormat||Zp,this.seriesLayoutBy=t.seriesLayoutBy||jp,this.startIndex=t.startIndex||0,this.dimensionsDetectedCount=t.dimensionsDetectedCount,this.metaRawOption=t.metaRawOption;var e=this.dimensionsDefine=t.dimensionsDefine;if(e)for(var n=0;nu&&(u=d)}s[0]=l,s[1]=u}},i=function(){return this._data?this._data.length/this._dimSize:0};function r(t){for(var e=0;e=0&&(s=o.interpolatedValue[l])}return null!=s?s+"":""})):void 0},t.prototype.getRawValue=function(t,e){return mf(this.getData(e),t)},t.prototype.formatTooltip=function(t,e,n){},t}();function bf(t){var e,n;return q(t)?t.type&&(n=t):e=t,{text:e,frag:n}}function wf(t){return new Sf(t)}var Sf=function(){function t(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0}return t.prototype.perform=function(t){var e,n=this._upstream,i=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(e=this._plan(this.context));var o,a=h(this._modBy),s=this._modDataCount||0,l=h(t&&t.modBy),u=t&&t.modDataCount||0;function h(t){return!(t>=1)&&(t=1),t}a===l&&s===u||(e="reset"),(this._dirty||"reset"===e)&&(this._dirty=!1,o=this._doReset(i)),this._modBy=l,this._modDataCount=u;var c=t&&t.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var p=this._dueIndex,d=Math.min(null!=c?this._dueIndex+c:1/0,this._dueEnd);if(!i&&(o||p1&&i>0?s:a}};return o;function a(){return e=t?null:oe},gte:function(t,e){return t>=e}},Af=function(){function t(t,e){if(!j(e)){var n="";0,wo(n)}this._opFn=Df[t],this._rvalFloat=yo(e)}return t.prototype.evaluate=function(t){return j(t)?this._opFn(t,this._rvalFloat):this._opFn(yo(t),this._rvalFloat)},t}(),kf=function(){function t(t,e){var n="desc"===t;this._resultLT=n?1:-1,null==e&&(e=n?"min":"max"),this._incomparable="min"===e?-1/0:1/0}return t.prototype.evaluate=function(t,e){var n=j(t)?t:yo(t),i=j(e)?e:yo(e),r=isNaN(n),o=isNaN(i);if(r&&(n=this._incomparable),o&&(i=this._incomparable),r&&o){var a=U(t),s=U(e);a&&(n=s?t:0),s&&(i=a?e:0)}return ni?-this._resultLT:0},t}(),Lf=function(){function t(t,e){this._rval=e,this._isEQ=t,this._rvalTypeof=typeof e,this._rvalFloat=yo(e)}return t.prototype.evaluate=function(t){var e=t===this._rval;if(!e){var n=typeof t;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(e=yo(t)===this._rvalFloat)}return this._isEQ?e:!e},t}();function Pf(t,e){return"eq"===t||"ne"===t?new Lf("eq"===t,e):_t(Df,t)?new Af(t,e):null}var Of=function(){function t(){}return t.prototype.getRawData=function(){throw new Error("not supported")},t.prototype.getRawDataItem=function(t){throw new Error("not supported")},t.prototype.cloneRawData=function(){},t.prototype.getDimensionInfo=function(t){},t.prototype.cloneAllDimensionInfo=function(){},t.prototype.count=function(){},t.prototype.retrieveValue=function(t,e){},t.prototype.retrieveValueFromItem=function(t,e){},t.prototype.convertValue=function(t,e){return If(t,e)},t}();function Rf(t){var e=t.sourceFormat;if(!Ff(e)){var n="";0,wo(n)}return t.data}function Nf(t){var e=t.sourceFormat,n=t.data;if(!Ff(e)){var i="";0,wo(i)}if(e===Hp){for(var r=[],o=0,a=n.length;o65535?Hf:Yf}function qf(t,e,n,i,r){var o=Zf[n||"float"];if(r){var a=t[e],s=a&&a.length;if(s!==i){for(var l=new o(i),u=0;ug[1]&&(g[1]=f)}return this._rawCount=this._count=s,{start:a,end:s}},t.prototype._initDataFromProvider=function(t,e,n){for(var i=this._provider,r=this._chunks,o=this._dimensions,a=o.length,s=this._rawExtent,l=z(o,(function(t){return t.property})),u=0;uy[1]&&(y[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=e,this._extent=[]},t.prototype.count=function(){return this._count},t.prototype.get=function(t,e){if(!(e>=0&&e=0&&e=this._rawCount||t<0)return-1;if(!this._indices)return t;var e=this._indices,n=e[t];if(null!=n&&nt))return o;r=o-1}}return-1},t.prototype.indicesOfNearest=function(t,e,n){var i=this._chunks[t],r=[];if(!i)return r;null==n&&(n=1/0);for(var o=1/0,a=-1,s=0,l=0,u=this.count();l=0&&a<0)&&(o=c,a=h,s=0),h===a&&(r[s++]=l))}return r.length=s,r},t.prototype.getIndices=function(){var t,e=this._indices;if(e){var n=e.constructor,i=this._count;if(n===Array){t=new n(i);for(var r=0;r=u&&x<=h||isNaN(x))&&(a[s++]=d),d++}p=!0}else if(2===r){f=c[i[0]];var y=c[i[1]],v=t[i[1]][0],m=t[i[1]][1];for(g=0;g=u&&x<=h||isNaN(x))&&(_>=v&&_<=m||isNaN(_))&&(a[s++]=d),d++}p=!0}}if(!p)if(1===r)for(g=0;g=u&&x<=h||isNaN(x))&&(a[s++]=b)}else for(g=0;gt[M][1])&&(w=!1)}w&&(a[s++]=e.getRawIndex(g))}return sy[1]&&(y[1]=g)}}}},t.prototype.lttbDownSample=function(t,e){var n,i,r,o=this.clone([t],!0),a=o._chunks[t],s=this.count(),l=0,u=Math.floor(1/e),h=this.getRawIndex(0),c=new(jf(this._rawCount))(Math.min(2*(Math.ceil(s/u)+2),s));c[l++]=h;for(var p=1;pn&&(n=i,r=I)}M>0&&M<_-x&&(c[l++]=Math.min(S,r),r=Math.max(S,r)),c[l++]=r,h=r}return c[l++]=this.getRawIndex(s-1),o._count=l,o._indices=c,o.getRawIndex=this._getRawIdx,o},t.prototype.downSample=function(t,e,n,i){for(var r=this.clone([t],!0),o=r._chunks,a=[],s=Math.floor(1/e),l=o[t],u=this.count(),h=r._rawExtent[t]=[1/0,-1/0],c=new(jf(this._rawCount))(Math.ceil(u/s)),p=0,d=0;du-d&&(s=u-d,a.length=s);for(var f=0;fh[1]&&(h[1]=y),c[p++]=v}return r._count=p,r._indices=c,r._updateGetRawIdx(),r},t.prototype.each=function(t,e){if(this._count)for(var n=t.length,i=this._chunks,r=0,o=this.count();ra&&(a=l)}return i=[o,a],this._extent[t]=i,i},t.prototype.getRawDataItem=function(t){var e=this.getRawIndex(t);if(this._provider.persistent)return this._provider.getItem(e);for(var n=[],i=this._chunks,r=0;r=0?this._indices[t]:-1},t.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},t.internalField=function(){function t(t,e,n,i){return If(t[i],this._dimensions[i])}Gf={arrayRows:t,objectRows:function(t,e,n,i){return If(t[e],this._dimensions[i])},keyedColumns:t,original:function(t,e,n,i){var r=t&&(null==t.value?t:t.value);return If(r instanceof Array?r[i]:r,this._dimensions[i])},typedArray:function(t,e,n,i){return t[i]}}}(),t}(),$f=function(){function t(t){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=t}return t.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},t.prototype._setLocalSource=function(t,e){this._sourceList=t,this._upstreamSignList=e,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},t.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},t.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},t.prototype._createSource=function(){this._setLocalSource([],[]);var t,e,n=this._sourceHost,i=this._getUpstreamSourceManagers(),r=!!i.length;if(Qf(n)){var o=n,a=void 0,s=void 0,l=void 0;if(r){var u=i[0];u.prepareSource(),a=(l=u.getSource()).data,s=l.sourceFormat,e=[u._getVersionSign()]}else s=$(a=o.get("data",!0))?Up:Wp,e=[];var h=this._getSourceMetaRawOption()||{},c=l&&l.metaRawOption||{},p=rt(h.seriesLayoutBy,c.seriesLayoutBy)||null,d=rt(h.sourceHeader,c.sourceHeader),f=rt(h.dimensions,c.dimensions);t=p!==c.seriesLayoutBy||!!d!=!!c.sourceHeader||f?[tf(a,{seriesLayoutBy:p,sourceHeader:d,dimensions:f},s)]:[]}else{var g=n;if(r){var y=this._applyTransform(i);t=y.sourceList,e=y.upstreamSignList}else{t=[tf(g.get("source",!0),this._getSourceMetaRawOption(),null)],e=[]}}this._setLocalSource(t,e)},t.prototype._applyTransform=function(t){var e,n=this._sourceHost,i=n.get("transform",!0),r=n.get("fromTransformResult",!0);if(null!=r){var o="";1!==t.length&&tg(o)}var a,s=[],l=[];return E(t,(function(t){t.prepareSource();var e=t.getSource(r||0),n="";null==r||e||tg(n),s.push(e),l.push(t._getVersionSign())})),i?e=function(t,e,n){var i=To(t),r=i.length,o="";r||wo(o);for(var a=0,s=r;a1||n>0&&!t.noHeader;return E(t.blocks,(function(t){var n=lg(t);n>=e&&(e=n+ +(i&&(!n||ag(t)&&!t.noHeader)))})),e}return 0}function ug(t,e,n,i){var r,o=e.noHeader,a=(r=lg(e),{html:ig[r],richText:rg[r]}),s=[],l=e.blocks||[];lt(!l||Y(l)),l=l||[];var u=t.orderMode;if(e.sortBlocks&&u){l=l.slice();var h={valueAsc:"asc",valueDesc:"desc"};if(_t(h,u)){var c=new kf(h[u],null);l.sort((function(t,e){return c.evaluate(t.sortParam,e.sortParam)}))}else"seriesDesc"===u&&l.reverse()}E(l,(function(n,r){var o=e.valueFormatter,l=sg(n)(o?A(A({},t),{valueFormatter:o}):t,n,r>0?a.html:0,i);null!=l&&s.push(l)}));var p="richText"===t.renderMode?s.join(a.richText):pg(s.join(""),o?n:a.html);if(o)return p;var d=mp(e.header,"ordinal",t.useUTC),f=ng(i,t.renderMode).nameStyle;return"richText"===t.renderMode?dg(t,d,f)+a.richText+p:pg('
    '+re(d)+"
    "+p,n)}function hg(t,e,n,i){var r=t.renderMode,o=e.noName,a=e.noValue,s=!e.markerType,l=e.name,u=t.useUTC,h=e.valueFormatter||t.valueFormatter||function(t){return z(t=Y(t)?t:[t],(function(t,e){return mp(t,Y(d)?d[e]:d,u)}))};if(!o||!a){var c=s?"":t.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||"#333",r),p=o?"":mp(l,"ordinal",u),d=e.valueType,f=a?[]:h(e.value,e.dataIndex),g=!s||!o,y=!s&&o,v=ng(i,r),m=v.nameStyle,x=v.valueStyle;return"richText"===r?(s?"":c)+(o?"":dg(t,p,m))+(a?"":function(t,e,n,i,r){var o=[r],a=i?10:20;return n&&o.push({padding:[0,0,0,a],align:"right"}),t.markupStyleCreator.wrapRichTextStyle(Y(e)?e.join(" "):e,o)}(t,f,g,y,x)):pg((s?"":c)+(o?"":function(t,e,n){return''+re(t)+""}(p,!s,m))+(a?"":function(t,e,n,i){var r=n?"10px":"20px",o=e?"float:right;margin-left:"+r:"";return t=Y(t)?t:[t],''+z(t,(function(t){return re(t)})).join("  ")+""}(f,g,y,x)),n)}}function cg(t,e,n,i,r,o){if(t)return sg(t)({useUTC:r,renderMode:n,orderMode:i,markupStyleCreator:e,valueFormatter:t.valueFormatter},t,0,o)}function pg(t,e){return'
    '+t+'
    '}function dg(t,e,n){return t.markupStyleCreator.wrapRichTextStyle(e,n)}function fg(t,e){return Sp(t.getData().getItemVisual(e,"style")[t.visualDrawType])}function gg(t,e){var n=t.get("padding");return null!=n?n:"richText"===e?[8,10]:10}var yg=function(){function t(){this.richTextStyles={},this._nextStyleNameId=mo()}return t.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},t.prototype.makeTooltipMarker=function(t,e,n){var i="richText"===n?this._generateStyleName():null,r=wp({color:e,type:t,renderMode:n,markerId:i});return U(r)?r:(this.richTextStyles[i]=r.style,r.content)},t.prototype.wrapRichTextStyle=function(t,e){var n={};Y(e)?E(e,(function(t){return A(n,t)})):A(n,e);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+t+"}"},t}();function vg(t){var e,n,i,r,o=t.series,a=t.dataIndex,s=t.multipleSeries,l=o.getData(),u=l.mapDimensionsAll("defaultedTooltip"),h=u.length,c=o.getRawValue(a),p=Y(c),d=fg(o,a);if(h>1||p&&!h){var f=function(t,e,n,i,r){var o=e.getData(),a=V(t,(function(t,e,n){var i=o.getDimensionInfo(n);return t||i&&!1!==i.tooltip&&null!=i.displayName}),!1),s=[],l=[],u=[];function h(t,e){var n=o.getDimensionInfo(e);n&&!1!==n.otherDims.tooltip&&(a?u.push(og("nameValue",{markerType:"subItem",markerColor:r,name:n.displayName,value:t,valueType:n.type})):(s.push(t),l.push(n.type)))}return i.length?E(i,(function(t){h(mf(o,n,t),t)})):E(t,h),{inlineValues:s,inlineValueTypes:l,blocks:u}}(c,o,a,u,d);e=f.inlineValues,n=f.inlineValueTypes,i=f.blocks,r=f.inlineValues[0]}else if(h){var g=l.getDimensionInfo(u[0]);r=e=mf(l,a,u[0]),n=g.type}else r=e=p?c[0]:c;var y=No(o),v=y&&o.name||"",m=l.getName(a),x=s?v:m;return og("section",{header:v,noHeader:s||!y,sortParam:r,blocks:[og("nameValue",{markerType:"item",markerColor:d,name:x,noName:!ut(x),value:e,valueType:n,dataIndex:a})].concat(i||[])})}var mg=Vo();function xg(t,e){return t.getName(e)||t.getId(e)}var _g="__universalTransitionEnabled",bg=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._selectedDataIndicesMap={},e}return n(e,t),e.prototype.init=function(t,e,n){this.seriesIndex=this.componentIndex,this.dataTask=wf({count:Sg,reset:Mg}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n),(mg(this).sourceManager=new $f(this)).prepareSource();var i=this.getInitialData(t,n);Tg(i,this),this.dataTask.context.data=i,mg(this).dataBeforeProcessed=i,wg(this),this._initSelectedMapFromData(i)},e.prototype.mergeDefaultAndTheme=function(t,e){var n=Pp(this),i=n?Rp(t):{},r=this.subType;zp.hasClass(r)&&(r+="Series"),C(t,e.getTheme().get(this.subType)),C(t,this.getDefaultOption()),Co(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&Op(t,i,n)},e.prototype.mergeOption=function(t,e){t=C(this.option,t,!0),this.fillDataTextStyle(t.data);var n=Pp(this);n&&Op(this.option,t,n);var i=mg(this).sourceManager;i.dirty(),i.prepareSource();var r=this.getInitialData(t,e);Tg(r,this),this.dataTask.dirty(),this.dataTask.context.data=r,mg(this).dataBeforeProcessed=r,wg(this),this._initSelectedMapFromData(r)},e.prototype.fillDataTextStyle=function(t){if(t&&!$(t))for(var e=["show"],n=0;nthis.getShallow("animationThreshold")&&(e=!1),!!e},e.prototype.restoreData=function(){this.dataTask.dirty()},e.prototype.getColorFromPalette=function(t,e,n){var i=this.ecModel,r=cd.prototype.getColorFromPalette.call(this,t,e,n);return r||(r=i.getColorFromPalette(t,e,n)),r},e.prototype.coordDimToDataDim=function(t){return this.getRawData().mapDimensionsAll(t)},e.prototype.getProgressive=function(){return this.get("progressive")},e.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},e.prototype.select=function(t,e){this._innerSelect(this.getData(e),t)},e.prototype.unselect=function(t,e){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,r=this.getData(e);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var o=0;o=0&&n.push(r)}return n},e.prototype.isSelected=function(t,e){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(e);return("all"===n||n[xg(i,t)])&&!i.getItemModel(t).get(["select","disabled"])},e.prototype.isUniversalTransitionEnabled=function(){if(this[_g])return!0;var t=this.option.universalTransition;return!!t&&(!0===t||t&&t.enabled)},e.prototype._innerSelect=function(t,e){var n,i,r=this.option,o=r.selectedMode,a=e.length;if(o&&a)if("series"===o)r.selectedMap="all";else if("multiple"===o){q(r.selectedMap)||(r.selectedMap={});for(var s=r.selectedMap,l=0;l0&&this._innerSelect(t,e)}},e.registerClass=function(t){return zp.registerClass(t)},e.protoInitialize=function(){var t=e.prototype;t.type="series.__base__",t.seriesIndex=0,t.ignoreStyleOnData=!1,t.hasSymbolVisual=!1,t.defaultSymbol="circle",t.visualStyleAccessPath="itemStyle",t.visualDrawType="fill"}(),e}(zp);function wg(t){var e=t.name;No(t)||(t.name=function(t){var e=t.getRawData(),n=e.mapDimensionsAll("seriesName"),i=[];return E(n,(function(t){var n=e.getDimensionInfo(t);n.displayName&&i.push(n.displayName)})),i.join(" ")}(t)||e)}function Sg(t){return t.model.getRawData().count()}function Mg(t){var e=t.model;return e.setData(e.getRawData().cloneShallow()),Ig}function Ig(t,e){e.outputData&&t.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function Tg(t,e){E(vt(t.CHANGABLE_METHODS,t.DOWNSAMPLE_METHODS),(function(n){t.wrapMethod(n,H(Cg,e))}))}function Cg(t,e){var n=Dg(t);return n&&n.setOutputEnd((e||this).count()),e}function Dg(t){var e=(t.ecModel||{}).scheduler,n=e&&e.getPipeline(t.uid);if(n){var i=n.currentTask;if(i){var r=i.agentStubMap;r&&(i=r.get(t.uid))}return i}}R(bg,_f),R(bg,cd),Jo(bg,zp);var Ag=function(){function t(){this.group=new Br,this.uid=Ac("viewComponent")}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){},t.prototype.updateLayout=function(t,e,n,i){},t.prototype.updateVisual=function(t,e,n,i){},t.prototype.toggleBlurSeries=function(t,e,n){},t.prototype.eachRendered=function(t){var e=this.group;e&&e.traverse(t)},t}();function kg(){var t=Vo();return function(e){var n=t(e),i=e.pipelineContext,r=!!n.large,o=!!n.progressiveRender,a=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(r===a&&o===s)&&"reset"}}$o(Ag),na(Ag);var Lg=Vo(),Pg=kg(),Og=function(){function t(){this.group=new Br,this.uid=Ac("viewChart"),this.renderTask=wf({plan:Eg,reset:zg}),this.renderTask.context={view:this}}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){0},t.prototype.highlight=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&Ng(r,i,"emphasis")},t.prototype.downplay=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&Ng(r,i,"normal")},t.prototype.remove=function(t,e){this.group.removeAll()},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateLayout=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateVisual=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.eachRendered=function(t){Jh(this.group,t)},t.markUpdateMethod=function(t,e){Lg(t).updateMethod=e},t.protoInitialize=void(t.prototype.type="chart"),t}();function Rg(t,e,n){t&&Ql(t)&&("emphasis"===e?Ol:Rl)(t,n)}function Ng(t,e,n){var i=zo(t,e),r=e&&null!=e.highlightKey?function(t){var e=sl[t];return null==e&&al<=32&&(e=sl[t]=al++),e}(e.highlightKey):null;null!=i?E(To(i),(function(e){Rg(t.getItemGraphicEl(e),n,r)})):t.eachItemGraphicEl((function(t){Rg(t,n,r)}))}function Eg(t){return Pg(t.model)}function zg(t){var e=t.model,n=t.ecModel,i=t.api,r=t.payload,o=e.pipelineContext.progressiveRender,a=t.view,s=r&&Lg(r).updateMethod,l=o?"incrementalPrepareRender":s&&a[s]?s:"render";return"render"!==l&&a[l](e,n,i,r),Vg[l]}$o(Og),na(Og);var Vg={incrementalPrepareRender:{progress:function(t,e){e.view.incrementalRender(t,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(t,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},Bg="\0__throttleOriginMethod",Fg="\0__throttleRate",Gg="\0__throttleType";function Wg(t,e,n){var i,r,o,a,s,l=0,u=0,h=null;function c(){u=(new Date).getTime(),h=null,t.apply(o,a||[])}e=e||0;var p=function(){for(var t=[],p=0;p=0?c():h=setTimeout(c,-r),l=i};return p.clear=function(){h&&(clearTimeout(h),h=null)},p.debounceNextCall=function(t){s=t},p}function Hg(t,e,n,i){var r=t[e];if(r){var o=r[Bg]||r,a=r[Gg];if(r[Fg]!==n||a!==i){if(null==n||!i)return t[e]=o;(r=t[e]=Wg(o,n,"debounce"===i))[Bg]=o,r[Gg]=i,r[Fg]=n}return r}}function Yg(t,e){var n=t[e];n&&n[Bg]&&(n.clear&&n.clear(),t[e]=n[Bg])}var Xg=Vo(),Ug={itemStyle:ia(Mc,!0),lineStyle:ia(bc,!0)},Zg={lineStyle:"stroke",itemStyle:"fill"};function jg(t,e){var n=t.visualStyleMapper||Ug[e];return n||(console.warn("Unknown style type '"+e+"'."),Ug.itemStyle)}function qg(t,e){var n=t.visualDrawType||Zg[e];return n||(console.warn("Unknown style type '"+e+"'."),"fill")}var Kg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=t.getModel(i),o=jg(t,i)(r),a=r.getShallow("decal");a&&(n.setVisual("decal",a),a.dirty=!0);var s=qg(t,i),l=o[s],u=X(l)?l:null,h="auto"===o.fill||"auto"===o.stroke;if(!o[s]||u||h){var c=t.getColorFromPalette(t.name,null,e.getSeriesCount());o[s]||(o[s]=c,n.setVisual("colorFromPalette",!0)),o.fill="auto"===o.fill||X(o.fill)?c:o.fill,o.stroke="auto"===o.stroke||X(o.stroke)?c:o.stroke}if(n.setVisual("style",o),n.setVisual("drawType",s),!e.isSeriesFiltered(t)&&u)return n.setVisual("colorFromPalette",!1),{dataEach:function(e,n){var i=t.getDataParams(n),r=A({},o);r[s]=u(i),e.setItemVisual(n,"style",r)}}}},$g=new Cc,Jg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){if(!t.ignoreStyleOnData&&!e.isSeriesFiltered(t)){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=jg(t,i),o=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(t,e){var n=t.getRawDataItem(e);if(n&&n[i]){$g.option=n[i];var a=r($g);A(t.ensureUniqueItemVisual(e,"style"),a),$g.option.decal&&(t.setItemVisual(e,"decal",$g.option.decal),$g.option.decal.dirty=!0),o in a&&t.setItemVisual(e,"colorFromPalette",!1)}}:null}}}},Qg={performRawSeries:!0,overallReset:function(t){var e=yt();t.eachSeries((function(t){var n=t.getColorBy();if(!t.isColorBySeries()){var i=t.type+"-"+n,r=e.get(i);r||(r={},e.set(i,r)),Xg(t).scope=r}})),t.eachSeries((function(e){if(!e.isColorBySeries()&&!t.isSeriesFiltered(e)){var n=e.getRawData(),i={},r=e.getData(),o=Xg(e).scope,a=e.visualStyleAccessPath||"itemStyle",s=qg(e,a);r.each((function(t){var e=r.getRawIndex(t);i[e]=t})),n.each((function(t){var a=i[t];if(r.getItemVisual(a,"colorFromPalette")){var l=r.ensureUniqueItemVisual(a,"style"),u=n.getName(t)||t+"",h=n.count();l[s]=e.getColorFromPalette(u,o,h)}}))}}))}},ty=Math.PI;var ey=function(){function t(t,e,n,i){this._stageTaskMap=yt(),this.ecInstance=t,this.api=e,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return t.prototype.restoreData=function(t,e){t.restoreData(e),this._stageTaskMap.each((function(t){var e=t.overallTask;e&&e.dirty()}))},t.prototype.getPerformArgs=function(t,e){if(t.__pipeline){var n=this._pipelineMap.get(t.__pipeline.id),i=n.context,r=!e&&n.progressiveEnabled&&(!i||i.progressiveRender)&&t.__idxInPipeline>n.blockIndex?n.step:null,o=i&&i.modDataCount;return{step:r,modBy:null!=o?Math.ceil(o/r):null,modDataCount:o}}},t.prototype.getPipeline=function(t){return this._pipelineMap.get(t)},t.prototype.updateStreamModes=function(t,e){var n=this._pipelineMap.get(t.uid),i=t.getData().count(),r=n.progressiveEnabled&&e.incrementalPrepareRender&&i>=n.threshold,o=t.get("large")&&i>=t.get("largeThreshold"),a="mod"===t.get("progressiveChunkMode")?i:null;t.pipelineContext=n.context={progressiveRender:r,modDataCount:a,large:o}},t.prototype.restorePipelines=function(t){var e=this,n=e._pipelineMap=yt();t.eachSeries((function(t){var i=t.getProgressive(),r=t.uid;n.set(r,{id:r,head:null,tail:null,threshold:t.getProgressiveThreshold(),progressiveEnabled:i&&!(t.preventIncremental&&t.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),e._pipe(t,t.dataTask)}))},t.prototype.prepareStageTasks=function(){var t=this._stageTaskMap,e=this.api.getModel(),n=this.api;E(this._allHandlers,(function(i){var r=t.get(i.uid)||t.set(i.uid,{}),o="";lt(!(i.reset&&i.overallReset),o),i.reset&&this._createSeriesStageTask(i,r,e,n),i.overallReset&&this._createOverallStageTask(i,r,e,n)}),this)},t.prototype.prepareView=function(t,e,n,i){var r=t.renderTask,o=r.context;o.model=e,o.ecModel=n,o.api=i,r.__block=!t.incrementalPrepareRender,this._pipe(e,r)},t.prototype.performDataProcessorTasks=function(t,e){this._performStageTasks(this._dataProcessorHandlers,t,e,{block:!0})},t.prototype.performVisualTasks=function(t,e,n){this._performStageTasks(this._visualHandlers,t,e,n)},t.prototype._performStageTasks=function(t,e,n,i){i=i||{};var r=!1,o=this;function a(t,e){return t.setDirty&&(!t.dirtyMap||t.dirtyMap.get(e.__pipeline.id))}E(t,(function(t,s){if(!i.visualType||i.visualType===t.visualType){var l=o._stageTaskMap.get(t.uid),u=l.seriesTaskMap,h=l.overallTask;if(h){var c,p=h.agentStubMap;p.each((function(t){a(i,t)&&(t.dirty(),c=!0)})),c&&h.dirty(),o.updatePayload(h,n);var d=o.getPerformArgs(h,i.block);p.each((function(t){t.perform(d)})),h.perform(d)&&(r=!0)}else u&&u.each((function(s,l){a(i,s)&&s.dirty();var u=o.getPerformArgs(s,i.block);u.skip=!t.performRawSeries&&e.isSeriesFiltered(s.context.model),o.updatePayload(s,n),s.perform(u)&&(r=!0)}))}})),this.unfinished=r||this.unfinished},t.prototype.performSeriesTasks=function(t){var e;t.eachSeries((function(t){e=t.dataTask.perform()||e})),this.unfinished=e||this.unfinished},t.prototype.plan=function(){this._pipelineMap.each((function(t){var e=t.tail;do{if(e.__block){t.blockIndex=e.__idxInPipeline;break}e=e.getUpstream()}while(e)}))},t.prototype.updatePayload=function(t,e){"remain"!==e&&(t.context.payload=e)},t.prototype._createSeriesStageTask=function(t,e,n,i){var r=this,o=e.seriesTaskMap,a=e.seriesTaskMap=yt(),s=t.seriesType,l=t.getTargetSeries;function u(e){var s=e.uid,l=a.set(s,o&&o.get(s)||wf({plan:ay,reset:sy,count:hy}));l.context={model:e,ecModel:n,api:i,useClearVisual:t.isVisual&&!t.isLayout,plan:t.plan,reset:t.reset,scheduler:r},r._pipe(e,l)}t.createOnAllSeries?n.eachRawSeries(u):s?n.eachRawSeriesByType(s,u):l&&l(n,i).each(u)},t.prototype._createOverallStageTask=function(t,e,n,i){var r=this,o=e.overallTask=e.overallTask||wf({reset:ny});o.context={ecModel:n,api:i,overallReset:t.overallReset,scheduler:r};var a=o.agentStubMap,s=o.agentStubMap=yt(),l=t.seriesType,u=t.getTargetSeries,h=!0,c=!1,p="";function d(t){var e=t.uid,n=s.set(e,a&&a.get(e)||(c=!0,wf({reset:iy,onDirty:oy})));n.context={model:t,overallProgress:h},n.agent=o,n.__block=h,r._pipe(t,n)}lt(!t.createOnAllSeries,p),l?n.eachRawSeriesByType(l,d):u?u(n,i).each(d):(h=!1,E(n.getSeries(),d)),c&&o.dirty()},t.prototype._pipe=function(t,e){var n=t.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=e),i.tail&&i.tail.pipe(e),i.tail=e,e.__idxInPipeline=i.count++,e.__pipeline=i},t.wrapStageHandler=function(t,e){return X(t)&&(t={overallReset:t,seriesType:cy(t)}),t.uid=Ac("stageHandler"),e&&(t.visualType=e),t},t}();function ny(t){t.overallReset(t.ecModel,t.api,t.payload)}function iy(t){return t.overallProgress&&ry}function ry(){this.agent.dirty(),this.getDownstream().dirty()}function oy(){this.agent&&this.agent.dirty()}function ay(t){return t.plan?t.plan(t.model,t.ecModel,t.api,t.payload):null}function sy(t){t.useClearVisual&&t.data.clearAllVisual();var e=t.resetDefines=To(t.reset(t.model,t.ecModel,t.api,t.payload));return e.length>1?z(e,(function(t,e){return uy(e)})):ly}var ly=uy(0);function uy(t){return function(e,n){var i=n.data,r=n.resetDefines[t];if(r&&r.dataEach)for(var o=e.start;o0&&h===r.length-u.length){var c=r.slice(0,h);"data"!==c&&(e.mainType=c,e[u.toLowerCase()]=t,s=!0)}}a.hasOwnProperty(r)&&(n[r]=t,s=!0),s||(i[r]=t)}))}return{cptQuery:e,dataQuery:n,otherQuery:i}},t.prototype.filter=function(t,e){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,r=n.packedEvent,o=n.model,a=n.view;if(!o||!a)return!0;var s=e.cptQuery,l=e.dataQuery;return u(s,o,"mainType")&&u(s,o,"subType")&&u(s,o,"index","componentIndex")&&u(s,o,"name")&&u(s,o,"id")&&u(l,r,"name")&&u(l,r,"dataIndex")&&u(l,r,"dataType")&&(!a.filterForExposedEvent||a.filterForExposedEvent(t,e.otherQuery,i,r));function u(t,e,n,i){return null==t[n]||e[i||n]===t[n]}},t.prototype.afterTrigger=function(){this.eventInfo=null},t}(),My=["symbol","symbolSize","symbolRotate","symbolOffset"],Iy=My.concat(["symbolKeepAspect"]),Ty={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData();if(t.legendIcon&&n.setVisual("legendIcon",t.legendIcon),t.hasSymbolVisual){for(var i={},r={},o=!1,a=0;a=0&&jy(l)?l:.5,t.createRadialGradient(a,s,0,a,s,l)}(t,e,n):function(t,e,n){var i=null==e.x?0:e.x,r=null==e.x2?1:e.x2,o=null==e.y?0:e.y,a=null==e.y2?0:e.y2;return e.global||(i=i*n.width+n.x,r=r*n.width+n.x,o=o*n.height+n.y,a=a*n.height+n.y),i=jy(i)?i:0,r=jy(r)?r:1,o=jy(o)?o:0,a=jy(a)?a:0,t.createLinearGradient(i,o,r,a)}(t,e,n),r=e.colorStops,o=0;o0&&(e=i.lineDash,n=i.lineWidth,e&&"solid"!==e&&n>0?"dashed"===e?[4*n,2*n]:"dotted"===e?[n]:j(e)?[e]:Y(e)?e:null:null),o=i.lineDashOffset;if(r){var a=i.strokeNoScale&&t.getLineScale?t.getLineScale():1;a&&1!==a&&(r=z(r,(function(t){return t/a})),o/=a)}return[r,o]}var Qy=new hs(!0);function tv(t){var e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))}function ev(t){return"string"==typeof t&&"none"!==t}function nv(t){var e=t.fill;return null!=e&&"none"!==e}function iv(t,e){if(null!=e.fillOpacity&&1!==e.fillOpacity){var n=t.globalAlpha;t.globalAlpha=e.fillOpacity*e.opacity,t.fill(),t.globalAlpha=n}else t.fill()}function rv(t,e){if(null!=e.strokeOpacity&&1!==e.strokeOpacity){var n=t.globalAlpha;t.globalAlpha=e.strokeOpacity*e.opacity,t.stroke(),t.globalAlpha=n}else t.stroke()}function ov(t,e,n){var i=la(e.image,e.__image,n);if(ha(i)){var r=t.createPattern(i,e.repeat||"repeat");if("function"==typeof DOMMatrix&&r&&r.setTransform){var o=new DOMMatrix;o.translateSelf(e.x||0,e.y||0),o.rotateSelf(0,0,(e.rotation||0)*wt),o.scaleSelf(e.scaleX||1,e.scaleY||1),r.setTransform(o)}return r}}var av=["shadowBlur","shadowOffsetX","shadowOffsetY"],sv=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function lv(t,e,n,i,r){var o=!1;if(!i&&e===(n=n||{}))return!1;if(i||e.opacity!==n.opacity){cv(t,r),o=!0;var a=Math.max(Math.min(e.opacity,1),0);t.globalAlpha=isNaN(a)?Ma.opacity:a}(i||e.blend!==n.blend)&&(o||(cv(t,r),o=!0),t.globalCompositeOperation=e.blend||Ma.blend);for(var s=0;s0&&t.unfinished);t.unfinished||this._zr.flush()}}},e.prototype.getDom=function(){return this._dom},e.prototype.getId=function(){return this.id},e.prototype.getZr=function(){return this._zr},e.prototype.isSSR=function(){return this._ssr},e.prototype.setOption=function(t,e,n){if(!this[Dv])if(this._disposed)om(this.id);else{var i,r,o;if(q(e)&&(n=e.lazyUpdate,i=e.silent,r=e.replaceMerge,o=e.transition,e=e.notMerge),this[Dv]=!0,!this._model||e){var a=new Md(this._api),s=this._theme,l=this._model=new gd;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,a)}this._model.setOption(t,{replaceMerge:r},um);var u={seriesTransition:o,optionChanged:!0};if(n)this[Av]={silent:i,updateParams:u},this[Dv]=!1,this.getZr().wakeUp();else{try{Ev(this),Bv.update.call(this,null,u)}catch(t){throw this[Av]=null,this[Dv]=!1,t}this._ssr||this._zr.flush(),this[Av]=null,this[Dv]=!1,Hv.call(this,i),Yv.call(this,i)}}},e.prototype.setTheme=function(){bo()},e.prototype.getModel=function(){return this._model},e.prototype.getOption=function(){return this._model&&this._model.getOption()},e.prototype.getWidth=function(){return this._zr.getWidth()},e.prototype.getHeight=function(){return this._zr.getHeight()},e.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||r.hasGlobalWindow&&window.devicePixelRatio||1},e.prototype.getRenderedCanvas=function(t){return this.renderToCanvas(t)},e.prototype.renderToCanvas=function(t){t=t||{};var e=this._zr.painter;return e.getRenderedCanvas({backgroundColor:t.backgroundColor||this._model.get("backgroundColor"),pixelRatio:t.pixelRatio||this.getDevicePixelRatio()})},e.prototype.renderToSVGString=function(t){t=t||{};var e=this._zr.painter;return e.renderToString({useViewBox:t.useViewBox})},e.prototype.getSvgDataURL=function(){if(r.svgSupported){var t=this._zr;return E(t.storage.getDisplayList(),(function(t){t.stopAnimation(null,!0)})),t.painter.toDataURL()}},e.prototype.getDataURL=function(t){if(!this._disposed){var e=(t=t||{}).excludeComponents,n=this._model,i=[],r=this;E(e,(function(t){n.eachComponent({mainType:t},(function(t){var e=r._componentsMap[t.__viewId];e.group.ignore||(i.push(e),e.group.ignore=!0)}))}));var o="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return E(i,(function(t){t.group.ignore=!1})),o}om(this.id)},e.prototype.getConnectedDataURL=function(t){if(!this._disposed){var e="svg"===t.type,n=this.group,i=Math.min,r=Math.max,o=1/0;if(fm[n]){var a=o,s=o,l=-1/0,u=-1/0,c=[],p=t&&t.pixelRatio||this.getDevicePixelRatio();E(dm,(function(o,h){if(o.group===n){var p=e?o.getZr().painter.getSvgDom().innerHTML:o.renderToCanvas(T(t)),d=o.getDom().getBoundingClientRect();a=i(d.left,a),s=i(d.top,s),l=r(d.right,l),u=r(d.bottom,u),c.push({dom:p,left:d.left,top:d.top})}}));var d=(l*=p)-(a*=p),f=(u*=p)-(s*=p),g=h.createCanvas(),y=Yr(g,{renderer:e?"svg":"canvas"});if(y.resize({width:d,height:f}),e){var v="";return E(c,(function(t){var e=t.left-a,n=t.top-s;v+=''+t.dom+""})),y.painter.getSvgRoot().innerHTML=v,t.connectedBackgroundColor&&y.painter.setBackgroundColor(t.connectedBackgroundColor),y.refreshImmediately(),y.painter.toDataURL()}return t.connectedBackgroundColor&&y.add(new Ws({shape:{x:0,y:0,width:d,height:f},style:{fill:t.connectedBackgroundColor}})),E(c,(function(t){var e=new Ns({style:{x:t.left*p-a,y:t.top*p-s,image:t.dom}});y.add(e)})),y.refreshImmediately(),g.toDataURL("image/"+(t&&t.type||"png"))}return this.getDataURL(t)}om(this.id)},e.prototype.convertToPixel=function(t,e){return Fv(this,"convertToPixel",t,e)},e.prototype.convertFromPixel=function(t,e){return Fv(this,"convertFromPixel",t,e)},e.prototype.containPixel=function(t,e){var n;if(!this._disposed)return E(Fo(this._model,t),(function(t,i){i.indexOf("Models")>=0&&E(t,(function(t){var r=t.coordinateSystem;if(r&&r.containPoint)n=n||!!r.containPoint(e);else if("seriesModels"===i){var o=this._chartsMap[t.__viewId];o&&o.containPoint&&(n=n||o.containPoint(e,t))}else 0}),this)}),this),!!n;om(this.id)},e.prototype.getVisual=function(t,e){var n=Fo(this._model,t,{defaultMainType:"series"}),i=n.seriesModel;var r=i.getData(),o=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?r.indexOfRawIndex(n.dataIndex):null;return null!=o?Dy(r,o,e):Ay(r,e)},e.prototype.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},e.prototype.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]},e.prototype._initEvents=function(){var t,e,n,i=this;E(rm,(function(t){var e=function(e){var n,r=i.getModel(),o=e.target,a="globalout"===t;if(a?n={}:o&&Oy(o,(function(t){var e=rl(t);if(e&&null!=e.dataIndex){var i=e.dataModel||r.getSeriesByIndex(e.seriesIndex);return n=i&&i.getDataParams(e.dataIndex,e.dataType,o)||{},!0}if(e.eventData)return n=A({},e.eventData),!0}),!0),n){var s=n.componentType,l=n.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=n.seriesIndex);var u=s&&null!=l&&r.getComponent(s,l),h=u&&i["series"===u.mainType?"_chartsMap":"_componentsMap"][u.__viewId];0,n.event=e,n.type=t,i._$eventProcessor.eventInfo={targetEl:o,packedEvent:n,model:u,view:h},i.trigger(t,n)}};e.zrEventfulCallAtLast=!0,i._zr.on(t,e,i)})),E(sm,(function(t,e){i._messageCenter.on(e,(function(t){this.trigger(e,t)}),i)})),E(["selectchanged"],(function(t){i._messageCenter.on(t,(function(e){this.trigger(t,e)}),i)})),t=this._messageCenter,e=this,n=this._api,t.on("selectchanged",(function(t){var i=n.getModel();t.isFromClick?(Py("map","selectchanged",e,i,t),Py("pie","selectchanged",e,i,t)):"select"===t.fromAction?(Py("map","selected",e,i,t),Py("pie","selected",e,i,t)):"unselect"===t.fromAction&&(Py("map","unselected",e,i,t),Py("pie","unselected",e,i,t))}))},e.prototype.isDisposed=function(){return this._disposed},e.prototype.clear=function(){this._disposed?om(this.id):this.setOption({series:[]},!0)},e.prototype.dispose=function(){if(this._disposed)om(this.id);else{this._disposed=!0,this.getDom()&&Xo(this.getDom(),vm,"");var t=this,e=t._api,n=t._model;E(t._componentsViews,(function(t){t.dispose(n,e)})),E(t._chartsViews,(function(t){t.dispose(n,e)})),t._zr.dispose(),t._dom=t._model=t._chartsMap=t._componentsMap=t._chartsViews=t._componentsViews=t._scheduler=t._api=t._zr=t._throttledZrFlush=t._theme=t._coordSysMgr=t._messageCenter=null,delete dm[t.id]}},e.prototype.resize=function(t){if(!this[Dv])if(this._disposed)om(this.id);else{this._zr.resize(t);var e=this._model;if(this._loadingFX&&this._loadingFX.resize(),e){var n=e.resetOption("media"),i=t&&t.silent;this[Av]&&(null==i&&(i=this[Av].silent),n=!0,this[Av]=null),this[Dv]=!0;try{n&&Ev(this),Bv.update.call(this,{type:"resize",animation:A({duration:0},t&&t.animation)})}catch(t){throw this[Dv]=!1,t}this[Dv]=!1,Hv.call(this,i),Yv.call(this,i)}}},e.prototype.showLoading=function(t,e){if(this._disposed)om(this.id);else if(q(t)&&(e=t,t=""),t=t||"default",this.hideLoading(),pm[t]){var n=pm[t](this._api,e),i=this._zr;this._loadingFX=n,i.add(n)}},e.prototype.hideLoading=function(){this._disposed?om(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},e.prototype.makeActionFromEvent=function(t){var e=A({},t);return e.type=sm[t.type],e},e.prototype.dispatchAction=function(t,e){if(this._disposed)om(this.id);else if(q(e)||(e={silent:!!e}),am[t.type]&&this._model)if(this[Dv])this._pendingActions.push(t);else{var n=e.silent;Wv.call(this,t,n);var i=e.flush;i?this._zr.flush():!1!==i&&r.browser.weChat&&this._throttledZrFlush(),Hv.call(this,n),Yv.call(this,n)}},e.prototype.updateLabelLayout=function(){wv.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},e.prototype.appendData=function(t){if(this._disposed)om(this.id);else{var e=t.seriesIndex,n=this.getModel().getSeriesByIndex(e);0,n.appendData(t),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},e.internalField=function(){function t(t){t.clearColorPalette(),t.eachSeries((function(t){t.clearColorPalette()}))}function e(t){for(var e=[],n=t.currentStates,i=0;i0?{duration:o,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered((function(t){if(t.states&&t.states.emphasis){if(xh(t))return;if(t instanceof ks&&function(t){var e=ll(t);e.normalFill=t.style.fill,e.normalStroke=t.style.stroke;var n=t.states.select||{};e.selectFill=n.style&&n.style.fill||null,e.selectStroke=n.style&&n.style.stroke||null}(t),t.__dirty){var n=t.prevStates;n&&t.useStates(n)}if(r){t.stateTransition=a;var i=t.getTextContent(),o=t.getTextGuideLine();i&&(i.stateTransition=a),o&&(o.stateTransition=a)}t.__dirty&&e(t)}}))}Ev=function(t){var e=t._scheduler;e.restorePipelines(t._model),e.prepareStageTasks(),zv(t,!0),zv(t,!1),e.plan()},zv=function(t,e){for(var n=t._model,i=t._scheduler,r=e?t._componentsViews:t._chartsViews,o=e?t._componentsMap:t._chartsMap,a=t._zr,s=t._api,l=0;le.get("hoverLayerThreshold")&&!r.node&&!r.worker&&e.eachSeries((function(e){if(!e.preventUsingHoverLayer){var n=t._chartsMap[e.__viewId];n.__alive&&n.eachRendered((function(t){t.states.emphasis&&(t.states.emphasis.hoverLayer=!0)}))}}))}(t,e),wv.trigger("series:afterupdate",e,n,l)},Jv=function(t){t[kv]=!0,t.getZr().wakeUp()},Qv=function(t){t[kv]&&(t.getZr().storage.traverse((function(t){xh(t)||e(t)})),t[kv]=!1)},Kv=function(t){return new(function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return n(i,e),i.prototype.getCoordinateSystems=function(){return t._coordSysMgr.getCoordinateSystems()},i.prototype.getComponentByElement=function(e){for(;e;){var n=e.__ecComponentInfo;if(null!=n)return t._model.getComponent(n.mainType,n.index);e=e.parent}},i.prototype.enterEmphasis=function(e,n){Ol(e,n),Jv(t)},i.prototype.leaveEmphasis=function(e,n){Rl(e,n),Jv(t)},i.prototype.enterBlur=function(e){Nl(e),Jv(t)},i.prototype.leaveBlur=function(e){El(e),Jv(t)},i.prototype.enterSelect=function(e){zl(e),Jv(t)},i.prototype.leaveSelect=function(e){Vl(e),Jv(t)},i.prototype.getModel=function(){return t.getModel()},i.prototype.getViewOfComponentModel=function(e){return t.getViewOfComponentModel(e)},i.prototype.getViewOfSeriesModel=function(e){return t.getViewOfSeriesModel(e)},i}(_d))(t)},$v=function(t){function e(t,e){for(var n=0;n=0)){Lm.push(n);var o=ey.wrapStageHandler(n,r);o.__prio=e,o.__raw=n,t.push(o)}}function Om(t,e){pm[t]=e}function Rm(t,e,n){var i=Mv("registerMap");i&&i(t,e,n)}var Nm=function(t){var e=(t=T(t)).type,n="";e||wo(n);var i=e.split(":");2!==i.length&&wo(n);var r=!1;"echarts"===i[0]&&(e=i[1],r=!0),t.__isBuiltIn=r,Vf.set(e,t)};km(Iv,Kg),km(Tv,Jg),km(Tv,Qg),km(Iv,Ty),km(Tv,Cy),km(7e3,(function(t,e){t.eachRawSeries((function(n){if(!t.isSeriesFiltered(n)){var i=n.getData();i.hasItemVisual()&&i.each((function(t){var n=i.getItemVisual(t,"decal");n&&(i.ensureUniqueItemVisual(t,"style").decal=mv(n,e))}));var r=i.getVisual("decal");if(r)i.getVisual("style").decal=mv(r,e)}}))})),wm(Xd),Sm(900,(function(t){var e=yt();t.eachSeries((function(t){var n=t.get("stack");if(n){var i=e.get(n)||e.set(n,[]),r=t.getData(),o={stackResultDimension:r.getCalculationInfo("stackResultDimension"),stackedOverDimension:r.getCalculationInfo("stackedOverDimension"),stackedDimension:r.getCalculationInfo("stackedDimension"),stackedByDimension:r.getCalculationInfo("stackedByDimension"),isStackedByIndex:r.getCalculationInfo("isStackedByIndex"),data:r,seriesModel:t};if(!o.stackedDimension||!o.isStackedByIndex&&!o.stackedByDimension)return;i.length&&r.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(o)}})),e.each(Ud)})),Om("default",(function(t,e){k(e=e||{},{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var n=new Br,i=new Ws({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4});n.add(i);var r,o=new Xs({style:{text:e.text,fill:e.textColor,fontSize:e.fontSize,fontWeight:e.fontWeight,fontStyle:e.fontStyle,fontFamily:e.fontFamily},zlevel:e.zlevel,z:10001}),a=new Ws({style:{fill:"none"},textContent:o,textConfig:{position:"right",distance:10},zlevel:e.zlevel,z:10001});return n.add(a),e.showSpinner&&((r=new nh({shape:{startAngle:-ty/2,endAngle:-ty/2+.1,r:e.spinnerRadius},style:{stroke:e.color,lineCap:"round",lineWidth:e.lineWidth},zlevel:e.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*ty/2}).start("circularInOut"),r.animateShape(!0).when(1e3,{startAngle:3*ty/2}).delay(300).start("circularInOut"),n.add(r)),n.resize=function(){var n=o.getBoundingRect().width,s=e.showSpinner?e.spinnerRadius:0,l=(t.getWidth()-2*s-(e.showSpinner&&n?10:0)-n)/2-(e.showSpinner&&n?0:5+n/2)+(e.showSpinner?0:n/2)+(n?0:s),u=t.getHeight()/2;e.showSpinner&&r.setShape({cx:l,cy:u}),a.setShape({x:l-s,y:u-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:t.getWidth(),height:t.getHeight()})},n.resize(),n})),Cm({type:dl,event:dl,update:dl},bt),Cm({type:fl,event:fl,update:fl},bt),Cm({type:gl,event:gl,update:gl},bt),Cm({type:yl,event:yl,update:yl},bt),Cm({type:vl,event:vl,update:vl},bt),bm("light",vy),bm("dark",wy);var Em=[],zm={registerPreprocessor:wm,registerProcessor:Sm,registerPostInit:Mm,registerPostUpdate:Im,registerUpdateLifecycle:Tm,registerAction:Cm,registerCoordinateSystem:Dm,registerLayout:Am,registerVisual:km,registerTransform:Nm,registerLoading:Om,registerMap:Rm,registerImpl:function(t,e){Sv[t]=e},PRIORITY:Cv,ComponentModel:zp,ComponentView:Ag,SeriesModel:bg,ChartView:Og,registerComponentModel:function(t){zp.registerClass(t)},registerComponentView:function(t){Ag.registerClass(t)},registerSeriesModel:function(t){bg.registerClass(t)},registerChartView:function(t){Og.registerClass(t)},registerSubTypeDefaulter:function(t,e){zp.registerSubTypeDefaulter(t,e)},registerPainter:function(t,e){Xr(t,e)}};function Vm(t){Y(t)?E(t,(function(t){Vm(t)})):P(Em,t)>=0||(Em.push(t),X(t)&&(t={install:t}),t.install(zm))}function Bm(t){return null==t?0:t.length||1}function Fm(t){return t}var Gm=function(){function t(t,e,n,i,r,o){this._old=t,this._new=e,this._oldKeyGetter=n||Fm,this._newKeyGetter=i||Fm,this.context=r,this._diffModeMultiple="multiple"===o}return t.prototype.add=function(t){return this._add=t,this},t.prototype.update=function(t){return this._update=t,this},t.prototype.updateManyToOne=function(t){return this._updateManyToOne=t,this},t.prototype.updateOneToMany=function(t){return this._updateOneToMany=t,this},t.prototype.updateManyToMany=function(t){return this._updateManyToMany=t,this},t.prototype.remove=function(t){return this._remove=t,this},t.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},t.prototype._executeOneToOne=function(){var t=this._old,e=this._new,n={},i=new Array(t.length),r=new Array(e.length);this._initIndexMap(t,null,i,"_oldKeyGetter"),this._initIndexMap(e,n,r,"_newKeyGetter");for(var o=0;o1){var u=s.shift();1===s.length&&(n[a]=s[0]),this._update&&this._update(u,o)}else 1===l?(n[a]=null,this._update&&this._update(s,o)):this._remove&&this._remove(o)}this._performRestAdd(r,n)},t.prototype._executeMultiple=function(){var t=this._old,e=this._new,n={},i={},r=[],o=[];this._initIndexMap(t,n,r,"_oldKeyGetter"),this._initIndexMap(e,i,o,"_newKeyGetter");for(var a=0;a1&&1===c)this._updateManyToOne&&this._updateManyToOne(u,l),i[s]=null;else if(1===h&&c>1)this._updateOneToMany&&this._updateOneToMany(u,l),i[s]=null;else if(1===h&&1===c)this._update&&this._update(u,l),i[s]=null;else if(h>1&&c>1)this._updateManyToMany&&this._updateManyToMany(u,l),i[s]=null;else if(h>1)for(var p=0;p1)for(var a=0;a30}var Qm,tx,ex,nx,ix,rx,ox,ax=q,sx=z,lx="undefined"==typeof Int32Array?Array:Int32Array,ux=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],hx=["_approximateExtent"],cx=function(){function t(t,e){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var i=!1;qm(t)?(n=t.dimensions,this._dimOmitted=t.isDimensionOmitted(),this._schema=t):(i=!0,n=t),n=n||["x","y"];for(var r={},o=[],a={},s=!1,l={},u=0;u=e)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,r=this._idList;if(n.getSource().sourceFormat===Wp&&!n.pure)for(var o=[],a=t;a0},t.prototype.ensureUniqueItemVisual=function(t,e){var n=this._itemVisuals,i=n[t];i||(i=n[t]={});var r=i[e];return null==r&&(Y(r=this.getVisual(e))?r=r.slice():ax(r)&&(r=A({},r)),i[e]=r),r},t.prototype.setItemVisual=function(t,e,n){var i=this._itemVisuals[t]||{};this._itemVisuals[t]=i,ax(e)?A(i,e):i[e]=n},t.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},t.prototype.setLayout=function(t,e){ax(t)?A(this._layout,t):this._layout[t]=e},t.prototype.getLayout=function(t){return this._layout[t]},t.prototype.getItemLayout=function(t){return this._itemLayouts[t]},t.prototype.setItemLayout=function(t,e,n){this._itemLayouts[t]=n?A(this._itemLayouts[t]||{},e):e},t.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},t.prototype.setItemGraphicEl=function(t,e){var n=this.hostModel&&this.hostModel.seriesIndex;ol(n,this.dataType,t,e),this._graphicEls[t]=e},t.prototype.getItemGraphicEl=function(t){return this._graphicEls[t]},t.prototype.eachItemGraphicEl=function(t,e){E(this._graphicEls,(function(n,i){n&&t&&t.call(e,n,i)}))},t.prototype.cloneShallow=function(e){return e||(e=new t(this._schema?this._schema:sx(this.dimensions,this._getDimInfo,this),this.hostModel)),ix(e,this),e._store=this._store,e},t.prototype.wrapMethod=function(t,e){var n=this[t];X(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var t=n.apply(this,arguments);return e.apply(this,[t].concat(at(arguments)))})},t.internalField=(Qm=function(t){var e=t._invertedIndicesMap;E(e,(function(n,i){var r=t._dimInfos[i],o=r.ordinalMeta,a=t._store;if(o){n=e[i]=new lx(o.categories.length);for(var s=0;s1&&(s+="__ec__"+u),i[e]=s}})),t}();function px(t,e){Qd(t)||(t=ef(t));var n=(e=e||{}).coordDimensions||[],i=e.dimensionsDefine||t.dimensionsDefine||[],r=yt(),o=[],a=function(t,e,n,i){var r=Math.max(t.dimensionsDetectedCount||1,e.length,n.length,i||0);return E(e,(function(t){var e;q(t)&&(e=t.dimsDef)&&(r=Math.max(r,e.length))})),r}(t,n,i,e.dimensionsCount),s=e.canOmitUnusedDimensions&&Jm(a),l=i===t.dimensionsDefine,u=l?$m(t):Km(i),h=e.encodeDefine;!h&&e.encodeDefaulter&&(h=e.encodeDefaulter(t,a));for(var c=yt(h),p=new Xf(a),d=0;d0&&(i.name=r+(o-1)),o++,e.set(r,o)}}(o),new jm({source:t,dimensions:o,fullDimensionCount:a,dimensionOmitted:s})}function dx(t,e,n){if(n||e.hasKey(t)){for(var i=0;e.hasKey(t+i);)i++;t+=i}return e.set(t,!0),t}var fx=function(t){this.coordSysDims=[],this.axisMap=yt(),this.categoryAxisMap=yt(),this.coordSysName=t};var gx={cartesian2d:function(t,e,n,i){var r=t.getReferringComponents("xAxis",Wo).models[0],o=t.getReferringComponents("yAxis",Wo).models[0];e.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),yx(r)&&(i.set("x",r),e.firstCategoryDimIndex=0),yx(o)&&(i.set("y",o),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},singleAxis:function(t,e,n,i){var r=t.getReferringComponents("singleAxis",Wo).models[0];e.coordSysDims=["single"],n.set("single",r),yx(r)&&(i.set("single",r),e.firstCategoryDimIndex=0)},polar:function(t,e,n,i){var r=t.getReferringComponents("polar",Wo).models[0],o=r.findAxisModel("radiusAxis"),a=r.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",a),yx(o)&&(i.set("radius",o),e.firstCategoryDimIndex=0),yx(a)&&(i.set("angle",a),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},geo:function(t,e,n,i){e.coordSysDims=["lng","lat"]},parallel:function(t,e,n,i){var r=t.ecModel,o=r.getComponent("parallel",t.get("parallelIndex")),a=e.coordSysDims=o.dimensions.slice();E(o.parallelAxisIndex,(function(t,o){var s=r.getComponent("parallelAxis",t),l=a[o];n.set(l,s),yx(s)&&(i.set(l,s),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=o))}))}};function yx(t){return"category"===t.get("type")}function vx(t,e,n){var i,r,o,a=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(t){return!qm(t.schema)}(e)?(r=e.schema,i=r.dimensions,o=e.store):i=e;var l,u,h,c,p=!(!t||!t.get("stack"));if(E(i,(function(t,e){U(t)&&(i[e]=t={name:t}),p&&!t.isExtraCoord&&(a||l||!t.ordinalMeta||(l=t),u||"ordinal"===t.type||"time"===t.type||s&&s!==t.coordDim||(u=t))})),!u||a||l||(a=!0),u){h="__\0ecstackresult_"+t.id,c="__\0ecstackedover_"+t.id,l&&(l.createInvertedIndices=!0);var d=u.coordDim,f=u.type,g=0;E(i,(function(t){t.coordDim===d&&g++}));var y={name:h,coordDim:d,coordDimIndex:g,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},v={name:c,coordDim:c,coordDimIndex:g+1,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};r?(o&&(y.storeDimIndex=o.ensureCalculationDimension(c,f),v.storeDimIndex=o.ensureCalculationDimension(h,f)),r.appendCalculationDimension(y),r.appendCalculationDimension(v)):(i.push(y),i.push(v))}return{stackedDimension:u&&u.name,stackedByDimension:l&&l.name,isStackedByIndex:a,stackedOverDimension:c,stackResultDimension:h}}function mx(t,e){return!!e&&e===t.getCalculationInfo("stackedDimension")}function xx(t,e){return mx(t,e)?t.getCalculationInfo("stackResultDimension"):e}function _x(t,e,n){n=n||{};var i,r=e.getSourceManager(),o=!1;t?(o=!0,i=ef(t)):o=(i=r.getSource()).sourceFormat===Wp;var a=function(t){var e=t.get("coordinateSystem"),n=new fx(e),i=gx[e];if(i)return i(t,n,n.axisMap,n.categoryAxisMap),n}(e),s=function(t,e){var n,i=t.get("coordinateSystem"),r=wd.get(i);return e&&e.coordSysDims&&(n=z(e.coordSysDims,(function(t){var n={name:t},i=e.axisMap.get(t);if(i){var r=i.get("type");n.type=Ym(r)}return n}))),n||(n=r&&(r.getDimensionsInfo?r.getDimensionsInfo():r.dimensions.slice())||["x","y"]),n}(e,a),l=n.useEncodeDefaulter,u=X(l)?l:l?H(td,s,e):null,h=px(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:e.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!o}),c=function(t,e,n){var i,r;return n&&E(t,(function(t,o){var a=t.coordDim,s=n.categoryAxisMap.get(a);s&&(null==i&&(i=o),t.ordinalMeta=s.getOrdinalMeta(),e&&(t.createInvertedIndices=!0)),null!=t.otherDims.itemName&&(r=!0)})),r||null==i||(t[i].otherDims.itemName=0),i}(h.dimensions,n.createInvertedIndices,a),p=o?null:r.getSharedDataStore(h),d=vx(e,{schema:h,store:p}),f=new cx(h,e);f.setCalculationInfo(d);var g=null!=c&&function(t){if(t.sourceFormat===Wp){var e=function(t){var e=0;for(;ee[1]&&(e[1]=t[1])},t.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=t),isNaN(e)||(n[1]=e)},t.prototype.isInExtentRange=function(t){return this._extent[0]<=t&&this._extent[1]>=t},t.prototype.isBlank=function(){return this._isBlank},t.prototype.setBlank=function(t){this._isBlank=t},t}();na(bx);var Sx=0,Mx=function(){function t(t){this.categories=t.categories||[],this._needCollect=t.needCollect,this._deduplication=t.deduplication,this.uid=++Sx}return t.createByAxisModel=function(e){var n=e.option,i=n.data,r=i&&z(i,Ix);return new t({categories:r,needCollect:!r,deduplication:!1!==n.dedplication})},t.prototype.getOrdinal=function(t){return this._getOrCreateMap().get(t)},t.prototype.parseAndCollect=function(t){var e,n=this._needCollect;if(!U(t)&&!n)return t;if(n&&!this._deduplication)return e=this.categories.length,this.categories[e]=t,e;var i=this._getOrCreateMap();return null==(e=i.get(t))&&(n?(e=this.categories.length,this.categories[e]=t,i.set(t,e)):e=NaN),e},t.prototype._getOrCreateMap=function(){return this._map||(this._map=yt(this.categories))},t}();function Ix(t){return q(t)&&null!=t.value?t.value:t+""}function Tx(t){return"interval"===t.type||"log"===t.type}function Cx(t,e,n,i){var r={},o=t[1]-t[0],a=r.interval=po(o/e,!0);null!=n&&ai&&(a=r.interval=i);var s=r.intervalPrecision=Ax(a);return function(t,e){!isFinite(t[0])&&(t[0]=e[0]),!isFinite(t[1])&&(t[1]=e[1]),kx(t,0,e),kx(t,1,e),t[0]>t[1]&&(t[0]=t[1])}(r.niceTickExtent=[Jr(Math.ceil(t[0]/a)*a,s),Jr(Math.floor(t[1]/a)*a,s)],t),r}function Dx(t){var e=Math.pow(10,co(t)),n=t/e;return n?2===n?n=3:3===n?n=5:n*=2:n=1,Jr(n*e)}function Ax(t){return to(t)+2}function kx(t,e,n){t[e]=Math.max(Math.min(t[e],n[1]),n[0])}function Lx(t,e){return t>=e[0]&&t<=e[1]}function Px(t,e){return e[1]===e[0]?.5:(t-e[0])/(e[1]-e[0])}function Ox(t,e){return t*(e[1]-e[0])+e[0]}var Rx=function(t){function e(e){var n=t.call(this,e)||this;n.type="ordinal";var i=n.getSetting("ordinalMeta");return i||(i=new Mx({})),Y(i)&&(i=new Mx({categories:z(i,(function(t){return q(t)?t.value:t}))})),n._ordinalMeta=i,n._extent=n.getSetting("extent")||[0,i.categories.length-1],n}return n(e,t),e.prototype.parse=function(t){return null==t?NaN:U(t)?this._ordinalMeta.getOrdinal(t):Math.round(t)},e.prototype.contain=function(t){return Lx(t=this.parse(t),this._extent)&&null!=this._ordinalMeta.categories[t]},e.prototype.normalize=function(t){return Px(t=this._getTickNumber(this.parse(t)),this._extent)},e.prototype.scale=function(t){return t=Math.round(Ox(t,this._extent)),this.getRawOrdinalNumber(t)},e.prototype.getTicks=function(){for(var t=[],e=this._extent,n=e[0];n<=e[1];)t.push({value:n}),n++;return t},e.prototype.getMinorTicks=function(t){},e.prototype.setSortInfo=function(t){if(null!=t){for(var e=t.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],r=0,o=this._ordinalMeta.categories.length,a=Math.min(o,e.length);r=0&&t=0&&t=t},e.prototype.getOrdinalMeta=function(){return this._ordinalMeta},e.prototype.calcNiceTicks=function(){},e.prototype.calcNiceExtent=function(){},e.type="ordinal",e}(bx);bx.registerClass(Rx);var Nx=Jr,Ex=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="interval",e._interval=0,e._intervalPrecision=2,e}return n(e,t),e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return Lx(t,this._extent)},e.prototype.normalize=function(t){return Px(t,this._extent)},e.prototype.scale=function(t){return Ox(t,this._extent)},e.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=parseFloat(t)),isNaN(e)||(n[1]=parseFloat(e))},e.prototype.unionExtent=function(t){var e=this._extent;t[0]e[1]&&(e[1]=t[1]),this.setExtent(e[0],e[1])},e.prototype.getInterval=function(){return this._interval},e.prototype.setInterval=function(t){this._interval=t,this._niceExtent=this._extent.slice(),this._intervalPrecision=Ax(t)},e.prototype.getTicks=function(t){var e=this._interval,n=this._extent,i=this._niceExtent,r=this._intervalPrecision,o=[];if(!e)return o;n[0]1e4)return[];var s=o.length?o[o.length-1].value:i[1];return n[1]>s&&(t?o.push({value:Nx(s+e,r)}):o.push({value:n[1]})),o},e.prototype.getMinorTicks=function(t){for(var e=this.getTicks(!0),n=[],i=this.getExtent(),r=1;ri[0]&&h0&&(o=null===o?s:Math.min(o,s))}n[i]=o}}return n}(t),n=[];return E(t,(function(t){var i,r=t.coordinateSystem.getBaseAxis(),o=r.getExtent();if("category"===r.type)i=r.getBandWidth();else if("value"===r.type||"time"===r.type){var a=r.dim+"_"+r.index,s=e[a],l=Math.abs(o[1]-o[0]),u=r.scale.getExtent(),h=Math.abs(u[1]-u[0]);i=s?l/h*s:l}else{var c=t.getData();i=Math.abs(o[1]-o[0])/c.count()}var p=$r(t.get("barWidth"),i),d=$r(t.get("barMaxWidth"),i),f=$r(t.get("barMinWidth")||(qx(t)?.5:1),i),g=t.get("barGap"),y=t.get("barCategoryGap");n.push({bandWidth:i,barWidth:p,barMaxWidth:d,barMinWidth:f,barGap:g,barCategoryGap:y,axisKey:Wx(r),stackId:Gx(t)})})),Xx(n)}function Xx(t){var e={};E(t,(function(t,n){var i=t.axisKey,r=t.bandWidth,o=e[i]||{bandWidth:r,remainedWidth:r,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},a=o.stacks;e[i]=o;var s=t.stackId;a[s]||o.autoWidthCount++,a[s]=a[s]||{width:0,maxWidth:0};var l=t.barWidth;l&&!a[s].width&&(a[s].width=l,l=Math.min(o.remainedWidth,l),o.remainedWidth-=l);var u=t.barMaxWidth;u&&(a[s].maxWidth=u);var h=t.barMinWidth;h&&(a[s].minWidth=h);var c=t.barGap;null!=c&&(o.gap=c);var p=t.barCategoryGap;null!=p&&(o.categoryGap=p)}));var n={};return E(e,(function(t,e){n[e]={};var i=t.stacks,r=t.bandWidth,o=t.categoryGap;if(null==o){var a=G(i).length;o=Math.max(35-4*a,15)+"%"}var s=$r(o,r),l=$r(t.gap,1),u=t.remainedWidth,h=t.autoWidthCount,c=(u-s)/(h+(h-1)*l);c=Math.max(c,0),E(i,(function(t){var e=t.maxWidth,n=t.minWidth;if(t.width){i=t.width;e&&(i=Math.min(i,e)),n&&(i=Math.max(i,n)),t.width=i,u-=i+l*i,h--}else{var i=c;e&&ei&&(i=n),i!==c&&(t.width=i,u-=i+l*i,h--)}})),c=(u-s)/(h+(h-1)*l),c=Math.max(c,0);var p,d=0;E(i,(function(t,e){t.width||(t.width=c),p=t,d+=t.width*(1+l)})),p&&(d-=p.width*l);var f=-d/2;E(i,(function(t,i){n[e][i]=n[e][i]||{bandWidth:r,offset:f,width:t.width},f+=t.width*(1+l)}))})),n}function Ux(t,e){var n=Hx(t,e),i=Yx(n);E(n,(function(t){var e=t.getData(),n=t.coordinateSystem.getBaseAxis(),r=Gx(t),o=i[Wx(n)][r],a=o.offset,s=o.width;e.setLayout({bandWidth:o.bandWidth,offset:a,size:s})}))}function Zx(t){return{seriesType:t,plan:kg(),reset:function(t){if(jx(t)){var e=t.getData(),n=t.coordinateSystem,i=n.getBaseAxis(),r=n.getOtherAxis(i),o=e.getDimensionIndex(e.mapDimension(r.dim)),a=e.getDimensionIndex(e.mapDimension(i.dim)),s=t.get("showBackground",!0),l=e.mapDimension(r.dim),u=e.getCalculationInfo("stackResultDimension"),h=mx(e,l)&&!!e.getCalculationInfo("stackedOnSeries"),c=r.isHorizontal(),p=function(t,e){var n=e.model.get("startValue");n||(n=0);return e.toGlobalCoord(e.dataToCoord("log"===e.type?n>0?n:1:n))}(0,r),d=qx(t),f=t.get("barMinHeight")||0,g=u&&e.getDimensionIndex(u),y=e.getLayout("size"),v=e.getLayout("offset");return{progress:function(t,e){for(var i,r=t.count,l=d&&Bx(3*r),u=d&&s&&Bx(3*r),m=d&&Bx(r),x=n.master.getRect(),_=c?x.width:x.height,b=e.getStore(),w=0;null!=(i=t.next());){var S=b.get(h?g:o,i),M=b.get(a,i),I=p,T=void 0;h&&(T=+S-b.get(o,i));var C=void 0,D=void 0,A=void 0,k=void 0;if(c){var L=n.dataToPoint([S,M]);if(h)I=n.dataToPoint([T,M])[0];C=I,D=L[1]+v,A=L[0]-I,k=y,Math.abs(A)0)for(var s=0;s=0;--s)if(l[u]){o=l[u];break}o=o||a.none}if(Y(o)){var h=null==t.level?0:t.level>=0?t.level:o.length+t.level;o=o[h=Math.min(h,o.length-1)]}}return Jc(new Date(t.value),o,r,i)}(t,e,n,this.getSetting("locale"),i)},e.prototype.getTicks=function(){var t=this._interval,e=this._extent,n=[];if(!t)return n;n.push({value:e[0],level:0});var i=this.getSetting("useUTC"),r=function(t,e,n,i){var r=1e4,o=jc,a=0;function s(t,e,n,r,o,a,s){for(var l=new Date(e),u=e,h=l[r]();u1&&0===u&&o.unshift({value:o[0].value-p})}}for(u=0;u=i[0]&&v<=i[1]&&c++)}var m=(i[1]-i[0])/e;if(c>1.5*m&&p>m/1.5)break;if(u.push(g),c>m||t===o[d])break}h=[]}}0;var x=B(z(u,(function(t){return B(t,(function(t){return t.value>=i[0]&&t.value<=i[1]&&!t.notAdd}))})),(function(t){return t.length>0})),_=[],b=x.length-1;for(d=0;dn&&(this._approxInterval=n);var o=$x.length,a=Math.min(function(t,e,n,i){for(;n>>1;t[r][1]16?16:t>7.5?7:t>3.5?4:t>1.5?2:1}function Qx(t){return(t/=2592e6)>6?6:t>3?3:t>2?2:1}function t_(t){return(t/=Gc)>12?12:t>6?6:t>3.5?4:t>2?2:1}function e_(t,e){return(t/=e?Fc:Bc)>30?30:t>20?20:t>15?15:t>10?10:t>5?5:t>2?2:1}function n_(t){return po(t,!0)}function i_(t,e,n){var i=new Date(t);switch(Kc(e)){case"year":case"month":i[up(n)](0);case"day":i[hp(n)](1);case"hour":i[cp(n)](0);case"minute":i[pp(n)](0);case"second":i[dp(n)](0),i[fp(n)](0)}return i.getTime()}bx.registerClass(Kx);var r_=bx.prototype,o_=Ex.prototype,a_=Jr,s_=Math.floor,l_=Math.ceil,u_=Math.pow,h_=Math.log,c_=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="log",e.base=10,e._originalScale=new Ex,e._interval=0,e}return n(e,t),e.prototype.getTicks=function(t){var e=this._originalScale,n=this._extent,i=e.getExtent();return z(o_.getTicks.call(this,t),(function(t){var e=t.value,r=Jr(u_(this.base,e));return r=e===n[0]&&this._fixMin?d_(r,i[0]):r,{value:r=e===n[1]&&this._fixMax?d_(r,i[1]):r}}),this)},e.prototype.setExtent=function(t,e){var n=h_(this.base);t=h_(Math.max(0,t))/n,e=h_(Math.max(0,e))/n,o_.setExtent.call(this,t,e)},e.prototype.getExtent=function(){var t=this.base,e=r_.getExtent.call(this);e[0]=u_(t,e[0]),e[1]=u_(t,e[1]);var n=this._originalScale.getExtent();return this._fixMin&&(e[0]=d_(e[0],n[0])),this._fixMax&&(e[1]=d_(e[1],n[1])),e},e.prototype.unionExtent=function(t){this._originalScale.unionExtent(t);var e=this.base;t[0]=h_(t[0])/h_(e),t[1]=h_(t[1])/h_(e),r_.unionExtent.call(this,t)},e.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},e.prototype.calcNiceTicks=function(t){t=t||10;var e=this._extent,n=e[1]-e[0];if(!(n===1/0||n<=0)){var i=ho(n);for(t/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var r=[Jr(l_(e[0]/i)*i),Jr(s_(e[1]/i)*i)];this._interval=i,this._niceExtent=r}},e.prototype.calcNiceExtent=function(t){o_.calcNiceExtent.call(this,t),this._fixMin=t.fixMin,this._fixMax=t.fixMax},e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return Lx(t=h_(t)/h_(this.base),this._extent)},e.prototype.normalize=function(t){return Px(t=h_(t)/h_(this.base),this._extent)},e.prototype.scale=function(t){return t=Ox(t,this._extent),u_(this.base,t)},e.type="log",e}(bx),p_=c_.prototype;function d_(t,e){return a_(t,to(e))}p_.getMinorTicks=o_.getMinorTicks,p_.getLabel=o_.getLabel,bx.registerClass(c_);var f_=function(){function t(t,e,n){this._prepareParams(t,e,n)}return t.prototype._prepareParams=function(t,e,n){n[1]0&&s>0&&!l&&(a=0),a<0&&s<0&&!u&&(s=0));var c=this._determinedMin,p=this._determinedMax;return null!=c&&(a=c,l=!0),null!=p&&(s=p,u=!0),{min:a,max:s,minFixed:l,maxFixed:u,isBlank:h}},t.prototype.modifyDataMinMax=function(t,e){this[y_[t]]=e},t.prototype.setDeterminedMinMax=function(t,e){var n=g_[t];this[n]=e},t.prototype.freeze=function(){this.frozen=!0},t}(),g_={min:"_determinedMin",max:"_determinedMax"},y_={min:"_dataMin",max:"_dataMax"};function v_(t,e,n){var i=t.rawExtentInfo;return i||(i=new f_(t,e,n),t.rawExtentInfo=i,i)}function m_(t,e){return null==e?null:nt(e)?NaN:t.parse(e)}function x_(t,e){var n=t.type,i=v_(t,e,t.getExtent()).calculate();t.setBlank(i.isBlank);var r=i.min,o=i.max,a=e.ecModel;if(a&&"time"===n){var s=Hx("bar",a),l=!1;if(E(s,(function(t){l=l||t.getBaseAxis()===e.axis})),l){var u=Yx(s),h=function(t,e,n,i){var r=n.axis.getExtent(),o=r[1]-r[0],a=function(t,e,n){if(t&&e){var i=t[Wx(e)];return null!=i&&null!=n?i[Gx(n)]:i}}(i,n.axis);if(void 0===a)return{min:t,max:e};var s=1/0;E(a,(function(t){s=Math.min(t.offset,s)}));var l=-1/0;E(a,(function(t){l=Math.max(t.offset+t.width,l)})),s=Math.abs(s),l=Math.abs(l);var u=s+l,h=e-t,c=h/(1-(s+l)/o)-h;return e+=c*(l/u),t-=c*(s/u),{min:t,max:e}}(r,o,e,u);r=h.min,o=h.max}}return{extent:[r,o],fixMin:i.minFixed,fixMax:i.maxFixed}}function __(t,e){var n=e,i=x_(t,n),r=i.extent,o=n.get("splitNumber");t instanceof c_&&(t.base=n.get("logBase"));var a=t.type,s=n.get("interval"),l="interval"===a||"time"===a;t.setExtent(r[0],r[1]),t.calcNiceExtent({splitNumber:o,fixMin:i.fixMin,fixMax:i.fixMax,minInterval:l?n.get("minInterval"):null,maxInterval:l?n.get("maxInterval"):null}),null!=s&&t.setInterval&&t.setInterval(s)}function b_(t,e){if(e=e||t.get("type"))switch(e){case"category":return new Rx({ordinalMeta:t.getOrdinalMeta?t.getOrdinalMeta():t.getCategories(),extent:[1/0,-1/0]});case"time":return new Kx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new(bx.getClass(e)||Ex)}}function w_(t){var e,n,i=t.getLabelModel().get("formatter"),r="category"===t.type?t.scale.getExtent()[0]:null;return"time"===t.scale.type?(n=i,function(e,i){return t.scale.getFormattedLabel(e,i,n)}):U(i)?function(e){return function(n){var i=t.scale.getLabel(n);return e.replace("{value}",null!=i?i:"")}}(i):X(i)?(e=i,function(n,i){return null!=r&&(i=n.value-r),e(S_(t,n),i,null!=n.level?{level:n.level}:null)}):function(e){return t.scale.getLabel(e)}}function S_(t,e){return"category"===t.type?t.scale.getLabel(e):e.value}function M_(t,e){var n=e*Math.PI/180,i=t.width,r=t.height,o=i*Math.abs(Math.cos(n))+Math.abs(r*Math.sin(n)),a=i*Math.abs(Math.sin(n))+Math.abs(r*Math.cos(n));return new ze(t.x,t.y,o,a)}function I_(t){var e=t.get("interval");return null==e?"auto":e}function T_(t){return"category"===t.type&&0===I_(t.getLabelModel())}function C_(t,e){var n={};return E(t.mapDimensionsAll(e),(function(e){n[xx(t,e)]=!0})),G(n)}var D_=function(){function t(){}return t.prototype.getNeedCrossZero=function(){return!this.option.scale},t.prototype.getCoordSysModel=function(){},t}();var A_={isDimensionStacked:mx,enableDataStack:vx,getStackedDimension:xx};var k_=Object.freeze({__proto__:null,createList:function(t){return _x(null,t)},getLayoutRect:kp,dataStack:A_,createScale:function(t,e){var n=e;e instanceof Cc||(n=new Cc(e));var i=b_(n);return i.setExtent(t[0],t[1]),__(i,n),i},mixinAxisModelCommonMethods:function(t){R(t,D_)},getECData:rl,createTextStyle:function(t,e){return oc(t,null,null,"normal"!==(e=e||{}).state)},createDimensions:function(t,e){return px(t,e).dimensions},createSymbol:Xy,enableHoverEmphasis:Ul});function L_(t,e){return Math.abs(t-e)<1e-8}function P_(t,e,n){var i=0,r=t[0];if(!r)return!1;for(var o=1;on&&(t=r,n=a)}if(t)return function(t){for(var e=0,n=0,i=0,r=t.length,o=t[r-1][0],a=t[r-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),r=s+=r,o=l+=o,i.push([s/n,l/n])}return i}function H_(t,e){return z(B((t=function(t){if(!t.UTF8Encoding)return t;var e=t,n=e.UTF8Scale;return null==n&&(n=1024),E(e.features,(function(t){var e=t.geometry,i=e.encodeOffsets,r=e.coordinates;if(i)switch(e.type){case"LineString":e.coordinates=W_(r,i,n);break;case"Polygon":case"MultiLineString":G_(r,i,n);break;case"MultiPolygon":E(r,(function(t,e){return G_(t,i[e],n)}))}})),e.UTF8Encoding=!1,e}(t)).features,(function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0})),(function(t){var n=t.properties,i=t.geometry,r=[];switch(i.type){case"Polygon":var o=i.coordinates;r.push(new z_(o[0],o.slice(1)));break;case"MultiPolygon":E(i.coordinates,(function(t){t[0]&&r.push(new z_(t[0],t.slice(1)))}));break;case"LineString":r.push(new V_([i.coordinates]));break;case"MultiLineString":r.push(new V_(i.coordinates))}var a=new B_(n[e||"name"],r,n.cp);return a.properties=n,a}))}var Y_=Object.freeze({__proto__:null,linearMap:Kr,round:Jr,asc:Qr,getPrecision:to,getPrecisionSafe:eo,getPixelPrecision:no,getPercentWithPrecision:function(t,e,n){return t[e]&&io(t,n)[e]||0},MAX_SAFE_INTEGER:oo,remRadian:ao,isRadianAroundZero:so,parseDate:uo,quantity:ho,quantityExponent:co,nice:po,quantile:fo,reformIntervals:go,isNumeric:vo,numericToNumber:yo}),X_=Object.freeze({__proto__:null,parse:uo,format:Jc}),U_=Object.freeze({__proto__:null,extendShape:Ch,extendPath:Ah,makePath:Ph,makeImage:Oh,mergePath:Nh,resizePath:Eh,createIcon:Uh,updateProps:vh,initProps:mh,getTransform:Bh,clipPointsByRect:Yh,clipRectByRect:Xh,registerShape:kh,getShapeClass:Lh,Group:Br,Image:Ns,Text:Xs,Circle:Su,Ellipse:Iu,Sector:Fu,Ring:Wu,Polygon:Xu,Polyline:Zu,Rect:Ws,Line:Ku,BezierCurve:th,Arc:nh,IncrementalDisplayable:dh,CompoundPath:ih,LinearGradient:oh,RadialGradient:ah,BoundingRect:ze}),Z_=Object.freeze({__proto__:null,addCommas:gp,toCamelCase:yp,normalizeCssArray:vp,encodeHTML:re,formatTpl:bp,getTooltipMarker:wp,formatTime:function(t,e,n){"week"!==t&&"month"!==t&&"quarter"!==t&&"half-year"!==t&&"year"!==t||(t="MM-dd\nyyyy");var i=uo(e),r=n?"getUTC":"get",o=i[r+"FullYear"](),a=i[r+"Month"]()+1,s=i[r+"Date"](),l=i[r+"Hours"](),u=i[r+"Minutes"](),h=i[r+"Seconds"](),c=i[r+"Milliseconds"]();return t=t.replace("MM",qc(a,2)).replace("M",a).replace("yyyy",o).replace("yy",qc(o%100+"",2)).replace("dd",qc(s,2)).replace("d",s).replace("hh",qc(l,2)).replace("h",l).replace("mm",qc(u,2)).replace("m",u).replace("ss",qc(h,2)).replace("s",h).replace("SSS",qc(c,3))},capitalFirst:function(t){return t?t.charAt(0).toUpperCase()+t.substr(1):t},truncateText:pa,getTextRect:function(t,e,n,i,r,o,a,s){return new Xs({style:{text:t,font:e,align:n,verticalAlign:i,padding:r,rich:o,overflow:a?"truncate":null,lineHeight:s}}).getBoundingRect()}}),j_=Object.freeze({__proto__:null,map:z,each:E,indexOf:P,inherits:O,reduce:V,filter:B,bind:W,curry:H,isArray:Y,isString:U,isObject:q,isFunction:X,extend:A,defaults:k,clone:T,merge:C}),q_=Vo();function K_(t,e){var n=z(e,(function(e){return t.scale.parse(e)}));return"time"===t.type&&n.length>0&&(n.sort(),n.unshift(n[0]),n.push(n[n.length-1])),n}function $_(t){var e=t.getLabelModel().get("customValues");if(e){var n=w_(t);return{labels:K_(t,e).map((function(e){var i={value:e};return{formattedLabel:n(i),rawLabel:t.scale.getLabel(i),tickValue:e}}))}}return"category"===t.type?function(t){var e=t.getLabelModel(),n=Q_(t,e);return!e.get("show")||t.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}(t):function(t){var e=t.scale.getTicks(),n=w_(t);return{labels:z(e,(function(e,i){return{level:e.level,formattedLabel:n(e,i),rawLabel:t.scale.getLabel(e),tickValue:e.value}}))}}(t)}function J_(t,e){var n=t.getTickModel().get("customValues");return n?{ticks:K_(t,n)}:"category"===t.type?function(t,e){var n,i,r=tb(t,"ticks"),o=I_(e),a=eb(r,o);if(a)return a;e.get("show")&&!t.scale.isBlank()||(n=[]);if(X(o))n=rb(t,o,!0);else if("auto"===o){var s=Q_(t,t.getLabelModel());i=s.labelCategoryInterval,n=z(s.labels,(function(t){return t.tickValue}))}else n=ib(t,i=o,!0);return nb(r,o,{ticks:n,tickCategoryInterval:i})}(t,e):{ticks:z(t.scale.getTicks(),(function(t){return t.value}))}}function Q_(t,e){var n,i,r=tb(t,"labels"),o=I_(e),a=eb(r,o);return a||(X(o)?n=rb(t,o):(i="auto"===o?function(t){var e=q_(t).autoInterval;return null!=e?e:q_(t).autoInterval=t.calculateCategoryInterval()}(t):o,n=ib(t,i)),nb(r,o,{labels:n,labelCategoryInterval:i}))}function tb(t,e){return q_(t)[e]||(q_(t)[e]=[])}function eb(t,e){for(var n=0;n1&&h/l>2&&(u=Math.round(Math.ceil(u/l)*l));var c=T_(t),p=a.get("showMinLabel")||c,d=a.get("showMaxLabel")||c;p&&u!==o[0]&&g(o[0]);for(var f=u;f<=o[1];f+=l)g(f);function g(t){var e={value:t};s.push(n?t:{formattedLabel:i(e),rawLabel:r.getLabel(e),tickValue:t})}return d&&f-l!==o[1]&&g(o[1]),s}function rb(t,e,n){var i=t.scale,r=w_(t),o=[];return E(i.getTicks(),(function(t){var a=i.getLabel(t),s=t.value;e(t.value,a)&&o.push(n?s:{formattedLabel:r(t),rawLabel:a,tickValue:s})})),o}var ob=[0,1],ab=function(){function t(t,e,n){this.onBand=!1,this.inverse=!1,this.dim=t,this.scale=e,this._extent=n||[0,0]}return t.prototype.contain=function(t){var e=this._extent,n=Math.min(e[0],e[1]),i=Math.max(e[0],e[1]);return t>=n&&t<=i},t.prototype.containData=function(t){return this.scale.contain(t)},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.getPixelPrecision=function(t){return no(t||this.scale.getExtent(),this._extent)},t.prototype.setExtent=function(t,e){var n=this._extent;n[0]=t,n[1]=e},t.prototype.dataToCoord=function(t,e){var n=this._extent,i=this.scale;return t=i.normalize(t),this.onBand&&"ordinal"===i.type&&sb(n=n.slice(),i.count()),Kr(t,ob,n,e)},t.prototype.coordToData=function(t,e){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&sb(n=n.slice(),i.count());var r=Kr(t,n,ob,e);return this.scale.scale(r)},t.prototype.pointToData=function(t,e){},t.prototype.getTicksCoords=function(t){var e=(t=t||{}).tickModel||this.getTickModel(),n=z(J_(this,e).ticks,(function(t){return{coord:this.dataToCoord("ordinal"===this.scale.type?this.scale.getRawOrdinalNumber(t):t),tickValue:t}}),this);return function(t,e,n,i){var r=e.length;if(!t.onBand||n||!r)return;var o,a,s=t.getExtent();if(1===r)e[0].coord=s[0],o=e[1]={coord:s[1]};else{var l=e[r-1].tickValue-e[0].tickValue,u=(e[r-1].coord-e[0].coord)/l;E(e,(function(t){t.coord-=u/2})),a=1+t.scale.getExtent()[1]-e[r-1].tickValue,o={coord:e[r-1].coord+u*a},e.push(o)}var h=s[0]>s[1];c(e[0].coord,s[0])&&(i?e[0].coord=s[0]:e.shift());i&&c(s[0],e[0].coord)&&e.unshift({coord:s[0]});c(s[1],o.coord)&&(i?o.coord=s[1]:e.pop());i&&c(o.coord,s[1])&&e.push({coord:s[1]});function c(t,e){return t=Jr(t),e=Jr(e),h?t>e:t0&&t<100||(t=5),z(this.scale.getMinorTicks(t),(function(t){return z(t,(function(t){return{coord:this.dataToCoord(t),tickValue:t}}),this)}),this)},t.prototype.getViewLabels=function(){return $_(this).labels},t.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},t.prototype.getTickModel=function(){return this.model.getModel("axisTick")},t.prototype.getBandWidth=function(){var t=this._extent,e=this.scale.getExtent(),n=e[1]-e[0]+(this.onBand?1:0);0===n&&(n=1);var i=Math.abs(t[1]-t[0]);return Math.abs(i)/n},t.prototype.calculateCategoryInterval=function(){return function(t){var e=function(t){var e=t.getLabelModel();return{axisRotate:t.getRotate?t.getRotate():t.isHorizontal&&!t.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}(t),n=w_(t),i=(e.axisRotate-e.labelRotate)/180*Math.PI,r=t.scale,o=r.getExtent(),a=r.count();if(o[1]-o[0]<1)return 0;var s=1;a>40&&(s=Math.max(1,Math.floor(a/40)));for(var l=o[0],u=t.dataToCoord(l+1)-t.dataToCoord(l),h=Math.abs(u*Math.cos(i)),c=Math.abs(u*Math.sin(i)),p=0,d=0;l<=o[1];l+=s){var f,g,y=Sr(n({value:l}),e.font,"center","top");f=1.3*y.width,g=1.3*y.height,p=Math.max(p,f,7),d=Math.max(d,g,7)}var v=p/h,m=d/c;isNaN(v)&&(v=1/0),isNaN(m)&&(m=1/0);var x=Math.max(0,Math.floor(Math.min(v,m))),_=q_(t.model),b=t.getExtent(),w=_.lastAutoInterval,S=_.lastTickCount;return null!=w&&null!=S&&Math.abs(w-x)<=1&&Math.abs(S-a)<=1&&w>x&&_.axisExtent0===b[0]&&_.axisExtent1===b[1]?x=w:(_.lastTickCount=a,_.lastAutoInterval=x,_.axisExtent0=b[0],_.axisExtent1=b[1]),x}(this)},t}();function sb(t,e){var n=(t[1]-t[0])/e/2;t[0]+=n,t[1]-=n}var lb=2*Math.PI,ub=hs.CMD,hb=["top","right","bottom","left"];function cb(t,e,n,i,r){var o=n.width,a=n.height;switch(t){case"top":i.set(n.x+o/2,n.y-e),r.set(0,-1);break;case"bottom":i.set(n.x+o/2,n.y+a+e),r.set(0,1);break;case"left":i.set(n.x-e,n.y+a/2),r.set(-1,0);break;case"right":i.set(n.x+o+e,n.y+a/2),r.set(1,0)}}function pb(t,e,n,i,r,o,a,s,l){a-=t,s-=e;var u=Math.sqrt(a*a+s*s),h=(a/=u)*n+t,c=(s/=u)*n+e;if(Math.abs(i-r)%lb<1e-4)return l[0]=h,l[1]=c,u-n;if(o){var p=i;i=gs(r),r=gs(p)}else i=gs(i),r=gs(r);i>r&&(r+=lb);var d=Math.atan2(s,a);if(d<0&&(d+=lb),d>=i&&d<=r||d+lb>=i&&d+lb<=r)return l[0]=h,l[1]=c,u-n;var f=n*Math.cos(i)+t,g=n*Math.sin(i)+e,y=n*Math.cos(r)+t,v=n*Math.sin(r)+e,m=(f-a)*(f-a)+(g-s)*(g-s),x=(y-a)*(y-a)+(v-s)*(v-s);return m0){e=e/180*Math.PI,mb.fromArray(t[0]),xb.fromArray(t[1]),_b.fromArray(t[2]),De.sub(bb,mb,xb),De.sub(wb,_b,xb);var n=bb.len(),i=wb.len();if(!(n<.001||i<.001)){bb.scale(1/n),wb.scale(1/i);var r=bb.dot(wb);if(Math.cos(e)1&&De.copy(Ib,_b),Ib.toArray(t[1])}}}}function Cb(t,e,n){if(n<=180&&n>0){n=n/180*Math.PI,mb.fromArray(t[0]),xb.fromArray(t[1]),_b.fromArray(t[2]),De.sub(bb,xb,mb),De.sub(wb,_b,xb);var i=bb.len(),r=wb.len();if(!(i<.001||r<.001))if(bb.scale(1/i),wb.scale(1/r),bb.dot(e)=a)De.copy(Ib,_b);else{Ib.scaleAndAdd(wb,o/Math.tan(Math.PI/2-s));var l=_b.x!==xb.x?(Ib.x-xb.x)/(_b.x-xb.x):(Ib.y-xb.y)/(_b.y-xb.y);if(isNaN(l))return;l<0?De.copy(Ib,xb):l>1&&De.copy(Ib,_b)}Ib.toArray(t[1])}}}function Db(t,e,n,i){var r="normal"===n,o=r?t:t.ensureState(n);o.ignore=e;var a=i.get("smooth");a&&!0===a&&(a=.3),o.shape=o.shape||{},a>0&&(o.shape.smooth=a);var s=i.getModel("lineStyle").getLineStyle();r?t.useStyle(s):o.style=s}function Ab(t,e){var n=e.smooth,i=e.points;if(i)if(t.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var r=Vt(i[0],i[1]),o=Vt(i[1],i[2]);if(!r||!o)return t.lineTo(i[1][0],i[1][1]),void t.lineTo(i[2][0],i[2][1]);var a=Math.min(r,o)*n,s=Gt([],i[1],i[0],a/r),l=Gt([],i[1],i[2],a/o),u=Gt([],s,l,.5);t.bezierCurveTo(s[0],s[1],s[0],s[1],u[0],u[1]),t.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var h=1;h0&&o&&_(-h/a,0,a);var f,g,y=t[0],v=t[a-1];return m(),f<0&&b(-f,.8),g<0&&b(g,.8),m(),x(f,g,1),x(g,f,-1),m(),f<0&&w(-f),g<0&&w(g),u}function m(){f=y.rect[e]-i,g=r-v.rect[e]-v.rect[n]}function x(t,e,n){if(t<0){var i=Math.min(e,-t);if(i>0){_(i*n,0,a);var r=i+t;r<0&&b(-r*n,1)}else b(-t*n,1)}}function _(n,i,r){0!==n&&(u=!0);for(var o=i;o0)for(l=0;l0;l--){_(-(o[l-1]*c),l,a)}}}function w(t){var e=t<0?-1:1;t=Math.abs(t);for(var n=Math.ceil(t/(a-1)),i=0;i0?_(n,0,i+1):_(-n,a-i-1,a),(t-=n)<=0)return}}function Rb(t,e,n,i){return Ob(t,"y","height",e,n,i)}function Nb(t){var e=[];t.sort((function(t,e){return e.priority-t.priority}));var n=new ze(0,0,0,0);function i(t){if(!t.ignore){var e=t.ensureState("emphasis");null==e.ignore&&(e.ignore=!1)}t.ignore=!0}for(var r=0;r=0&&n.attr(d.oldLayoutSelect),P(u,"emphasis")>=0&&n.attr(d.oldLayoutEmphasis)),vh(n,s,e,a)}else if(n.attr(s),!pc(n).valueAnimation){var h=rt(n.style.opacity,1);n.style.opacity=0,mh(n,{style:{opacity:h}},e,a)}if(d.oldLayout=s,n.states.select){var c=d.oldLayoutSelect={};Wb(c,s,Hb),Wb(c,n.states.select,Hb)}if(n.states.emphasis){var p=d.oldLayoutEmphasis={};Wb(p,s,Hb),Wb(p,n.states.emphasis,Hb)}fc(n,a,l,e,e)}if(i&&!i.ignore&&!i.invisible){r=(d=Gb(i)).oldLayout;var d,f={points:i.shape.points};r?(i.attr({shape:r}),vh(i,{shape:f},e)):(i.setShape(f),i.style.strokePercent=0,mh(i,{style:{strokePercent:1}},e)),d.oldLayout=f}},t}(),Xb=Vo();var Ub=Math.sin,Zb=Math.cos,jb=Math.PI,qb=2*Math.PI,Kb=180/jb,$b=function(){function t(){}return t.prototype.reset=function(t){this._start=!0,this._d=[],this._str="",this._p=Math.pow(10,t||4)},t.prototype.moveTo=function(t,e){this._add("M",t,e)},t.prototype.lineTo=function(t,e){this._add("L",t,e)},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){this._add("C",t,e,n,i,r,o)},t.prototype.quadraticCurveTo=function(t,e,n,i){this._add("Q",t,e,n,i)},t.prototype.arc=function(t,e,n,i,r,o){this.ellipse(t,e,n,n,0,i,r,o)},t.prototype.ellipse=function(t,e,n,i,r,o,a,s){var l=a-o,u=!s,h=Math.abs(l),c=pi(h-qb)||(u?l>=qb:-l>=qb),p=l>0?l%qb:l%qb+qb,d=!1;d=!!c||!pi(h)&&p>=jb==!!u;var f=t+n*Zb(o),g=e+i*Ub(o);this._start&&this._add("M",f,g);var y=Math.round(r*Kb);if(c){var v=1/this._p,m=(u?1:-1)*(qb-v);this._add("A",n,i,y,1,+u,t+n*Zb(o+m),e+i*Ub(o+m)),v>.01&&this._add("A",n,i,y,0,+u,f,g)}else{var x=t+n*Zb(a),_=e+i*Ub(a);this._add("A",n,i,y,+d,+u,x,_)}},t.prototype.rect=function(t,e,n,i){this._add("M",t,e),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},t.prototype.closePath=function(){this._d.length>0&&this._add("Z")},t.prototype._add=function(t,e,n,i,r,o,a,s,l){for(var u=[],h=this._p,c=1;c"}(r,o)+("style"!==r?re(a):a||"")+(i?""+n+z(i,(function(e){return t(e)})).join(n)+n:"")+("")}(t)}function uw(t){return{zrId:t,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssStyleCache:{},cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function hw(t,e,n,i){return sw("svg","root",{width:t,height:e,xmlns:iw,"xmlns:xlink":rw,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+t+" "+e},n)}var cw=0;function pw(){return cw++}var dw={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},fw="transform-origin";function gw(t,e,n){var i=A({},t.shape);A(i,e),t.buildPath(n,i);var r=new $b;return r.reset(wi(t)),n.rebuildPath(r,1),r.generateStr(),r.getStr()}function yw(t,e){var n=e.originX,i=e.originY;(n||i)&&(t[fw]=n+"px "+i+"px")}var vw={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function mw(t,e){var n=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[n]=t,n}function xw(t){return U(t)?dw[t]?"cubic-bezier("+dw[t]+")":Pn(t)?t:"":""}function _w(t,e,n,i){var r=t.animators,o=r.length,a=[];if(t instanceof ih){var s=function(t,e,n){var i,r,o=t.shape.paths,a={};if(E(o,(function(t){var e=uw(n.zrId);e.animation=!0,_w(t,{},e,!0);var o=e.cssAnims,s=e.cssNodes,l=G(o),u=l.length;if(u){var h=o[r=l[u-1]];for(var c in h){var p=h[c];a[c]=a[c]||{d:""},a[c].d+=p.d||""}for(var d in s){var f=s[d].animation;f.indexOf(r)>=0&&(i=f)}}})),i){e.d=!1;var s=mw(a,n);return i.replace(r,s)}}(t,e,n);if(s)a.push(s);else if(!o)return}else if(!o)return;for(var l={},u=0;u0})).length)return mw(h,n)+" "+r[0]+" both"}for(var y in l){(s=g(l[y]))&&a.push(s)}if(a.length){var v=n.zrId+"-cls-"+pw();n.cssNodes["."+v]={animation:a.join(",")},e.class=v}}function bw(t,e,n,i){var r=JSON.stringify(t),o=n.cssStyleCache[r];o||(o=n.zrId+"-cls-"+pw(),n.cssStyleCache[r]=o,n.cssNodes["."+o+(i?":hover":"")]=t),e.class=e.class?e.class+" "+o:o}var ww=Math.round;function Sw(t){return t&&U(t.src)}function Mw(t){return t&&X(t.toDataURL)}function Iw(t,e,n,i){nw((function(r,o){var a="fill"===r||"stroke"===r;a&&_i(o)?Ew(e,t,r,i):a&&vi(o)?zw(n,t,r,i):t[r]=a&&"none"===o?"transparent":o}),e,n,!1),function(t,e,n){var i=t.style;if(function(t){return t&&(t.shadowBlur||t.shadowOffsetX||t.shadowOffsetY)}(i)){var r=function(t){var e=t.style,n=t.getGlobalScale();return[e.shadowColor,(e.shadowBlur||0).toFixed(2),(e.shadowOffsetX||0).toFixed(2),(e.shadowOffsetY||0).toFixed(2),n[0],n[1]].join(",")}(t),o=n.shadowCache,a=o[r];if(!a){var s=t.getGlobalScale(),l=s[0],u=s[1];if(!l||!u)return;var h=i.shadowOffsetX||0,c=i.shadowOffsetY||0,p=i.shadowBlur,d=hi(i.shadowColor),f=d.opacity,g=d.color,y=p/2/l+" "+p/2/u;a=n.zrId+"-s"+n.shadowIdx++,n.defs[a]=sw("filter",a,{id:a,x:"-100%",y:"-100%",width:"300%",height:"300%"},[sw("feDropShadow","",{dx:h/l,dy:c/u,stdDeviation:y,"flood-color":g,"flood-opacity":f})]),o[r]=a}e.filter=bi(a)}}(n,t,i)}function Tw(t,e){var n=Ur(e);n&&(n.each((function(e,n){null!=e&&(t[(ow+n).toLowerCase()]=e+"")})),e.isSilent()&&(t[ow+"silent"]="true"))}function Cw(t){return pi(t[0]-1)&&pi(t[1])&&pi(t[2])&&pi(t[3]-1)}function Dw(t,e,n){if(e&&(!function(t){return pi(t[4])&&pi(t[5])}(e)||!Cw(e))){var i=n?10:1e4;t.transform=Cw(e)?"translate("+ww(e[4]*i)/i+" "+ww(e[5]*i)/i+")":function(t){return"matrix("+di(t[0])+","+di(t[1])+","+di(t[2])+","+di(t[3])+","+fi(t[4])+","+fi(t[5])+")"}(e)}}function Aw(t,e,n){for(var i=t.points,r=[],o=0;o=0&&a||o;s&&(r=si(s))}var l=i.lineWidth;l&&(l/=!i.strokeNoScale&&t.transform?t.transform[0]:1);var u={cursor:"pointer"};r&&(u.fill=r),i.stroke&&(u.stroke=i.stroke),l&&(u["stroke-width"]=l),bw(u,e,n,!0)}}(t,o,e),sw(s,t.id+"",o)}function Nw(t,e){return t instanceof ks?Rw(t,e):t instanceof Ns?function(t,e){var n=t.style,i=n.image;if(i&&!U(i)&&(Sw(i)?i=i.src:Mw(i)&&(i=i.toDataURL())),i){var r=n.x||0,o=n.y||0,a={href:i,width:n.width,height:n.height};return r&&(a.x=r),o&&(a.y=o),Dw(a,t.transform),Iw(a,n,t,e),Tw(a,t),e.animation&&_w(t,a,e),sw("image",t.id+"",a)}}(t,e):t instanceof Ps?function(t,e){var n=t.style,i=n.text;if(null!=i&&(i+=""),i&&!isNaN(n.x)&&!isNaN(n.y)){var r=n.font||a,s=n.x||0,l=function(t,e,n){return"top"===n?t+=e/2:"bottom"===n&&(t-=e/2),t}(n.y||0,Tr(r),n.textBaseline),u={"dominant-baseline":"central","text-anchor":gi[n.textAlign]||n.textAlign};if($s(n)){var h="",c=n.fontStyle,p=qs(n.fontSize);if(!parseFloat(p))return;var d=n.fontFamily||o,f=n.fontWeight;h+="font-size:"+p+";font-family:"+d+";",c&&"normal"!==c&&(h+="font-style:"+c+";"),f&&"normal"!==f&&(h+="font-weight:"+f+";"),u.style=h}else u.style="font: "+r;return i.match(/\s/)&&(u["xml:space"]="preserve"),s&&(u.x=s),l&&(u.y=l),Dw(u,t.transform),Iw(u,n,t,e),Tw(u,t),e.animation&&_w(t,u,e),sw("text",t.id+"",u,void 0,i)}}(t,e):void 0}function Ew(t,e,n,i){var r,o=t[n],a={gradientUnits:o.global?"userSpaceOnUse":"objectBoundingBox"};if(mi(o))r="linearGradient",a.x1=o.x,a.y1=o.y,a.x2=o.x2,a.y2=o.y2;else{if(!xi(o))return void 0;r="radialGradient",a.cx=rt(o.x,.5),a.cy=rt(o.y,.5),a.r=rt(o.r,.5)}for(var s=o.colorStops,l=[],u=0,h=s.length;ul?Jw(t,null==n[c+1]?null:n[c+1].elm,n,s,c):Qw(t,e,a,l))}(n,i,r):jw(r)?(jw(t.text)&&Xw(n,""),Jw(n,null,r,0,r.length-1)):jw(i)?Qw(n,i,0,i.length-1):jw(t.text)&&Xw(n,""):t.text!==e.text&&(jw(i)&&Qw(n,i,0,i.length-1),Xw(n,e.text)))}var nS=0,iS=function(){function t(t,e,n){if(this.type="svg",this.refreshHover=rS("refreshHover"),this.configLayer=rS("configLayer"),this.storage=e,this._opts=n=A({},n),this.root=t,this._id="zr"+nS++,this._oldVNode=hw(n.width,n.height),t&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var r=this._svgDom=this._oldVNode.elm=aw("svg");tS(null,this._oldVNode),i.appendChild(r),t.appendChild(i)}this.resize(n.width,n.height)}return t.prototype.getType=function(){return this.type},t.prototype.getViewportRoot=function(){return this._viewport},t.prototype.getViewportRootOffset=function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},t.prototype.getSvgDom=function(){return this._svgDom},t.prototype.refresh=function(){if(this.root){var t=this.renderToVNode({willUpdate:!0});t.attrs.style="position:absolute;left:0;top:0;user-select:none",function(t,e){if(Kw(t,e))eS(t,e);else{var n=t.elm,i=Hw(n);$w(e),null!==i&&(Fw(i,e.elm,Yw(n)),Qw(i,[t],0,0))}}(this._oldVNode,t),this._oldVNode=t}},t.prototype.renderOneToVNode=function(t){return Nw(t,uw(this._id))},t.prototype.renderToVNode=function(t){t=t||{};var e=this.storage.getDisplayList(!0),n=this._width,i=this._height,r=uw(this._id);r.animation=t.animation,r.willUpdate=t.willUpdate,r.compress=t.compress,r.emphasis=t.emphasis;var o=[],a=this._bgVNode=function(t,e,n,i){var r;if(n&&"none"!==n)if(r=sw("rect","bg",{width:t,height:e,x:"0",y:"0"}),_i(n))Ew({fill:n},r.attrs,"fill",i);else if(vi(n))zw({style:{fill:n},dirty:bt,getBoundingRect:function(){return{width:t,height:e}}},r.attrs,"fill",i);else{var o=hi(n),a=o.color,s=o.opacity;r.attrs.fill=a,s<1&&(r.attrs["fill-opacity"]=s)}return r}(n,i,this._backgroundColor,r);a&&o.push(a);var s=t.compress?null:this._mainVNode=sw("g","main",{},[]);this._paintList(e,r,s?s.children:o),s&&o.push(s);var l=z(G(r.defs),(function(t){return r.defs[t]}));if(l.length&&o.push(sw("defs","defs",{},l)),t.animation){var u=function(t,e,n){var i=(n=n||{}).newline?"\n":"",r=" {"+i,o=i+"}",a=z(G(t),(function(e){return e+r+z(G(t[e]),(function(n){return n+":"+t[e][n]+";"})).join(i)+o})).join(i),s=z(G(e),(function(t){return"@keyframes "+t+r+z(G(e[t]),(function(n){return n+r+z(G(e[t][n]),(function(i){var r=e[t][n][i];return"d"===i&&(r='path("'+r+'")'),i+":"+r+";"})).join(i)+o})).join(i)+o})).join(i);return a||s?[""].join(i):""}(r.cssNodes,r.cssAnims,{newline:!0});if(u){var h=sw("style","stl",{},[],u);o.push(h)}}return hw(n,i,o,t.useViewBox)},t.prototype.renderToString=function(t){return t=t||{},lw(this.renderToVNode({animation:rt(t.cssAnimation,!0),emphasis:rt(t.cssEmphasis,!0),willUpdate:!1,compress:!0,useViewBox:rt(t.useViewBox,!0)}),{newline:!0})},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t},t.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},t.prototype._paintList=function(t,e,n){for(var i,r,o=t.length,a=[],s=0,l=0,u=0;u=0&&(!c||!r||c[f]!==r[f]);f--);for(var g=d-1;g>f;g--)i=a[--s-1];for(var y=f+1;y=a)}}for(var h=this.__startIndex;h15)break}n.prevElClipPaths&&u.restore()};if(p)if(0===p.length)s=l.__endIndex;else for(var _=d.dpr,b=0;b0&&t>i[0]){for(s=0;st);s++);a=n[i[s]]}if(i.splice(s+1,0,t),n[t]=e,!e.virtual)if(a){var l=a.dom;l.nextSibling?o.insertBefore(e.dom,l.nextSibling):o.appendChild(e.dom)}else o.firstChild?o.insertBefore(e.dom,o.firstChild):o.appendChild(e.dom);e.painter||(e.painter=this)}},t.prototype.eachLayer=function(t,e){for(var n=this._zlevelList,i=0;i0?uS:0),this._needsManuallyCompositing),u.__builtin__||I("ZLevel "+l+" has been used by unkown layer "+u.id),u!==o&&(u.__used=!0,u.__startIndex!==r&&(u.__dirty=!0),u.__startIndex=r,u.incremental?u.__drawIndex=-1:u.__drawIndex=r,e(r),o=u),1&s.__dirty&&!s.__inHover&&(u.__dirty=!0,u.incremental&&u.__drawIndex<0&&(u.__drawIndex=r))}e(r),this.eachBuiltinLayer((function(t,e){!t.__used&&t.getElementCount()>0&&(t.__dirty=!0,t.__startIndex=t.__endIndex=t.__drawIndex=0),t.__dirty&&t.__drawIndex<0&&(t.__drawIndex=t.__startIndex)}))},t.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},t.prototype._clearLayer=function(t){t.clear()},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t,E(this._layers,(function(t){t.setUnpainted()}))},t.prototype.configLayer=function(t,e){if(e){var n=this._layerConfig;n[t]?C(n[t],e,!0):n[t]=e;for(var i=0;i-1&&(s.style.stroke=s.style.fill,s.style.fill="#fff",s.style.lineWidth=2),e},e.type="series.line",e.dependencies=["grid","polar"],e.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},e}(bg);function pS(t,e){var n=t.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var r=mf(t,e,n[0]);return null!=r?r+"":null}if(i){for(var o=[],a=0;a=0&&i.push(e[o])}return i.join(" ")}var fS=function(t){function e(e,n,i,r){var o=t.call(this)||this;return o.updateData(e,n,i,r),o}return n(e,t),e.prototype._createSymbol=function(t,e,n,i,r){this.removeAll();var o=Xy(t,-1,-1,2,2,null,r);o.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),o.drift=gS,this._symbolType=t,this.add(o)},e.prototype.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(null,t)},e.prototype.getSymbolType=function(){return this._symbolType},e.prototype.getSymbolPath=function(){return this.childAt(0)},e.prototype.highlight=function(){Ol(this.childAt(0))},e.prototype.downplay=function(){Rl(this.childAt(0))},e.prototype.setZ=function(t,e){var n=this.childAt(0);n.zlevel=t,n.z=e},e.prototype.setDraggable=function(t,e){var n=this.childAt(0);n.draggable=t,n.cursor=!e&&t?"move":n.cursor},e.prototype.updateData=function(t,n,i,r){this.silent=!1;var o=t.getItemVisual(n,"symbol")||"circle",a=t.hostModel,s=e.getSymbolSize(t,n),l=o!==this._symbolType,u=r&&r.disableAnimation;if(l){var h=t.getItemVisual(n,"symbolKeepAspect");this._createSymbol(o,t,n,s,h)}else{(p=this.childAt(0)).silent=!1;var c={scaleX:s[0]/2,scaleY:s[1]/2};u?p.attr(c):vh(p,c,a,n),Sh(p)}if(this._updateCommon(t,n,s,i,r),l){var p=this.childAt(0);if(!u){c={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:p.style.opacity}};p.scaleX=p.scaleY=0,p.style.opacity=0,mh(p,c,a,n)}}u&&this.childAt(0).stopAnimation("leave")},e.prototype._updateCommon=function(t,e,n,i,r){var o,a,s,l,u,h,c,p,d,f=this.childAt(0),g=t.hostModel;if(i&&(o=i.emphasisItemStyle,a=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,u=i.blurScope,c=i.labelStatesModels,p=i.hoverScale,d=i.cursorStyle,h=i.emphasisDisabled),!i||t.hasItemOption){var y=i&&i.itemModel?i.itemModel:t.getItemModel(e),v=y.getModel("emphasis");o=v.getModel("itemStyle").getItemStyle(),s=y.getModel(["select","itemStyle"]).getItemStyle(),a=y.getModel(["blur","itemStyle"]).getItemStyle(),l=v.get("focus"),u=v.get("blurScope"),h=v.get("disabled"),c=rc(y),p=v.getShallow("scale"),d=y.getShallow("cursor")}var m=t.getItemVisual(e,"symbolRotate");f.attr("rotation",(m||0)*Math.PI/180||0);var x=Zy(t.getItemVisual(e,"symbolOffset"),n);x&&(f.x=x[0],f.y=x[1]),d&&f.attr("cursor",d);var _=t.getItemVisual(e,"style"),b=_.fill;if(f instanceof Ns){var w=f.style;f.useStyle(A({image:w.image,x:w.x,y:w.y,width:w.width,height:w.height},_))}else f.__isEmptyBrush?f.useStyle(A({},_)):f.useStyle(_),f.style.decal=null,f.setColor(b,r&&r.symbolInnerColor),f.style.strokeNoScale=!0;var S=t.getItemVisual(e,"liftZ"),M=this._z2;null!=S?null==M&&(this._z2=f.z2,f.z2+=S):null!=M&&(f.z2=M,this._z2=null);var I=r&&r.useNameLabel;ic(f,c,{labelFetcher:g,labelDataIndex:e,defaultText:function(e){return I?t.getName(e):pS(t,e)},inheritColor:b,defaultOpacity:_.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var T=f.ensureState("emphasis");T.style=o,f.ensureState("select").style=s,f.ensureState("blur").style=a;var C=null==p||!0===p?Math.max(1.1,3/this._sizeY):isFinite(p)&&p>0?+p:1;T.scaleX=this._sizeX*C,T.scaleY=this._sizeY*C,this.setSymbolScale(1),Zl(this,l,u,h)},e.prototype.setSymbolScale=function(t){this.scaleX=this.scaleY=t},e.prototype.fadeOut=function(t,e,n){var i=this.childAt(0),r=rl(this).dataIndex,o=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var a=i.getTextContent();a&&_h(a,{style:{opacity:0}},e,{dataIndex:r,removeOpt:o,cb:function(){i.removeTextContent()}})}else i.removeTextContent();_h(i,{style:{opacity:0},scaleX:0,scaleY:0},e,{dataIndex:r,cb:t,removeOpt:o})},e.getSymbolSize=function(t,e){return Uy(t.getItemVisual(e,"symbolSize"))},e}(Br);function gS(t,e){this.parent.drift(t,e)}function yS(t,e,n,i){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(e[0],e[1]))&&"none"!==t.getItemVisual(n,"symbol")}function vS(t){return null==t||q(t)||(t={isIgnore:t}),t||{}}function mS(t){var e=t.hostModel,n=e.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:e.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:e.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:rc(e),cursorStyle:e.get("cursor")}}var xS=function(){function t(t){this.group=new Br,this._SymbolCtor=t||fS}return t.prototype.updateData=function(t,e){this._progressiveEls=null,e=vS(e);var n=this.group,i=t.hostModel,r=this._data,o=this._SymbolCtor,a=e.disableAnimation,s=mS(t),l={disableAnimation:a},u=e.getSymbolPoint||function(e){return t.getItemLayout(e)};r||n.removeAll(),t.diff(r).add((function(i){var r=u(i);if(yS(t,r,i,e)){var a=new o(t,i,s,l);a.setPosition(r),t.setItemGraphicEl(i,a),n.add(a)}})).update((function(h,c){var p=r.getItemGraphicEl(c),d=u(h);if(yS(t,d,h,e)){var f=t.getItemVisual(h,"symbol")||"circle",g=p&&p.getSymbolType&&p.getSymbolType();if(!p||g&&g!==f)n.remove(p),(p=new o(t,h,s,l)).setPosition(d);else{p.updateData(t,h,s,l);var y={x:d[0],y:d[1]};a?p.attr(y):vh(p,y,i)}n.add(p),t.setItemGraphicEl(h,p)}else n.remove(p)})).remove((function(t){var e=r.getItemGraphicEl(t);e&&e.fadeOut((function(){n.remove(e)}),i)})).execute(),this._getSymbolPoint=u,this._data=t},t.prototype.updateLayout=function(){var t=this,e=this._data;e&&e.eachItemGraphicEl((function(e,n){var i=t._getSymbolPoint(n);e.setPosition(i),e.markRedraw()}))},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=mS(t),this._data=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e,n){function i(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[],n=vS(n);for(var r=t.start;r0?n=i[0]:i[1]<0&&(n=i[1]);return n}(r,n),a=i.dim,s=r.dim,l=e.mapDimension(s),u=e.mapDimension(a),h="x"===s||"radius"===s?1:0,c=z(t.dimensions,(function(t){return e.mapDimension(t)})),p=!1,d=e.getCalculationInfo("stackResultDimension");return mx(e,c[0])&&(p=!0,c[0]=d),mx(e,c[1])&&(p=!0,c[1]=d),{dataDimsForPoint:c,valueStart:o,valueAxisDim:s,baseAxisDim:a,stacked:!!p,valueDim:l,baseDim:u,baseDataOffset:h,stackedOverDimension:e.getCalculationInfo("stackedOverDimension")}}function bS(t,e,n,i){var r=NaN;t.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(r)&&(r=t.valueStart);var o=t.baseDataOffset,a=[];return a[o]=n.get(t.baseDim,i),a[1-o]=r,e.dataToPoint(a)}var wS=Math.min,SS=Math.max;function MS(t,e){return isNaN(t)||isNaN(e)}function IS(t,e,n,i,r,o,a,s,l){for(var u,h,c,p,d,f,g=n,y=0;y=r||g<0)break;if(MS(v,m)){if(l){g+=o;continue}break}if(g===n)t[o>0?"moveTo":"lineTo"](v,m),c=v,p=m;else{var x=v-u,_=m-h;if(x*x+_*_<.5){g+=o;continue}if(a>0){for(var b=g+o,w=e[2*b],S=e[2*b+1];w===v&&S===m&&y=i||MS(w,S))d=v,f=m;else{T=w-u,C=S-h;var k=v-u,L=w-v,P=m-h,O=S-m,R=void 0,N=void 0;if("x"===s){var E=T>0?1:-1;d=v-E*(R=Math.abs(k))*a,f=m,D=v+E*(N=Math.abs(L))*a,A=m}else if("y"===s){var z=C>0?1:-1;d=v,f=m-z*(R=Math.abs(P))*a,D=v,A=m+z*(N=Math.abs(O))*a}else R=Math.sqrt(k*k+P*P),d=v-T*a*(1-(I=(N=Math.sqrt(L*L+O*O))/(N+R))),f=m-C*a*(1-I),A=m+C*a*I,D=wS(D=v+T*a*I,SS(w,v)),A=wS(A,SS(S,m)),D=SS(D,wS(w,v)),f=m-(C=(A=SS(A,wS(S,m)))-m)*R/N,d=wS(d=v-(T=D-v)*R/N,SS(u,v)),f=wS(f,SS(h,m)),D=v+(T=v-(d=SS(d,wS(u,v))))*N/R,A=m+(C=m-(f=SS(f,wS(h,m))))*N/R}t.bezierCurveTo(c,p,d,f,v,m),c=D,p=A}else t.lineTo(v,m)}u=v,h=m,g+=o}return y}var TS=function(){this.smooth=0,this.smoothConstraint=!0},CS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polyline",n}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new TS},e.prototype.buildPath=function(t,e){var n=e.points,i=0,r=n.length/2;if(e.connectNulls){for(;r>0&&MS(n[2*r-2],n[2*r-1]);r--);for(;i=0){var y=a?(h-i)*g+i:(u-n)*g+n;return a?[t,y]:[y,t]}n=u,i=h;break;case o.C:u=r[l++],h=r[l++],c=r[l++],p=r[l++],d=r[l++],f=r[l++];var v=a?_n(n,u,c,d,t,s):_n(i,h,p,f,t,s);if(v>0)for(var m=0;m=0){y=a?mn(i,h,p,f,x):mn(n,u,c,d,x);return a?[t,y]:[y,t]}}n=d,i=f}}},e}(ks),DS=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e}(TS),AS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polygon",n}return n(e,t),e.prototype.getDefaultShape=function(){return new DS},e.prototype.buildPath=function(t,e){var n=e.points,i=e.stackedOnPoints,r=0,o=n.length/2,a=e.smoothMonotone;if(e.connectNulls){for(;o>0&&MS(n[2*o-2],n[2*o-1]);o--);for(;r=0;a--){var s=t.getDimensionInfo(i[a].dimension);if("x"===(r=s&&s.coordDim)||"y"===r){o=i[a];break}}if(o){var l=e.getAxis(r),u=z(o.stops,(function(t){return{coord:l.toGlobalCoord(l.dataToCoord(t.value)),color:t.color}})),h=u.length,c=o.outerColors.slice();h&&u[0].coord>u[h-1].coord&&(u.reverse(),c.reverse());var p=function(t,e){var n,i,r=[],o=t.length;function a(t,e,n){var i=t.coord;return{coord:n,color:ti((n-i)/(e.coord-i),[t.color,e.color])}}for(var s=0;se){i?r.push(a(i,l,e)):n&&r.push(a(n,l,0),a(n,l,e));break}n&&(r.push(a(n,l,0)),n=null),r.push(l),i=l}}return r}(u,"x"===r?n.getWidth():n.getHeight()),d=p.length;if(!d&&h)return u[0].coord<0?c[1]?c[1]:u[h-1].color:c[0]?c[0]:u[0].color;var f=p[0].coord-10,g=p[d-1].coord+10,y=g-f;if(y<.001)return"transparent";E(p,(function(t){t.offset=(t.coord-f)/y})),p.push({offset:d?p[d-1].offset:.5,color:c[1]||"transparent"}),p.unshift({offset:d?p[0].offset:.5,color:c[0]||"transparent"});var v=new oh(0,0,0,0,p,!0);return v[r]=f,v[r+"2"]=g,v}}}function FS(t,e,n){var i=t.get("showAllSymbol"),r="auto"===i;if(!i||r){var o=n.getAxesByScale("ordinal")[0];if(o&&(!r||!function(t,e){var n=t.getExtent(),i=Math.abs(n[1]-n[0])/t.scale.count();isNaN(i)&&(i=0);for(var r=e.count(),o=Math.max(1,Math.round(r/5)),a=0;ai)return!1;return!0}(o,e))){var a=e.mapDimension(o.dim),s={};return E(o.getViewLabels(),(function(t){var e=o.scale.getRawOrdinalNumber(t.tickValue);s[e]=1})),function(t){return!s.hasOwnProperty(e.get(a,t))}}}}function GS(t,e){return[t[2*e],t[2*e+1]]}function WS(t){if(t.get(["endLabel","show"]))return!0;for(var e=0;e0&&"bolder"===t.get(["emphasis","lineStyle","width"]))&&(d.getState("emphasis").style.lineWidth=+d.style.lineWidth+1);rl(d).seriesIndex=t.seriesIndex,Zl(d,L,P,O);var R=zS(t.get("smooth")),N=t.get("smoothMonotone");if(d.setShape({smooth:R,smoothMonotone:N,connectNulls:w}),f){var E=a.getCalculationInfo("stackedOnSeries"),z=0;f.useStyle(k(l.getAreaStyle(),{fill:C,opacity:.7,lineJoin:"bevel",decal:a.getVisual("style").decal})),E&&(z=zS(E.get("smooth"))),f.setShape({smooth:R,stackedOnSmooth:z,smoothMonotone:N,connectNulls:w}),$l(f,t,"areaStyle"),rl(f).seriesIndex=t.seriesIndex,Zl(f,L,P,O)}var V=function(t){i._changePolyState(t)};a.eachItemGraphicEl((function(t){t&&(t.onHoverStateChange=V)})),this._polyline.onHoverStateChange=V,this._data=a,this._coordSys=r,this._stackedOnPoints=_,this._points=u,this._step=T,this._valueOrigin=m,t.get("triggerLineEvent")&&(this.packEventData(t,d),f&&this.packEventData(t,f))},e.prototype.packEventData=function(t,e){rl(e).eventData={componentType:"series",componentSubType:"line",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"line"}},e.prototype.highlight=function(t,e,n,i){var r=t.getData(),o=zo(r,i);if(this._changePolyState("emphasis"),!(o instanceof Array)&&null!=o&&o>=0){var a=r.getLayout("points"),s=r.getItemGraphicEl(o);if(!s){var l=a[2*o],u=a[2*o+1];if(isNaN(l)||isNaN(u))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,u))return;var h=t.get("zlevel")||0,c=t.get("z")||0;(s=new fS(r,o)).x=l,s.y=u,s.setZ(h,c);var p=s.getSymbolPath().getTextContent();p&&(p.zlevel=h,p.z=c,p.z2=this._polyline.z2+1),s.__temp=!0,r.setItemGraphicEl(o,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else Og.prototype.highlight.call(this,t,e,n,i)},e.prototype.downplay=function(t,e,n,i){var r=t.getData(),o=zo(r,i);if(this._changePolyState("normal"),null!=o&&o>=0){var a=r.getItemGraphicEl(o);a&&(a.__temp?(r.setItemGraphicEl(o,null),this.group.remove(a)):a.downplay())}else Og.prototype.downplay.call(this,t,e,n,i)},e.prototype._changePolyState=function(t){var e=this._polygon;Dl(this._polyline,t),e&&Dl(e,t)},e.prototype._newPolyline=function(t){var e=this._polyline;return e&&this._lineGroup.remove(e),e=new CS({shape:{points:t},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(e),this._polyline=e,e},e.prototype._newPolygon=function(t,e){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new AS({shape:{points:t,stackedOnPoints:e},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},e.prototype._initSymbolLabelAnimation=function(t,e,n){var i,r,o=e.getBaseAxis(),a=o.inverse;"cartesian2d"===e.type?(i=o.isHorizontal(),r=!1):"polar"===e.type&&(i="angle"===o.dim,r=!0);var s=t.hostModel,l=s.get("animationDuration");X(l)&&(l=l(null));var u=s.get("animationDelay")||0,h=X(u)?u(null):u;t.eachItemGraphicEl((function(t,o){var s=t;if(s){var c=[t.x,t.y],p=void 0,d=void 0,f=void 0;if(n)if(r){var g=n,y=e.pointToCoord(c);i?(p=g.startAngle,d=g.endAngle,f=-y[1]/180*Math.PI):(p=g.r0,d=g.r,f=y[0])}else{var v=n;i?(p=v.x,d=v.x+v.width,f=t.x):(p=v.y+v.height,d=v.y,f=t.y)}var m=d===p?0:(f-p)/(d-p);a&&(m=1-m);var x=X(u)?u(o):l*m+h,_=s.getSymbolPath(),b=_.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),b&&b.animateFrom({style:{opacity:0}},{duration:300,delay:x}),_.disableLabelAnimation=!0}}))},e.prototype._initOrUpdateEndLabel=function(t,e,n){var i=t.getModel("endLabel");if(WS(t)){var r=t.getData(),o=this._polyline,a=r.getLayout("points");if(!a)return o.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new Xs({z2:200})).ignoreClip=!0,o.setTextContent(this._endLabel),o.disableLabelAnimation=!0);var l=function(t){for(var e,n,i=t.length/2;i>0&&(e=t[2*i-2],n=t[2*i-1],isNaN(e)||isNaN(n));i--);return i-1}(a);l>=0&&(ic(o,rc(t,"endLabel"),{inheritColor:n,labelFetcher:t,labelDataIndex:l,defaultText:function(t,e,n){return null!=n?dS(r,n):pS(r,t)},enableTextSetter:!0},function(t,e){var n=e.getBaseAxis(),i=n.isHorizontal(),r=n.inverse,o=i?r?"right":"left":"center",a=i?"middle":r?"top":"bottom";return{normal:{align:t.get("align")||o,verticalAlign:t.get("verticalAlign")||a}}}(i,e)),o.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},e.prototype._endLabelOnDuring=function(t,e,n,i,r,o,a){var s=this._endLabel,l=this._polyline;if(s){t<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var u=n.getLayout("points"),h=n.hostModel,c=h.get("connectNulls"),p=o.get("precision"),d=o.get("distance")||0,f=a.getBaseAxis(),g=f.isHorizontal(),y=f.inverse,v=e.shape,m=y?g?v.x:v.y+v.height:g?v.x+v.width:v.y,x=(g?d:0)*(y?-1:1),_=(g?0:-d)*(y?-1:1),b=g?"x":"y",w=function(t,e,n){for(var i,r,o=t.length/2,a="x"===n?0:1,s=0,l=-1,u=0;u=e||i>=e&&r<=e){l=u;break}s=u,i=r}else i=r;return{range:[s,l],t:(e-i)/(r-i)}}(u,m,b),S=w.range,M=S[1]-S[0],I=void 0;if(M>=1){if(M>1&&!c){var T=GS(u,S[0]);s.attr({x:T[0]+x,y:T[1]+_}),r&&(I=h.getRawValue(S[0]))}else{(T=l.getPointOn(m,b))&&s.attr({x:T[0]+x,y:T[1]+_});var C=h.getRawValue(S[0]),D=h.getRawValue(S[1]);r&&(I=Zo(n,p,C,D,w.t))}i.lastFrameIndex=S[0]}else{var A=1===t||i.lastFrameIndex>0?S[0]:0;T=GS(u,A);r&&(I=h.getRawValue(A)),s.attr({x:T[0]+x,y:T[1]+_})}if(r){var k=pc(s);"function"==typeof k.setLabelText&&k.setLabelText(I)}}},e.prototype._doUpdateAnimation=function(t,e,n,i,r,o,a){var s=this._polyline,l=this._polygon,u=t.hostModel,h=function(t,e,n,i,r,o,a,s){for(var l=function(t,e){var n=[];return e.diff(t).add((function(t){n.push({cmd:"+",idx:t})})).update((function(t,e){n.push({cmd:"=",idx:e,idx1:t})})).remove((function(t){n.push({cmd:"-",idx:t})})).execute(),n}(t,e),u=[],h=[],c=[],p=[],d=[],f=[],g=[],y=_S(r,e,a),v=t.getLayout("points")||[],m=e.getLayout("points")||[],x=0;x3e3||l&&ES(p,f)>3e3)return s.stopAnimation(),s.setShape({points:d}),void(l&&(l.stopAnimation(),l.setShape({points:d,stackedOnPoints:f})));s.shape.__points=h.current,s.shape.points=c;var g={shape:{points:d}};h.current!==c&&(g.shape.__points=h.next),s.stopAnimation(),vh(s,g,u),l&&(l.setShape({points:c,stackedOnPoints:p}),l.stopAnimation(),vh(l,{shape:{stackedOnPoints:f}},u),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var y=[],v=h.status,m=0;me&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;ne&&(e=o,n=r)}return isFinite(n)?n:NaN},nearest:function(t){return t[0]}},ZS=function(t){return Math.round(t.length/2)};function jS(t){return{seriesType:t,reset:function(t,e,n){var i=t.getData(),r=t.get("sampling"),o=t.coordinateSystem,a=i.count();if(a>10&&"cartesian2d"===o.type&&r){var s=o.getBaseAxis(),l=o.getOtherAxis(s),u=s.getExtent(),h=n.getDevicePixelRatio(),c=Math.abs(u[1]-u[0])*(h||1),p=Math.round(a/c);if(isFinite(p)&&p>1){"lttb"===r&&t.setData(i.lttbDownSample(i.mapDimension(l.dim),1/p));var d=void 0;U(r)?d=US[r]:X(r)&&(d=r),d&&t.setData(i.downSample(i.mapDimension(l.dim),1/p,d,ZS))}}}}}var qS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(t,e){return _x(null,this,{useEncodeDefaulter:!0})},e.prototype.getMarkerPosition=function(t,e,n){var i=this.coordinateSystem;if(i&&i.clampData){var r=i.clampData(t),o=i.dataToPoint(r);if(n)E(i.getAxes(),(function(t,n){if("category"===t.type&&null!=e){var i=t.getTicksCoords(),a=t.getTickModel().get("alignWithLabel"),s=r[n],l="x1"===e[n]||"y1"===e[n];if(l&&!a&&(s+=1),i.length<2)return;if(2===i.length)return void(o[n]=t.toGlobalCoord(t.getExtent()[l?1:0]));for(var u=void 0,h=void 0,c=1,p=0;ps){h=(d+u)/2;break}1===p&&(c=f-i[0].tickValue)}null==h&&(u?u&&(h=i[i.length-1].coord):h=i[0].coord),o[n]=t.toGlobalCoord(h)}}));else{var a=this.getData(),s=a.getLayout("offset"),l=a.getLayout("size"),u=i.getBaseAxis().isHorizontal()?0:1;o[u]+=s+l/2}return o}return[NaN,NaN]},e.type="series.__base_bar__",e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},e}(bg);bg.registerClass(qS);var KS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(){return _x(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},e.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},e.prototype.getProgressiveThreshold=function(){var t=this.get("progressiveThreshold"),e=this.get("largeThreshold");return e>t&&(t=e),t},e.prototype.brushSelector=function(t,e,n){return n.rect(e.getItemLayout(t))},e.type="series.bar",e.dependencies=["grid","polar"],e.defaultOption=kc(qS.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),e}(qS),$S=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},JS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="sausage",n}return n(e,t),e.prototype.getDefaultShape=function(){return new $S},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r0||0,0),o=Math.max(e.r,0),a=.5*(o-r),s=r+a,l=e.startAngle,u=e.endAngle,h=e.clockwise,c=2*Math.PI,p=h?u-lo)return!0;o=u}return!1},e.prototype._isOrderDifferentInView=function(t,e){for(var n=e.scale,i=n.getExtent(),r=Math.max(0,i[0]),o=Math.min(i[1],n.getOrdinalMeta().categories.length-1);r<=o;++r)if(t.ordinalNumbers[r]!==n.getRawOrdinalNumber(r))return!0},e.prototype._updateSortWithinSameData=function(t,e,n,i){if(this._isOrderChangedWithinSameData(t,e,n)){var r=this._dataSort(t,n,e);this._isOrderDifferentInView(r,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:r}))}},e.prototype._dispatchInitSort=function(t,e,n){var i=e.baseAxis,r=this._dataSort(t,i,(function(n){return t.get(t.mapDimension(e.otherAxis.dim),n)}));n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:r})},e.prototype.remove=function(t,e){this._clear(this._model),this._removeOnRenderedListener(e)},e.prototype.dispose=function(t,e){this._removeOnRenderedListener(e)},e.prototype._removeOnRenderedListener=function(t){this._onRendered&&(t.getZr().off("rendered",this._onRendered),this._onRendered=null)},e.prototype._clear=function(t){var e=this.group,n=this._data;t&&t.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl((function(e){wh(e,t,rl(e).dataIndex)}))):e.removeAll(),this._data=null,this._isFirstFrame=!0},e.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},e.type="bar",e}(Og),oM={cartesian2d:function(t,e){var n=e.width<0?-1:1,i=e.height<0?-1:1;n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height);var r=t.x+t.width,o=t.y+t.height,a=nM(e.x,t.x),s=iM(e.x+e.width,r),l=nM(e.y,t.y),u=iM(e.y+e.height,o),h=sr?s:a,e.y=c&&l>o?u:l,e.width=h?0:s-a,e.height=c?0:u-l,n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height),h||c},polar:function(t,e){var n=e.r0<=e.r?1:-1;if(n<0){var i=e.r;e.r=e.r0,e.r0=i}var r=iM(e.r,t.r),o=nM(e.r0,t.r0);e.r=r,e.r0=o;var a=r-o<0;if(n<0){i=e.r;e.r=e.r0,e.r0=i}return a}},aM={cartesian2d:function(t,e,n,i,r,o,a,s,l){var u=new Ws({shape:A({},i),z2:1});(u.__dataIndex=n,u.name="item",o)&&(u.shape[r?"height":"width"]=0);return u},polar:function(t,e,n,i,r,o,a,s,l){var u=!r&&l?JS:Fu,h=new u({shape:i,z2:1});h.name="item";var c,p,d=dM(r);if(h.calculateTextPosition=(c=d,p=({isRoundCap:u===JS}||{}).isRoundCap,function(t,e,n){var i=e.position;if(!i||i instanceof Array)return Dr(t,e,n);var r=c(i),o=null!=e.distance?e.distance:5,a=this.shape,s=a.cx,l=a.cy,u=a.r,h=a.r0,d=(u+h)/2,f=a.startAngle,g=a.endAngle,y=(f+g)/2,v=p?Math.abs(u-h)/2:0,m=Math.cos,x=Math.sin,_=s+u*m(f),b=l+u*x(f),w="left",S="top";switch(r){case"startArc":_=s+(h-o)*m(y),b=l+(h-o)*x(y),w="center",S="top";break;case"insideStartArc":_=s+(h+o)*m(y),b=l+(h+o)*x(y),w="center",S="bottom";break;case"startAngle":_=s+d*m(f)+QS(f,o+v,!1),b=l+d*x(f)+tM(f,o+v,!1),w="right",S="middle";break;case"insideStartAngle":_=s+d*m(f)+QS(f,-o+v,!1),b=l+d*x(f)+tM(f,-o+v,!1),w="left",S="middle";break;case"middle":_=s+d*m(y),b=l+d*x(y),w="center",S="middle";break;case"endArc":_=s+(u+o)*m(y),b=l+(u+o)*x(y),w="center",S="bottom";break;case"insideEndArc":_=s+(u-o)*m(y),b=l+(u-o)*x(y),w="center",S="top";break;case"endAngle":_=s+d*m(g)+QS(g,o+v,!0),b=l+d*x(g)+tM(g,o+v,!0),w="left",S="middle";break;case"insideEndAngle":_=s+d*m(g)+QS(g,-o+v,!0),b=l+d*x(g)+tM(g,-o+v,!0),w="right",S="middle";break;default:return Dr(t,e,n)}return(t=t||{}).x=_,t.y=b,t.align=w,t.verticalAlign=S,t}),o){var f=r?"r":"endAngle",g={};h.shape[f]=r?i.r0:i.startAngle,g[f]=i[f],(s?vh:mh)(h,{shape:g},o)}return h}};function sM(t,e,n,i,r,o,a,s){var l,u;o?(u={x:i.x,width:i.width},l={y:i.y,height:i.height}):(u={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(a?vh:mh)(n,{shape:l},e,r,null),(a?vh:mh)(n,{shape:u},e?t.baseAxis.model:null,r)}function lM(t,e){for(var n=0;n0?1:-1,a=i.height>0?1:-1;return{x:i.x+o*r/2,y:i.y+a*r/2,width:i.width-o*r,height:i.height-a*r}},polar:function(t,e,n){var i=t.getItemLayout(e);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function dM(t){return function(t){var e=t?"Arc":"Angle";return function(t){switch(t){case"start":case"insideStart":case"end":case"insideEnd":return t+e;default:return t}}}(t)}function fM(t,e,n,i,r,o,a,s){var l=e.getItemVisual(n,"style");if(s){if(!o.get("roundCap")){var u=t.shape;A(u,eM(i.getModel("itemStyle"),u,!0)),t.setShape(u)}}else{var h=i.get(["itemStyle","borderRadius"])||0;t.setShape("r",h)}t.useStyle(l);var c=i.getShallow("cursor");c&&t.attr("cursor",c);var p=s?a?r.r>=r.r0?"endArc":"startArc":r.endAngle>=r.startAngle?"endAngle":"startAngle":a?r.height>=0?"bottom":"top":r.width>=0?"right":"left",d=rc(i);ic(t,d,{labelFetcher:o,labelDataIndex:n,defaultText:pS(o.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:p});var f=t.getTextContent();if(s&&f){var g=i.get(["label","position"]);t.textConfig.inside="middle"===g||null,function(t,e,n,i){if(j(i))t.setTextConfig({rotation:i});else if(Y(e))t.setTextConfig({rotation:0});else{var r,o=t.shape,a=o.clockwise?o.startAngle:o.endAngle,s=o.clockwise?o.endAngle:o.startAngle,l=(a+s)/2,u=n(e);switch(u){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":r=l;break;case"startAngle":case"insideStartAngle":r=a;break;case"endAngle":case"insideEndAngle":r=s;break;default:return void t.setTextConfig({rotation:0})}var h=1.5*Math.PI-r;"middle"===u&&h>Math.PI/2&&h<1.5*Math.PI&&(h-=Math.PI),t.setTextConfig({rotation:h})}}(t,"outside"===g?p:g,dM(a),i.get(["label","rotate"]))}dc(f,d,o.getRawValue(n),(function(t){return dS(e,t)}));var y=i.getModel(["emphasis"]);Zl(t,y.get("focus"),y.get("blurScope"),y.get("disabled")),$l(t,i),function(t){return null!=t.startAngle&&null!=t.endAngle&&t.startAngle===t.endAngle}(r)&&(t.style.fill="none",t.style.stroke="none",E(t.states,(function(t){t.style&&(t.style.fill=t.style.stroke="none")})))}var gM=function(){},yM=function(t){function e(e){var n=t.call(this,e)||this;return n.type="largeBar",n}return n(e,t),e.prototype.getDefaultShape=function(){return new gM},e.prototype.buildPath=function(t,e){for(var n=e.points,i=this.baseDimIdx,r=1-this.baseDimIdx,o=[],a=[],s=this.barWidth,l=0;l=s[0]&&e<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return a[h]}return-1}(this,t.offsetX,t.offsetY);rl(this).dataIndex=e>=0?e:null}),30,!1);function xM(t,e,n){if(OS(n,"cartesian2d")){var i=e,r=n.getArea();return{x:t?i.x:r.x,y:t?r.y:i.y,width:t?i.width:r.width,height:t?r.height:i.height}}var o=e;return{cx:(r=n.getArea()).cx,cy:r.cy,r0:t?r.r0:o.r0,r:t?r.r:o.r,startAngle:t?o.startAngle:0,endAngle:t?o.endAngle:2*Math.PI}}var _M=2*Math.PI,bM=Math.PI/180;function wM(t,e){return kp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function SM(t,e){var n=wM(t,e),i=t.get("center"),r=t.get("radius");Y(r)||(r=[0,r]);var o,a,s=$r(n.width,e.getWidth()),l=$r(n.height,e.getHeight()),u=Math.min(s,l),h=$r(r[0],u/2),c=$r(r[1],u/2),p=t.coordinateSystem;if(p){var d=p.dataToPoint(i);o=d[0]||0,a=d[1]||0}else Y(i)||(i=[i,i]),o=$r(i[0],s)+n.x,a=$r(i[1],l)+n.y;return{cx:o,cy:a,r0:h,r:c}}function MM(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.getData(),i=e.mapDimension("value"),r=wM(t,n),o=SM(t,n),a=o.cx,s=o.cy,l=o.r,u=o.r0,h=-t.get("startAngle")*bM,c=t.get("endAngle"),p=t.get("padAngle")*bM;c="auto"===c?h-_M:-c*bM;var d=t.get("minAngle")*bM+p,f=0;e.each(i,(function(t){!isNaN(t)&&f++}));var g=e.getSum(i),y=Math.PI/(g||f)*2,v=t.get("clockwise"),m=t.get("roseType"),x=t.get("stillShowZeroSum"),_=e.getDataExtent(i);_[0]=0;var b=v?1:-1,w=[h,c],S=b*p/2;us(w,!v),h=w[0],c=w[1];var M=IM(t);M.startAngle=h,M.endAngle=c,M.clockwise=v;var I=Math.abs(c-h),T=I,C=0,D=h;if(e.setLayout({viewRect:r,r:l}),e.each(i,(function(t,n){var i;if(isNaN(t))e.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:v,cx:a,cy:s,r0:u,r:m?NaN:l});else{(i="area"!==m?0===g&&x?y:t*y:I/f)i?h=o=D+b*i/2:(o=D+S,h=r-S),e.setItemLayout(n,{angle:i,startAngle:o,endAngle:h,clockwise:v,cx:a,cy:s,r0:u,r:m?Kr(t,_,[u,l]):l}),D=r}})),T<_M&&f)if(T<=.001){var A=I/f;e.each(i,(function(t,n){if(!isNaN(t)){var i=e.getItemLayout(n);i.angle=A;var r=0,o=0;An?a:o,h=Math.abs(l.label.y-n);if(h>=u.maxY){var c=l.label.x-e-l.len2*r,p=i+l.len,f=Math.abs(c)t.unconstrainedWidth?null:d:null;i.setStyle("width",f)}var g=i.getBoundingRect();o.width=g.width;var y=(i.style.margin||0)+2.1;o.height=g.height+y,o.y-=(o.height-c)/2}}}function kM(t){return"center"===t.position}function LM(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*CM,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,h=s.x,c=s.y,p=s.height;function d(t){t.ignore=!0}i.each((function(t){var s=i.getItemGraphicEl(t),c=s.shape,p=s.getTextContent(),f=s.getTextGuideLine(),g=i.getItemModel(t),y=g.getModel("label"),v=y.get("position")||g.get(["emphasis","label","position"]),m=y.get("distanceToLabelLine"),x=y.get("alignTo"),_=$r(y.get("edgeDistance"),u),b=y.get("bleedMargin"),w=g.getModel("labelLine"),S=w.get("length");S=$r(S,u);var M=w.get("length2");if(M=$r(M,u),Math.abs(c.endAngle-c.startAngle)0?"right":"left":k>0?"left":"right"}var B=Math.PI,F=0,G=y.get("rotate");if(j(G))F=G*(B/180);else if("center"===v)F=0;else if("radial"===G||!0===G){F=k<0?-A+B:-A}else if("tangential"===G&&"outside"!==v&&"outer"!==v){var W=Math.atan2(k,L);W<0&&(W=2*B+W),L>0&&(W=B+W),F=W-B}if(o=!!F,p.x=I,p.y=T,p.rotation=F,p.setStyle({verticalAlign:"middle"}),P){p.setStyle({align:D});var H=p.states.select;H&&(H.x+=p.x,H.y+=p.y)}else{var Y=p.getBoundingRect().clone();Y.applyTransform(p.getComputedTransform());var X=(p.style.margin||0)+2.1;Y.y-=X/2,Y.height+=X,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new De(k,L),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:x,edgeDistance:_,bleedMargin:b,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:p.style.width})}s.setTextConfig({inside:P})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p0){for(var l=o.getItemLayout(0),u=1;isNaN(l&&l.startAngle)&&u=n.r0}},e.type="pie",e}(Og);function RM(t,e,n){e=Y(e)&&{coordDimensions:e}||A({encodeDefine:t.getEncode()},e);var i=t.getSource(),r=px(i,e).dimensions,o=new cx(r,t);return o.initData(i,n),o}var NM=function(){function t(t,e){this._getDataWithEncodedVisual=t,this._getRawData=e}return t.prototype.getAllNames=function(){var t=this._getRawData();return t.mapArray(t.getName)},t.prototype.containName=function(t){return this._getRawData().indexOfName(t)>=0},t.prototype.indexOfName=function(t){return this._getDataWithEncodedVisual().indexOfName(t)},t.prototype.getItemVisual=function(t,e){return this._getDataWithEncodedVisual().getItemVisual(t,e)},t}(),EM=Vo(),zM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new NM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.mergeOption=function(){t.prototype.mergeOption.apply(this,arguments)},e.prototype.getInitialData=function(){return RM(this,{coordDimensions:["value"],encodeDefaulter:H(ed,this)})},e.prototype.getDataParams=function(e){var n=this.getData(),i=EM(n),r=i.seats;if(!r){var o=[];n.each(n.mapDimension("value"),(function(t){o.push(t)})),r=i.seats=io(o,n.hostModel.get("percentPrecision"))}var a=t.prototype.getDataParams.call(this,e);return a.percent=r[e]||0,a.$vars.push("percent"),a},e.prototype._defaultLabelLine=function(t){Co(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.type="series.pie",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,endAngle:"auto",padAngle:0,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},e}(bg);var VM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){return _x(null,this,{useEncodeDefaulter:!0})},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?5e3:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?1e4:this.get("progressiveThreshold"):t},e.prototype.brushSelector=function(t,e,n){return n.point(e.getItemLayout(t))},e.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},e.type="series.scatter",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}},e}(bg),BM=function(){},FM=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.getDefaultShape=function(){return new BM},e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.buildPath=function(t,e){var n,i=e.points,r=e.size,o=this.symbolProxy,a=o.shape,s=t.getContext?t.getContext():t,l=s&&r[0]<4,u=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,u=i[l]-o/2,h=i[l+1]-a/2;if(t>=u&&e>=h&&t<=u+o&&e<=h+a)return s}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape,n=e.points,i=e.size,r=i[0],o=i[1],a=1/0,s=1/0,l=-1/0,u=-1/0,h=0;h=0&&(l.dataIndex=n+(t.startIndex||0))}))},t.prototype.remove=function(){this._clear()},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),WM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).updateData(i,{clipShape:this._getClipShape(t)}),this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).incrementalPrepareUpdate(i),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._symbolDraw.incrementalUpdate(t,e.getData(),{clipShape:this._getClipShape(e)}),this._finished=t.end===e.getData().count()},e.prototype.updateTransform=function(t,e,n){var i=t.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var r=XS("").reset(t,e,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},e.prototype.eachRendered=function(t){this._symbolDraw&&this._symbolDraw.eachRendered(t)},e.prototype._getClipShape=function(t){if(t.get("clip",!0)){var e=t.coordinateSystem;return e&&e.getArea&&e.getArea(.1)}},e.prototype._updateSymbolDraw=function(t,e){var n=this._symbolDraw,i=e.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new GM:new xS,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},e.prototype.dispose=function(){},e.type="scatter",e}(Og),HM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.type="grid",e.dependencies=["xAxis","yAxis"],e.layoutMode="box",e.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},e}(zp),YM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",Wo).models[0]},e.type="cartesian2dAxis",e}(zp);R(YM,D_);var XM={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},UM=C({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},XM),ZM=C({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},XM),jM={category:UM,value:ZM,time:C({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},ZM),log:k({logBase:10},ZM)},qM={value:1,category:1,time:1,log:1};function KM(t,e,i,r){E(qM,(function(o,a){var s=C(C({},jM[a],!0),r,!0),l=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e+"Axis."+a,n}return n(i,t),i.prototype.mergeDefaultAndTheme=function(t,e){var n=Pp(this),i=n?Rp(t):{};C(t,e.getTheme().get(a+"Axis")),C(t,this.getDefaultOption()),t.type=$M(t),n&&Op(t,i,n)},i.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=Mx.createByAxisModel(this))},i.prototype.getCategories=function(t){var e=this.option;if("category"===e.type)return t?e.data:this.__ordinalMeta.categories},i.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},i.type=e+"Axis."+a,i.defaultOption=s,i}(i);t.registerComponentModel(l)})),t.registerSubTypeDefaulter(e+"Axis",$M)}function $M(t){return t.type||(t.data?"category":"value")}var JM=function(){function t(t){this.type="cartesian",this._dimList=[],this._axes={},this.name=t||""}return t.prototype.getAxis=function(t){return this._axes[t]},t.prototype.getAxes=function(){return z(this._dimList,(function(t){return this._axes[t]}),this)},t.prototype.getAxesByScale=function(t){return t=t.toLowerCase(),B(this.getAxes(),(function(e){return e.scale.type===t}))},t.prototype.addAxis=function(t){var e=t.dim;this._axes[e]=t,this._dimList.push(e)},t}(),QM=["x","y"];function tI(t){return"interval"===t.type||"time"===t.type}var eI=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="cartesian2d",e.dimensions=QM,e}return n(e,t),e.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var t=this.getAxis("x").scale,e=this.getAxis("y").scale;if(tI(t)&&tI(e)){var n=t.getExtent(),i=e.getExtent(),r=this.dataToPoint([n[0],i[0]]),o=this.dataToPoint([n[1],i[1]]),a=n[1]-n[0],s=i[1]-i[0];if(a&&s){var l=(o[0]-r[0])/a,u=(o[1]-r[1])/s,h=r[0]-n[0]*l,c=r[1]-i[0]*u,p=this._transform=[l,0,0,u,h,c];this._invTransform=Ie([],p)}}},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},e.prototype.containPoint=function(t){var e=this.getAxis("x"),n=this.getAxis("y");return e.contain(e.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},e.prototype.containData=function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},e.prototype.containZone=function(t,e){var n=this.dataToPoint(t),i=this.dataToPoint(e),r=this.getArea(),o=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]);return r.intersect(o)},e.prototype.dataToPoint=function(t,e,n){n=n||[];var i=t[0],r=t[1];if(this._transform&&null!=i&&isFinite(i)&&null!=r&&isFinite(r))return Wt(n,t,this._transform);var o=this.getAxis("x"),a=this.getAxis("y");return n[0]=o.toGlobalCoord(o.dataToCoord(i,e)),n[1]=a.toGlobalCoord(a.dataToCoord(r,e)),n},e.prototype.clampData=function(t,e){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,r=n.getExtent(),o=i.getExtent(),a=n.parse(t[0]),s=i.parse(t[1]);return(e=e||[])[0]=Math.min(Math.max(Math.min(r[0],r[1]),a),Math.max(r[0],r[1])),e[1]=Math.min(Math.max(Math.min(o[0],o[1]),s),Math.max(o[0],o[1])),e},e.prototype.pointToData=function(t,e){var n=[];if(this._invTransform)return Wt(n,t,this._invTransform);var i=this.getAxis("x"),r=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(t[0]),e),n[1]=r.coordToData(r.toLocalCoord(t[1]),e),n},e.prototype.getOtherAxis=function(t){return this.getAxis("x"===t.dim?"y":"x")},e.prototype.getArea=function(t){t=t||0;var e=this.getAxis("x").getGlobalExtent(),n=this.getAxis("y").getGlobalExtent(),i=Math.min(e[0],e[1])-t,r=Math.min(n[0],n[1])-t,o=Math.max(e[0],e[1])-i+t,a=Math.max(n[0],n[1])-r+t;return new ze(i,r,o,a)},e}(JM),nI=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.index=0,a.type=r||"value",a.position=o||"bottom",a}return n(e,t),e.prototype.isHorizontal=function(){var t=this.position;return"top"===t||"bottom"===t},e.prototype.getGlobalExtent=function(t){var e=this.getExtent();return e[0]=this.toGlobalCoord(e[0]),e[1]=this.toGlobalCoord(e[1]),t&&e[0]>e[1]&&e.reverse(),e},e.prototype.pointToData=function(t,e){return this.coordToData(this.toLocalCoord(t["x"===this.dim?0:1]),e)},e.prototype.setCategorySortInfo=function(t){if("category"!==this.type)return!1;this.model.option.categorySortInfo=t,this.scale.setSortInfo(t)},e}(ab);function iI(t,e,n){n=n||{};var i=t.coordinateSystem,r=e.axis,o={},a=r.getAxesOnZeroOf()[0],s=r.position,l=a?"onZero":s,u=r.dim,h=i.getRect(),c=[h.x,h.x+h.width,h.y,h.y+h.height],p={left:0,right:1,top:0,bottom:1,onZero:2},d=e.get("offset")||0,f="x"===u?[c[2]-d,c[3]+d]:[c[0]-d,c[1]+d];if(a){var g=a.toGlobalCoord(a.dataToCoord(0));f[p.onZero]=Math.max(Math.min(g,f[1]),f[0])}o.position=["y"===u?f[p[l]]:c[0],"x"===u?f[p[l]]:c[3]],o.rotation=Math.PI/2*("x"===u?0:1);o.labelDirection=o.tickDirection=o.nameDirection={top:-1,bottom:1,left:-1,right:1}[s],o.labelOffset=a?f[p[s]]-f[p.onZero]:0,e.get(["axisTick","inside"])&&(o.tickDirection=-o.tickDirection),it(n.labelInside,e.get(["axisLabel","inside"]))&&(o.labelDirection=-o.labelDirection);var y=e.get(["axisLabel","rotate"]);return o.labelRotate="top"===l?-y:y,o.z2=1,o}function rI(t){return"cartesian2d"===t.get("coordinateSystem")}function oI(t){var e={xAxisModel:null,yAxisModel:null};return E(e,(function(n,i){var r=i.replace(/Model$/,""),o=t.getReferringComponents(r,Wo).models[0];e[i]=o})),e}var aI=Math.log;function sI(t,e,n){var i=Ex.prototype,r=i.getTicks.call(n),o=i.getTicks.call(n,!0),a=r.length-1,s=i.getInterval.call(n),l=x_(t,e),u=l.extent,h=l.fixMin,c=l.fixMax;if("log"===t.type){var p=aI(t.base);u=[aI(u[0])/p,aI(u[1])/p]}t.setExtent(u[0],u[1]),t.calcNiceExtent({splitNumber:a,fixMin:h,fixMax:c});var d=i.getExtent.call(t);h&&(u[0]=d[0]),c&&(u[1]=d[1]);var f=i.getInterval.call(t),g=u[0],y=u[1];if(h&&c)f=(y-g)/a;else if(h)for(y=u[0]+f*a;yu[0]&&isFinite(g)&&isFinite(u[0]);)f=Dx(f),g=u[1]-f*a;else{t.getTicks().length-1>a&&(f=Dx(f));var v=f*a;(g=Jr((y=Math.ceil(u[1]/f)*f)-v))<0&&u[0]>=0?(g=0,y=Jr(v)):y>0&&u[1]<=0&&(y=0,g=-Jr(v))}var m=(r[0].value-o[0].value)/s,x=(r[a].value-o[a].value)/s;i.setExtent.call(t,g+f*m,y+f*x),i.setInterval.call(t,f),(m||x)&&i.setNiceExtent.call(t,g+f,y-f)}var lI=function(){function t(t,e,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=QM,this._initCartesian(t,e,n),this.model=t}return t.prototype.getRect=function(){return this._rect},t.prototype.update=function(t,e){var n=this._axesMap;function i(t){var e,n=G(t),i=n.length;if(i){for(var r=[],o=i-1;o>=0;o--){var a=t[+n[o]],s=a.model,l=a.scale;Tx(l)&&s.get("alignTicks")&&null==s.get("interval")?r.push(a):(__(l,s),Tx(l)&&(e=a))}r.length&&(e||__((e=r.pop()).scale,e.model),E(r,(function(t){sI(t.scale,t.model,e.scale)})))}}this._updateScale(t,this.model),i(n.x),i(n.y);var r={};E(n.x,(function(t){hI(n,"y",t,r)})),E(n.y,(function(t){hI(n,"x",t,r)})),this.resize(this.model,e)},t.prototype.resize=function(t,e,n){var i=t.getBoxLayoutParams(),r=!n&&t.get("containLabel"),o=kp(i,{width:e.getWidth(),height:e.getHeight()});this._rect=o;var a=this._axesList;function s(){E(a,(function(t){var e=t.isHorizontal(),n=e?[0,o.width]:[0,o.height],i=t.inverse?1:0;t.setExtent(n[i],n[1-i]),function(t,e){var n=t.getExtent(),i=n[0]+n[1];t.toGlobalCoord="x"===t.dim?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord="x"===t.dim?function(t){return t-e}:function(t){return i-t+e}}(t,e?o.x:o.y)}))}s(),r&&(E(a,(function(t){if(!t.model.get(["axisLabel","inside"])){var e=function(t){var e=t.model,n=t.scale;if(e.get(["axisLabel","show"])&&!n.isBlank()){var i,r,o=n.getExtent();r=n instanceof Rx?n.count():(i=n.getTicks()).length;var a,s=t.getLabelModel(),l=w_(t),u=1;r>40&&(u=Math.ceil(r/40));for(var h=0;h0&&i>0||n<0&&i<0)}(t)}var pI=Math.PI,dI=function(){function t(t,e){this.group=new Br,this.opt=e,this.axisModel=t,k(e,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0,handleAutoShown:function(){return!0}});var n=new Br({x:e.position[0],y:e.position[1],rotation:e.rotation});n.updateTransform(),this._transformGroup=n}return t.prototype.hasBuilder=function(t){return!!fI[t]},t.prototype.add=function(t){fI[t](this.opt,this.axisModel,this.group,this._transformGroup)},t.prototype.getGroup=function(){return this.group},t.innerTextLayout=function(t,e,n){var i,r,o=ao(e-t);return so(o)?(r=n>0?"top":"bottom",i="center"):so(o-pI)?(r=n>0?"bottom":"top",i="center"):(r="middle",i=o>0&&o0?"right":"left":n>0?"left":"right"),{rotation:o,textAlign:i,textVerticalAlign:r}},t.makeAxisEventDataBase=function(t){var e={componentType:t.mainType,componentIndex:t.componentIndex};return e[t.mainType+"Index"]=t.componentIndex,e},t.isLabelSilent=function(t){var e=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||e&&e.show)},t}(),fI={axisLine:function(t,e,n,i){var r=e.get(["axisLine","show"]);if("auto"===r&&t.handleAutoShown&&(r=t.handleAutoShown("axisLine")),r){var o=e.axis.getExtent(),a=i.transform,s=[o[0],0],l=[o[1],0],u=s[0]>l[0];a&&(Wt(s,s,a),Wt(l,l,a));var h=A({lineCap:"round"},e.getModel(["axisLine","lineStyle"]).getLineStyle()),c=new Ku({shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:h,strokeContainThreshold:t.strokeContainThreshold||5,silent:!0,z2:1});zh(c.shape,c.style.lineWidth),c.anid="line",n.add(c);var p=e.get(["axisLine","symbol"]);if(null!=p){var d=e.get(["axisLine","symbolSize"]);U(p)&&(p=[p,p]),(U(d)||j(d))&&(d=[d,d]);var f=Zy(e.get(["axisLine","symbolOffset"])||0,d),g=d[0],y=d[1];E([{rotate:t.rotation+Math.PI/2,offset:f[0],r:0},{rotate:t.rotation-Math.PI/2,offset:f[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],(function(e,i){if("none"!==p[i]&&null!=p[i]){var r=Xy(p[i],-g/2,-y/2,g,y,h.stroke,!0),o=e.r+e.offset,a=u?l:s;r.attr({rotation:e.rotate,x:a[0]+o*Math.cos(t.rotation),y:a[1]-o*Math.sin(t.rotation),silent:!0,z2:11}),n.add(r)}}))}}},axisTickLabel:function(t,e,n,i){var r=function(t,e,n,i){var r=n.axis,o=n.getModel("axisTick"),a=o.get("show");"auto"===a&&i.handleAutoShown&&(a=i.handleAutoShown("axisTick"));if(!a||r.scale.isBlank())return;for(var s=o.getModel("lineStyle"),l=i.tickDirection*o.get("length"),u=mI(r.getTicksCoords(),e.transform,l,k(s.getLineStyle(),{stroke:n.get(["axisLine","lineStyle","color"])}),"ticks"),h=0;hc[1]?-1:1,d=["start"===s?c[0]-p*h:"end"===s?c[1]+p*h:(c[0]+c[1])/2,vI(s)?t.labelOffset+l*h:0],f=e.get("nameRotate");null!=f&&(f=f*pI/180),vI(s)?o=dI.innerTextLayout(t.rotation,null!=f?f:t.rotation,l):(o=function(t,e,n,i){var r,o,a=ao(n-t),s=i[0]>i[1],l="start"===e&&!s||"start"!==e&&s;so(a-pI/2)?(o=l?"bottom":"top",r="center"):so(a-1.5*pI)?(o=l?"top":"bottom",r="center"):(o="middle",r=a<1.5*pI&&a>pI/2?l?"left":"right":l?"right":"left");return{rotation:a,textAlign:r,textVerticalAlign:o}}(t.rotation,s,f||0,c),null!=(a=t.axisNameAvailableWidth)&&(a=Math.abs(a/Math.sin(o.rotation)),!isFinite(a)&&(a=null)));var g=u.getFont(),y=e.get("nameTruncate",!0)||{},v=y.ellipsis,m=it(t.nameTruncateMaxWidth,y.maxWidth,a),x=new Xs({x:d[0],y:d[1],rotation:o.rotation,silent:dI.isLabelSilent(e),style:oc(u,{text:r,font:g,overflow:"truncate",width:m,ellipsis:v,fill:u.getTextColor()||e.get(["axisLine","lineStyle","color"]),align:u.get("align")||o.textAlign,verticalAlign:u.get("verticalAlign")||o.textVerticalAlign}),z2:1});if(Kh({el:x,componentModel:e,itemName:r}),x.__fullText=r,x.anid="name",e.get("triggerEvent")){var _=dI.makeAxisEventDataBase(e);_.targetType="axisName",_.name=r,rl(x).eventData=_}i.add(x),x.updateTransform(),n.add(x),x.decomposeTransform()}}};function gI(t){t&&(t.ignore=!0)}function yI(t,e){var n=t&&t.getBoundingRect().clone(),i=e&&e.getBoundingRect().clone();if(n&&i){var r=xe([]);return Se(r,r,-t.rotation),n.applyTransform(be([],r,t.getLocalTransform())),i.applyTransform(be([],r,e.getLocalTransform())),n.intersect(i)}}function vI(t){return"middle"===t||"center"===t}function mI(t,e,n,i,r){for(var o=[],a=[],s=[],l=0;l=0||t===e}function bI(t){var e=wI(t);if(e){var n=e.axisPointerModel,i=e.axis.scale,r=n.option,o=n.get("status"),a=n.get("value");null!=a&&(a=i.parse(a));var s=SI(n);null==o&&(r.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==a||a>l[1])&&(a=l[1]),a0&&!c.min?c.min=0:null!=c.min&&c.min<0&&!c.max&&(c.max=0);var p=a;null!=c.color&&(p=k({color:c.color},a));var d=C(T(c),{boundaryGap:t,splitNumber:e,scale:n,axisLine:i,axisTick:r,axisLabel:o,name:c.text,showName:s,nameLocation:"end",nameGap:u,nameTextStyle:p,triggerEvent:h},!1);if(U(l)){var f=d.name;d.name=l.replace("{value}",null!=f?f:"")}else X(l)&&(d.name=l(d.name,d));var g=new Cc(d,null,this.ecModel);return R(g,D_.prototype),g.mainType="radar",g.componentIndex=this.componentIndex,g}),this);this._indicatorModels=c},e.prototype.getIndicatorModels=function(){return this._indicatorModels},e.type="radar",e.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:C({lineStyle:{color:"#bbb"}},XI.axisLine),axisLabel:UI(XI.axisLabel,!1),axisTick:UI(XI.axisTick,!1),splitLine:UI(XI.splitLine,!0),splitArea:UI(XI.splitArea,!0),indicator:[]},e}(zp),jI=["axisLine","axisTickLabel","axisName"],qI=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll(),this._buildAxes(t),this._buildSplitLineAndArea(t)},e.prototype._buildAxes=function(t){var e=t.coordinateSystem;E(z(e.getIndicatorAxes(),(function(t){var n=t.model.get("showName")?t.name:"";return new dI(t.model,{axisName:n,position:[e.cx,e.cy],rotation:t.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})})),(function(t){E(jI,t.add,t),this.group.add(t.getGroup())}),this)},e.prototype._buildSplitLineAndArea=function(t){var e=t.coordinateSystem,n=e.getIndicatorAxes();if(n.length){var i=t.get("shape"),r=t.getModel("splitLine"),o=t.getModel("splitArea"),a=r.getModel("lineStyle"),s=o.getModel("areaStyle"),l=r.get("show"),u=o.get("show"),h=a.get("color"),c=s.get("color"),p=Y(h)?h:[h],d=Y(c)?c:[c],f=[],g=[];if("circle"===i)for(var y=n[0].getTicksCoords(),v=e.cx,m=e.cy,x=0;x3?1.4:r>1?1.2:1.1;iT(this,"zoom","zoomOnMouseWheel",t,{scale:i>0?s:1/s,originX:o,originY:a,isAvailableBehavior:null})}if(n){var l=Math.abs(i);iT(this,"scrollMove","moveOnMouseWheel",t,{scrollDelta:(i>0?1:-1)*(l>3?.4:l>1?.15:.05),originX:o,originY:a,isAvailableBehavior:null})}}},e.prototype._pinchHandler=function(t){tT(this._zr,"globalPan")||iT(this,"zoom",null,t,{scale:t.pinchScale>1?1.1:1/1.1,originX:t.pinchX,originY:t.pinchY,isAvailableBehavior:null})},e}(jt);function iT(t,e,n,i,r){t.pointerChecker&&t.pointerChecker(i,r.originX,r.originY)&&(de(i.event),rT(t,e,n,i,r))}function rT(t,e,n,i,r){r.isAvailableBehavior=W(oT,null,n,i),t.trigger(e,r)}function oT(t,e,n){var i=n[t];return!t||i&&(!U(i)||e.event[i+"Key"])}function aT(t,e,n){var i=t.target;i.x+=e,i.y+=n,i.dirty()}function sT(t,e,n,i){var r=t.target,o=t.zoomLimit,a=t.zoom=t.zoom||1;if(a*=e,o){var s=o.min||0,l=o.max||1/0;a=Math.max(Math.min(l,a),s)}var u=a/t.zoom;t.zoom=a,r.x-=(n-r.x)*(u-1),r.y-=(i-r.y)*(u-1),r.scaleX*=u,r.scaleY*=u,r.dirty()}var lT,uT={axisPointer:1,tooltip:1,brush:1};function hT(t,e,n){var i=e.getComponentByElement(t.topTarget),r=i&&i.coordinateSystem;return i&&i!==n&&!uT.hasOwnProperty(i.mainType)&&r&&r.model!==n}function cT(t){U(t)&&(t=(new DOMParser).parseFromString(t,"text/xml"));var e=t;for(9===e.nodeType&&(e=e.firstChild);"svg"!==e.nodeName.toLowerCase()||1!==e.nodeType;)e=e.nextSibling;return e}var pT={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},dT=G(pT),fT={"alignment-baseline":"textBaseline","stop-color":"stopColor"},gT=G(fT),yT=function(){function t(){this._defs={},this._root=null}return t.prototype.parse=function(t,e){e=e||{};var n=cT(t);this._defsUsePending=[];var i=new Br;this._root=i;var r=[],o=n.getAttribute("viewBox")||"",a=parseFloat(n.getAttribute("width")||e.width),s=parseFloat(n.getAttribute("height")||e.height);isNaN(a)&&(a=null),isNaN(s)&&(s=null),wT(n,i,null,!0,!1);for(var l,u,h=n.firstChild;h;)this._parseNode(h,i,r,null,!1,!1),h=h.nextSibling;if(function(t,e){for(var n=0;n=4&&(l={x:parseFloat(c[0]||0),y:parseFloat(c[1]||0),width:parseFloat(c[2]),height:parseFloat(c[3])})}if(l&&null!=a&&null!=s&&(u=LT(l,{x:0,y:0,width:a,height:s}),!e.ignoreViewBox)){var p=i;(i=new Br).add(p),p.scaleX=p.scaleY=u.scale,p.x=u.x,p.y=u.y}return e.ignoreRootClip||null==a||null==s||i.setClipPath(new Ws({shape:{x:0,y:0,width:a,height:s}})),{root:i,width:a,height:s,viewBoxRect:l,viewBoxTransform:u,named:r}},t.prototype._parseNode=function(t,e,n,i,r,o){var a,s=t.nodeName.toLowerCase(),l=i;if("defs"===s&&(r=!0),"text"===s&&(o=!0),"defs"===s||"switch"===s)a=e;else{if(!r){var u=lT[s];if(u&&_t(lT,s)){a=u.call(this,t,e);var h=t.getAttribute("name");if(h){var c={name:h,namedFrom:null,svgNodeTagLower:s,el:a};n.push(c),"g"===s&&(l=c)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:a});e.add(a)}}var p=vT[s];if(p&&_t(vT,s)){var d=p.call(this,t),f=t.getAttribute("id");f&&(this._defs[f]=d)}}if(a&&a.isGroup)for(var g=t.firstChild;g;)1===g.nodeType?this._parseNode(g,a,n,l,r,o):3===g.nodeType&&o&&this._parseText(g,a),g=g.nextSibling},t.prototype._parseText=function(t,e){var n=new Ps({style:{text:t.textContent},silent:!0,x:this._textX||0,y:this._textY||0});_T(e,n),wT(t,n,this._defsUsePending,!1,!1),function(t,e){var n=e.__selfStyle;if(n){var i=n.textBaseline,r=i;i&&"auto"!==i?"baseline"===i?r="alphabetic":"before-edge"===i||"text-before-edge"===i?r="top":"after-edge"===i||"text-after-edge"===i?r="bottom":"central"!==i&&"mathematical"!==i||(r="middle"):r="alphabetic",t.style.textBaseline=r}var o=e.__inheritedStyle;if(o){var a=o.textAlign,s=a;a&&("middle"===a&&(s="center"),t.style.textAlign=s)}}(n,e);var i=n.style,r=i.fontSize;r&&r<9&&(i.fontSize=9,n.scaleX*=r/9,n.scaleY*=r/9);var o=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=o;var a=n.getBoundingRect();return this._textX+=a.width,e.add(n),n},t.internalField=void(lT={g:function(t,e){var n=new Br;return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n},rect:function(t,e){var n=new Ws;return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(t.getAttribute("x")||"0"),y:parseFloat(t.getAttribute("y")||"0"),width:parseFloat(t.getAttribute("width")||"0"),height:parseFloat(t.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(t,e){var n=new Su;return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),r:parseFloat(t.getAttribute("r")||"0")}),n.silent=!0,n},line:function(t,e){var n=new Ku;return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(t.getAttribute("x1")||"0"),y1:parseFloat(t.getAttribute("y1")||"0"),x2:parseFloat(t.getAttribute("x2")||"0"),y2:parseFloat(t.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(t,e){var n=new Iu;return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),rx:parseFloat(t.getAttribute("rx")||"0"),ry:parseFloat(t.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(t,e){var n,i=t.getAttribute("points");i&&(n=bT(i));var r=new Xu({shape:{points:n||[]},silent:!0});return _T(e,r),wT(t,r,this._defsUsePending,!1,!1),r},polyline:function(t,e){var n,i=t.getAttribute("points");i&&(n=bT(i));var r=new Zu({shape:{points:n||[]},silent:!0});return _T(e,r),wT(t,r,this._defsUsePending,!1,!1),r},image:function(t,e){var n=new Ns;return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n.setStyle({image:t.getAttribute("xlink:href")||t.getAttribute("href"),x:+t.getAttribute("x"),y:+t.getAttribute("y"),width:+t.getAttribute("width"),height:+t.getAttribute("height")}),n.silent=!0,n},text:function(t,e){var n=t.getAttribute("x")||"0",i=t.getAttribute("y")||"0",r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(r),this._textY=parseFloat(i)+parseFloat(o);var a=new Br;return _T(e,a),wT(t,a,this._defsUsePending,!1,!0),a},tspan:function(t,e){var n=t.getAttribute("x"),i=t.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0",a=new Br;return _T(e,a),wT(t,a,this._defsUsePending,!1,!0),this._textX+=parseFloat(r),this._textY+=parseFloat(o),a},path:function(t,e){var n=_u(t.getAttribute("d")||"");return _T(e,n),wT(t,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),t}(),vT={lineargradient:function(t){var e=parseInt(t.getAttribute("x1")||"0",10),n=parseInt(t.getAttribute("y1")||"0",10),i=parseInt(t.getAttribute("x2")||"10",10),r=parseInt(t.getAttribute("y2")||"0",10),o=new oh(e,n,i,r);return mT(t,o),xT(t,o),o},radialgradient:function(t){var e=parseInt(t.getAttribute("cx")||"0",10),n=parseInt(t.getAttribute("cy")||"0",10),i=parseInt(t.getAttribute("r")||"0",10),r=new ah(e,n,i);return mT(t,r),xT(t,r),r}};function mT(t,e){"userSpaceOnUse"===t.getAttribute("gradientUnits")&&(e.global=!0)}function xT(t,e){for(var n=t.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),r=void 0;r=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var o={};kT(n,o,o);var a=o.stopColor||n.getAttribute("stop-color")||"#000000";e.colorStops.push({offset:r,color:a})}n=n.nextSibling}}function _T(t,e){t&&t.__inheritedStyle&&(e.__inheritedStyle||(e.__inheritedStyle={}),k(e.__inheritedStyle,t.__inheritedStyle))}function bT(t){for(var e=TT(t),n=[],i=0;i0;o-=2){var a=i[o],s=i[o-1],l=TT(a);switch(r=r||[1,0,0,1,0,0],s){case"translate":we(r,r,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":Me(r,r,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":Se(r,r,-parseFloat(l[0])*DT,[parseFloat(l[1]||"0"),parseFloat(l[2]||"0")]);break;case"skewX":be(r,[1,0,Math.tan(parseFloat(l[0])*DT),1,0,0],r);break;case"skewY":be(r,[1,Math.tan(parseFloat(l[0])*DT),0,1,0,0],r);break;case"matrix":r[0]=parseFloat(l[0]),r[1]=parseFloat(l[1]),r[2]=parseFloat(l[2]),r[3]=parseFloat(l[3]),r[4]=parseFloat(l[4]),r[5]=parseFloat(l[5])}}e.setLocalTransform(r)}}(t,e),kT(t,a,s),i||function(t,e,n){for(var i=0;i0,f={api:n,geo:s,mapOrGeoModel:t,data:a,isVisualEncodedByVisualMap:d,isGeo:o,transformInfoRaw:c};"geoJSON"===s.resourceType?this._buildGeoJSON(f):"geoSVG"===s.resourceType&&this._buildSVG(f),this._updateController(t,e,n),this._updateMapSelectHandler(t,l,n,i)},t.prototype._buildGeoJSON=function(t){var e=this._regionsGroupByName=yt(),n=yt(),i=this._regionsGroup,r=t.transformInfoRaw,o=t.mapOrGeoModel,a=t.data,s=t.geo.projection,l=s&&s.stream;function u(t,e){return e&&(t=e(t)),t&&[t[0]*r.scaleX+r.x,t[1]*r.scaleY+r.y]}function h(t){for(var e=[],n=!l&&s&&s.project,i=0;i=0)&&(p=r);var d=a?{normal:{align:"center",verticalAlign:"middle"}}:null;ic(e,rc(i),{labelFetcher:p,labelDataIndex:c,defaultText:n},d);var f=e.getTextContent();if(f&&(JT(f).ignore=f.ignore,e.textConfig&&a)){var g=e.getBoundingRect().clone();e.textConfig.layoutRect=g,e.textConfig.position=[(a[0]-g.x)/g.width*100+"%",(a[1]-g.y)/g.height*100+"%"]}e.disableLabelAnimation=!0}else e.removeTextContent(),e.removeTextConfig(),e.disableLabelAnimation=null}function rC(t,e,n,i,r,o){t.data?t.data.setItemGraphicEl(o,e):rl(e).eventData={componentType:"geo",componentIndex:r.componentIndex,geoIndex:r.componentIndex,name:n,region:i&&i.option||{}}}function oC(t,e,n,i,r){t.data||Kh({el:e,componentModel:r,itemName:n,itemTooltipOption:i.get("tooltip")})}function aC(t,e,n,i,r){e.highDownSilentOnTouch=!!r.get("selectedMode");var o=i.getModel("emphasis"),a=o.get("focus");return Zl(e,a,o.get("blurScope"),o.get("disabled")),t.isGeo&&function(t,e,n){var i=rl(t);i.componentMainType=e.mainType,i.componentIndex=e.componentIndex,i.componentHighDownName=n}(e,r,n),a}function sC(t,e,n){var i,r=[];function o(){i=[]}function a(){i.length&&(r.push(i),i=[])}var s=e({polygonStart:o,polygonEnd:a,lineStart:o,lineEnd:a,point:function(t,e){isFinite(t)&&isFinite(e)&&i.push([t,e])},sphere:function(){}});return!n&&s.polygonStart(),E(t,(function(t){s.lineStart();for(var e=0;e-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},e.type="series.map",e.dependencies=["geo"],e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"},e}(bg);function hC(t){var e={};t.eachSeriesByType("map",(function(t){var n=t.getHostGeoModel(),i=n?"o"+n.id:"i"+t.getMapType();(e[i]=e[i]||[]).push(t)})),E(e,(function(t,e){for(var n,i,r,o=(n=z(t,(function(t){return t.getData()})),i=t[0].get("mapValueCalculation"),r={},E(n,(function(t){t.each(t.mapDimension("value"),(function(e,n){var i="ec-"+t.getName(n);r[i]=r[i]||[],isNaN(e)||r[i].push(e)}))})),n[0].map(n[0].mapDimension("value"),(function(t,e){for(var o="ec-"+n[0].getName(e),a=0,s=1/0,l=-1/0,u=r[o].length,h=0;h1?(d.width=p,d.height=p/x):(d.height=p,d.width=p*x),d.y=c[1]-d.height/2,d.x=c[0]-d.width/2;else{var b=t.getBoxLayoutParams();b.aspect=x,d=kp(b,{width:v,height:m})}this.setViewRect(d.x,d.y,d.width,d.height),this.setCenter(t.get("center"),e),this.setZoom(t.get("zoom"))}R(vC,dC);var _C=function(){function t(){this.dimensions=yC}return t.prototype.create=function(t,e){var n=[];function i(t){return{nameProperty:t.get("nameProperty"),aspectScale:t.get("aspectScale"),projection:t.get("projection")}}t.eachComponent("geo",(function(t,r){var o=t.get("map"),a=new vC(o+r,o,A({nameMap:t.get("nameMap")},i(t)));a.zoomLimit=t.get("scaleLimit"),n.push(a),t.coordinateSystem=a,a.model=t,a.resize=xC,a.resize(t,e)})),t.eachSeries((function(t){if("geo"===t.get("coordinateSystem")){var e=t.get("geoIndex")||0;t.coordinateSystem=n[e]}}));var r={};return t.eachSeriesByType("map",(function(t){if(!t.getHostGeoModel()){var e=t.getMapType();r[e]=r[e]||[],r[e].push(t)}})),E(r,(function(t,r){var o=z(t,(function(t){return t.get("nameMap")})),a=new vC(r,r,A({nameMap:D(o)},i(t[0])));a.zoomLimit=it.apply(null,z(t,(function(t){return t.get("scaleLimit")}))),n.push(a),a.resize=xC,a.resize(t[0],e),E(t,(function(t){t.coordinateSystem=a,function(t,e){E(e.get("geoCoord"),(function(e,n){t.addGeoCoord(n,e)}))}(a,t)}))})),n},t.prototype.getFilledRegions=function(t,e,n,i){for(var r=(t||[]).slice(),o=yt(),a=0;a=0;){var o=e[n];o.hierNode.prelim+=i,o.hierNode.modifier+=i,r+=o.hierNode.change,i+=o.hierNode.shift+r}}(t);var o=(n[0].hierNode.prelim+n[n.length-1].hierNode.prelim)/2;r?(t.hierNode.prelim=r.hierNode.prelim+e(t,r),t.hierNode.modifier=t.hierNode.prelim-o):t.hierNode.prelim=o}else r&&(t.hierNode.prelim=r.hierNode.prelim+e(t,r));t.parentNode.hierNode.defaultAncestor=function(t,e,n,i){if(e){for(var r=t,o=t,a=o.parentNode.children[0],s=e,l=r.hierNode.modifier,u=o.hierNode.modifier,h=a.hierNode.modifier,c=s.hierNode.modifier;s=PC(s),o=OC(o),s&&o;){r=PC(r),a=OC(a),r.hierNode.ancestor=t;var p=s.hierNode.prelim+c-o.hierNode.prelim-u+i(s,o);p>0&&(NC(RC(s,t,n),t,p),u+=p,l+=p),c+=s.hierNode.modifier,u+=o.hierNode.modifier,l+=r.hierNode.modifier,h+=a.hierNode.modifier}s&&!PC(r)&&(r.hierNode.thread=s,r.hierNode.modifier+=c-l),o&&!OC(a)&&(a.hierNode.thread=o,a.hierNode.modifier+=u-h,n=t)}return n}(t,r,t.parentNode.hierNode.defaultAncestor||i[0],e)}function AC(t){var e=t.hierNode.prelim+t.parentNode.hierNode.modifier;t.setLayout({x:e},!0),t.hierNode.modifier+=t.parentNode.hierNode.modifier}function kC(t){return arguments.length?t:EC}function LC(t,e){return t-=Math.PI/2,{x:e*Math.cos(t),y:e*Math.sin(t)}}function PC(t){var e=t.children;return e.length&&t.isExpand?e[e.length-1]:t.hierNode.thread}function OC(t){var e=t.children;return e.length&&t.isExpand?e[0]:t.hierNode.thread}function RC(t,e,n){return t.hierNode.ancestor.parentNode===e.parentNode?t.hierNode.ancestor:n}function NC(t,e,n){var i=n/(e.hierNode.i-t.hierNode.i);e.hierNode.change-=i,e.hierNode.shift+=n,e.hierNode.modifier+=n,e.hierNode.prelim+=n,t.hierNode.change+=i}function EC(t,e){return t.parentNode===e.parentNode?1:2}var zC=function(){this.parentPoint=[],this.childPoints=[]},VC=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new zC},e.prototype.buildPath=function(t,e){var n=e.childPoints,i=n.length,r=e.parentPoint,o=n[0],a=n[i-1];if(1===i)return t.moveTo(r[0],r[1]),void t.lineTo(o[0],o[1]);var s=e.orient,l="TB"===s||"BT"===s?0:1,u=1-l,h=$r(e.forkPosition,1),c=[];c[l]=r[l],c[u]=r[u]+(a[u]-r[u])*h,t.moveTo(r[0],r[1]),t.lineTo(c[0],c[1]),t.moveTo(o[0],o[1]),c[l]=o[l],t.lineTo(c[0],c[1]),c[l]=a[l],t.lineTo(c[0],c[1]),t.lineTo(a[0],a[1]);for(var p=1;pm.x)||(_-=Math.PI);var S=b?"left":"right",M=s.getModel("label"),I=M.get("rotate"),T=I*(Math.PI/180),C=y.getTextContent();C&&(y.setTextConfig({position:M.get("position")||S,rotation:null==I?-_:T,origin:"center"}),C.setStyle("verticalAlign","middle"))}var D=s.get(["emphasis","focus"]),A="relative"===D?vt(a.getAncestorsIndices(),a.getDescendantIndices()):"ancestor"===D?a.getAncestorsIndices():"descendant"===D?a.getDescendantIndices():null;A&&(rl(n).focus=A),function(t,e,n,i,r,o,a,s){var l=e.getModel(),u=t.get("edgeShape"),h=t.get("layout"),c=t.getOrient(),p=t.get(["lineStyle","curveness"]),d=t.get("edgeForkPosition"),f=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===u)e.parentNode&&e.parentNode!==n&&(g||(g=i.__edge=new th({shape:XC(h,c,p,r,r)})),vh(g,{shape:XC(h,c,p,o,a)},t));else if("polyline"===u)if("orthogonal"===h){if(e!==n&&e.children&&0!==e.children.length&&!0===e.isExpand){for(var y=e.children,v=[],m=0;me&&(e=i.height)}this.height=e+1},t.prototype.getNodeById=function(t){if(this.getId()===t)return this;for(var e=0,n=this.children,i=n.length;e=0&&this.hostTree.data.setItemLayout(this.dataIndex,t,e)},t.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(t)},t.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},t.prototype.setVisual=function(t,e){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,t,e)},t.prototype.getVisual=function(t){return this.hostTree.data.getItemVisual(this.dataIndex,t)},t.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},t.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},t.prototype.getChildIndex=function(){if(this.parentNode){for(var t=this.parentNode.children,e=0;e=0){var i=n.getData().tree.root,r=t.targetNode;if(U(r)&&(r=i.getNodeById(r)),r&&i.contains(r))return{node:r};var o=t.targetNodeId;if(null!=o&&(r=i.getNodeById(o)))return{node:r}}}function rD(t){for(var e=[];t;)(t=t.parentNode)&&e.push(t);return e.reverse()}function oD(t,e){return P(rD(t),e)>=0}function aD(t,e){for(var n=[];t;){var i=t.dataIndex;n.push({name:t.name,dataIndex:i,value:e.getRawValue(i)}),t=t.parentNode}return n.reverse(),n}var sD=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.hasSymbolVisual=!0,e.ignoreStyleOnData=!0,e}return n(e,t),e.prototype.getInitialData=function(t){var e={name:t.name,children:t.data},n=t.leaves||{},i=new Cc(n,this,this.ecModel),r=nD.createTree(e,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e);return n&&n.children.length&&n.isExpand||(t.parentModel=i),t}))}));var o=0;r.eachNode("preorder",(function(t){t.depth>o&&(o=t.depth)}));var a=t.expandAndCollapse&&t.initialTreeDepth>=0?t.initialTreeDepth:o;return r.root.eachNode("preorder",(function(t){var e=t.hostTree.data.getRawDataItem(t.dataIndex);t.isExpand=e&&null!=e.collapsed?!e.collapsed:t.depth<=a})),r.data},e.prototype.getOrient=function(){var t=this.get("orient");return"horizontal"===t?t="LR":"vertical"===t&&(t="TB"),t},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.formatTooltip=function(t,e,n){for(var i=this.getData().tree,r=i.root.children[0],o=i.getNodeByDataIndex(t),a=o.getValue(),s=o.name;o&&o!==r;)s=o.parentNode.name+"."+s,o=o.parentNode;return og("nameValue",{name:s,value:a,noValue:isNaN(a)||null==a})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=aD(i,this),n.collapsed=!i.isExpand,n},e.type="series.tree",e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500},e}(bg);function lD(t,e){for(var n,i=[t];n=i.pop();)if(e(n),n.isExpand){var r=n.children;if(r.length)for(var o=r.length-1;o>=0;o--)i.push(r[o])}}function uD(t,e){t.eachSeriesByType("tree",(function(t){!function(t,e){var n=function(t,e){return kp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=n;var i=t.get("layout"),r=0,o=0,a=null;"radial"===i?(r=2*Math.PI,o=Math.min(n.height,n.width)/2,a=kC((function(t,e){return(t.parentNode===e.parentNode?1:2)/t.depth}))):(r=n.width,o=n.height,a=kC());var s=t.getData().tree.root,l=s.children[0];if(l){!function(t){var e=t;e.hierNode={defaultAncestor:null,ancestor:e,prelim:0,modifier:0,change:0,shift:0,i:0,thread:null};for(var n,i,r=[e];n=r.pop();)if(i=n.children,n.isExpand&&i.length)for(var o=i.length-1;o>=0;o--){var a=i[o];a.hierNode={defaultAncestor:null,ancestor:a,prelim:0,modifier:0,change:0,shift:0,i:o,thread:null},r.push(a)}}(s),function(t,e,n){for(var i,r=[t],o=[];i=r.pop();)if(o.push(i),i.isExpand){var a=i.children;if(a.length)for(var s=0;sh.getLayout().x&&(h=t),t.depth>c.depth&&(c=t)}));var p=u===h?1:a(u,h)/2,d=p-u.getLayout().x,f=0,g=0,y=0,v=0;if("radial"===i)f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),lD(l,(function(t){y=(t.getLayout().x+d)*f,v=(t.depth-1)*g;var e=LC(y,v);t.setLayout({x:e.x,y:e.y,rawX:y,rawY:v},!0)}));else{var m=t.getOrient();"RL"===m||"LR"===m?(g=o/(h.getLayout().x+p+d),f=r/(c.depth-1||1),lD(l,(function(t){v=(t.getLayout().x+d)*g,y="LR"===m?(t.depth-1)*f:r-(t.depth-1)*f,t.setLayout({x:y,y:v},!0)}))):"TB"!==m&&"BT"!==m||(f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),lD(l,(function(t){y=(t.getLayout().x+d)*f,v="TB"===m?(t.depth-1)*g:o-(t.depth-1)*g,t.setLayout({x:y,y:v},!0)})))}}}(t,e)}))}function hD(t){t.eachSeriesByType("tree",(function(t){var e=t.getData();e.tree.eachNode((function(t){var n=t.getModel().getModel("itemStyle").getItemStyle();A(e.ensureUniqueItemVisual(t.dataIndex,"style"),n)}))}))}var cD=["treemapZoomToNode","treemapRender","treemapMove"];function pD(t){var e=t.getData().tree,n={};e.eachNode((function(e){for(var i=e;i&&i.depth>1;)i=i.parentNode;var r=pd(t.ecModel,i.name||i.dataIndex+"",n);e.setVisual("decal",r)}))}var dD=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventUsingHoverLayer=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};fD(n);var i=t.levels||[],r=this.designatedVisualItemStyle={},o=new Cc({itemStyle:r},this,e);i=t.levels=function(t,e){var n,i,r=To(e.get("color")),o=To(e.get(["aria","decal","decals"]));if(!r)return;t=t||[],E(t,(function(t){var e=new Cc(t),r=e.get("color"),o=e.get("decal");(e.get(["itemStyle","color"])||r&&"none"!==r)&&(n=!0),(e.get(["itemStyle","decal"])||o&&"none"!==o)&&(i=!0)}));var a=t[0]||(t[0]={});n||(a.color=r.slice());!i&&o&&(a.decal=o.slice());return t}(i,e);var a=z(i||[],(function(t){return new Cc(t,o,e)}),this),s=nD.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=s.getNodeByDataIndex(e),i=n?a[n.depth]:null;return t.parentModel=i||o,t}))}));return s.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.getRawValue(t);return og("nameValue",{name:i.getName(t),value:r})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=aD(i,this),n.treePathInfo=n.treeAncestors,n},e.prototype.setLayoutInfo=function(t){this.layoutInfo=this.layoutInfo||{},A(this.layoutInfo,t)},e.prototype.mapIdToIndex=function(t){var e=this._idIndexMap;e||(e=this._idIndexMap=yt(),this._idIndexMapCount=0);var n=e.get(t);return null==n&&e.set(t,n=this._idIndexMapCount++),n},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){pD(this)},e.type="series.treemap",e.layoutMode="box",e.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,scaleLimit:null,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}},emphasis:{itemStyle:{color:"rgba(0,0,0,0.9)"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},e}(bg);function fD(t){var e=0;E(t.children,(function(t){fD(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var gD=function(){function t(t){this.group=new Br,t.add(this.group)}return t.prototype.render=function(t,e,n,i){var r=t.getModel("breadcrumb"),o=this.group;if(o.removeAll(),r.get("show")&&n){var a=r.getModel("itemStyle"),s=r.getModel("emphasis"),l=a.getModel("textStyle"),u=s.getModel(["itemStyle","textStyle"]),h={pos:{left:r.get("left"),right:r.get("right"),top:r.get("top"),bottom:r.get("bottom")},box:{width:e.getWidth(),height:e.getHeight()},emptyItemWidth:r.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,h,l),this._renderContent(t,h,a,s,l,u,i),Lp(o,h.pos,h.box)}},t.prototype._prepare=function(t,e,n){for(var i=t;i;i=i.parentNode){var r=Ro(i.getModel().get("name"),""),o=n.getTextRect(r),a=Math.max(o.width+16,e.emptyItemWidth);e.totalWidth+=a+8,e.renderList.push({node:i,text:r,width:a})}},t.prototype._renderContent=function(t,e,n,i,r,o,a){for(var s,l,u,h,c,p,d,f,g,y=0,v=e.emptyItemWidth,m=t.get(["breadcrumb","height"]),x=(s=e.pos,l=e.box,h=l.width,c=l.height,p=$r(s.left,h),d=$r(s.top,c),f=$r(s.right,h),g=$r(s.bottom,c),(isNaN(p)||isNaN(parseFloat(s.left)))&&(p=0),(isNaN(f)||isNaN(parseFloat(s.right)))&&(f=h),(isNaN(d)||isNaN(parseFloat(s.top)))&&(d=0),(isNaN(g)||isNaN(parseFloat(s.bottom)))&&(g=c),u=vp(u||0),{width:Math.max(f-p-u[1]-u[3],0),height:Math.max(g-d-u[0]-u[2],0)}),_=e.totalWidth,b=e.renderList,w=i.getModel("itemStyle").getItemStyle(),S=b.length-1;S>=0;S--){var M=b[S],I=M.node,T=M.width,C=M.text;_>x.width&&(_-=T-v,T=v,C=null);var D=new Xu({shape:{points:yD(y,0,T,m,S===b.length-1,0===S)},style:k(n.getItemStyle(),{lineJoin:"bevel"}),textContent:new Xs({style:oc(r,{text:C})}),textConfig:{position:"inside"},z2:1e5,onclick:H(a,I)});D.disableLabelAnimation=!0,D.getTextContent().ensureState("emphasis").style=oc(o,{text:C}),D.ensureState("emphasis").style=w,Zl(D,i.get("focus"),i.get("blurScope"),i.get("disabled")),this.group.add(D),vD(D,t,I),y+=T+8}},t.prototype.remove=function(){this.group.removeAll()},t}();function yD(t,e,n,i,r,o){var a=[[r?t:t-5,e],[t+n,e],[t+n,e+i],[r?t:t-5,e+i]];return!o&&a.splice(2,0,[t+n+5,e+i/2]),!r&&a.push([t,e+i/2]),a}function vD(t,e,n){rl(t).eventData={componentType:"series",componentSubType:"treemap",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&aD(n,e)}}var mD=function(){function t(){this._storage=[],this._elExistsMap={}}return t.prototype.add=function(t,e,n,i,r){return!this._elExistsMap[t.id]&&(this._elExistsMap[t.id]=!0,this._storage.push({el:t,target:e,duration:n,delay:i,easing:r}),!0)},t.prototype.finished=function(t){return this._finishedCallback=t,this},t.prototype.start=function(){for(var t=this,e=this._storage.length,n=function(){--e<=0&&(t._storage.length=0,t._elExistsMap={},t._finishedCallback&&t._finishedCallback())},i=0,r=this._storage.length;i3||Math.abs(t.dy)>3)){var e=this.seriesModel.getData().tree.root;if(!e)return;var n=e.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+t.dx,y:n.y+t.dy,width:n.width,height:n.height}})}},e.prototype._onZoom=function(t){var e=t.originX,n=t.originY,i=t.scale;if("animating"!==this._state){var r=this.seriesModel.getData().tree.root;if(!r)return;var o=r.getLayout();if(!o)return;var a,s=new ze(o.x,o.y,o.width,o.height),l=this._controllerHost;a=l.zoomLimit;var u=l.zoom=l.zoom||1;if(u*=i,a){var h=a.min||0,c=a.max||1/0;u=Math.max(Math.min(c,u),h)}var p=u/l.zoom;l.zoom=u;var d=this.seriesModel.layoutInfo,f=[1,0,0,1,0,0];we(f,f,[-(e-=d.x),-(n-=d.y)]),Me(f,f,[p,p]),we(f,f,[e,n]),s.applyTransform(f),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:s.x,y:s.y,width:s.width,height:s.height}})}},e.prototype._initEvents=function(t){var e=this;t.on("click",(function(t){if("ready"===e._state){var n=e.seriesModel.get("nodeClick",!0);if(n){var i=e.findTarget(t.offsetX,t.offsetY);if(i){var r=i.node;if(r.getLayout().isLeafRoot)e._rootToNode(i);else if("zoomToNode"===n)e._zoomToNode(i);else if("link"===n){var o=r.hostTree.data.getItemModel(r.dataIndex),a=o.get("link",!0),s=o.get("target",!0)||"blank";a&&Mp(a,s)}}}}}),this)},e.prototype._renderBreadcrumb=function(t,e,n){var i=this;n||(n=null!=t.get("leafDepth",!0)?{node:t.getViewRoot()}:this.findTarget(e.getWidth()/2,e.getHeight()/2))||(n={node:t.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new gD(this.group))).render(t,e,n.node,(function(e){"animating"!==i._state&&(oD(t.getViewRoot(),e)?i._rootToNode({node:e}):i._zoomToNode({node:e}))}))},e.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},e.prototype.dispose=function(){this._clearController()},e.prototype._zoomToNode=function(t){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype._rootToNode=function(t){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype.findTarget=function(t,e){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},(function(i){var r=this._storage.background[i.getRawIndex()];if(r){var o=r.transformCoordToLocal(t,e),a=r.shape;if(!(a.x<=o[0]&&o[0]<=a.x+a.width&&a.y<=o[1]&&o[1]<=a.y+a.height))return!1;n={node:i,offsetX:o[0],offsetY:o[1]}}}),this),n},e.type="treemap",e}(Og);var CD=E,DD=q,AD=-1,kD=function(){function t(e){var n=e.mappingMethod,i=e.type,r=this.option=T(e);this.type=i,this.mappingMethod=n,this._normalizeData=FD[n];var o=t.visualHandlers[i];this.applyVisual=o.applyVisual,this.getColorMapper=o.getColorMapper,this._normalizedToVisual=o._normalizedToVisual[n],"piecewise"===n?(LD(r),function(t){var e=t.pieceList;t.hasSpecialVisual=!1,E(e,(function(e,n){e.originIndex=n,null!=e.visual&&(t.hasSpecialVisual=!0)}))}(r)):"category"===n?r.categories?function(t){var e=t.categories,n=t.categoryMap={},i=t.visual;if(CD(e,(function(t,e){n[t]=e})),!Y(i)){var r=[];q(i)?CD(i,(function(t,e){var i=n[e];r[null!=i?i:AD]=t})):r[-1]=i,i=BD(t,r)}for(var o=e.length-1;o>=0;o--)null==i[o]&&(delete n[e[o]],e.pop())}(r):LD(r,!0):(lt("linear"!==n||r.dataExtent),LD(r))}return t.prototype.mapValueToVisual=function(t){var e=this._normalizeData(t);return this._normalizedToVisual(e,t)},t.prototype.getNormalizer=function(){return W(this._normalizeData,this)},t.listVisualTypes=function(){return G(t.visualHandlers)},t.isValidType=function(e){return t.visualHandlers.hasOwnProperty(e)},t.eachVisual=function(t,e,n){q(t)?E(t,e,n):e.call(n,t)},t.mapVisual=function(e,n,i){var r,o=Y(e)?[]:q(e)?{}:(r=!0,null);return t.eachVisual(e,(function(t,e){var a=n.call(i,t,e);r?o=a:o[e]=a})),o},t.retrieveVisuals=function(e){var n,i={};return e&&CD(t.visualHandlers,(function(t,r){e.hasOwnProperty(r)&&(i[r]=e[r],n=!0)})),n?i:null},t.prepareVisualTypes=function(t){if(Y(t))t=t.slice();else{if(!DD(t))return[];var e=[];CD(t,(function(t,n){e.push(n)})),t=e}return t.sort((function(t,e){return"color"===e&&"color"!==t&&0===t.indexOf("color")?1:-1})),t},t.dependsOn=function(t,e){return"color"===e?!(!t||0!==t.indexOf(e)):t===e},t.findPieceIndex=function(t,e,n){for(var i,r=1/0,o=0,a=e.length;ou[1]&&(u[1]=l);var h=e.get("colorMappingBy"),c={type:a.name,dataExtent:u,visual:a.range};"color"!==c.type||"index"!==h&&"id"!==h?c.mappingMethod="linear":(c.mappingMethod="category",c.loop=!0);var p=new kD(c);return WD(p).drColorMappingBy=h,p}(0,r,o,0,u,d);E(d,(function(t,e){if(t.depth>=n.length||t===n[t.depth]){var o=function(t,e,n,i,r,o){var a=A({},e);if(r){var s=r.type,l="color"===s&&WD(r).drColorMappingBy,u="index"===l?i:"id"===l?o.mapIdToIndex(n.getId()):n.getValue(t.get("visualDimension"));a[s]=r.mapValueToVisual(u)}return a}(r,u,t,e,f,i);YD(t,o,n,i)}}))}else s=XD(u),h.fill=s}}function XD(t){var e=UD(t,"color");if(e){var n=UD(t,"colorAlpha"),i=UD(t,"colorSaturation");return i&&(e=ni(e,null,null,i)),n&&(e=ii(e,n)),e}}function UD(t,e){var n=t[e];if(null!=n&&"none"!==n)return n}function ZD(t,e){var n=t.get(e);return Y(n)&&n.length?{name:e,range:n}:null}var jD=Math.max,qD=Math.min,KD=it,$D=E,JD=["itemStyle","borderWidth"],QD=["itemStyle","gapWidth"],tA=["upperLabel","show"],eA=["upperLabel","height"],nA={seriesType:"treemap",reset:function(t,e,n,i){var r=n.getWidth(),o=n.getHeight(),a=t.option,s=kp(t.getBoxLayoutParams(),{width:n.getWidth(),height:n.getHeight()}),l=a.size||[],u=$r(KD(s.width,l[0]),r),h=$r(KD(s.height,l[1]),o),c=i&&i.type,p=iD(i,["treemapZoomToNode","treemapRootToNode"],t),d="treemapRender"===c||"treemapMove"===c?i.rootRect:null,f=t.getViewRoot(),g=rD(f);if("treemapMove"!==c){var y="treemapZoomToNode"===c?function(t,e,n,i,r){var o,a=(e||{}).node,s=[i,r];if(!a||a===n)return s;var l=i*r,u=l*t.option.zoomToNodeRatio;for(;o=a.parentNode;){for(var h=0,c=o.children,p=0,d=c.length;poo&&(u=oo),a=o}ua[1]&&(a[1]=e)}))):a=[NaN,NaN];return{sum:i,dataExtent:a}}(e,a,s);if(0===u.sum)return t.viewChildren=[];if(u.sum=function(t,e,n,i,r){if(!i)return n;for(var o=t.get("visibleMin"),a=r.length,s=a,l=a-1;l>=0;l--){var u=r["asc"===i?a-l-1:l].getValue();u/n*ei&&(i=a));var l=t.area*t.area,u=e*e*n;return l?jD(u*i/l,l/(u*r)):1/0}function oA(t,e,n,i,r){var o=e===n.width?0:1,a=1-o,s=["x","y"],l=["width","height"],u=n[s[o]],h=e?t.area/e:0;(r||h>n[l[a]])&&(h=n[l[a]]);for(var c=0,p=t.length;ci&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a0&&(m[0]=-m[0],m[1]=-m[1]);var _=v[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var b=-Math.atan2(v[1],v[0]);u[0].8?"left":h[0]<-.8?"right":"center",p=h[1]>.8?"top":h[1]<-.8?"bottom":"middle";break;case"start":i.x=-h[0]*f+l[0],i.y=-h[1]*g+l[1],c=h[0]>.8?"right":h[0]<-.8?"left":"center",p=h[1]>.8?"bottom":h[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=f*_+l[0],i.y=l[1]+w,c=v[0]<0?"right":"left",i.originX=-f*_,i.originY=-w;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+w,c="center",i.originY=-w;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-f*_+u[0],i.y=u[1]+w,c=v[0]>=0?"right":"left",i.originX=f*_,i.originY=-w}i.scaleX=i.scaleY=r,i.setStyle({verticalAlign:i.__verticalAlign||p,align:i.__align||c})}}}function S(t,e){var n=t.__specifiedRotation;if(null==n){var i=a.tangentAt(e);t.attr("rotation",(1===e?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else t.attr("rotation",n)}},e}(Br),YA=function(){function t(t){this.group=new Br,this._LineCtor=t||HA}return t.prototype.updateData=function(t){var e=this;this._progressiveEls=null;var n=this,i=n.group,r=n._lineData;n._lineData=t,r||i.removeAll();var o=XA(t);t.diff(r).add((function(n){e._doAdd(t,n,o)})).update((function(n,i){e._doUpdate(r,t,i,n,o)})).remove((function(t){i.remove(r.getItemGraphicEl(t))})).execute()},t.prototype.updateLayout=function(){var t=this._lineData;t&&t.eachItemGraphicEl((function(e,n){e.updateLayout(t,n)}),this)},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=XA(t),this._lineData=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e){function n(t){t.isGroup||function(t){return t.animators&&t.animators.length>0}(t)||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[];for(var i=t.start;i=0?i+=u:i-=u:f>=0?i-=u:i+=u}return i}function ek(t,e){var n=[],i=Dn,r=[[],[],[]],o=[[],[]],a=[];e/=2,t.eachEdge((function(t,s){var l=t.getLayout(),u=t.getVisual("fromSymbol"),h=t.getVisual("toSymbol");l.__original||(l.__original=[Tt(l[0]),Tt(l[1])],l[2]&&l.__original.push(Tt(l[2])));var c=l.__original;if(null!=l[2]){if(It(r[0],c[0]),It(r[1],c[2]),It(r[2],c[1]),u&&"none"!==u){var p=SA(t.node1),d=tk(r,c[0],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[0][0]=n[3],r[1][0]=n[4],i(r[0][1],r[1][1],r[2][1],d,n),r[0][1]=n[3],r[1][1]=n[4]}if(h&&"none"!==h){p=SA(t.node2),d=tk(r,c[1],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[1][0]=n[1],r[2][0]=n[2],i(r[0][1],r[1][1],r[2][1],d,n),r[1][1]=n[1],r[2][1]=n[2]}It(l[0],r[0]),It(l[1],r[2]),It(l[2],r[1])}else{if(It(o[0],c[0]),It(o[1],c[1]),kt(a,o[1],o[0]),Et(a,a),u&&"none"!==u){p=SA(t.node1);At(o[0],o[0],a,p*e)}if(h&&"none"!==h){p=SA(t.node2);At(o[1],o[1],a,-p*e)}It(l[0],o[0]),It(l[1],o[1])}}))}function nk(t){return"view"===t.type}var ik=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){var n=new xS,i=new YA,r=this.group;this._controller=new nT(e.getZr()),this._controllerHost={target:r},r.add(n.group),r.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},e.prototype.render=function(t,e,n){var i=this,r=t.coordinateSystem;this._model=t;var o=this._symbolDraw,a=this._lineDraw,s=this.group;if(nk(r)){var l={x:r.x,y:r.y,scaleX:r.scaleX,scaleY:r.scaleY};this._firstRender?s.attr(l):vh(s,l,t)}ek(t.getGraph(),wA(t));var u=t.getData();o.updateData(u);var h=t.getEdgeData();a.updateData(h),this._updateNodeAndLinkScale(),this._updateController(t,e,n),clearTimeout(this._layoutTimeout);var c=t.forceLayout,p=t.get(["force","layoutAnimation"]);c&&this._startForceLayoutIteration(c,p);var d=t.get("layout");u.graph.eachNode((function(e){var n=e.dataIndex,r=e.getGraphicEl(),o=e.getModel();if(r){r.off("drag").off("dragend");var a=o.get("draggable");a&&r.on("drag",(function(o){switch(d){case"force":c.warmUp(),!i._layouting&&i._startForceLayoutIteration(c,p),c.setFixed(n),u.setItemLayout(n,[r.x,r.y]);break;case"circular":u.setItemLayout(n,[r.x,r.y]),e.setLayout({fixed:!0},!0),TA(t,"symbolSize",e,[o.offsetX,o.offsetY]),i.updateLayout(t);break;default:u.setItemLayout(n,[r.x,r.y]),_A(t.getGraph(),t),i.updateLayout(t)}})).on("dragend",(function(){c&&c.setUnfixed(n)})),r.setDraggable(a,!!o.get("cursor")),"adjacency"===o.get(["emphasis","focus"])&&(rl(r).focus=e.getAdjacentDataIndices())}})),u.graph.eachEdge((function(t){var e=t.getGraphicEl(),n=t.getModel().get(["emphasis","focus"]);e&&"adjacency"===n&&(rl(e).focus={edge:[t.dataIndex],node:[t.node1.dataIndex,t.node2.dataIndex]})}));var f="circular"===t.get("layout")&&t.get(["circular","rotateLabel"]),g=u.getLayout("cx"),y=u.getLayout("cy");u.graph.eachNode((function(t){DA(t,f,g,y)})),this._firstRender=!1},e.prototype.dispose=function(){this.remove(),this._controller&&this._controller.dispose(),this._controllerHost=null},e.prototype._startForceLayoutIteration=function(t,e){var n=this;!function i(){t.step((function(t){n.updateLayout(n._model),(n._layouting=!t)&&(e?n._layoutTimeout=setTimeout(i,16):i())}))}()},e.prototype._updateController=function(t,e,n){var i=this,r=this._controller,o=this._controllerHost,a=this.group;r.setPointerChecker((function(e,i,r){var o=a.getBoundingRect();return o.applyTransform(a.transform),o.contain(i,r)&&!hT(e,n,t)})),nk(t.coordinateSystem)?(r.enable(t.get("roam")),o.zoomLimit=t.get("scaleLimit"),o.zoom=t.coordinateSystem.getZoom(),r.off("pan").off("zoom").on("pan",(function(e){aT(o,e.dx,e.dy),n.dispatchAction({seriesId:t.id,type:"graphRoam",dx:e.dx,dy:e.dy})})).on("zoom",(function(e){sT(o,e.scale,e.originX,e.originY),n.dispatchAction({seriesId:t.id,type:"graphRoam",zoom:e.scale,originX:e.originX,originY:e.originY}),i._updateNodeAndLinkScale(),ek(t.getGraph(),wA(t)),i._lineDraw.updateLayout(),n.updateLabelLayout()}))):r.disable()},e.prototype._updateNodeAndLinkScale=function(){var t=this._model,e=t.getData(),n=wA(t);e.eachItemGraphicEl((function(t,e){t&&t.setSymbolScale(n)}))},e.prototype.updateLayout=function(t){ek(t.getGraph(),wA(t)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout()},e.prototype.remove=function(){clearTimeout(this._layoutTimeout),this._layouting=!1,this._layoutTimeout=null,this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove()},e.type="graph",e}(Og);function rk(t){return"_EC_"+t}var ok=function(){function t(t){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=t||!1}return t.prototype.isDirected=function(){return this._directed},t.prototype.addNode=function(t,e){t=null==t?""+e:""+t;var n=this._nodesMap;if(!n[rk(t)]){var i=new ak(t,e);return i.hostGraph=this,this.nodes.push(i),n[rk(t)]=i,i}},t.prototype.getNodeByIndex=function(t){var e=this.data.getRawIndex(t);return this.nodes[e]},t.prototype.getNodeById=function(t){return this._nodesMap[rk(t)]},t.prototype.addEdge=function(t,e,n){var i=this._nodesMap,r=this._edgesMap;if(j(t)&&(t=this.nodes[t]),j(e)&&(e=this.nodes[e]),t instanceof ak||(t=i[rk(t)]),e instanceof ak||(e=i[rk(e)]),t&&e){var o=t.id+"-"+e.id,a=new sk(t,e,n);return a.hostGraph=this,this._directed&&(t.outEdges.push(a),e.inEdges.push(a)),t.edges.push(a),t!==e&&e.edges.push(a),this.edges.push(a),r[o]=a,a}},t.prototype.getEdgeByIndex=function(t){var e=this.edgeData.getRawIndex(t);return this.edges[e]},t.prototype.getEdge=function(t,e){t instanceof ak&&(t=t.id),e instanceof ak&&(e=e.id);var n=this._edgesMap;return this._directed?n[t+"-"+e]:n[t+"-"+e]||n[e+"-"+t]},t.prototype.eachNode=function(t,e){for(var n=this.nodes,i=n.length,r=0;r=0&&t.call(e,n[r],r)},t.prototype.eachEdge=function(t,e){for(var n=this.edges,i=n.length,r=0;r=0&&n[r].node1.dataIndex>=0&&n[r].node2.dataIndex>=0&&t.call(e,n[r],r)},t.prototype.breadthFirstTraverse=function(t,e,n,i){if(e instanceof ak||(e=this._nodesMap[rk(e)]),e){for(var r="out"===n?"outEdges":"in"===n?"inEdges":"edges",o=0;o=0&&n.node2.dataIndex>=0}));for(r=0,o=i.length;r=0&&this[t][e].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[t][e].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[t][e].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[t][e].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[t][e].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[t][e].getRawIndex(this.dataIndex)}}}function uk(t,e,n,i,r){for(var o=new ok(i),a=0;a "+p)),u++)}var d,f=n.get("coordinateSystem");if("cartesian2d"===f||"polar"===f)d=_x(t,n);else{var g=wd.get(f),y=g&&g.dimensions||[];P(y,"value")<0&&y.concat(["value"]);var v=px(t,{coordDimensions:y,encodeDefine:n.getEncode()}).dimensions;(d=new cx(v,n)).initData(t)}var m=new cx(["value"],n);return m.initData(l,s),r&&r(d,m),ZC({mainData:d,struct:o,structAttr:"graph",datas:{node:d,edge:m},datasAttr:{node:"data",edge:"edgeData"}}),o.update(),o}R(ak,lk("hostGraph","data")),R(sk,lk("hostGraph","edgeData"));var hk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments);var n=this;function i(){return n._categoriesData}this.legendVisualProvider=new NM(i,i),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeDefaultAndTheme=function(e){t.prototype.mergeDefaultAndTheme.apply(this,arguments),Co(e,"edgeLabel",["show"])},e.prototype.getInitialData=function(t,e){var n,i=t.edges||t.links||[],r=t.data||t.nodes||[],o=this;if(r&&i){dA(n=this)&&(n.__curvenessList=[],n.__edgeMap={},fA(n));var a=uk(r,i,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t){var e=o._categoriesModels[t.getShallow("category")];return e&&(e.parentModel=t.parentModel,t.parentModel=e),t}));var n=Cc.prototype.getModel;function i(t,e){var i=n.call(this,t,e);return i.resolveParentPath=r,i}function r(t){if(t&&("label"===t[0]||"label"===t[1])){var e=t.slice();return"label"===t[0]?e[0]="edgeLabel":"label"===t[1]&&(e[1]="edgeLabel"),e}return t}e.wrapMethod("getItemModel",(function(t){return t.resolveParentPath=r,t.getModel=i,t}))}));return E(a.edges,(function(t){!function(t,e,n,i){if(dA(n)){var r=gA(t,e,n),o=n.__edgeMap,a=o[yA(r)];o[r]&&!a?o[r].isForward=!0:a&&o[r]&&(a.isForward=!0,o[r].isForward=!1),o[r]=o[r]||[],o[r].push(i)}}(t.node1,t.node2,this,t.dataIndex)}),this),a.data}},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.getCategoriesData=function(){return this._categoriesData},e.prototype.formatTooltip=function(t,e,n){if("edge"===n){var i=this.getData(),r=this.getDataParams(t,n),o=i.graph.getEdgeByIndex(t),a=i.getName(o.node1.dataIndex),s=i.getName(o.node2.dataIndex),l=[];return null!=a&&l.push(a),null!=s&&l.push(s),og("nameValue",{name:l.join(" > "),value:r.value,noValue:null==r.value})}return vg({series:this,dataIndex:t,multipleSeries:e})},e.prototype._updateCategoriesData=function(){var t=z(this.option.categories||[],(function(t){return null!=t.value?t:A({value:0},t)})),e=new cx(["value"],this);e.initData(t),this._categoriesData=e,this._categoriesModels=e.mapArray((function(t){return e.getItemModel(t)}))},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.isAnimationEnabled=function(){return t.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},e.type="series.graph",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(bg),ck={type:"graphRoam",event:"graphRoam",update:"none"};var pk=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},dk=function(t){function e(e){var n=t.call(this,e)||this;return n.type="pointer",n}return n(e,t),e.prototype.getDefaultShape=function(){return new pk},e.prototype.buildPath=function(t,e){var n=Math.cos,i=Math.sin,r=e.r,o=e.width,a=e.angle,s=e.x-n(a)*o*(o>=r/3?1:2),l=e.y-i(a)*o*(o>=r/3?1:2);a=e.angle-Math.PI/2,t.moveTo(s,l),t.lineTo(e.x+n(a)*o,e.y+i(a)*o),t.lineTo(e.x+n(e.angle)*r,e.y+i(e.angle)*r),t.lineTo(e.x-n(a)*o,e.y-i(a)*o),t.lineTo(s,l)},e}(ks);function fk(t,e){var n=null==t?"":t+"";return e&&(U(e)?n=e.replace("{value}",n):X(e)&&(n=e(t))),n}var gk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll();var i=t.get(["axisLine","lineStyle","color"]),r=function(t,e){var n=t.get("center"),i=e.getWidth(),r=e.getHeight(),o=Math.min(i,r);return{cx:$r(n[0],e.getWidth()),cy:$r(n[1],e.getHeight()),r:$r(t.get("radius"),o/2)}}(t,n);this._renderMain(t,e,n,i,r),this._data=t.getData()},e.prototype.dispose=function(){},e.prototype._renderMain=function(t,e,n,i,r){var o=this.group,a=t.get("clockwise"),s=-t.get("startAngle")/180*Math.PI,l=-t.get("endAngle")/180*Math.PI,u=t.getModel("axisLine"),h=u.get("roundCap")?JS:Fu,c=u.get("show"),p=u.getModel("lineStyle"),d=p.get("width"),f=[s,l];us(f,!a);for(var g=(l=f[1])-(s=f[0]),y=s,v=[],m=0;c&&m=t&&(0===e?0:i[e-1][0])Math.PI/2&&(V+=Math.PI):"tangential"===z?V=-M-Math.PI/2:j(z)&&(V=z*Math.PI/180),0===V?c.add(new Xs({style:oc(x,{text:O,x:N,y:E,verticalAlign:h<-.8?"top":h>.8?"bottom":"middle",align:u<-.4?"left":u>.4?"right":"center"},{inheritColor:R}),silent:!0})):c.add(new Xs({style:oc(x,{text:O,x:N,y:E,verticalAlign:"middle",align:"center"},{inheritColor:R}),silent:!0,originX:N,originY:E,rotation:V}))}if(m.get("show")&&k!==_){P=(P=m.get("distance"))?P+l:l;for(var B=0;B<=b;B++){u=Math.cos(M),h=Math.sin(M);var F=new Ku({shape:{x1:u*(f-P)+p,y1:h*(f-P)+d,x2:u*(f-S-P)+p,y2:h*(f-S-P)+d},silent:!0,style:D});"auto"===D.stroke&&F.setStyle({stroke:i((k+B/b)/_)}),c.add(F),M+=T}M-=T}else M+=I}},e.prototype._renderPointer=function(t,e,n,i,r,o,a,s,l){var u=this.group,h=this._data,c=this._progressEls,p=[],d=t.get(["pointer","show"]),f=t.getModel("progress"),g=f.get("show"),y=t.getData(),v=y.mapDimension("value"),m=+t.get("min"),x=+t.get("max"),_=[m,x],b=[o,a];function w(e,n){var i,o=y.getItemModel(e).getModel("pointer"),a=$r(o.get("width"),r.r),s=$r(o.get("length"),r.r),l=t.get(["pointer","icon"]),u=o.get("offsetCenter"),h=$r(u[0],r.r),c=$r(u[1],r.r),p=o.get("keepAspect");return(i=l?Xy(l,h-a/2,c-s,a,s,null,p):new dk({shape:{angle:-Math.PI/2,width:a,r:s,x:h,y:c}})).rotation=-(n+Math.PI/2),i.x=r.cx,i.y=r.cy,i}function S(t,e){var n=f.get("roundCap")?JS:Fu,i=f.get("overlap"),a=i?f.get("width"):l/y.count(),u=i?r.r-a:r.r-(t+1)*a,h=i?r.r:r.r-t*a,c=new n({shape:{startAngle:o,endAngle:e,cx:r.cx,cy:r.cy,clockwise:s,r0:u,r:h}});return i&&(c.z2=x-y.get(v,t)%x),c}(g||d)&&(y.diff(h).add((function(e){var n=y.get(v,e);if(d){var i=w(e,o);mh(i,{rotation:-((isNaN(+n)?b[0]:Kr(n,_,b,!0))+Math.PI/2)},t),u.add(i),y.setItemGraphicEl(e,i)}if(g){var r=S(e,o),a=f.get("clip");mh(r,{shape:{endAngle:Kr(n,_,b,a)}},t),u.add(r),ol(t.seriesIndex,y.dataType,e,r),p[e]=r}})).update((function(e,n){var i=y.get(v,e);if(d){var r=h.getItemGraphicEl(n),a=r?r.rotation:o,s=w(e,a);s.rotation=a,vh(s,{rotation:-((isNaN(+i)?b[0]:Kr(i,_,b,!0))+Math.PI/2)},t),u.add(s),y.setItemGraphicEl(e,s)}if(g){var l=c[n],m=S(e,l?l.shape.endAngle:o),x=f.get("clip");vh(m,{shape:{endAngle:Kr(i,_,b,x)}},t),u.add(m),ol(t.seriesIndex,y.dataType,e,m),p[e]=m}})).execute(),y.each((function(t){var e=y.getItemModel(t),n=e.getModel("emphasis"),r=n.get("focus"),o=n.get("blurScope"),a=n.get("disabled");if(d){var s=y.getItemGraphicEl(t),l=y.getItemVisual(t,"style"),u=l.fill;if(s instanceof Ns){var h=s.style;s.useStyle(A({image:h.image,x:h.x,y:h.y,width:h.width,height:h.height},l))}else s.useStyle(l),"pointer"!==s.type&&s.setColor(u);s.setStyle(e.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===s.style.fill&&s.setStyle("fill",i(Kr(y.get(v,t),_,[0,1],!0))),s.z2EmphasisLift=0,$l(s,e),Zl(s,r,o,a)}if(g){var c=p[t];c.useStyle(y.getItemVisual(t,"style")),c.setStyle(e.getModel(["progress","itemStyle"]).getItemStyle()),c.z2EmphasisLift=0,$l(c,e),Zl(c,r,o,a)}})),this._progressEls=p)},e.prototype._renderAnchor=function(t,e){var n=t.getModel("anchor");if(n.get("show")){var i=n.get("size"),r=n.get("icon"),o=n.get("offsetCenter"),a=n.get("keepAspect"),s=Xy(r,e.cx-i/2+$r(o[0],e.r),e.cy-i/2+$r(o[1],e.r),i,i,null,a);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},e.prototype._renderTitleAndDetail=function(t,e,n,i,r){var o=this,a=t.getData(),s=a.mapDimension("value"),l=+t.get("min"),u=+t.get("max"),h=new Br,c=[],p=[],d=t.isAnimationEnabled(),f=t.get(["pointer","showAbove"]);a.diff(this._data).add((function(t){c[t]=new Xs({silent:!0}),p[t]=new Xs({silent:!0})})).update((function(t,e){c[t]=o._titleEls[e],p[t]=o._detailEls[e]})).execute(),a.each((function(e){var n=a.getItemModel(e),o=a.get(s,e),g=new Br,y=i(Kr(o,[l,u],[0,1],!0)),v=n.getModel("title");if(v.get("show")){var m=v.get("offsetCenter"),x=r.cx+$r(m[0],r.r),_=r.cy+$r(m[1],r.r);(D=c[e]).attr({z2:f?0:2,style:oc(v,{x:x,y:_,text:a.getName(e),align:"center",verticalAlign:"middle"},{inheritColor:y})}),g.add(D)}var b=n.getModel("detail");if(b.get("show")){var w=b.get("offsetCenter"),S=r.cx+$r(w[0],r.r),M=r.cy+$r(w[1],r.r),I=$r(b.get("width"),r.r),T=$r(b.get("height"),r.r),C=t.get(["progress","show"])?a.getItemVisual(e,"style").fill:y,D=p[e],A=b.get("formatter");D.attr({z2:f?0:2,style:oc(b,{x:S,y:M,text:fk(o,A),width:isNaN(I)?null:I,height:isNaN(T)?null:T,align:"center",verticalAlign:"middle"},{inheritColor:C})}),dc(D,{normal:b},o,(function(t){return fk(t,A)})),d&&fc(D,e,a,t,{getFormattedLabel:function(t,e,n,i,r,a){return fk(a?a.interpolatedValue:o,A)}}),g.add(D)}h.add(g)})),this.group.add(h),this._titleEls=c,this._detailEls=p},e.type="gauge",e}(Og),yk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="itemStyle",n}return n(e,t),e.prototype.getInitialData=function(t,e){return RM(this,["value"])},e.type="series.gauge",e.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}},e}(bg);var vk=["itemStyle","opacity"],mk=function(t){function e(e,n){var i=t.call(this)||this,r=i,o=new Zu,a=new Xs;return r.setTextContent(a),i.setTextGuideLine(o),i.updateData(e,n,!0),i}return n(e,t),e.prototype.updateData=function(t,e,n){var i=this,r=t.hostModel,o=t.getItemModel(e),a=t.getItemLayout(e),s=o.getModel("emphasis"),l=o.get(vk);l=null==l?1:l,n||Sh(i),i.useStyle(t.getItemVisual(e,"style")),i.style.lineJoin="round",n?(i.setShape({points:a.points}),i.style.opacity=0,mh(i,{style:{opacity:l}},r,e)):vh(i,{style:{opacity:l},shape:{points:a.points}},r,e),$l(i,o),this._updateLabel(t,e),Zl(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},e.prototype._updateLabel=function(t,e){var n=this,i=this.getTextGuideLine(),r=n.getTextContent(),o=t.hostModel,a=t.getItemModel(e),s=t.getItemLayout(e).label,l=t.getItemVisual(e,"style"),u=l.fill;ic(r,rc(a),{labelFetcher:t.hostModel,labelDataIndex:e,defaultOpacity:l.opacity,defaultText:t.getName(e)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}}),n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:u,outsideFill:u});var h=s.linePoints;i.setShape({points:h}),n.textGuideLineConfig={anchor:h?new De(h[0][0],h[0][1]):null},vh(r,{style:{x:s.x,y:s.y}},o,e),r.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),kb(n,Lb(a),{stroke:u})},e}(Xu),xk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreLabelLineUpdate=!0,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this._data,o=this.group;i.diff(r).add((function(t){var e=new mk(i,t);i.setItemGraphicEl(t,e),o.add(e)})).update((function(t,e){var n=r.getItemGraphicEl(e);n.updateData(i,t),o.add(n),i.setItemGraphicEl(t,n)})).remove((function(e){wh(r.getItemGraphicEl(e),t,e)})).execute(),this._data=i},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.prototype.dispose=function(){},e.type="funnel",e}(Og),_k=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new NM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.getInitialData=function(t,e){return RM(this,{coordDimensions:["value"],encodeDefaulter:H(ed,this)})},e.prototype._defaultLabelLine=function(t){Co(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.prototype.getDataParams=function(e){var n=this.getData(),i=t.prototype.getDataParams.call(this,e),r=n.mapDimension("value"),o=n.getSum(r);return i.percent=o?+(n.get(r,e)/o*100).toFixed(2):0,i.$vars.push("percent"),i},e.type="series.funnel",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(bg);function bk(t,e){t.eachSeriesByType("funnel",(function(t){var n=t.getData(),i=n.mapDimension("value"),r=t.get("sort"),o=function(t,e){return kp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e),a=t.get("orient"),s=o.width,l=o.height,u=function(t,e){for(var n=t.mapDimension("value"),i=t.mapArray(n,(function(t){return t})),r=[],o="ascending"===e,a=0,s=t.count();a5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&Rk(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function Rk(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var Nk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&C(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){E(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];E(B(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(zp),Ek=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(ab);function zk(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=Bk(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=Bk(s,[0,a]),r=o=Bk(s,[r,o]),i=0}e[0]=Bk(e[0],n),e[1]=Bk(e[1],n);var l=Vk(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=Bk(e[i],c),u=Vk(e,i),null!=r&&(u.sign!==l.sign||u.spano&&(e[1-i]=e[i]+u.sign*o),e}function Vk(t,e){var n=t[e]-t[1-e];return{span:Math.abs(n),sign:n>0?-1:n<0?1:e?-1:1}}function Bk(t,e){return Math.min(null!=e[1]?e[1]:1/0,Math.max(null!=e[0]?e[0]:-1/0,t))}var Fk=E,Gk=Math.min,Wk=Math.max,Hk=Math.floor,Yk=Math.ceil,Xk=Jr,Uk=Math.PI,Zk=function(){function t(t,e,n){this.type="parallel",this._axesMap=yt(),this._axesLayout={},this.dimensions=t.dimensions,this._model=t,this._init(t,e,n)}return t.prototype._init=function(t,e,n){var i=t.dimensions,r=t.parallelAxisIndex;Fk(i,(function(t,n){var i=r[n],o=e.getComponent("parallelAxis",i),a=this._axesMap.set(t,new Ek(t,b_(o),[0,0],o.get("type"),i)),s="category"===a.type;a.onBand=s&&o.get("boundaryGap"),a.inverse=o.get("inverse"),o.axis=a,a.model=o,a.coordinateSystem=o.coordinateSystem=this}),this)},t.prototype.update=function(t,e){this._updateAxesFromSeries(this._model,t)},t.prototype.containPoint=function(t){var e=this._makeLayoutInfo(),n=e.axisBase,i=e.layoutBase,r=e.pixelDimIndex,o=t[1-r],a=t[r];return o>=n&&o<=n+e.axisLength&&a>=i&&a<=i+e.layoutLength},t.prototype.getModel=function(){return this._model},t.prototype._updateAxesFromSeries=function(t,e){e.eachSeries((function(n){if(t.contains(n,e)){var i=n.getData();Fk(this.dimensions,(function(t){var e=this._axesMap.get(t);e.scale.unionExtentFromData(i,i.mapDimension(t)),__(e.scale,e.model)}),this)}}),this)},t.prototype.resize=function(t,e){this._rect=kp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()}),this._layoutAxes()},t.prototype.getRect=function(){return this._rect},t.prototype._makeLayoutInfo=function(){var t,e=this._model,n=this._rect,i=["x","y"],r=["width","height"],o=e.get("layout"),a="horizontal"===o?0:1,s=n[r[a]],l=[0,s],u=this.dimensions.length,h=jk(e.get("axisExpandWidth"),l),c=jk(e.get("axisExpandCount")||0,[0,u]),p=e.get("axisExpandable")&&u>3&&u>c&&c>1&&h>0&&s>0,d=e.get("axisExpandWindow");d?(t=jk(d[1]-d[0],l),d[1]=d[0]+t):(t=jk(h*(c-1),l),(d=[h*(e.get("axisExpandCenter")||Hk(u/2))-t/2])[1]=d[0]+t);var f=(s-t)/(u-c);f<3&&(f=0);var g=[Hk(Xk(d[0]/h,1))+1,Yk(Xk(d[1]/h,1))-1],y=f/h*d[0];return{layout:o,pixelDimIndex:a,layoutBase:n[i[a]],layoutLength:s,axisBase:n[i[1-a]],axisLength:n[r[1-a]],axisExpandable:p,axisExpandWidth:h,axisCollapseWidth:f,axisExpandWindow:d,axisCount:u,winInnerIndices:g,axisExpandWindow0Pos:y}},t.prototype._layoutAxes=function(){var t=this._rect,e=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),r=i.layout;e.each((function(t){var e=[0,i.axisLength],n=t.inverse?1:0;t.setExtent(e[n],e[1-n])})),Fk(n,(function(e,n){var o=(i.axisExpandable?Kk:qk)(n,i),a={horizontal:{x:o.position,y:i.axisLength},vertical:{x:0,y:o.position}},s={horizontal:Uk/2,vertical:0},l=[a[r].x+t.x,a[r].y+t.y],u=s[r],h=[1,0,0,1,0,0];Se(h,h,u),we(h,h,l),this._axesLayout[e]={position:l,rotation:u,transform:h,axisNameAvailableWidth:o.axisNameAvailableWidth,axisLabelShow:o.axisLabelShow,nameTruncateMaxWidth:o.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}}),this)},t.prototype.getAxis=function(t){return this._axesMap.get(t)},t.prototype.dataToPoint=function(t,e){return this.axisCoordToPoint(this._axesMap.get(e).dataToCoord(t),e)},t.prototype.eachActiveState=function(t,e,n,i){null==n&&(n=0),null==i&&(i=t.count());var r=this._axesMap,o=this.dimensions,a=[],s=[];E(o,(function(e){a.push(t.mapDimension(e)),s.push(r.get(e).model)}));for(var l=this.hasAxisBrushed(),u=n;ur*(1-h[0])?(l="jump",a=s-r*(1-h[2])):(a=s-r*h[1])>=0&&(a=s-r*(1-h[1]))<=0&&(a=0),(a*=e.axisExpandWidth/u)?zk(a,i,o,"all"):l="none";else{var p=i[1]-i[0];(i=[Wk(0,o[1]*s/p-p/2)])[1]=Gk(o[1],i[0]+p),i[0]=i[1]-p}return{axisExpandWindow:i,behavior:l}},t}();function jk(t,e){return Gk(Wk(t,e[0]),e[1])}function qk(t,e){var n=e.layoutLength/(e.axisCount-1);return{position:n*t,axisNameAvailableWidth:n,axisLabelShow:!0}}function Kk(t,e){var n,i,r=e.layoutLength,o=e.axisExpandWidth,a=e.axisCount,s=e.axisCollapseWidth,l=e.winInnerIndices,u=s,h=!1;return t=0;n--)Qr(e[n])},e.prototype.getActiveState=function(t){var e=this.activeIntervals;if(!e.length)return"normal";if(null==t||isNaN(+t))return"inactive";if(1===e.length){var n=e[0];if(n[0]<=t&&t<=n[1])return"active"}else for(var i=0,r=e.length;i6}(t)||o){if(a&&!o){"single"===s.brushMode&&vL(t);var l=T(s);l.brushType=RL(l.brushType,a),l.panelId=a===Qk?null:a.panelId,o=t._creatingCover=uL(t,l),t._covers.push(o)}if(o){var u=zL[RL(t._brushType,a)];o.__brushOption.range=u.getCreatingRange(kL(t,o,t._track)),i&&(hL(t,o),u.updateCommon(t,o)),cL(t,o),r={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&gL(t,e,n)&&vL(t)&&(r={isEnd:i,removeOnClick:!0});return r}function RL(t,e){return"auto"===t?e.defaultBrushType:t}var NL={mousedown:function(t){if(this._dragging)EL(this,t);else if(!t.target||!t.target.draggable){LL(t);var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);this._creatingCover=null,(this._creatingPanel=gL(this,t,e))&&(this._dragging=!0,this._track=[e.slice()])}},mousemove:function(t){var e=t.offsetX,n=t.offsetY,i=this.group.transformCoordToLocal(e,n);if(function(t,e,n){if(t._brushType&&!function(t,e,n){var i=t._zr;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}(t,e.offsetX,e.offsetY)){var i=t._zr,r=t._covers,o=gL(t,e,n);if(!t._dragging)for(var a=0;a=0&&(o[r[a].depth]=new Cc(r[a],this,e));if(i&&n){var s=uk(i,n,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getData().getItemLayout(e);if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t})),e.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getGraph().getEdgeByIndex(e).node1.getLayout();if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t}))}));return s.data}},e.prototype.setNodePosition=function(t,e){var n=(this.option.data||this.option.nodes)[t];n.localX=e[0],n.localY=e[1]},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.formatTooltip=function(t,e,n){function i(t){return isNaN(t)||null==t}if("edge"===n){var r=this.getDataParams(t,n),o=r.data,a=r.value;return og("nameValue",{name:o.source+" -- "+o.target,value:a,noValue:i(a)})}var s=this.getGraph().getNodeByIndex(t).getLayout().value,l=this.getDataParams(t,n).data.name;return og("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},e.prototype.optionUpdated=function(){},e.prototype.getDataParams=function(e,n){var i=t.prototype.getDataParams.call(this,e,n);if(null==i.value&&"node"===n){var r=this.getGraph().getNodeByIndex(e).getLayout().value;i.value=r}return i},e.type="series.sankey",e.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3},e}(bg);function QL(t,e){t.eachSeriesByType("sankey",(function(t){var n=t.get("nodeWidth"),i=t.get("nodeGap"),r=function(t,e){return kp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=r;var o=r.width,a=r.height,s=t.getGraph(),l=s.nodes,u=s.edges;!function(t){E(t,(function(t){var e=uP(t.outEdges,lP),n=uP(t.inEdges,lP),i=t.getValue()||0,r=Math.max(e,n,i);t.setLayout({value:r},!0)}))}(l),function(t,e,n,i,r,o,a,s,l){(function(t,e,n,i,r,o,a){for(var s=[],l=[],u=[],h=[],c=0,p=0;p=0;v&&y.depth>d&&(d=y.depth),g.setLayout({depth:v?y.depth:c},!0),"vertical"===o?g.setLayout({dy:n},!0):g.setLayout({dx:n},!0);for(var m=0;mc-1?d:c-1;a&&"left"!==a&&function(t,e,n,i){if("right"===e){for(var r=[],o=t,a=0;o.length;){for(var s=0;s0;o--)nP(s,l*=.99,a),eP(s,r,n,i,a),hP(s,l,a),eP(s,r,n,i,a)}(t,e,o,r,i,a,s),function(t,e){var n="vertical"===e?"x":"y";E(t,(function(t){t.outEdges.sort((function(t,e){return t.node2.getLayout()[n]-e.node2.getLayout()[n]})),t.inEdges.sort((function(t,e){return t.node1.getLayout()[n]-e.node1.getLayout()[n]}))})),E(t,(function(t){var e=0,n=0;E(t.outEdges,(function(t){t.setLayout({sy:e},!0),e+=t.getLayout().dy})),E(t.inEdges,(function(t){t.setLayout({ty:n},!0),n+=t.getLayout().dy}))}))}(t,s)}(l,u,n,i,o,a,0!==B(l,(function(t){return 0===t.getLayout().value})).length?0:t.get("layoutIterations"),t.get("orient"),t.get("nodeAlign"))}))}function tP(t){var e=t.hostGraph.data.getRawDataItem(t.dataIndex);return null!=e.depth&&e.depth>=0}function eP(t,e,n,i,r){var o="vertical"===r?"x":"y";E(t,(function(t){var a,s,l;t.sort((function(t,e){return t.getLayout()[o]-e.getLayout()[o]}));for(var u=0,h=t.length,c="vertical"===r?"dx":"dy",p=0;p0&&(a=s.getLayout()[o]+l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]+s.getLayout()[c]+e;if((l=u-e-("vertical"===r?i:n))>0){a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0),u=a;for(p=h-2;p>=0;--p)(l=(s=t[p]).getLayout()[o]+s.getLayout()[c]+e-u)>0&&(a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]}}))}function nP(t,e,n){E(t.slice().reverse(),(function(t){E(t,(function(t){if(t.outEdges.length){var i=uP(t.outEdges,iP,n)/uP(t.outEdges,lP);if(isNaN(i)){var r=t.outEdges.length;i=r?uP(t.outEdges,rP,n)/r:0}if("vertical"===n){var o=t.getLayout().x+(i-sP(t,n))*e;t.setLayout({x:o},!0)}else{var a=t.getLayout().y+(i-sP(t,n))*e;t.setLayout({y:a},!0)}}}))}))}function iP(t,e){return sP(t.node2,e)*t.getValue()}function rP(t,e){return sP(t.node2,e)}function oP(t,e){return sP(t.node1,e)*t.getValue()}function aP(t,e){return sP(t.node1,e)}function sP(t,e){return"vertical"===e?t.getLayout().x+t.getLayout().dx/2:t.getLayout().y+t.getLayout().dy/2}function lP(t){return t.getValue()}function uP(t,e,n){for(var i=0,r=t.length,o=-1;++oo&&(o=e)})),E(n,(function(e){var n=new kD({type:"color",mappingMethod:"linear",dataExtent:[r,o],visual:t.get("color")}).mapValueToVisual(e.getLayout().value),i=e.getModel().get(["itemStyle","color"]);null!=i?(e.setVisual("color",i),e.setVisual("style",{fill:i})):(e.setVisual("color",n),e.setVisual("style",{fill:n}))}))}i.length&&E(i,(function(t){var e=t.getModel().get("lineStyle");t.setVisual("style",e)}))}))}var pP=function(){function t(){}return t.prototype.getInitialData=function(t,e){var n,i,r=e.getComponent("xAxis",this.get("xAxisIndex")),o=e.getComponent("yAxis",this.get("yAxisIndex")),a=r.get("type"),s=o.get("type");"category"===a?(t.layout="horizontal",n=r.getOrdinalMeta(),i=!0):"category"===s?(t.layout="vertical",n=o.getOrdinalMeta(),i=!0):t.layout=t.layout||"horizontal";var l=["x","y"],u="horizontal"===t.layout?0:1,h=this._baseAxisDim=l[u],c=l[1-u],p=[r,o],d=p[u].get("type"),f=p[1-u].get("type"),g=t.data;if(g&&i){var y=[];E(g,(function(t,e){var n;Y(t)?(n=t.slice(),t.unshift(e)):Y(t.value)?((n=A({},t)).value=n.value.slice(),t.value.unshift(e)):n=t,y.push(n)})),t.data=y}var v=this.defaultValueDimensions,m=[{name:h,type:Ym(d),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:c,type:Ym(f),dimsDef:v.slice()}];return RM(this,{coordDimensions:m,dimensionsCount:v.length+1,encodeDefaulter:H(td,m,this)})},t.prototype.getBaseAxis=function(){var t=this._baseAxisDim;return this.ecModel.getComponent(t+"Axis",this.get(t+"AxisIndex")).axis},t}(),dP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],n.visualDrawType="stroke",n}return n(e,t),e.type="series.boxplot",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},e}(bg);R(dP,pP,!0);var fP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this.group,o=this._data;this._data||r.removeAll();var a="horizontal"===t.get("layout")?1:0;i.diff(o).add((function(t){if(i.hasValue(t)){var e=vP(i.getItemLayout(t),i,t,a,!0);i.setItemGraphicEl(t,e),r.add(e)}})).update((function(t,e){var n=o.getItemGraphicEl(e);if(i.hasValue(t)){var s=i.getItemLayout(t);n?(Sh(n),mP(s,n,i,t)):n=vP(s,i,t,a),r.add(n),i.setItemGraphicEl(t,n)}else r.remove(n)})).remove((function(t){var e=o.getItemGraphicEl(t);e&&r.remove(e)})).execute(),this._data=i},e.prototype.remove=function(t){var e=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl((function(t){t&&e.remove(t)}))},e.type="boxplot",e}(Og),gP=function(){},yP=function(t){function e(e){var n=t.call(this,e)||this;return n.type="boxplotBoxPath",n}return n(e,t),e.prototype.getDefaultShape=function(){return new gP},e.prototype.buildPath=function(t,e){var n=e.points,i=0;for(t.moveTo(n[i][0],n[i][1]),i++;i<4;i++)t.lineTo(n[i][0],n[i][1]);for(t.closePath();ig){var _=[v,x];i.push(_)}}}return{boxData:n,outliers:i}}(e.getRawData(),t.config);return[{dimensions:["ItemName","Low","Q1","Q2","Q3","High"],data:i.boxData},{data:i.outliers}]}};var SP=["color","borderColor"],MP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeClipPath(),this._progressiveEls=null,this._updateDrawMode(t),this._isLargeDraw?this._renderLarge(t):this._renderNormal(t)},e.prototype.incrementalPrepareRender=function(t,e,n){this._clear(),this._updateDrawMode(t)},e.prototype.incrementalRender=function(t,e,n,i){this._progressiveEls=[],this._isLargeDraw?this._incrementalRenderLarge(t,e):this._incrementalRenderNormal(t,e)},e.prototype.eachRendered=function(t){Jh(this._progressiveEls||this.group,t)},e.prototype._updateDrawMode=function(t){var e=t.pipelineContext.large;null!=this._isLargeDraw&&e===this._isLargeDraw||(this._isLargeDraw=e,this._clear())},e.prototype._renderNormal=function(t){var e=t.getData(),n=this._data,i=this.group,r=e.getLayout("isSimpleBox"),o=t.get("clip",!0),a=t.coordinateSystem,s=a.getArea&&a.getArea();this._data||i.removeAll(),e.diff(n).add((function(n){if(e.hasValue(n)){var a=e.getItemLayout(n);if(o&&DP(s,a))return;var l=CP(a,n,!0);mh(l,{shape:{points:a.ends}},t,n),AP(l,e,n,r),i.add(l),e.setItemGraphicEl(n,l)}})).update((function(a,l){var u=n.getItemGraphicEl(l);if(e.hasValue(a)){var h=e.getItemLayout(a);o&&DP(s,h)?i.remove(u):(u?(vh(u,{shape:{points:h.ends}},t,a),Sh(u)):u=CP(h),AP(u,e,a,r),i.add(u),e.setItemGraphicEl(a,u))}else i.remove(u)})).remove((function(t){var e=n.getItemGraphicEl(t);e&&i.remove(e)})).execute(),this._data=e},e.prototype._renderLarge=function(t){this._clear(),OP(t,this.group);var e=t.get("clip",!0)?PS(t.coordinateSystem,!1,t):null;e?this.group.setClipPath(e):this.group.removeClipPath()},e.prototype._incrementalRenderNormal=function(t,e){for(var n,i=e.getData(),r=i.getLayout("isSimpleBox");null!=(n=t.next());){var o=CP(i.getItemLayout(n));AP(o,i,n,r),o.incremental=!0,this.group.add(o),this._progressiveEls.push(o)}},e.prototype._incrementalRenderLarge=function(t,e){OP(e,this.group,this._progressiveEls,!0)},e.prototype.remove=function(t){this._clear()},e.prototype._clear=function(){this.group.removeAll(),this._data=null},e.type="candlestick",e}(Og),IP=function(){},TP=function(t){function e(e){var n=t.call(this,e)||this;return n.type="normalCandlestickBox",n}return n(e,t),e.prototype.getDefaultShape=function(){return new IP},e.prototype.buildPath=function(t,e){var n=e.points;this.__simpleBox?(t.moveTo(n[4][0],n[4][1]),t.lineTo(n[6][0],n[6][1])):(t.moveTo(n[0][0],n[0][1]),t.lineTo(n[1][0],n[1][1]),t.lineTo(n[2][0],n[2][1]),t.lineTo(n[3][0],n[3][1]),t.closePath(),t.moveTo(n[4][0],n[4][1]),t.lineTo(n[5][0],n[5][1]),t.moveTo(n[6][0],n[6][1]),t.lineTo(n[7][0],n[7][1]))},e}(ks);function CP(t,e,n){var i=t.ends;return new TP({shape:{points:n?kP(i,t):i},z2:100})}function DP(t,e){for(var n=!0,i=0;i0?"borderColor":"borderColor0"])||n.get(["itemStyle",t>0?"color":"color0"]);0===t&&(r=n.get(["itemStyle","borderColorDoji"]));var o=n.getModel("itemStyle").getItemStyle(SP);e.useStyle(o),e.style.fill=null,e.style.stroke=r}var NP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return n(e,t),e.prototype.getShadowDim=function(){return"open"},e.prototype.brushSelector=function(t,e,n){var i=e.getItemLayout(t);return i&&n.rect(i.brushRect)},e.type="series.candlestick",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},e}(bg);function EP(t){t&&Y(t.series)&&E(t.series,(function(t){q(t)&&"k"===t.type&&(t.type="candlestick")}))}R(NP,pP,!0);var zP=["itemStyle","borderColor"],VP=["itemStyle","borderColor0"],BP=["itemStyle","borderColorDoji"],FP=["itemStyle","color"],GP=["itemStyle","color0"],WP={seriesType:"candlestick",plan:kg(),performRawSeries:!0,reset:function(t,e){function n(t,e){return e.get(t>0?FP:GP)}function i(t,e){return e.get(0===t?BP:t>0?zP:VP)}if(!e.isSeriesFiltered(t))return!t.pipelineContext.large&&{progress:function(t,e){for(var r;null!=(r=t.next());){var o=e.getItemModel(r),a=e.getItemLayout(r).sign,s=o.getItemStyle();s.fill=n(a,o),s.stroke=i(a,o)||s.fill,A(e.ensureUniqueItemVisual(r,"style"),s)}}}}},HP={seriesType:"candlestick",plan:kg(),reset:function(t){var e=t.coordinateSystem,n=t.getData(),i=function(t,e){var n,i=t.getBaseAxis(),r="category"===i.type?i.getBandWidth():(n=i.getExtent(),Math.abs(n[1]-n[0])/e.count()),o=$r(rt(t.get("barMaxWidth"),r),r),a=$r(rt(t.get("barMinWidth"),1),r),s=t.get("barWidth");return null!=s?$r(s,r):Math.max(Math.min(r/2,o),a)}(t,n),r=["x","y"],o=n.getDimensionIndex(n.mapDimension(r[0])),a=z(n.mapDimensionsAll(r[1]),n.getDimensionIndex,n),s=a[0],l=a[1],u=a[2],h=a[3];if(n.setLayout({candleWidth:i,isSimpleBox:i<=1.3}),!(o<0||a.length<4))return{progress:t.pipelineContext.large?function(n,i){var r,a,c=Bx(4*n.count),p=0,d=[],f=[],g=i.getStore(),y=!!t.get(["itemStyle","borderColorDoji"]);for(;null!=(a=n.next());){var v=g.get(o,a),m=g.get(s,a),x=g.get(l,a),_=g.get(u,a),b=g.get(h,a);isNaN(v)||isNaN(_)||isNaN(b)?(c[p++]=NaN,p+=3):(c[p++]=YP(g,a,m,x,l,y),d[0]=v,d[1]=_,r=e.dataToPoint(d,null,f),c[p++]=r?r[0]:NaN,c[p++]=r?r[1]:NaN,d[1]=b,r=e.dataToPoint(d,null,f),c[p++]=r?r[1]:NaN)}i.setLayout("largePoints",c)}:function(t,n){var r,a=n.getStore();for(;null!=(r=t.next());){var c=a.get(o,r),p=a.get(s,r),d=a.get(l,r),f=a.get(u,r),g=a.get(h,r),y=Math.min(p,d),v=Math.max(p,d),m=M(y,c),x=M(v,c),_=M(f,c),b=M(g,c),w=[];I(w,x,0),I(w,m,1),w.push(C(b),C(x),C(_),C(m));var S=!!n.getItemModel(r).get(["itemStyle","borderColorDoji"]);n.setItemLayout(r,{sign:YP(a,r,p,d,l,S),initBaseline:p>d?x[1]:m[1],ends:w,brushRect:T(f,g,c)})}function M(t,n){var i=[];return i[0]=n,i[1]=t,isNaN(n)||isNaN(t)?[NaN,NaN]:e.dataToPoint(i)}function I(t,e,n){var r=e.slice(),o=e.slice();r[0]=Vh(r[0]+i/2,1,!1),o[0]=Vh(o[0]-i/2,1,!0),n?t.push(r,o):t.push(o,r)}function T(t,e,n){var r=M(t,n),o=M(e,n);return r[0]-=i/2,o[0]-=i/2,{x:r[0],y:r[1],width:i,height:o[1]-r[1]}}function C(t){return t[0]=Vh(t[0],1),t}}}}};function YP(t,e,n,i,r,o){return n>i?-1:n0?t.get(r,e-1)<=i?1:-1:1}function XP(t,e){var n=e.rippleEffectColor||e.color;t.eachChild((function(t){t.attr({z:e.z,zlevel:e.zlevel,style:{stroke:"stroke"===e.brushType?n:null,fill:"fill"===e.brushType?n:null}})}))}var UP=function(t){function e(e,n){var i=t.call(this)||this,r=new fS(e,n),o=new Br;return i.add(r),i.add(o),i.updateData(e,n),i}return n(e,t),e.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},e.prototype.startEffectAnimation=function(t){for(var e=t.symbolType,n=t.color,i=t.rippleNumber,r=this.childAt(1),o=0;o0&&(o=this._getLineLength(i)/l*1e3),o!==this._period||a!==this._loop||s!==this._roundTrip){i.stopAnimation();var h=void 0;h=X(u)?u(n):u,i.__t>0&&(h=-o*i.__t),this._animateSymbol(i,o,h,a,s)}this._period=o,this._loop=a,this._roundTrip=s}},e.prototype._animateSymbol=function(t,e,n,i,r){if(e>0){t.__t=0;var o=this,a=t.animate("",i).when(r?2*e:e,{__t:r?2:1}).delay(n).during((function(){o._updateSymbolPosition(t)}));i||a.done((function(){o.remove(t)})),a.start()}},e.prototype._getLineLength=function(t){return Vt(t.__p1,t.__cp1)+Vt(t.__cp1,t.__p2)},e.prototype._updateAnimationPoints=function(t,e){t.__p1=e[0],t.__p2=e[1],t.__cp1=e[2]||[(e[0][0]+e[1][0])/2,(e[0][1]+e[1][1])/2]},e.prototype.updateData=function(t,e,n){this.childAt(0).updateData(t,e,n),this._updateEffectSymbol(t,e)},e.prototype._updateSymbolPosition=function(t){var e=t.__p1,n=t.__p2,i=t.__cp1,r=t.__t<1?t.__t:2-t.__t,o=[t.x,t.y],a=o.slice(),s=In,l=Tn;o[0]=s(e[0],i[0],n[0],r),o[1]=s(e[1],i[1],n[1],r);var u=t.__t<1?l(e[0],i[0],n[0],r):l(n[0],i[0],e[0],1-r),h=t.__t<1?l(e[1],i[1],n[1],r):l(n[1],i[1],e[1],1-r);t.rotation=-Math.atan2(h,u)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==t.__lastT&&t.__lastT=0&&!(i[o]<=e);o--);o=Math.min(o,r-2)}else{for(o=a;oe);o++);o=Math.min(o-1,r-2)}var s=(e-i[o])/(i[o+1]-i[o]),l=n[o],u=n[o+1];t.x=l[0]*(1-s)+s*u[0],t.y=l[1]*(1-s)+s*u[1];var h=t.__t<1?u[0]-l[0]:l[0]-u[0],c=t.__t<1?u[1]-l[1]:l[1]-u[1];t.rotation=-Math.atan2(c,h)-Math.PI/2,this._lastFrame=o,this._lastFramePercent=e,t.ignore=!1}},e}(qP),JP=function(){this.polyline=!1,this.curveness=0,this.segs=[]},QP=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new JP},e.prototype.buildPath=function(t,e){var n,i=e.segs,r=e.curveness;if(e.polyline)for(n=this._off;n0){t.moveTo(i[n++],i[n++]);for(var a=1;a0){var c=(s+u)/2-(l-h)*r,p=(l+h)/2-(u-s)*r;t.quadraticCurveTo(c,p,u,h)}else t.lineTo(u,h)}this.incremental&&(this._off=n,this.notClear=!0)},e.prototype.findDataIndex=function(t,e){var n=this.shape,i=n.segs,r=n.curveness,o=this.style.lineWidth;if(n.polyline)for(var a=0,s=0;s0)for(var u=i[s++],h=i[s++],c=1;c0){if(ds(u,h,(u+p)/2-(h-d)*r,(h+d)/2-(p-u)*r,p,d,o,t,e))return a}else if(cs(u,h,p,d,o,t,e))return a;a++}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape.segs,n=1/0,i=1/0,r=-1/0,o=-1/0,a=0;a0&&(o.dataIndex=n+t.__startIndex)}))},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),eO={seriesType:"lines",plan:kg(),reset:function(t){var e=t.coordinateSystem;if(e){var n=t.get("polyline"),i=t.pipelineContext.large;return{progress:function(r,o){var a=[];if(i){var s=void 0,l=r.end-r.start;if(n){for(var u=0,h=r.start;h0&&(l||s.configLayer(o,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(a/10+.9,1),0)})),r.updateData(i);var u=t.get("clip",!0)&&PS(t.coordinateSystem,!1,t);u?this.group.setClipPath(u):this.group.removeClipPath(),this._lastZlevel=o,this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateLineDraw(i,t).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._lineDraw.incrementalUpdate(t,e.getData()),this._finished=t.end===e.getData().count()},e.prototype.eachRendered=function(t){this._lineDraw&&this._lineDraw.eachRendered(t)},e.prototype.updateTransform=function(t,e,n){var i=t.getData(),r=t.pipelineContext;if(!this._finished||r.large||r.progressiveRender)return{update:!0};var o=eO.reset(t,e,n);o.progress&&o.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},e.prototype._updateLineDraw=function(t,e){var n=this._lineDraw,i=this._showEffect(e),r=!!e.get("polyline"),o=e.pipelineContext.large;return n&&i===this._hasEffet&&r===this._isPolyline&&o===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=o?new tO:new YA(r?i?$P:KP:i?qP:HA),this._hasEffet=i,this._isPolyline=r,this._isLargeDraw=o),this.group.add(n.group),n},e.prototype._showEffect=function(t){return!!t.get(["effect","show"])},e.prototype._clearLayer=function(t){var e=t.getZr();"svg"===e.painter.getType()||null==this._lastZlevel||e.painter.getLayer(this._lastZlevel).clear(!0)},e.prototype.remove=function(t,e){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(e)},e.prototype.dispose=function(t,e){this.remove(t,e)},e.type="lines",e}(Og),iO="undefined"==typeof Uint32Array?Array:Uint32Array,rO="undefined"==typeof Float64Array?Array:Float64Array;function oO(t){var e=t.data;e&&e[0]&&e[0][0]&&e[0][0].coord&&(t.data=z(e,(function(t){var e={coords:[t[0].coord,t[1].coord]};return t[0].name&&(e.fromName=t[0].name),t[1].name&&(e.toName=t[1].name),D([e,t[0],t[1]])})))}var aO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}return n(e,t),e.prototype.init=function(e){e.data=e.data||[],oO(e);var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count)),t.prototype.init.apply(this,arguments)},e.prototype.mergeOption=function(e){if(oO(e),e.data){var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count))}t.prototype.mergeOption.apply(this,arguments)},e.prototype.appendData=function(t){var e=this._processFlatCoordsArray(t.data);e.flatCoords&&(this._flatCoords?(this._flatCoords=vt(this._flatCoords,e.flatCoords),this._flatCoordsOffset=vt(this._flatCoordsOffset,e.flatCoordsOffset)):(this._flatCoords=e.flatCoords,this._flatCoordsOffset=e.flatCoordsOffset),t.data=new Float32Array(e.count)),this.getRawData().appendData(t.data)},e.prototype._getCoordsFromItemModel=function(t){var e=this.getData().getItemModel(t),n=e.option instanceof Array?e.option:e.getShallow("coords");return n},e.prototype.getLineCoordsCount=function(t){return this._flatCoordsOffset?this._flatCoordsOffset[2*t+1]:this._getCoordsFromItemModel(t).length},e.prototype.getLineCoords=function(t,e){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*t],i=this._flatCoordsOffset[2*t+1],r=0;r ")})},e.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?1e4:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?2e4:this.get("progressiveThreshold"):t},e.prototype.getZLevelKey=function(){var t=this.getModel("effect"),e=t.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:t.get("show")&&e>0?e+"":""},e.type="series.lines",e.dependencies=["grid","polar","geo","calendar"],e.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}},e}(bg);function sO(t){return t instanceof Array||(t=[t,t]),t}var lO={seriesType:"lines",reset:function(t){var e=sO(t.get("symbol")),n=sO(t.get("symbolSize")),i=t.getData();return i.setVisual("fromSymbol",e&&e[0]),i.setVisual("toSymbol",e&&e[1]),i.setVisual("fromSymbolSize",n&&n[0]),i.setVisual("toSymbolSize",n&&n[1]),{dataEach:i.hasItemOption?function(t,e){var n=t.getItemModel(e),i=sO(n.getShallow("symbol",!0)),r=sO(n.getShallow("symbolSize",!0));i[0]&&t.setItemVisual(e,"fromSymbol",i[0]),i[1]&&t.setItemVisual(e,"toSymbol",i[1]),r[0]&&t.setItemVisual(e,"fromSymbolSize",r[0]),r[1]&&t.setItemVisual(e,"toSymbolSize",r[1])}:null}}};var uO=function(){function t(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var t=h.createCanvas();this.canvas=t}return t.prototype.update=function(t,e,n,i,r,o){var a=this._getBrush(),s=this._getGradient(r,"inRange"),l=this._getGradient(r,"outOfRange"),u=this.pointSize+this.blurSize,h=this.canvas,c=h.getContext("2d"),p=t.length;h.width=e,h.height=n;for(var d=0;d0){var I=o(v)?s:l;v>0&&(v=v*S+w),x[_++]=I[M],x[_++]=I[M+1],x[_++]=I[M+2],x[_++]=I[M+3]*v*256}else _+=4}return c.putImageData(m,0,0),h},t.prototype._getBrush=function(){var t=this._brushCanvas||(this._brushCanvas=h.createCanvas()),e=this.pointSize+this.blurSize,n=2*e;t.width=n,t.height=n;var i=t.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-e,e,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),t},t.prototype._getGradient=function(t,e){for(var n=this._gradientPixels,i=n[e]||(n[e]=new Uint8ClampedArray(1024)),r=[0,0,0,0],o=0,a=0;a<256;a++)t[e](a/255,!0,r),i[o++]=r[0],i[o++]=r[1],i[o++]=r[2],i[o++]=r[3];return i},t}();function hO(t){var e=t.dimensions;return"lng"===e[0]&&"lat"===e[1]}var cO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i;e.eachComponent("visualMap",(function(e){e.eachTargetSeries((function(n){n===t&&(i=e)}))})),this._progressiveEls=null,this.group.removeAll();var r=t.coordinateSystem;"cartesian2d"===r.type||"calendar"===r.type?this._renderOnCartesianAndCalendar(t,n,0,t.getData().count()):hO(r)&&this._renderOnGeo(r,t,i,n)},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,e,n,i){var r=e.coordinateSystem;r&&(hO(r)?this.render(e,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(e,i,t.start,t.end,!0)))},e.prototype.eachRendered=function(t){Jh(this._progressiveEls||this.group,t)},e.prototype._renderOnCartesianAndCalendar=function(t,e,n,i,r){var o,a,s,l,u=t.coordinateSystem,h=OS(u,"cartesian2d");if(h){var c=u.getAxis("x"),p=u.getAxis("y");0,o=c.getBandWidth()+.5,a=p.getBandWidth()+.5,s=c.scale.getExtent(),l=p.scale.getExtent()}for(var d=this.group,f=t.getData(),g=t.getModel(["emphasis","itemStyle"]).getItemStyle(),y=t.getModel(["blur","itemStyle"]).getItemStyle(),v=t.getModel(["select","itemStyle"]).getItemStyle(),m=t.get(["itemStyle","borderRadius"]),x=rc(t),_=t.getModel("emphasis"),b=_.get("focus"),w=_.get("blurScope"),S=_.get("disabled"),M=h?[f.mapDimension("x"),f.mapDimension("y"),f.mapDimension("value")]:[f.mapDimension("time"),f.mapDimension("value")],I=n;Is[1]||Al[1])continue;var k=u.dataToPoint([D,A]);T=new Ws({shape:{x:k[0]-o/2,y:k[1]-a/2,width:o,height:a},style:C})}else{if(isNaN(f.get(M[1],I)))continue;T=new Ws({z2:1,shape:u.dataToRect([f.get(M[0],I)]).contentShape,style:C})}if(f.hasItemOption){var L=f.getItemModel(I),P=L.getModel("emphasis");g=P.getModel("itemStyle").getItemStyle(),y=L.getModel(["blur","itemStyle"]).getItemStyle(),v=L.getModel(["select","itemStyle"]).getItemStyle(),m=L.get(["itemStyle","borderRadius"]),b=P.get("focus"),w=P.get("blurScope"),S=P.get("disabled"),x=rc(L)}T.shape.r=m;var O=t.getRawValue(I),R="-";O&&null!=O[2]&&(R=O[2]+""),ic(T,x,{labelFetcher:t,labelDataIndex:I,defaultOpacity:C.opacity,defaultText:R}),T.ensureState("emphasis").style=g,T.ensureState("blur").style=y,T.ensureState("select").style=v,Zl(T,b,w,S),T.incremental=r,r&&(T.states.emphasis.hoverLayer=!0),d.add(T),f.setItemGraphicEl(I,T),this._progressiveEls&&this._progressiveEls.push(T)}},e.prototype._renderOnGeo=function(t,e,n,i){var r=n.targetVisuals.inRange,o=n.targetVisuals.outOfRange,a=e.getData(),s=this._hmLayer||this._hmLayer||new uO;s.blurSize=e.get("blurSize"),s.pointSize=e.get("pointSize"),s.minOpacity=e.get("minOpacity"),s.maxOpacity=e.get("maxOpacity");var l=t.getViewRect().clone(),u=t.getRoamTransform();l.applyTransform(u);var h=Math.max(l.x,0),c=Math.max(l.y,0),p=Math.min(l.width+l.x,i.getWidth()),d=Math.min(l.height+l.y,i.getHeight()),f=p-h,g=d-c,y=[a.mapDimension("lng"),a.mapDimension("lat"),a.mapDimension("value")],v=a.mapArray(y,(function(e,n,i){var r=t.dataToPoint([e,n]);return r[0]-=h,r[1]-=c,r.push(i),r})),m=n.getExtent(),x="visualMap.continuous"===n.type?function(t,e){var n=t[1]-t[0];return e=[(e[0]-t[0])/n,(e[1]-t[0])/n],function(t){return t>=e[0]&&t<=e[1]}}(m,n.option.range):function(t,e,n){var i=t[1]-t[0],r=(e=z(e,(function(e){return{interval:[(e.interval[0]-t[0])/i,(e.interval[1]-t[0])/i]}}))).length,o=0;return function(t){var i;for(i=o;i=0;i--){var a;if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}}return i>=0&&i0?1:-1}(n,o,r,i,c),function(t,e,n,i,r,o,a,s,l,u){var h,c=l.valueDim,p=l.categoryDim,d=Math.abs(n[p.wh]),f=t.getItemVisual(e,"symbolSize");h=Y(f)?f.slice():null==f?["100%","100%"]:[f,f];h[p.index]=$r(h[p.index],d),h[c.index]=$r(h[c.index],i?d:Math.abs(o)),u.symbolSize=h;var g=u.symbolScale=[h[0]/s,h[1]/s];g[c.index]*=(l.isHorizontal?-1:1)*a}(t,e,r,o,0,c.boundingLength,c.pxSign,u,i,c),function(t,e,n,i,r){var o=t.get(dO)||0;o&&(gO.attr({scaleX:e[0],scaleY:e[1],rotation:n}),gO.updateTransform(),o/=gO.getLineScale(),o*=e[i.valueDim.index]);r.valueLineWidth=o||0}(n,c.symbolScale,l,i,c);var p=c.symbolSize,d=Zy(n.get("symbolOffset"),p);return function(t,e,n,i,r,o,a,s,l,u,h,c){var p=h.categoryDim,d=h.valueDim,f=c.pxSign,g=Math.max(e[d.index]+s,0),y=g;if(i){var v=Math.abs(l),m=it(t.get("symbolMargin"),"15%")+"",x=!1;m.lastIndexOf("!")===m.length-1&&(x=!0,m=m.slice(0,m.length-1));var _=$r(m,e[d.index]),b=Math.max(g+2*_,0),w=x?0:2*_,S=vo(i),M=S?i:OO((v+w)/b);b=g+2*(_=(v-M*g)/2/(x?M:Math.max(M-1,1))),w=x?0:2*_,S||"fixed"===i||(M=u?OO((Math.abs(u)+w)/b):0),y=M*b-w,c.repeatTimes=M,c.symbolMargin=_}var I=f*(y/2),T=c.pathPosition=[];T[p.index]=n[p.wh]/2,T[d.index]="start"===a?I:"end"===a?l-I:l/2,o&&(T[0]+=o[0],T[1]+=o[1]);var C=c.bundlePosition=[];C[p.index]=n[p.xy],C[d.index]=n[d.xy];var D=c.barRectShape=A({},n);D[d.wh]=f*Math.max(Math.abs(n[d.wh]),Math.abs(T[d.index]+I)),D[p.wh]=n[p.wh];var k=c.clipShape={};k[p.xy]=-n[p.xy],k[p.wh]=h.ecSize[p.wh],k[d.xy]=0,k[d.wh]=n[d.wh]}(n,p,r,o,0,d,s,c.valueLineWidth,c.boundingLength,c.repeatCutLength,i,c),c}function mO(t,e){return t.toGlobalCoord(t.dataToCoord(t.scale.parse(e)))}function xO(t){var e=t.symbolPatternSize,n=Xy(t.symbolType,-e/2,-e/2,e,e);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function _O(t,e,n,i){var r=t.__pictorialBundle,o=n.symbolSize,a=n.valueLineWidth,s=n.pathPosition,l=e.valueDim,u=n.repeatTimes||0,h=0,c=o[e.valueDim.index]+a+2*n.symbolMargin;for(kO(t,(function(t){t.__pictorialAnimationIndex=h,t.__pictorialRepeatTimes=u,h0:i<0)&&(r=u-1-t),e[l.index]=c*(r-u/2+.5)+s[l.index],{x:e[0],y:e[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function bO(t,e,n,i){var r=t.__pictorialBundle,o=t.__pictorialMainPath;o?LO(o,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(o=t.__pictorialMainPath=xO(n),r.add(o),LO(o,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function wO(t,e,n){var i=A({},e.barRectShape),r=t.__pictorialBarRect;r?LO(r,null,{shape:i},e,n):((r=t.__pictorialBarRect=new Ws({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,t.add(r))}function SO(t,e,n,i){if(n.symbolClip){var r=t.__pictorialClipPath,o=A({},n.clipShape),a=e.valueDim,s=n.animationModel,l=n.dataIndex;if(r)vh(r,{shape:o},s,l);else{o[a.wh]=0,r=new Ws({shape:o}),t.__pictorialBundle.setClipPath(r),t.__pictorialClipPath=r;var u={};u[a.wh]=n.clipShape[a.wh],Qh[i?"updateProps":"initProps"](r,{shape:u},s,l)}}}function MO(t,e){var n=t.getItemModel(e);return n.getAnimationDelayParams=IO,n.isAnimationEnabled=TO,n}function IO(t){return{index:t.__pictorialAnimationIndex,count:t.__pictorialRepeatTimes}}function TO(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function CO(t,e,n,i){var r=new Br,o=new Br;return r.add(o),r.__pictorialBundle=o,o.x=n.bundlePosition[0],o.y=n.bundlePosition[1],n.symbolRepeat?_O(r,e,n):bO(r,0,n),wO(r,n,i),SO(r,e,n,i),r.__pictorialShapeStr=AO(t,n),r.__pictorialSymbolMeta=n,r}function DO(t,e,n,i){var r=i.__pictorialBarRect;r&&r.removeTextContent();var o=[];kO(i,(function(t){o.push(t)})),i.__pictorialMainPath&&o.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),E(o,(function(t){_h(t,{scaleX:0,scaleY:0},n,e,(function(){i.parent&&i.parent.remove(i)}))})),t.setItemGraphicEl(e,null)}function AO(t,e){return[t.getItemVisual(e.dataIndex,"symbol")||"none",!!e.symbolRepeat,!!e.symbolClip].join(":")}function kO(t,e,n){E(t.__pictorialBundle.children(),(function(i){i!==t.__pictorialBarRect&&e.call(n,i)}))}function LO(t,e,n,i,r,o){e&&t.attr(e),i.symbolClip&&!r?n&&t.attr(n):n&&Qh[r?"updateProps":"initProps"](t,n,i.animationModel,i.dataIndex,o)}function PO(t,e,n){var i=n.dataIndex,r=n.itemModel,o=r.getModel("emphasis"),a=o.getModel("itemStyle").getItemStyle(),s=r.getModel(["blur","itemStyle"]).getItemStyle(),l=r.getModel(["select","itemStyle"]).getItemStyle(),u=r.getShallow("cursor"),h=o.get("focus"),c=o.get("blurScope"),p=o.get("scale");kO(t,(function(t){if(t instanceof Ns){var e=t.style;t.useStyle(A({image:e.image,x:e.x,y:e.y,width:e.width,height:e.height},n.style))}else t.useStyle(n.style);var i=t.ensureState("emphasis");i.style=a,p&&(i.scaleX=1.1*t.scaleX,i.scaleY=1.1*t.scaleY),t.ensureState("blur").style=s,t.ensureState("select").style=l,u&&(t.cursor=u),t.z2=n.z2}));var d=e.valueDim.posDesc[+(n.boundingLength>0)],f=t.__pictorialBarRect;f.ignoreClip=!0,ic(f,rc(r),{labelFetcher:e.seriesModel,labelDataIndex:i,defaultText:pS(e.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:d}),Zl(t,h,c,o.get("disabled"))}function OO(t){var e=Math.round(t);return Math.abs(t-e)<1e-4?e:Math.ceil(t)}var RO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}return n(e,t),e.prototype.getInitialData=function(e){return e.stack=null,t.prototype.getInitialData.apply(this,arguments)},e.type="series.pictorialBar",e.dependencies=["grid"],e.defaultOption=kc(qS.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",clip:!1,progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}}),e}(qS);var NO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._layers=[],n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this,o=this.group,a=t.getLayerSeries(),s=i.getLayout("layoutInfo"),l=s.rect,u=s.boundaryGap;function h(t){return t.name}o.x=0,o.y=l.y+u[0];var c=new Gm(this._layersSeries||[],a,h,h),p=[];function d(e,n,s){var l=r._layers;if("remove"!==e){for(var u,h,c=[],d=[],f=a[n].indices,g=0;go&&(o=s),i.push(s)}for(var u=0;uo&&(o=c)}return{y0:r,max:o}}(l),h=u.y0,c=n/u.max,p=o.length,d=o[0].indices.length,f=0;fI&&!so(C-I)&&C0?(r.virtualPiece?r.virtualPiece.updateData(!1,i,t,e,n):(r.virtualPiece=new BO(i,t,e,n),l.add(r.virtualPiece)),o.piece.off("click"),r.virtualPiece.on("click",(function(t){r._rootToNode(o.parentNode)}))):r.virtualPiece&&(l.remove(r.virtualPiece),r.virtualPiece=null)}(a,s),this._initEvents(),this._oldChildren=h},e.prototype._initEvents=function(){var t=this;this.group.off("click"),this.group.on("click",(function(e){var n=!1;t.seriesModel.getViewRoot().eachNode((function(i){if(!n&&i.piece&&i.piece===e.target){var r=i.getModel().get("nodeClick");if("rootToNode"===r)t._rootToNode(i);else if("link"===r){var o=i.getModel(),a=o.get("link");if(a)Mp(a,o.get("target",!0)||"_blank")}n=!0}}))}))},e.prototype._rootToNode=function(t){t!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:FO,from:this.uid,seriesId:this.seriesModel.id,targetNode:t})},e.prototype.containPoint=function(t,e){var n=e.getData().getItemLayout(0);if(n){var i=t[0]-n.cx,r=t[1]-n.cy,o=Math.sqrt(i*i+r*r);return o<=n.r&&o>=n.r0}},e.type="sunburst",e}(Og),HO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreStyleOnData=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};YO(n);var i=this._levelModels=z(t.levels||[],(function(t){return new Cc(t,this,e)}),this),r=nD.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e),o=i[n.depth];return o&&(t.parentModel=o),t}))}));return r.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treePathInfo=aD(i,this),n},e.prototype.getLevelModel=function(t){return this._levelModels&&this._levelModels[t.depth]},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){pD(this)},e.type="series.sunburst",e.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"},e}(bg);function YO(t){var e=0;E(t.children,(function(t){YO(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var XO=Math.PI/180;function UO(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.get("center"),i=t.get("radius");Y(i)||(i=[0,i]),Y(e)||(e=[e,e]);var r=n.getWidth(),o=n.getHeight(),a=Math.min(r,o),s=$r(e[0],r),l=$r(e[1],o),u=$r(i[0],a/2),h=$r(i[1],a/2),c=-t.get("startAngle")*XO,p=t.get("minAngle")*XO,d=t.getData().tree.root,f=t.getViewRoot(),g=f.depth,y=t.get("sort");null!=y&&ZO(f,y);var v=0;E(f.children,(function(t){!isNaN(t.getValue())&&v++}));var m=f.getValue(),x=Math.PI/(m||v)*2,_=f.depth>0,b=f.height-(_?-1:1),w=(h-u)/(b||1),S=t.get("clockwise"),M=t.get("stillShowZeroSum"),I=S?1:-1,T=function(e,n){if(e){var i=n;if(e!==d){var r=e.getValue(),o=0===m&&M?x:r*x;o1;)r=r.parentNode;var o=n.getColorFromPalette(r.name||r.dataIndex+"",e);return t.depth>1&&U(o)&&(o=$n(o,(t.depth-1)/(i-1)*.5)),o}(r,t,i.root.height)),A(n.ensureUniqueItemVisual(r.dataIndex,"style"),o)}))}))}var qO={color:"fill",borderColor:"stroke"},KO={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},$O=Vo(),JO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},e.prototype.getInitialData=function(t,e){return _x(null,this)},e.prototype.getDataParams=function(e,n,i){var r=t.prototype.getDataParams.call(this,e,n);return i&&(r.info=$O(i).info),r},e.type="series.custom",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},e}(bg);function QO(t,e){return e=e||[0,0],z(["x","y"],(function(n,i){var r=this.getAxis(n),o=e[i],a=t[i]/2;return"category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a))}),this)}function tR(t,e){return e=e||[0,0],z([0,1],(function(n){var i=e[n],r=t[n]/2,o=[],a=[];return o[n]=i-r,a[n]=i+r,o[1-n]=a[1-n]=e[1-n],Math.abs(this.dataToPoint(o)[n]-this.dataToPoint(a)[n])}),this)}function eR(t,e){var n=this.getAxis(),i=e instanceof Array?e[0]:e,r=(t instanceof Array?t[0]:t)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-r)-n.dataToCoord(i+r))}function nR(t,e){return e=e||[0,0],z(["Radius","Angle"],(function(n,i){var r=this["get"+n+"Axis"](),o=e[i],a=t[i]/2,s="category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function iR(t,e,n,i){return t&&(t.legacy||!1!==t.legacy&&!n&&!i&&"tspan"!==e&&("text"===e||_t(t,"text")))}function rR(t,e,n){var i,r,o,a=t;if("text"===e)o=a;else{o={},_t(a,"text")&&(o.text=a.text),_t(a,"rich")&&(o.rich=a.rich),_t(a,"textFill")&&(o.fill=a.textFill),_t(a,"textStroke")&&(o.stroke=a.textStroke),_t(a,"fontFamily")&&(o.fontFamily=a.fontFamily),_t(a,"fontSize")&&(o.fontSize=a.fontSize),_t(a,"fontStyle")&&(o.fontStyle=a.fontStyle),_t(a,"fontWeight")&&(o.fontWeight=a.fontWeight),r={type:"text",style:o,silent:!0},i={};var s=_t(a,"textPosition");n?i.position=s?a.textPosition:"inside":s&&(i.position=a.textPosition),_t(a,"textPosition")&&(i.position=a.textPosition),_t(a,"textOffset")&&(i.offset=a.textOffset),_t(a,"textRotation")&&(i.rotation=a.textRotation),_t(a,"textDistance")&&(i.distance=a.textDistance)}return oR(o,t),E(o.rich,(function(t){oR(t,t)})),{textConfig:i,textContent:r}}function oR(t,e){e&&(e.font=e.textFont||e.font,_t(e,"textStrokeWidth")&&(t.lineWidth=e.textStrokeWidth),_t(e,"textAlign")&&(t.align=e.textAlign),_t(e,"textVerticalAlign")&&(t.verticalAlign=e.textVerticalAlign),_t(e,"textLineHeight")&&(t.lineHeight=e.textLineHeight),_t(e,"textWidth")&&(t.width=e.textWidth),_t(e,"textHeight")&&(t.height=e.textHeight),_t(e,"textBackgroundColor")&&(t.backgroundColor=e.textBackgroundColor),_t(e,"textPadding")&&(t.padding=e.textPadding),_t(e,"textBorderColor")&&(t.borderColor=e.textBorderColor),_t(e,"textBorderWidth")&&(t.borderWidth=e.textBorderWidth),_t(e,"textBorderRadius")&&(t.borderRadius=e.textBorderRadius),_t(e,"textBoxShadowColor")&&(t.shadowColor=e.textBoxShadowColor),_t(e,"textBoxShadowBlur")&&(t.shadowBlur=e.textBoxShadowBlur),_t(e,"textBoxShadowOffsetX")&&(t.shadowOffsetX=e.textBoxShadowOffsetX),_t(e,"textBoxShadowOffsetY")&&(t.shadowOffsetY=e.textBoxShadowOffsetY))}function aR(t,e,n){var i=t;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var r=i.textPosition.indexOf("inside")>=0,o=t.fill||"#000";sR(i,e);var a=null==i.textFill;return r?a&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=o),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(a&&(i.textFill=t.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=e.text,i.rich=e.rich,E(e.rich,(function(t){sR(t,t)})),i}function sR(t,e){e&&(_t(e,"fill")&&(t.textFill=e.fill),_t(e,"stroke")&&(t.textStroke=e.fill),_t(e,"lineWidth")&&(t.textStrokeWidth=e.lineWidth),_t(e,"font")&&(t.font=e.font),_t(e,"fontStyle")&&(t.fontStyle=e.fontStyle),_t(e,"fontWeight")&&(t.fontWeight=e.fontWeight),_t(e,"fontSize")&&(t.fontSize=e.fontSize),_t(e,"fontFamily")&&(t.fontFamily=e.fontFamily),_t(e,"align")&&(t.textAlign=e.align),_t(e,"verticalAlign")&&(t.textVerticalAlign=e.verticalAlign),_t(e,"lineHeight")&&(t.textLineHeight=e.lineHeight),_t(e,"width")&&(t.textWidth=e.width),_t(e,"height")&&(t.textHeight=e.height),_t(e,"backgroundColor")&&(t.textBackgroundColor=e.backgroundColor),_t(e,"padding")&&(t.textPadding=e.padding),_t(e,"borderColor")&&(t.textBorderColor=e.borderColor),_t(e,"borderWidth")&&(t.textBorderWidth=e.borderWidth),_t(e,"borderRadius")&&(t.textBorderRadius=e.borderRadius),_t(e,"shadowColor")&&(t.textBoxShadowColor=e.shadowColor),_t(e,"shadowBlur")&&(t.textBoxShadowBlur=e.shadowBlur),_t(e,"shadowOffsetX")&&(t.textBoxShadowOffsetX=e.shadowOffsetX),_t(e,"shadowOffsetY")&&(t.textBoxShadowOffsetY=e.shadowOffsetY),_t(e,"textShadowColor")&&(t.textShadowColor=e.textShadowColor),_t(e,"textShadowBlur")&&(t.textShadowBlur=e.textShadowBlur),_t(e,"textShadowOffsetX")&&(t.textShadowOffsetX=e.textShadowOffsetX),_t(e,"textShadowOffsetY")&&(t.textShadowOffsetY=e.textShadowOffsetY))}var lR={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},uR=G(lR),hR=(V(mr,(function(t,e){return t[e]=1,t}),{}),mr.join(", "),["","style","shape","extra"]),cR=Vo();function pR(t,e,n,i,r){var o=t+"Animation",a=gh(t,i,r)||{},s=cR(e).userDuring;return a.duration>0&&(a.during=s?W(xR,{el:e,userDuring:s}):null,a.setToFinal=!0,a.scope=t),A(a,n[o]),a}function dR(t,e,n,i){var r=(i=i||{}).dataIndex,o=i.isInit,a=i.clearStyle,s=n.isAnimationEnabled(),l=cR(t),u=e.style;l.userDuring=e.during;var h={},c={};if(function(t,e,n){for(var i=0;i=0)){var c=t.getAnimationStyleProps(),p=c?c.style:null;if(p){!r&&(r=i.style={});var d=G(n);for(u=0;u0&&t.animateFrom(p,d)}else!function(t,e,n,i,r){if(r){var o=pR("update",t,e,i,n);o.duration>0&&t.animateFrom(r,o)}}(t,e,r||0,n,h);fR(t,e),u?t.dirty():t.markRedraw()}function fR(t,e){for(var n=cR(t).leaveToProps,i=0;i=0){!o&&(o=i[t]={});var p=G(a);for(h=0;hi[1]&&i.reverse(),{coordSys:{type:"polar",cx:t.cx,cy:t.cy,r:i[1],r0:i[0]},api:{coord:function(i){var r=e.dataToRadius(i[0]),o=n.dataToAngle(i[1]),a=t.coordToPoint([r,o]);return a.push(r,o*Math.PI/180),a},size:W(nR,t)}}},calendar:function(t){var e=t.getRect(),n=t.getRangeInfo();return{coordSys:{type:"calendar",x:e.x,y:e.y,width:e.width,height:e.height,cellWidth:t.getCellWidth(),cellHeight:t.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(e,n){return t.dataToPoint(e,n)}}}}};function zR(t){return t instanceof ks}function VR(t){return t instanceof Da}var BR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){this._progressiveEls=null;var r=this._data,o=t.getData(),a=this.group,s=YR(t,o,e,n);r||a.removeAll(),o.diff(r).add((function(e){UR(n,null,e,s(e,i),t,a,o)})).remove((function(e){var n=r.getItemGraphicEl(e);n&&gR(n,$O(n).option,t)})).update((function(e,l){var u=r.getItemGraphicEl(l);UR(n,u,e,s(e,i),t,a,o)})).execute();var l=t.get("clip",!0)?PS(t.coordinateSystem,!1,t):null;l?a.setClipPath(l):a.removeClipPath(),this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll(),this._data=null},e.prototype.incrementalRender=function(t,e,n,i,r){var o=e.getData(),a=YR(e,o,n,i),s=this._progressiveEls=[];function l(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}for(var u=t.start;u=0?e.getStore().get(r,n):void 0}var o=e.get(i.name,n),a=i&&i.ordinalMeta;return a?a.categories[o]:o},styleEmphasis:function(n,i){0;null==i&&(i=s);var r=m(i,CR).getItemStyle(),o=x(i,CR),a=oc(o,null,null,!0,!0);a.text=o.getShallow("show")?ot(t.getFormattedLabel(i,CR),t.getFormattedLabel(i,DR),pS(e,i)):null;var l=ac(o,null,!0);return b(n,r),r=aR(r,a,l),n&&_(r,n),r.legacy=!0,r},visual:function(t,n){if(null==n&&(n=s),_t(qO,t)){var i=e.getItemVisual(n,"style");return i?i[qO[t]]:null}if(_t(KO,t))return e.getItemVisual(n,t)},barLayout:function(t){if("cartesian2d"===o.type){return function(t){var e=[],n=t.axis,i="axis0";if("category"===n.type){for(var r=n.getBandWidth(),o=0;o=c;f--){var g=e.childAt(f);JR(e,g,r)}}(t,c,n,i,r),a>=0?o.replaceAt(c,a):o.add(c),c}function jR(t,e,n){var i,r=$O(t),o=e.type,a=e.shape,s=e.style;return n.isUniversalTransitionEnabled()||null!=o&&o!==r.customGraphicType||"path"===o&&((i=a)&&(_t(i,"pathData")||_t(i,"d")))&&nN(a)!==r.customPathData||"image"===o&&_t(s,"image")&&s.image!==r.customImagePath}function qR(t,e,n){var i=e?KR(t,e):t,r=e?$R(t,i,CR):t.style,o=t.type,a=i?i.textConfig:null,s=t.textContent,l=s?e?KR(s,e):s:null;if(r&&(n.isLegacy||iR(r,o,!!a,!!l))){n.isLegacy=!0;var u=rR(r,o,!e);!a&&u.textConfig&&(a=u.textConfig),!l&&u.textContent&&(l=u.textContent)}if(!e&&l){var h=l;!h.type&&(h.type="text")}var c=e?n[e]:n.normal;c.cfg=a,c.conOpt=l}function KR(t,e){return e?t?t[e]:null:t}function $R(t,e,n){var i=e&&e.style;return null==i&&n===CR&&t&&(i=t.styleEmphasis),i}function JR(t,e,n){e&&gR(e,$O(t).option,n)}function QR(t,e){var n=t&&t.name;return null!=n?n:"e\0\0"+e}function tN(t,e){var n=this.context,i=null!=t?n.newChildren[t]:null,r=null!=e?n.oldChildren[e]:null;ZR(n.api,r,n.dataIndex,i,n.seriesModel,n.group)}function eN(t){var e=this.context,n=e.oldChildren[t];n&&gR(n,$O(n).option,e.seriesModel)}function nN(t){return t&&(t.pathData||t.d)}var iN=Vo(),rN=T,oN=W,aN=function(){function t(){this._dragging=!1,this.animationThreshold=15}return t.prototype.render=function(t,e,n,i){var r=e.get("value"),o=e.get("status");if(this._axisModel=t,this._axisPointerModel=e,this._api=n,i||this._lastValue!==r||this._lastStatus!==o){this._lastValue=r,this._lastStatus=o;var a=this._group,s=this._handle;if(!o||"hide"===o)return a&&a.hide(),void(s&&s.hide());a&&a.show(),s&&s.show();var l={};this.makeElOption(l,r,t,e,n);var u=l.graphicKey;u!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=u;var h=this._moveAnimation=this.determineAnimation(t,e);if(a){var c=H(sN,e,h);this.updatePointerEl(a,l,c),this.updateLabelEl(a,l,c,e)}else a=this._group=new Br,this.createPointerEl(a,l,t,e),this.createLabelEl(a,l,t,e),n.getZr().add(a);cN(a,e,!0),this._renderHandle(r)}},t.prototype.remove=function(t){this.clear(t)},t.prototype.dispose=function(t){this.clear(t)},t.prototype.determineAnimation=function(t,e){var n=e.get("animation"),i=t.axis,r="category"===i.type,o=e.get("snap");if(!o&&!r)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(r&&i.getBandWidth()>a)return!0;if(o){var s=wI(t).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>a}return!1}return!0===n},t.prototype.makeElOption=function(t,e,n,i,r){},t.prototype.createPointerEl=function(t,e,n,i){var r=e.pointer;if(r){var o=iN(t).pointerEl=new Qh[r.type](rN(e.pointer));t.add(o)}},t.prototype.createLabelEl=function(t,e,n,i){if(e.label){var r=iN(t).labelEl=new Xs(rN(e.label));t.add(r),uN(r,i)}},t.prototype.updatePointerEl=function(t,e,n){var i=iN(t).pointerEl;i&&e.pointer&&(i.setStyle(e.pointer.style),n(i,{shape:e.pointer.shape}))},t.prototype.updateLabelEl=function(t,e,n,i){var r=iN(t).labelEl;r&&(r.setStyle(e.label.style),n(r,{x:e.label.x,y:e.label.y}),uN(r,i))},t.prototype._renderHandle=function(t){if(!this._dragging&&this.updateHandleTransform){var e,n=this._axisPointerModel,i=this._api.getZr(),r=this._handle,o=n.getModel("handle"),a=n.get("status");if(!o.get("show")||!a||"hide"===a)return r&&i.remove(r),void(this._handle=null);this._handle||(e=!0,r=this._handle=Uh(o.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(t){de(t.event)},onmousedown:oN(this._onHandleDragMove,this,0,0),drift:oN(this._onHandleDragMove,this),ondragend:oN(this._onHandleDragEnd,this)}),i.add(r)),cN(r,n,!1),r.setStyle(o.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=o.get("size");Y(s)||(s=[s,s]),r.scaleX=s[0]/2,r.scaleY=s[1]/2,Hg(this,"_doDispatchAxisPointer",o.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,e)}},t.prototype._moveHandleToValue=function(t,e){sN(this._axisPointerModel,!e&&this._moveAnimation,this._handle,hN(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},t.prototype._onHandleDragMove=function(t,e){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(hN(n),[t,e],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(hN(i)),iN(n).lastProp=null,this._doDispatchAxisPointer()}},t.prototype._doDispatchAxisPointer=function(){if(this._handle){var t=this._payloadInfo,e=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:e.axis.dim,axisIndex:e.componentIndex}]})}},t.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},t.prototype.clear=function(t){this._lastValue=null,this._lastStatus=null;var e=t.getZr(),n=this._group,i=this._handle;e&&n&&(this._lastGraphicKey=null,n&&e.remove(n),i&&e.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),Yg(this,"_doDispatchAxisPointer")},t.prototype.doClear=function(){},t.prototype.buildLabel=function(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}},t}();function sN(t,e,n,i){lN(iN(n).lastProp,i)||(iN(n).lastProp=i,e?vh(n,i,t):(n.stopAnimation(),n.attr(i)))}function lN(t,e){if(q(t)&&q(e)){var n=!0;return E(e,(function(e,i){n=n&&lN(t[i],e)})),!!n}return t===e}function uN(t,e){t[e.get(["label","show"])?"show":"hide"]()}function hN(t){return{x:t.x||0,y:t.y||0,rotation:t.rotation||0}}function cN(t,e,n){var i=e.get("z"),r=e.get("zlevel");t&&t.traverse((function(t){"group"!==t.type&&(null!=i&&(t.z=i),null!=r&&(t.zlevel=r),t.silent=n)}))}function pN(t){var e,n=t.get("type"),i=t.getModel(n+"Style");return"line"===n?(e=i.getLineStyle()).fill=null:"shadow"===n&&((e=i.getAreaStyle()).stroke=null),e}function dN(t,e,n,i,r){var o=fN(n.get("value"),e.axis,e.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),a=n.getModel("label"),s=vp(a.get("padding")||0),l=a.getFont(),u=Sr(o,l),h=r.position,c=u.width+s[1]+s[3],p=u.height+s[0]+s[2],d=r.align;"right"===d&&(h[0]-=c),"center"===d&&(h[0]-=c/2);var f=r.verticalAlign;"bottom"===f&&(h[1]-=p),"middle"===f&&(h[1]-=p/2),function(t,e,n,i){var r=i.getWidth(),o=i.getHeight();t[0]=Math.min(t[0]+e,r)-e,t[1]=Math.min(t[1]+n,o)-n,t[0]=Math.max(t[0],0),t[1]=Math.max(t[1],0)}(h,c,p,i);var g=a.get("backgroundColor");g&&"auto"!==g||(g=e.get(["axisLine","lineStyle","color"])),t.label={x:h[0],y:h[1],style:oc(a,{text:o,font:l,fill:a.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function fN(t,e,n,i,r){t=e.scale.parse(t);var o=e.scale.getLabel({value:t},{precision:r.precision}),a=r.formatter;if(a){var s={value:S_(e,{value:t}),axisDimension:e.dim,axisIndex:e.index,seriesData:[]};E(i,(function(t){var e=n.getSeriesByIndex(t.seriesIndex),i=t.dataIndexInside,r=e&&e.getDataParams(i);r&&s.seriesData.push(r)})),U(a)?o=a.replace("{value}",o):X(a)&&(o=a(s))}return o}function gN(t,e,n){var i=[1,0,0,1,0,0];return Se(i,i,n.rotation),we(i,i,n.position),Fh([t.dataToCoord(e),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function yN(t,e,n,i,r,o){var a=dI.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get(["label","margin"]),dN(e,i,r,o,{position:gN(i.axis,t,n),align:a.textAlign,verticalAlign:a.textVerticalAlign})}function vN(t,e,n){return{x1:t[n=n||0],y1:t[1-n],x2:e[n],y2:e[1-n]}}function mN(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}}function xN(t,e,n,i,r,o){return{cx:t,cy:e,r0:n,r:i,startAngle:r,endAngle:o,clockwise:!0}}var _N=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.grid,s=i.get("type"),l=bN(a,o).getOtherAxis(o).getGlobalExtent(),u=o.toGlobalCoord(o.dataToCoord(e,!0));if(s&&"none"!==s){var h=pN(i),c=wN[s](o,u,l);c.style=h,t.graphicKey=c.type,t.pointer=c}yN(e,t,iI(a.model,n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=iI(e.axis.grid.model,e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=gN(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.grid,a=r.getGlobalExtent(!0),s=bN(o,r).getOtherAxis(r).getGlobalExtent(),l="x"===r.dim?0:1,u=[t.x,t.y];u[l]+=e[l],u[l]=Math.min(a[1],u[l]),u[l]=Math.max(a[0],u[l]);var h=(s[1]+s[0])/2,c=[h,h];c[l]=u[l];return{x:u[0],y:u[1],rotation:t.rotation,cursorPoint:c,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},e}(aN);function bN(t,e){var n={};return n[e.dim+"AxisIndex"]=e.index,t.getCartesian(n)}var wN={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:vN([e,n[0]],[e,n[1]],SN(t))}},shadow:function(t,e,n){var i=Math.max(1,t.getBandWidth()),r=n[1]-n[0];return{type:"Rect",shape:mN([e-i/2,n[0]],[i,r],SN(t))}}};function SN(t){return"x"===t.dim?0:1}var MN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="axisPointer",e.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},e}(zp),IN=Vo(),TN=E;function CN(t,e,n){if(!r.node){var i=e.getZr();IN(i).records||(IN(i).records={}),function(t,e){if(IN(t).initialized)return;function n(n,i){t.on(n,(function(n){var r=function(t){var e={showTip:[],hideTip:[]},n=function(i){var r=e[i.type];r?r.push(i):(i.dispatchAction=n,t.dispatchAction(i))};return{dispatchAction:n,pendings:e}}(e);TN(IN(t).records,(function(t){t&&i(t,n,r.dispatchAction)})),function(t,e){var n,i=t.showTip.length,r=t.hideTip.length;i?n=t.showTip[i-1]:r&&(n=t.hideTip[r-1]);n&&(n.dispatchAction=null,e.dispatchAction(n))}(r.pendings,e)}))}IN(t).initialized=!0,n("click",H(AN,"click")),n("mousemove",H(AN,"mousemove")),n("globalout",DN)}(i,e),(IN(i).records[t]||(IN(i).records[t]={})).handler=n}}function DN(t,e,n){t.handler("leave",null,n)}function AN(t,e,n,i){e.handler(t,n,i)}function kN(t,e){if(!r.node){var n=e.getZr();(IN(n).records||{})[t]&&(IN(n).records[t]=null)}}var LN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=e.getComponent("tooltip"),r=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";CN("axisPointer",n,(function(t,e,n){"none"!==r&&("leave"===t||r.indexOf(t)>=0)&&n({type:"updateAxisPointer",currTrigger:t,x:e&&e.offsetX,y:e&&e.offsetY})}))},e.prototype.remove=function(t,e){kN("axisPointer",e)},e.prototype.dispose=function(t,e){kN("axisPointer",e)},e.type="axisPointer",e}(Ag);function PN(t,e){var n,i=[],r=t.seriesIndex;if(null==r||!(n=e.getSeriesByIndex(r)))return{point:[]};var o=n.getData(),a=zo(o,t);if(null==a||a<0||Y(a))return{point:[]};var s=o.getItemGraphicEl(a),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(a)||[];else if(l&&l.dataToPoint)if(t.isStacked){var u=l.getBaseAxis(),h=l.getOtherAxis(u).dim,c=u.dim,p="x"===h||"radius"===h?1:0,d=o.mapDimension(c),f=[];f[p]=o.get(d,a),f[1-p]=o.get(o.getCalculationInfo("stackResultDimension"),a),i=l.dataToPoint(f)||[]}else i=l.dataToPoint(o.getValues(z(l.dimensions,(function(t){return o.mapDimension(t)})),a))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var ON=Vo();function RN(t,e,n){var i=t.currTrigger,r=[t.x,t.y],o=t,a=t.dispatchAction||W(n.dispatchAction,n),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){BN(r)&&(r=PN({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},e).point);var l=BN(r),u=o.axesInfo,h=s.axesInfo,c="leave"===i||BN(r),p={},d={},f={list:[],map:{}},g={showPointer:H(EN,d),showTooltip:H(zN,f)};E(s.coordSysMap,(function(t,e){var n=l||t.containPoint(r);E(s.coordSysAxesInfo[e],(function(t,e){var i=t.axis,o=function(t,e){for(var n=0;n<(t||[]).length;n++){var i=t[n];if(e.axis.dim===i.axisDim&&e.axis.model.componentIndex===i.axisIndex)return i}}(u,t);if(!c&&n&&(!u||o)){var a=o&&o.value;null!=a||l||(a=i.pointToData(r)),null!=a&&NN(t,a,g,!1,p)}}))}));var y={};return E(h,(function(t,e){var n=t.linkGroup;n&&!d[e]&&E(n.axesInfo,(function(e,i){var r=d[i];if(e!==t&&r){var o=r.value;n.mapper&&(o=t.axis.scale.parse(n.mapper(o,VN(e),VN(t)))),y[t.key]=o}}))})),E(y,(function(t,e){NN(h[e],t,g,!0,p)})),function(t,e,n){var i=n.axesInfo=[];E(e,(function(e,n){var r=e.axisPointerModel.option,o=t[n];o?(!e.useHandle&&(r.status="show"),r.value=o.value,r.seriesDataIndices=(o.payloadBatch||[]).slice()):!e.useHandle&&(r.status="hide"),"show"===r.status&&i.push({axisDim:e.axis.dim,axisIndex:e.axis.model.componentIndex,value:r.value})}))}(d,h,p),function(t,e,n,i){if(BN(e)||!t.list.length)return void i({type:"hideTip"});var r=((t.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:t.list})}(f,r,t,a),function(t,e,n){var i=n.getZr(),r="axisPointerLastHighlights",o=ON(i)[r]||{},a=ON(i)[r]={};E(t,(function(t,e){var n=t.axisPointerModel.option;"show"===n.status&&t.triggerEmphasis&&E(n.seriesDataIndices,(function(t){var e=t.seriesIndex+" | "+t.dataIndex;a[e]=t}))}));var s=[],l=[];E(o,(function(t,e){!a[e]&&l.push(t)})),E(a,(function(t,e){!o[e]&&s.push(t)})),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(h,0,n),p}}function NN(t,e,n,i,r){var o=t.axis;if(!o.scale.isBlank()&&o.containData(e))if(t.involveSeries){var a=function(t,e){var n=e.axis,i=n.dim,r=t,o=[],a=Number.MAX_VALUE,s=-1;return E(e.seriesModels,(function(e,l){var u,h,c=e.getData().mapDimensionsAll(i);if(e.getAxisTooltipData){var p=e.getAxisTooltipData(c,t,n);h=p.dataIndices,u=p.nestestValue}else{if(!(h=e.getData().indicesOfNearest(c[0],t,"category"===n.type?.5:null)).length)return;u=e.getData().get(c[0],h[0])}if(null!=u&&isFinite(u)){var d=t-u,f=Math.abs(d);f<=a&&((f=0&&s<0)&&(a=f,s=d,r=u,o.length=0),E(h,(function(t){o.push({seriesIndex:e.seriesIndex,dataIndexInside:t,dataIndex:e.getData().getRawIndex(t)})})))}})),{payloadBatch:o,snapToValue:r}}(e,t),s=a.payloadBatch,l=a.snapToValue;s[0]&&null==r.seriesIndex&&A(r,s[0]),!i&&t.snap&&o.containData(l)&&null!=l&&(e=l),n.showPointer(t,e,s),n.showTooltip(t,a,l)}else n.showPointer(t,e)}function EN(t,e,n,i){t[e.key]={value:n,payloadBatch:i}}function zN(t,e,n,i){var r=n.payloadBatch,o=e.axis,a=o.model,s=e.axisPointerModel;if(e.triggerTooltip&&r.length){var l=e.coordSys.model,u=MI(l),h=t.map[u];h||(h=t.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},t.list.push(h)),h.dataByAxis.push({axisDim:o.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:r.slice()})}}function VN(t){var e=t.axis.model,n={},i=n.axisDim=t.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=e.componentIndex,n.axisName=n[i+"AxisName"]=e.name,n.axisId=n[i+"AxisId"]=e.id,n}function BN(t){return!t||null==t[0]||isNaN(t[0])||null==t[1]||isNaN(t[1])}function FN(t){TI.registerAxisPointerClass("CartesianAxisPointer",_N),t.registerComponentModel(MN),t.registerComponentView(LN),t.registerPreprocessor((function(t){if(t){(!t.axisPointer||0===t.axisPointer.length)&&(t.axisPointer={});var e=t.axisPointer.link;e&&!Y(e)&&(t.axisPointer.link=[e])}})),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,(function(t,e){t.getComponent("axisPointer").coordSysAxesInfo=xI(t,e)})),t.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},RN)}var GN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis;"angle"===o.dim&&(this.animationThreshold=Math.PI/18);var a=o.polar,s=a.getOtherAxis(o).getExtent(),l=o.dataToCoord(e),u=i.get("type");if(u&&"none"!==u){var h=pN(i),c=WN[u](o,a,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}var p=function(t,e,n,i,r){var o=e.axis,a=o.dataToCoord(t),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,u,h,c=i.getRadiusAxis().getExtent();if("radius"===o.dim){var p=[1,0,0,1,0,0];Se(p,p,s),we(p,p,[i.cx,i.cy]),l=Fh([a,-r],p);var d=e.getModel("axisLabel").get("rotate")||0,f=dI.innerTextLayout(s,d*Math.PI/180,-1);u=f.textAlign,h=f.textVerticalAlign}else{var g=c[1];l=i.coordToPoint([g+r,a]);var y=i.cx,v=i.cy;u=Math.abs(l[0]-y)/g<.3?"center":l[0]>y?"left":"right",h=Math.abs(l[1]-v)/g<.3?"middle":l[1]>v?"top":"bottom"}return{position:l,align:u,verticalAlign:h}}(e,n,0,a,i.get(["label","margin"]));dN(t,n,i,r,p)},e}(aN);var WN={line:function(t,e,n,i){return"angle"===t.dim?{type:"Line",shape:vN(e.coordToPoint([i[0],n]),e.coordToPoint([i[1],n]))}:{type:"Circle",shape:{cx:e.cx,cy:e.cy,r:n}}},shadow:function(t,e,n,i){var r=Math.max(1,t.getBandWidth()),o=Math.PI/180;return"angle"===t.dim?{type:"Sector",shape:xN(e.cx,e.cy,i[0],i[1],(-n-r/2)*o,(r/2-n)*o)}:{type:"Sector",shape:xN(e.cx,e.cy,n-r/2,n+r/2,0,2*Math.PI)}}},HN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.findAxisModel=function(t){var e;return this.ecModel.eachComponent(t,(function(t){t.getCoordSysModel()===this&&(e=t)}),this),e},e.type="polar",e.dependencies=["radiusAxis","angleAxis"],e.defaultOption={z:0,center:["50%","50%"],radius:"80%"},e}(zp),YN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",Wo).models[0]},e.type="polarAxis",e}(zp);R(YN,D_);var XN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="angleAxis",e}(YN),UN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="radiusAxis",e}(YN),ZN=function(t){function e(e,n){return t.call(this,"radius",e,n)||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e}(ab);ZN.prototype.dataToRadius=ab.prototype.dataToCoord,ZN.prototype.radiusToData=ab.prototype.coordToData;var jN=Vo(),qN=function(t){function e(e,n){return t.call(this,"angle",e,n||[0,360])||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e.prototype.calculateCategoryInterval=function(){var t=this,e=t.getLabelModel(),n=t.scale,i=n.getExtent(),r=n.count();if(i[1]-i[0]<1)return 0;var o=i[0],a=t.dataToCoord(o+1)-t.dataToCoord(o),s=Math.abs(a),l=Sr(null==o?"":o+"",e.getFont(),"center","top"),u=Math.max(l.height,7)/s;isNaN(u)&&(u=1/0);var h=Math.max(0,Math.floor(u)),c=jN(t.model),p=c.lastAutoInterval,d=c.lastTickCount;return null!=p&&null!=d&&Math.abs(p-h)<=1&&Math.abs(d-r)<=1&&p>h?h=p:(c.lastTickCount=r,c.lastAutoInterval=h),h},e}(ab);qN.prototype.dataToAngle=ab.prototype.dataToCoord,qN.prototype.angleToData=ab.prototype.coordToData;var KN=["radius","angle"],$N=function(){function t(t){this.dimensions=KN,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new ZN,this._angleAxis=new qN,this.axisPointerEnabled=!0,this.name=t||"",this._radiusAxis.polar=this._angleAxis.polar=this}return t.prototype.containPoint=function(t){var e=this.pointToCoord(t);return this._radiusAxis.contain(e[0])&&this._angleAxis.contain(e[1])},t.prototype.containData=function(t){return this._radiusAxis.containData(t[0])&&this._angleAxis.containData(t[1])},t.prototype.getAxis=function(t){return this["_"+t+"Axis"]},t.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},t.prototype.getAxesByScale=function(t){var e=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===t&&e.push(n),i.scale.type===t&&e.push(i),e},t.prototype.getAngleAxis=function(){return this._angleAxis},t.prototype.getRadiusAxis=function(){return this._radiusAxis},t.prototype.getOtherAxis=function(t){var e=this._angleAxis;return t===e?this._radiusAxis:e},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},t.prototype.getTooltipAxes=function(t){var e=null!=t&&"auto"!==t?this.getAxis(t):this.getBaseAxis();return{baseAxes:[e],otherAxes:[this.getOtherAxis(e)]}},t.prototype.dataToPoint=function(t,e){return this.coordToPoint([this._radiusAxis.dataToRadius(t[0],e),this._angleAxis.dataToAngle(t[1],e)])},t.prototype.pointToData=function(t,e){var n=this.pointToCoord(t);return[this._radiusAxis.radiusToData(n[0],e),this._angleAxis.angleToData(n[1],e)]},t.prototype.pointToCoord=function(t){var e=t[0]-this.cx,n=t[1]-this.cy,i=this.getAngleAxis(),r=i.getExtent(),o=Math.min(r[0],r[1]),a=Math.max(r[0],r[1]);i.inverse?o=a-360:a=o+360;var s=Math.sqrt(e*e+n*n);e/=s,n/=s;for(var l=Math.atan2(-n,e)/Math.PI*180,u=la;)l+=360*u;return[s,l]},t.prototype.coordToPoint=function(t){var e=t[0],n=t[1]/180*Math.PI;return[Math.cos(n)*e+this.cx,-Math.sin(n)*e+this.cy]},t.prototype.getArea=function(){var t=this.getAngleAxis(),e=this.getRadiusAxis().getExtent().slice();e[0]>e[1]&&e.reverse();var n=t.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:e[0],r:e[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:t.inverse,contain:function(t,e){var n=t-this.cx,i=e-this.cy,r=n*n+i*i-1e-4,o=this.r,a=this.r0;return r<=o*o&&r>=a*a}}},t.prototype.convertToPixel=function(t,e,n){return JN(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return JN(e)===this?this.pointToData(n):null},t}();function JN(t){var e=t.seriesModel,n=t.polarModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}function QN(t,e){var n=this,i=n.getAngleAxis(),r=n.getRadiusAxis();if(i.scale.setExtent(1/0,-1/0),r.scale.setExtent(1/0,-1/0),t.eachSeries((function(t){if(t.coordinateSystem===n){var e=t.getData();E(C_(e,"radius"),(function(t){r.scale.unionExtentFromData(e,t)})),E(C_(e,"angle"),(function(t){i.scale.unionExtentFromData(e,t)}))}})),__(i.scale,i.model),__(r.scale,r.model),"category"===i.type&&!i.onBand){var o=i.getExtent(),a=360/i.scale.count();i.inverse?o[1]+=a:o[1]-=a,i.setExtent(o[0],o[1])}}function tE(t,e){var n;if(t.type=e.get("type"),t.scale=b_(e),t.onBand=e.get("boundaryGap")&&"category"===t.type,t.inverse=e.get("inverse"),function(t){return"angleAxis"===t.mainType}(e)){t.inverse=t.inverse!==e.get("clockwise");var i=e.get("startAngle"),r=null!==(n=e.get("endAngle"))&&void 0!==n?n:i+(t.inverse?-360:360);t.setExtent(i,r)}e.axis=t,t.model=e}var eE={dimensions:KN,create:function(t,e){var n=[];return t.eachComponent("polar",(function(t,i){var r=new $N(i+"");r.update=QN;var o=r.getRadiusAxis(),a=r.getAngleAxis(),s=t.findAxisModel("radiusAxis"),l=t.findAxisModel("angleAxis");tE(o,s),tE(a,l),function(t,e,n){var i=e.get("center"),r=n.getWidth(),o=n.getHeight();t.cx=$r(i[0],r),t.cy=$r(i[1],o);var a=t.getRadiusAxis(),s=Math.min(r,o)/2,l=e.get("radius");null==l?l=[0,"100%"]:Y(l)||(l=[0,l]);var u=[$r(l[0],s),$r(l[1],s)];a.inverse?a.setExtent(u[1],u[0]):a.setExtent(u[0],u[1])}(r,t,e),n.push(r),t.coordinateSystem=r,r.model=t})),t.eachSeries((function(t){if("polar"===t.get("coordinateSystem")){var e=t.getReferringComponents("polar",Wo).models[0];0,t.coordinateSystem=e.coordinateSystem}})),n}},nE=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function iE(t,e,n){e[1]>e[0]&&(e=e.slice().reverse());var i=t.coordToPoint([e[0],n]),r=t.coordToPoint([e[1],n]);return{x1:i[0],y1:i[1],x2:r[0],y2:r[1]}}function rE(t){return t.getRadiusAxis().inverse?0:1}function oE(t){var e=t[0],n=t[t.length-1];e&&n&&Math.abs(Math.abs(e.coord-n.coord)-360)<1e-4&&t.pop()}var aE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="PolarAxisPointer",n}return n(e,t),e.prototype.render=function(t,e){if(this.group.removeAll(),t.get("show")){var n=t.axis,i=n.polar,r=i.getRadiusAxis().getExtent(),o=n.getTicksCoords(),a=n.getMinorTicksCoords(),s=z(n.getViewLabels(),(function(t){t=T(t);var e=n.scale,i="ordinal"===e.type?e.getRawOrdinalNumber(t.tickValue):t.tickValue;return t.coord=n.dataToCoord(i),t}));oE(s),oE(o),E(nE,(function(e){!t.get([e,"show"])||n.scale.isBlank()&&"axisLine"!==e||sE[e](this.group,t,i,o,a,r,s)}),this)}},e.type="angleAxis",e}(TI),sE={axisLine:function(t,e,n,i,r,o){var a,s=e.getModel(["axisLine","lineStyle"]),l=n.getAngleAxis(),u=Math.PI/180,h=l.getExtent(),c=rE(n),p=c?0:1,d=360===Math.abs(h[1]-h[0])?"Circle":"Arc";(a=0===o[p]?new Qh[d]({shape:{cx:n.cx,cy:n.cy,r:o[c],startAngle:-h[0]*u,endAngle:-h[1]*u,clockwise:l.inverse},style:s.getLineStyle(),z2:1,silent:!0}):new Wu({shape:{cx:n.cx,cy:n.cy,r:o[c],r0:o[p]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,t.add(a)},axisTick:function(t,e,n,i,r,o){var a=e.getModel("axisTick"),s=(a.get("inside")?-1:1)*a.get("length"),l=o[rE(n)],u=z(i,(function(t){return new Ku({shape:iE(n,[l,l+s],t.coord)})}));t.add(Nh(u,{style:k(a.getModel("lineStyle").getLineStyle(),{stroke:e.get(["axisLine","lineStyle","color"])})}))},minorTick:function(t,e,n,i,r,o){if(r.length){for(var a=e.getModel("axisTick"),s=e.getModel("minorTick"),l=(a.get("inside")?-1:1)*s.get("length"),u=o[rE(n)],h=[],c=0;cf?"left":"right",v=Math.abs(d[1]-g)/p<.3?"middle":d[1]>g?"top":"bottom";if(s&&s[c]){var m=s[c];q(m)&&m.textStyle&&(a=new Cc(m.textStyle,l,l.ecModel))}var x=new Xs({silent:dI.isLabelSilent(e),style:oc(a,{x:d[0],y:d[1],fill:a.getTextColor()||e.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:y,verticalAlign:v})});if(t.add(x),h){var _=dI.makeAxisEventDataBase(e);_.targetType="axisLabel",_.value=i.rawLabel,rl(x).eventData=_}}),this)},splitLine:function(t,e,n,i,r,o){var a=e.getModel("splitLine").getModel("lineStyle"),s=a.get("color"),l=0;s=s instanceof Array?s:[s];for(var u=[],h=0;h=0?"p":"n",C=b;m&&(i[s][I]||(i[s][I]={p:b,n:b}),C=i[s][I][T]);var D=void 0,A=void 0,k=void 0,L=void 0;if("radius"===c.dim){var P=c.dataToCoord(M)-b,O=o.dataToCoord(I);Math.abs(P)=L})}}}))}var gE={startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:0}},yE={splitNumber:5},vE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="polar",e}(Ag);function mE(t,e){e=e||{};var n=t.coordinateSystem,i=t.axis,r={},o=i.position,a=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],u={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};r.position=["vertical"===a?u.vertical[o]:l[0],"horizontal"===a?u.horizontal[o]:l[3]];r.rotation=Math.PI/2*{horizontal:0,vertical:1}[a];r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,right:1,left:-1}[o],t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),it(e.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var h=e.rotate;return null==h&&(h=t.get(["axisLabel","rotate"])),r.labelRotation="top"===o?-h:h,r.z2=1,r}var xE=["axisLine","axisTickLabel","axisName"],_E=["splitArea","splitLine"],bE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="SingleAxisPointer",n}return n(e,t),e.prototype.render=function(e,n,i,r){var o=this.group;o.removeAll();var a=this._axisGroup;this._axisGroup=new Br;var s=mE(e),l=new dI(e,s);E(xE,l.add,l),o.add(this._axisGroup),o.add(l.getGroup()),E(_E,(function(t){e.get([t,"show"])&&wE[t](this,this.group,this._axisGroup,e)}),this),Hh(a,this._axisGroup,e),t.prototype.render.call(this,e,n,i,r)},e.prototype.remove=function(){AI(this)},e.type="singleAxis",e}(TI),wE={splitLine:function(t,e,n,i){var r=i.axis;if(!r.scale.isBlank()){var o=i.getModel("splitLine"),a=o.getModel("lineStyle"),s=a.get("color");s=s instanceof Array?s:[s];for(var l=a.get("width"),u=i.coordinateSystem.getRect(),h=r.isHorizontal(),c=[],p=0,d=r.getTicksCoords({tickModel:o}),f=[],g=[],y=0;y=e.y&&t[1]<=e.y+e.height:n.contain(n.toLocalCoord(t[1]))&&t[0]>=e.y&&t[0]<=e.y+e.height},t.prototype.pointToData=function(t){var e=this.getAxis();return[e.coordToData(e.toLocalCoord(t["horizontal"===e.orient?0:1]))]},t.prototype.dataToPoint=function(t){var e=this.getAxis(),n=this.getRect(),i=[],r="horizontal"===e.orient?0:1;return t instanceof Array&&(t=t[0]),i[r]=e.toGlobalCoord(e.dataToCoord(+t)),i[1-r]=0===r?n.y+n.height/2:n.x+n.width/2,i},t.prototype.convertToPixel=function(t,e,n){return CE(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return CE(e)===this?this.pointToData(n):null},t}();function CE(t){var e=t.seriesModel,n=t.singleAxisModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}var DE={create:function(t,e){var n=[];return t.eachComponent("singleAxis",(function(i,r){var o=new TE(i,t,e);o.name="single_"+r,o.resize(i,e),i.coordinateSystem=o,n.push(o)})),t.eachSeries((function(t){if("singleAxis"===t.get("coordinateSystem")){var e=t.getReferringComponents("singleAxis",Wo).models[0];t.coordinateSystem=e&&e.coordinateSystem}})),n},dimensions:IE},AE=["x","y"],kE=["width","height"],LE=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.coordinateSystem,s=RE(a,1-OE(o)),l=a.dataToPoint(e)[0],u=i.get("type");if(u&&"none"!==u){var h=pN(i),c=PE[u](o,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}yN(e,t,mE(n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=mE(e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=gN(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.coordinateSystem,a=OE(r),s=RE(o,a),l=[t.x,t.y];l[a]+=e[a],l[a]=Math.min(s[1],l[a]),l[a]=Math.max(s[0],l[a]);var u=RE(o,1-a),h=(u[1]+u[0])/2,c=[h,h];return c[a]=l[a],{x:l[0],y:l[1],rotation:t.rotation,cursorPoint:c,tooltipOption:{verticalAlign:"middle"}}},e}(aN),PE={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:vN([e,n[0]],[e,n[1]],OE(t))}},shadow:function(t,e,n){var i=t.getBandWidth(),r=n[1]-n[0];return{type:"Rect",shape:mN([e-i/2,n[0]],[i,r],OE(t))}}};function OE(t){return t.isHorizontal()?0:1}function RE(t,e){var n=t.getRect();return[n[AE[e]],n[AE[e]]+n[kE[e]]]}var NE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="single",e}(Ag);var EE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e,n,i){var r=Rp(e);t.prototype.init.apply(this,arguments),zE(e,r)},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),zE(this.option,e)},e.prototype.getCellSize=function(){return this.option.cellSize},e.type="calendar",e.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},e}(zp);function zE(t,e){var n,i=t.cellSize;1===(n=Y(i)?i:t.cellSize=[i,i]).length&&(n[1]=n[0]);var r=z([0,1],(function(t){return function(t,e){return null!=t[Cp[e][0]]||null!=t[Cp[e][1]]&&null!=t[Cp[e][2]]}(e,t)&&(n[t]="auto"),null!=n[t]&&"auto"!==n[t]}));Op(t,e,{type:"box",ignoreSize:r})}var VE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=this.group;i.removeAll();var r=t.coordinateSystem,o=r.getRangeInfo(),a=r.getOrient(),s=e.getLocaleModel();this._renderDayRect(t,o,i),this._renderLines(t,o,a,i),this._renderYearText(t,o,a,i),this._renderMonthText(t,s,a,i),this._renderWeekText(t,s,o,a,i)},e.prototype._renderDayRect=function(t,e,n){for(var i=t.coordinateSystem,r=t.getModel("itemStyle").getItemStyle(),o=i.getCellWidth(),a=i.getCellHeight(),s=e.start.time;s<=e.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToRect([s],!1).tl,u=new Ws({shape:{x:l[0],y:l[1],width:o,height:a},cursor:"default",style:r});n.add(u)}},e.prototype._renderLines=function(t,e,n,i){var r=this,o=t.coordinateSystem,a=t.getModel(["splitLine","lineStyle"]).getLineStyle(),s=t.get(["splitLine","show"]),l=a.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var u=e.start,h=0;u.time<=e.end.time;h++){p(u.formatedDate),0===h&&(u=o.getDateInfo(e.start.y+"-"+e.start.m));var c=u.date;c.setMonth(c.getMonth()+1),u=o.getDateInfo(c)}function p(e){r._firstDayOfMonth.push(o.getDateInfo(e)),r._firstDayPoints.push(o.dataToRect([e],!1).tl);var l=r._getLinePointsOfOneWeek(t,e,n);r._tlpoints.push(l[0]),r._blpoints.push(l[l.length-1]),s&&r._drawSplitline(l,a,i)}p(o.getNextNDay(e.end.time,1).formatedDate),s&&this._drawSplitline(r._getEdgesPoints(r._tlpoints,l,n),a,i),s&&this._drawSplitline(r._getEdgesPoints(r._blpoints,l,n),a,i)},e.prototype._getEdgesPoints=function(t,e,n){var i=[t[0].slice(),t[t.length-1].slice()],r="horizontal"===n?0:1;return i[0][r]=i[0][r]-e/2,i[1][r]=i[1][r]+e/2,i},e.prototype._drawSplitline=function(t,e,n){var i=new Zu({z2:20,shape:{points:t},style:e});n.add(i)},e.prototype._getLinePointsOfOneWeek=function(t,e,n){for(var i=t.coordinateSystem,r=i.getDateInfo(e),o=[],a=0;a<7;a++){var s=i.getNextNDay(r.time,a),l=i.dataToRect([s.time],!1);o[2*s.day]=l.tl,o[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return o},e.prototype._formatterLabel=function(t,e){return U(t)&&t?(n=t,E(e,(function(t,e){n=n.replace("{"+e+"}",i?re(t):t)})),n):X(t)?t(e):e.nameMap;var n,i},e.prototype._yearTextPositionControl=function(t,e,n,i,r){var o=e[0],a=e[1],s=["center","bottom"];"bottom"===i?(a+=r,s=["center","top"]):"left"===i?o-=r:"right"===i?(o+=r,s=["center","top"]):a-=r;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:o,y:a,style:{align:s[0],verticalAlign:s[1]}}},e.prototype._renderYearText=function(t,e,n,i){var r=t.getModel("yearLabel");if(r.get("show")){var o=r.get("margin"),a=r.get("position");a||(a="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,u=(s[0][1]+s[1][1])/2,h="horizontal"===n?0:1,c={top:[l,s[h][1]],bottom:[l,s[1-h][1]],left:[s[1-h][0],u],right:[s[h][0],u]},p=e.start.y;+e.end.y>+e.start.y&&(p=p+"-"+e.end.y);var d=r.get("formatter"),f={start:e.start.y,end:e.end.y,nameMap:p},g=this._formatterLabel(d,f),y=new Xs({z2:30,style:oc(r,{text:g})});y.attr(this._yearTextPositionControl(y,c[a],n,a,o)),i.add(y)}},e.prototype._monthTextPositionControl=function(t,e,n,i,r){var o="left",a="top",s=t[0],l=t[1];return"horizontal"===n?(l+=r,e&&(o="center"),"start"===i&&(a="bottom")):(s+=r,e&&(a="middle"),"start"===i&&(o="right")),{x:s,y:l,align:o,verticalAlign:a}},e.prototype._renderMonthText=function(t,e,n,i){var r=t.getModel("monthLabel");if(r.get("show")){var o=r.get("nameMap"),a=r.get("margin"),s=r.get("position"),l=r.get("align"),u=[this._tlpoints,this._blpoints];o&&!U(o)||(o&&(e=Vc(o)||e),o=e.get(["time","monthAbbr"])||[]);var h="start"===s?0:1,c="horizontal"===n?0:1;a="start"===s?-a:a;for(var p="center"===l,d=0;d=i.start.time&&n.timea.end.time&&t.reverse(),t},t.prototype._getRangeInfo=function(t){var e,n=[this.getDateInfo(t[0]),this.getDateInfo(t[1])];n[0].time>n[1].time&&(e=!0,n.reverse());var i=Math.floor(n[1].time/BE)-Math.floor(n[0].time/BE)+1,r=new Date(n[0].time),o=r.getDate(),a=n[1].date.getDate();r.setDate(o+i-1);var s=r.getDate();if(s!==a)for(var l=r.getTime()-n[1].time>0?1:-1;(s=r.getDate())!==a&&(r.getTime()-n[1].time)*l>0;)i-=l,r.setDate(s-l);var u=Math.floor((i+n[0].day+6)/7),h=e?1-u:u-1;return e&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:u,nthWeek:h,fweek:n[0].day,lweek:n[1].day}},t.prototype._getDateByWeeksAndDay=function(t,e,n){var i=this._getRangeInfo(n);if(t>i.weeks||0===t&&ei.lweek)return null;var r=7*(t-1)-i.fweek+e,o=new Date(i.start.time);return o.setDate(+i.start.d+r),this.getDateInfo(o)},t.create=function(e,n){var i=[];return e.eachComponent("calendar",(function(r){var o=new t(r,e,n);i.push(o),r.coordinateSystem=o})),e.eachSeries((function(t){"calendar"===t.get("coordinateSystem")&&(t.coordinateSystem=i[t.get("calendarIndex")||0])})),i},t.dimensions=["time","value"],t}();function GE(t){var e=t.calendarModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem:null}function WE(t,e){var n;return E(e,(function(e){null!=t[e]&&"auto"!==t[e]&&(n=!0)})),n}var HE=["transition","enterFrom","leaveTo"],YE=HE.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function XE(t,e,n){if(n&&(!t[n]&&e[n]&&(t[n]={}),t=t[n],e=e[n]),t&&e)for(var i=n?HE:YE,r=0;r=0;l--){var p,d,f;if(f=null!=(d=Ro((p=n[l]).id,null))?r.get(d):null){var g=f.parent,y=(c=jE(g),{}),v=Lp(f,p,g===i?{width:o,height:a}:{width:c.width,height:c.height},null,{hv:p.hv,boundingMode:p.bounding},y);if(!jE(f).isNew&&v){for(var m=p.transition,x={},_=0;_=0)?x[b]=w:f[b]=w}vh(f,x,t,0)}else f.attr(y)}}},e.prototype._clear=function(){var t=this,e=this._elMap;e.each((function(n){JE(n,jE(n).option,e,t._lastGraphicModel)})),this._elMap=yt()},e.prototype.dispose=function(){this._clear()},e.type="graphic",e}(Ag);function KE(t){var e=_t(ZE,t)?ZE[t]:Lh(t);var n=new e({});return jE(n).type=t,n}function $E(t,e,n,i){var r=KE(n);return e.add(r),i.set(t,r),jE(r).id=t,jE(r).isNew=!0,r}function JE(t,e,n,i){t&&t.parent&&("group"===t.type&&t.traverse((function(t){JE(t,e,n,i)})),gR(t,e,i),n.removeKey(jE(t).id))}function QE(t,e,n,i){t.isGroup||E([["cursor",Da.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],(function(n){var i=n[0];_t(e,i)?t[i]=rt(e[i],n[1]):null==t[i]&&(t[i]=n[1])})),E(G(e),(function(n){if(0===n.indexOf("on")){var i=e[n];t[n]=X(i)?i:null}})),_t(e,"draggable")&&(t.draggable=e.draggable),null!=e.name&&(t.name=e.name),null!=e.id&&(t.id=e.id)}var tz=["x","y","radius","angle","single"],ez=["cartesian2d","polar","singleAxis"];function nz(t){return t+"Axis"}function iz(t,e){var n,i=yt(),r=[],o=yt();t.eachComponent({mainType:"dataZoom",query:e},(function(t){o.get(t.uid)||s(t)}));do{n=!1,t.eachComponent("dataZoom",a)}while(n);function a(t){!o.get(t.uid)&&function(t){var e=!1;return t.eachTargetAxis((function(t,n){var r=i.get(t);r&&r[n]&&(e=!0)})),e}(t)&&(s(t),n=!0)}function s(t){o.set(t.uid,!0),r.push(t),t.eachTargetAxis((function(t,e){(i.get(t)||i.set(t,[]))[e]=!0}))}return r}function rz(t){var e=t.ecModel,n={infoList:[],infoMap:yt()};return t.eachTargetAxis((function(t,i){var r=e.getComponent(nz(t),i);if(r){var o=r.getCoordSysModel();if(o){var a=o.uid,s=n.infoMap.get(a);s||(s={model:o,axisModels:[]},n.infoList.push(s),n.infoMap.set(a,s)),s.axisModels.push(r)}}})),n}var oz=function(){function t(){this.indexList=[],this.indexMap=[]}return t.prototype.add=function(t){this.indexMap[t]||(this.indexList.push(t),this.indexMap[t]=!0)},t}(),az=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return n(e,t),e.prototype.init=function(t,e,n){var i=sz(t);this.settledOption=i,this.mergeDefaultAndTheme(t,n),this._doInit(i)},e.prototype.mergeOption=function(t){var e=sz(t);C(this.option,t,!0),C(this.settledOption,e,!0),this._doInit(e)},e.prototype._doInit=function(t){var e=this.option;this._setDefaultThrottle(t),this._updateRangeUse(t);var n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(t,i){"value"===this._rangePropMode[i]&&(e[t[0]]=n[t[0]]=null)}),this),this._resetTarget()},e.prototype._resetTarget=function(){var t=this.get("orient",!0),e=this._targetAxisInfoMap=yt();this._fillSpecifiedTargetAxis(e)?this._orient=t||this._makeAutoOrientByTargetAxis():(this._orient=t||"horizontal",this._fillAutoTargetAxisByOrient(e,this._orient)),this._noTarget=!0,e.each((function(t){t.indexList.length&&(this._noTarget=!1)}),this)},e.prototype._fillSpecifiedTargetAxis=function(t){var e=!1;return E(tz,(function(n){var i=this.getReferringComponents(nz(n),Ho);if(i.specified){e=!0;var r=new oz;E(i.models,(function(t){r.add(t.componentIndex)})),t.set(n,r)}}),this),e},e.prototype._fillAutoTargetAxisByOrient=function(t,e){var n=this.ecModel,i=!0;if(i){var r="vertical"===e?"y":"x";o(n.findComponents({mainType:r+"Axis"}),r)}i&&o(n.findComponents({mainType:"singleAxis",filter:function(t){return t.get("orient",!0)===e}}),"single");function o(e,n){var r=e[0];if(r){var o=new oz;if(o.add(r.componentIndex),t.set(n,o),i=!1,"x"===n||"y"===n){var a=r.getReferringComponents("grid",Wo).models[0];a&&E(e,(function(t){r.componentIndex!==t.componentIndex&&a===t.getReferringComponents("grid",Wo).models[0]&&o.add(t.componentIndex)}))}}}i&&E(tz,(function(e){if(i){var r=n.findComponents({mainType:nz(e),filter:function(t){return"category"===t.get("type",!0)}});if(r[0]){var o=new oz;o.add(r[0].componentIndex),t.set(e,o),i=!1}}}),this)},e.prototype._makeAutoOrientByTargetAxis=function(){var t;return this.eachTargetAxis((function(e){!t&&(t=e)}),this),"y"===t?"vertical":"horizontal"},e.prototype._setDefaultThrottle=function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var e=this.ecModel.option;this.option.throttle=e.animation&&e.animationDurationUpdate>0?100:20}},e.prototype._updateRangeUse=function(t){var e=this._rangePropMode,n=this.get("rangeMode");E([["start","startValue"],["end","endValue"]],(function(i,r){var o=null!=t[i[0]],a=null!=t[i[1]];o&&!a?e[r]="percent":!o&&a?e[r]="value":n?e[r]=n[r]:o&&(e[r]="percent")}))},e.prototype.noTarget=function(){return this._noTarget},e.prototype.getFirstTargetAxisModel=function(){var t;return this.eachTargetAxis((function(e,n){null==t&&(t=this.ecModel.getComponent(nz(e),n))}),this),t},e.prototype.eachTargetAxis=function(t,e){this._targetAxisInfoMap.each((function(n,i){E(n.indexList,(function(n){t.call(e,i,n)}))}))},e.prototype.getAxisProxy=function(t,e){var n=this.getAxisModel(t,e);if(n)return n.__dzAxisProxy},e.prototype.getAxisModel=function(t,e){var n=this._targetAxisInfoMap.get(t);if(n&&n.indexMap[e])return this.ecModel.getComponent(nz(t),e)},e.prototype.setRawRange=function(t){var e=this.option,n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(i){null==t[i[0]]&&null==t[i[1]]||(e[i[0]]=n[i[0]]=t[i[0]],e[i[1]]=n[i[1]]=t[i[1]])}),this),this._updateRangeUse(t)},e.prototype.setCalculatedRange=function(t){var e=this.option;E(["start","startValue","end","endValue"],(function(n){e[n]=t[n]}))},e.prototype.getPercentRange=function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getDataPercentWindow()},e.prototype.getValueRange=function(t,e){if(null!=t||null!=e)return this.getAxisProxy(t,e).getDataValueWindow();var n=this.findRepresentativeAxisProxy();return n?n.getDataValueWindow():void 0},e.prototype.findRepresentativeAxisProxy=function(t){if(t)return t.__dzAxisProxy;for(var e,n=this._targetAxisInfoMap.keys(),i=0;i=0}(e)){var n=nz(this._dimName),i=e.getReferringComponents(n,Wo).models[0];i&&this._axisIndex===i.componentIndex&&t.push(e)}}),this),t},t.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},t.prototype.getMinMaxSpan=function(){return T(this._minMaxSpan)},t.prototype.calculateDataWindow=function(t){var e,n=this._dataExtent,i=this.getAxisModel().axis.scale,r=this._dataZoomModel.getRangePropMode(),o=[0,100],a=[],s=[];cz(["start","end"],(function(l,u){var h=t[l],c=t[l+"Value"];"percent"===r[u]?(null==h&&(h=o[u]),c=i.parse(Kr(h,o,n))):(e=!0,h=Kr(c=null==c?n[u]:i.parse(c),n,o)),s[u]=null==c||isNaN(c)?n[u]:c,a[u]=null==h||isNaN(h)?o[u]:h})),pz(s),pz(a);var l=this._minMaxSpan;function u(t,e,n,r,o){var a=o?"Span":"ValueSpan";zk(0,t,n,"all",l["min"+a],l["max"+a]);for(var s=0;s<2;s++)e[s]=Kr(t[s],n,r,!0),o&&(e[s]=i.parse(e[s]))}return e?u(s,a,n,o,!1):u(a,s,o,n,!0),{valueWindow:s,percentWindow:a}},t.prototype.reset=function(t){if(t===this._dataZoomModel){var e=this.getTargetSeriesModels();this._dataExtent=function(t,e,n){var i=[1/0,-1/0];cz(n,(function(t){!function(t,e,n){e&&E(C_(e,n),(function(n){var i=e.getApproximateExtent(n);i[0]t[1]&&(t[1]=i[1])}))}(i,t.getData(),e)}));var r=t.getAxisModel(),o=v_(r.axis.scale,r,i).calculate();return[o.min,o.max]}(this,this._dimName,e),this._updateMinMaxSpan();var n=this.calculateDataWindow(t.settledOption);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,this._setAxisModel()}},t.prototype.filterData=function(t,e){if(t===this._dataZoomModel){var n=this._dimName,i=this.getTargetSeriesModels(),r=t.get("filterMode"),o=this._valueWindow;"none"!==r&&cz(i,(function(t){var e=t.getData(),i=e.mapDimensionsAll(n);if(i.length){if("weakFilter"===r){var a=e.getStore(),s=z(i,(function(t){return e.getDimensionIndex(t)}),e);e.filterSelf((function(t){for(var e,n,r,l=0;lo[1];if(h&&!c&&!p)return!0;h&&(r=!0),c&&(e=!0),p&&(n=!0)}return r&&e&&n}))}else cz(i,(function(n){if("empty"===r)t.setData(e=e.map(n,(function(t){return function(t){return t>=o[0]&&t<=o[1]}(t)?t:NaN})));else{var i={};i[n]=o,e.selectRange(i)}}));cz(i,(function(t){e.setApproximateExtent(o,t)}))}}))}},t.prototype._updateMinMaxSpan=function(){var t=this._minMaxSpan={},e=this._dataZoomModel,n=this._dataExtent;cz(["min","max"],(function(i){var r=e.get(i+"Span"),o=e.get(i+"ValueSpan");null!=o&&(o=this.getAxisModel().axis.scale.parse(o)),null!=o?r=Kr(n[0]+o,n,[0,100],!0):null!=r&&(o=Kr(r,[0,100],n,!0)-n[0]),t[i+"Span"]=r,t[i+"ValueSpan"]=o}),this)},t.prototype._setAxisModel=function(){var t=this.getAxisModel(),e=this._percentWindow,n=this._valueWindow;if(e){var i=no(n,[0,500]);i=Math.min(i,20);var r=t.axis.scale.rawExtentInfo;0!==e[0]&&r.setDeterminedMinMax("min",+n[0].toFixed(i)),100!==e[1]&&r.setDeterminedMinMax("max",+n[1].toFixed(i)),r.freeze()}},t}();var fz={getTargetSeries:function(t){function e(e){t.eachComponent("dataZoom",(function(n){n.eachTargetAxis((function(i,r){var o=t.getComponent(nz(i),r);e(i,r,o,n)}))}))}e((function(t,e,n,i){n.__dzAxisProxy=null}));var n=[];e((function(e,i,r,o){r.__dzAxisProxy||(r.__dzAxisProxy=new dz(e,i,o,t),n.push(r.__dzAxisProxy))}));var i=yt();return E(n,(function(t){E(t.getTargetSeriesModels(),(function(t){i.set(t.uid,t)}))})),i},overallReset:function(t,e){t.eachComponent("dataZoom",(function(t){t.eachTargetAxis((function(e,n){t.getAxisProxy(e,n).reset(t)})),t.eachTargetAxis((function(n,i){t.getAxisProxy(n,i).filterData(t,e)}))})),t.eachComponent("dataZoom",(function(t){var e=t.findRepresentativeAxisProxy();if(e){var n=e.getDataPercentWindow(),i=e.getDataValueWindow();t.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}}))}};var gz=!1;function yz(t){gz||(gz=!0,t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,fz),function(t){t.registerAction("dataZoom",(function(t,e){E(iz(e,t),(function(e){e.setRawRange({start:t.start,end:t.end,startValue:t.startValue,endValue:t.endValue})}))}))}(t),t.registerSubTypeDefaulter("dataZoom",(function(){return"slider"})))}function vz(t){t.registerComponentModel(lz),t.registerComponentView(hz),yz(t)}var mz=function(){},xz={};function _z(t,e){xz[t]=e}function bz(t){return xz[t]}var wz=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){t.prototype.optionUpdated.apply(this,arguments);var e=this.ecModel;E(this.option.feature,(function(t,n){var i=bz(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(e)),C(t,i.defaultOption))}))},e.type="toolbox",e.layoutMode={type:"box",ignoreSize:!0},e.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}},e}(zp);function Sz(t,e){var n=vp(e.get("padding")),i=e.getItemStyle(["color","opacity"]);return i.fill=e.get("backgroundColor"),t=new Ws({shape:{x:t.x-n[3],y:t.y-n[0],width:t.width+n[1]+n[3],height:t.height+n[0]+n[2],r:e.get("borderRadius")},style:i,silent:!0,z2:-1})}var Mz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this.group;if(r.removeAll(),t.get("show")){var o=+t.get("itemSize"),a="vertical"===t.get("orient"),s=t.get("feature")||{},l=this._features||(this._features={}),u=[];E(s,(function(t,e){u.push(e)})),new Gm(this._featureNames||[],u).add(h).update(h).remove(H(h,null)).execute(),this._featureNames=u,function(t,e,n){var i=e.getBoxLayoutParams(),r=e.get("padding"),o={width:n.getWidth(),height:n.getHeight()},a=kp(i,o,r);Ap(e.get("orient"),t,e.get("itemGap"),a.width,a.height),Lp(t,i,o,r)}(r,t,n),r.add(Sz(r.getBoundingRect(),t)),a||r.eachChild((function(t){var e=t.__title,i=t.ensureState("emphasis"),a=i.textConfig||(i.textConfig={}),s=t.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!X(l)&&e){var u=l.style||(l.style={}),h=Sr(e,Xs.makeFont(u)),c=t.x+r.x,p=!1;t.y+r.y+o+h.height>n.getHeight()&&(a.position="top",p=!0);var d=p?-5-h.height:o+10;c+h.width/2>n.getWidth()?(a.position=["100%",d],u.align="right"):c-h.width/2<0&&(a.position=[0,d],u.align="left")}}))}function h(h,c){var p,d=u[h],f=u[c],g=s[d],y=new Cc(g,t,t.ecModel);if(i&&null!=i.newTitle&&i.featureName===d&&(g.title=i.newTitle),d&&!f){if(function(t){return 0===t.indexOf("my")}(d))p={onclick:y.option.onclick,featureName:d};else{var v=bz(d);if(!v)return;p=new v}l[d]=p}else if(!(p=l[f]))return;p.uid=Ac("toolbox-feature"),p.model=y,p.ecModel=e,p.api=n;var m=p instanceof mz;d||!f?!y.get("show")||m&&p.unusable?m&&p.remove&&p.remove(e,n):(!function(i,s,l){var u,h,c=i.getModel("iconStyle"),p=i.getModel(["emphasis","iconStyle"]),d=s instanceof mz&&s.getIcons?s.getIcons():i.get("icon"),f=i.get("title")||{};U(d)?(u={})[l]=d:u=d;U(f)?(h={})[l]=f:h=f;var g=i.iconPaths={};E(u,(function(l,u){var d=Uh(l,{},{x:-o/2,y:-o/2,width:o,height:o});d.setStyle(c.getItemStyle()),d.ensureState("emphasis").style=p.getItemStyle();var f=new Xs({style:{text:h[u],align:p.get("textAlign"),borderRadius:p.get("textBorderRadius"),padding:p.get("textPadding"),fill:null,font:cc({fontStyle:p.get("textFontStyle"),fontFamily:p.get("textFontFamily"),fontSize:p.get("textFontSize"),fontWeight:p.get("textFontWeight")},e)},ignore:!0});d.setTextContent(f),Kh({el:d,componentModel:t,itemName:u,formatterParamsExtra:{title:h[u]}}),d.__title=h[u],d.on("mouseover",(function(){var e=p.getItemStyle(),i=a?null==t.get("right")&&"right"!==t.get("left")?"right":"left":null==t.get("bottom")&&"bottom"!==t.get("top")?"bottom":"top";f.setStyle({fill:p.get("textFill")||e.fill||e.stroke||"#000",backgroundColor:p.get("textBackgroundColor")}),d.setTextConfig({position:p.get("textPosition")||i}),f.ignore=!t.get("showTitle"),n.enterEmphasis(this)})).on("mouseout",(function(){"emphasis"!==i.get(["iconStatus",u])&&n.leaveEmphasis(this),f.hide()})),("emphasis"===i.get(["iconStatus",u])?Ol:Rl)(d),r.add(d),d.on("click",W(s.onclick,s,e,n,u)),g[u]=d}))}(y,p,d),y.setIconStatus=function(t,e){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[t]=e,i[t]&&("emphasis"===e?Ol:Rl)(i[t])},p instanceof mz&&p.render&&p.render(y,e,n,i)):m&&p.dispose&&p.dispose(e,n)}},e.prototype.updateView=function(t,e,n,i){E(this._features,(function(t){t instanceof mz&&t.updateView&&t.updateView(t.model,e,n,i)}))},e.prototype.remove=function(t,e){E(this._features,(function(n){n instanceof mz&&n.remove&&n.remove(t,e)})),this.group.removeAll()},e.prototype.dispose=function(t,e){E(this._features,(function(n){n instanceof mz&&n.dispose&&n.dispose(t,e)}))},e.type="toolbox",e}(Ag);var Iz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",o="svg"===e.getZr().painter.getType(),a=o?"svg":n.get("type",!0)||"png",s=e.getConnectedDataURL({type:a,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),l=r.browser;if("function"!=typeof MouseEvent||!l.newEdge&&(l.ie||l.edge))if(window.navigator.msSaveOrOpenBlob||o){var u=s.split(","),h=u[0].indexOf("base64")>-1,c=o?decodeURIComponent(u[1]):u[1];h&&(c=window.atob(c));var p=i+"."+a;if(window.navigator.msSaveOrOpenBlob){for(var d=c.length,f=new Uint8Array(d);d--;)f[d]=c.charCodeAt(d);var g=new Blob([f]);window.navigator.msSaveOrOpenBlob(g,p)}else{var y=document.createElement("iframe");document.body.appendChild(y);var v=y.contentWindow,m=v.document;m.open("image/svg+xml","replace"),m.write(c),m.close(),v.focus(),m.execCommand("SaveAs",!0,p),document.body.removeChild(y)}}else{var x=n.get("lang"),_='',b=window.open();b.document.write(_),b.document.title=i}else{var w=document.createElement("a");w.download=i+"."+a,w.target="_blank",w.href=s;var S=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});w.dispatchEvent(S)}},e.getDefaultOption=function(t){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:t.getLocaleModel().get(["toolbox","saveAsImage","lang"])}},e}(mz),Tz="__ec_magicType_stack__",Cz=[["line","bar"],["stack"]],Dz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return E(t.get("type"),(function(t){e[t]&&(n[t]=e[t])})),n},e.getDefaultOption=function(t){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},e.prototype.onclick=function(t,e,n){var i=this.model,r=i.get(["seriesIndex",n]);if(Az[n]){var o,a={series:[]};E(Cz,(function(t){P(t,n)>=0&&E(t,(function(t){i.setIconStatus(t,"normal")}))})),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},(function(t){var e=t.subType,r=t.id,o=Az[n](e,r,t,i);o&&(k(o,t.option),a.series.push(o));var s=t.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var u=l.dim+"Axis",h=t.getReferringComponents(u,Wo).models[0].componentIndex;a[u]=a[u]||[];for(var c=0;c<=h;c++)a[u][h]=a[u][h]||{};a[u][h].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(o=C({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),e.dispatchAction({type:"changeMagicType",currentType:s,newOption:a,newTitle:o,featureName:"magicType"})}},e}(mz),Az={line:function(t,e,n,i){if("bar"===t)return C({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(t,e,n,i){if("line"===t)return C({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(t,e,n,i){var r=n.get("stack")===Tz;if("line"===t||"bar"===t)return i.setIconStatus("stack",r?"normal":"emphasis"),C({id:e,stack:r?"":Tz},i.get(["option","stack"])||{},!0)}};Cm({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(t,e){e.mergeOption(t.newOption)}));var kz=new Array(60).join("-"),Lz="\t";function Pz(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var Oz=new RegExp("[\t]+","g");function Rz(t,e){var n=t.split(new RegExp("\n*"+kz+"\n*","g")),i={series:[]};return E(n,(function(t,n){if(function(t){if(t.slice(0,t.indexOf("\n")).indexOf(Lz)>=0)return!0}(t)){var r=function(t){for(var e=t.split(/\n+/g),n=[],i=z(Pz(e.shift()).split(Oz),(function(t){return{name:t,data:[]}})),r=0;r=0)&&t(r,i._targetInfoList)}))}return t.prototype.setOutputRanges=function(t,e){return this.matchOutputRanges(t,e,(function(t,e,n){if((t.coordRanges||(t.coordRanges=[])).push(e),!t.coordRange){t.coordRange=e;var i=jz[t.brushType](0,n,e);t.__rangeOffset={offset:Kz[t.brushType](i.values,t.range,[1,1]),xyMinMax:i.xyMinMax}}})),t},t.prototype.matchOutputRanges=function(t,e,n){E(t,(function(t){var i=this.findTargetInfo(t,e);i&&!0!==i&&E(i.coordSyses,(function(i){var r=jz[t.brushType](1,i,t.range,!0);n(t,r.values,i,e)}))}),this)},t.prototype.setInputRanges=function(t,e){E(t,(function(t){var n,i,r,o,a,s=this.findTargetInfo(t,e);if(t.range=t.range||[],s&&!0!==s){t.panelId=s.panelId;var l=jz[t.brushType](0,s.coordSys,t.coordRange),u=t.__rangeOffset;t.range=u?Kz[t.brushType](l.values,u.offset,(n=l.xyMinMax,i=u.xyMinMax,r=Jz(n),o=Jz(i),a=[r[0]/o[0],r[1]/o[1]],isNaN(a[0])&&(a[0]=1),isNaN(a[1])&&(a[1]=1),a)):l.values}}),this)},t.prototype.makePanelOpts=function(t,e){return z(this._targetInfoList,(function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:e?e(n):null,clipPath:BL(i),isTargetByCursor:GL(i,t,n.coordSysModel),getLinearBrushOtherExtent:FL(i)}}))},t.prototype.controlSeries=function(t,e,n){var i=this.findTargetInfo(t,n);return!0===i||i&&P(i.coordSyses,e.coordinateSystem)>=0},t.prototype.findTargetInfo=function(t,e){for(var n=this._targetInfoList,i=Yz(e,t),r=0;rt[1]&&t.reverse(),t}function Yz(t,e){return Fo(t,e,{includeMainTypes:Gz})}var Xz={grid:function(t,e){var n=t.xAxisModels,i=t.yAxisModels,r=t.gridModels,o=yt(),a={},s={};(n||i||r)&&(E(n,(function(t){var e=t.axis.grid.model;o.set(e.id,e),a[e.id]=!0})),E(i,(function(t){var e=t.axis.grid.model;o.set(e.id,e),s[e.id]=!0})),E(r,(function(t){o.set(t.id,t),a[t.id]=!0,s[t.id]=!0})),o.each((function(t){var r=t.coordinateSystem,o=[];E(r.getCartesians(),(function(t,e){(P(n,t.getAxis("x").model)>=0||P(i,t.getAxis("y").model)>=0)&&o.push(t)})),e.push({panelId:"grid--"+t.id,gridModel:t,coordSysModel:t,coordSys:o[0],coordSyses:o,getPanelRect:Zz.grid,xAxisDeclared:a[t.id],yAxisDeclared:s[t.id]})})))},geo:function(t,e){E(t.geoModels,(function(t){var n=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:n,coordSyses:[n],getPanelRect:Zz.geo})}))}},Uz=[function(t,e){var n=t.xAxisModel,i=t.yAxisModel,r=t.gridModel;return!r&&n&&(r=n.axis.grid.model),!r&&i&&(r=i.axis.grid.model),r&&r===e.gridModel},function(t,e){var n=t.geoModel;return n&&n===e.geoModel}],Zz={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var t=this.coordSys,e=t.getBoundingRect().clone();return e.applyTransform(Bh(t)),e}},jz={lineX:H(qz,0),lineY:H(qz,1),rect:function(t,e,n,i){var r=t?e.pointToData([n[0][0],n[1][0]],i):e.dataToPoint([n[0][0],n[1][0]],i),o=t?e.pointToData([n[0][1],n[1][1]],i):e.dataToPoint([n[0][1],n[1][1]],i),a=[Hz([r[0],o[0]]),Hz([r[1],o[1]])];return{values:a,xyMinMax:a}},polygon:function(t,e,n,i){var r=[[1/0,-1/0],[1/0,-1/0]];return{values:z(n,(function(n){var o=t?e.pointToData(n,i):e.dataToPoint(n,i);return r[0][0]=Math.min(r[0][0],o[0]),r[1][0]=Math.min(r[1][0],o[1]),r[0][1]=Math.max(r[0][1],o[0]),r[1][1]=Math.max(r[1][1],o[1]),o})),xyMinMax:r}}};function qz(t,e,n,i){var r=n.getAxis(["x","y"][t]),o=Hz(z([0,1],(function(t){return e?r.coordToData(r.toLocalCoord(i[t]),!0):r.toGlobalCoord(r.dataToCoord(i[t]))}))),a=[];return a[t]=o,a[1-t]=[NaN,NaN],{values:o,xyMinMax:a}}var Kz={lineX:H($z,0),lineY:H($z,1),rect:function(t,e,n){return[[t[0][0]-n[0]*e[0][0],t[0][1]-n[0]*e[0][1]],[t[1][0]-n[1]*e[1][0],t[1][1]-n[1]*e[1][1]]]},polygon:function(t,e,n){return z(t,(function(t,i){return[t[0]-n[0]*e[i][0],t[1]-n[1]*e[i][1]]}))}};function $z(t,e,n,i){return[e[0]-i[t]*n[0],e[1]-i[t]*n[1]]}function Jz(t){return t?[t[0][1]-t[0][0],t[1][1]-t[1][0]]:[NaN,NaN]}var Qz,tV,eV=E,nV=Io+"toolbox-dataZoom_",iV=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){this._brushController||(this._brushController=new lL(n.getZr()),this._brushController.on("brush",W(this._onBrush,this)).mount()),function(t,e,n,i,r){var o=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(o="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=o,t.setIconStatus("zoom",o?"emphasis":"normal");var a=new Wz(oV(t),e,{include:["grid"]}),s=a.makePanelOpts(r,(function(t){return t.xAxisDeclared&&!t.yAxisDeclared?"lineX":!t.xAxisDeclared&&t.yAxisDeclared?"lineY":"rect"}));n._brushController.setPanels(s).enableBrush(!(!o||!s.length)&&{brushType:"auto",brushStyle:t.getModel("brushStyle").getItemStyle()})}(t,e,this,i,n),function(t,e){t.setIconStatus("back",function(t){return Bz(t).length}(e)>1?"emphasis":"normal")}(t,e)},e.prototype.onclick=function(t,e,n){rV[n].call(this)},e.prototype.remove=function(t,e){this._brushController&&this._brushController.unmount()},e.prototype.dispose=function(t,e){this._brushController&&this._brushController.dispose()},e.prototype._onBrush=function(t){var e=t.areas;if(t.isEnd&&e.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new Wz(oV(this.model),i,{include:["grid"]}).matchOutputRanges(e,i,(function(t,e,n){if("cartesian2d"===n.type){var i=t.brushType;"rect"===i?(r("x",n,e[0]),r("y",n,e[1])):r({lineX:"x",lineY:"y"}[i],n,e)}})),function(t,e){var n=Bz(t);zz(e,(function(e,i){for(var r=n.length-1;r>=0&&!n[r][i];r--);if(r<0){var o=t.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(o){var a=o.getPercentRange();n[0][i]={dataZoomId:i,start:a[0],end:a[1]}}}})),n.push(e)}(i,n),this._dispatchZoomAction(n)}function r(t,e,r){var o=e.getAxis(t),a=o.model,s=function(t,e,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},(function(n){n.getAxisModel(t,e.componentIndex)&&(i=n)})),i}(t,a,i),l=s.findRepresentativeAxisProxy(a).getMinMaxSpan();null==l.minValueSpan&&null==l.maxValueSpan||(r=zk(0,r.slice(),o.scale.getExtent(),0,l.minValueSpan,l.maxValueSpan)),s&&(n[s.id]={dataZoomId:s.id,startValue:r[0],endValue:r[1]})}},e.prototype._dispatchZoomAction=function(t){var e=[];eV(t,(function(t,n){e.push(T(t))})),e.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:e})},e.getDefaultOption=function(t){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:t.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}}},e}(mz),rV={zoom:function(){var t=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:t})},back:function(){this._dispatchZoomAction(function(t){var e=Bz(t),n=e[e.length-1];e.length>1&&e.pop();var i={};return zz(n,(function(t,n){for(var r=e.length-1;r>=0;r--)if(t=e[r][n]){i[n]=t;break}})),i}(this.ecModel))}};function oV(t){var e={xAxisIndex:t.get("xAxisIndex",!0),yAxisIndex:t.get("yAxisIndex",!0),xAxisId:t.get("xAxisId",!0),yAxisId:t.get("yAxisId",!0)};return null==e.xAxisIndex&&null==e.xAxisId&&(e.xAxisIndex="all"),null==e.yAxisIndex&&null==e.yAxisId&&(e.yAxisIndex="all"),e}Qz="dataZoom",tV=function(t){var e=t.getComponent("toolbox",0),n=["feature","dataZoom"];if(e&&null!=e.get(n)){var i=e.getModel(n),r=[],o=Fo(t,oV(i));return eV(o.xAxisModels,(function(t){return a(t,"xAxis","xAxisIndex")})),eV(o.yAxisModels,(function(t){return a(t,"yAxis","yAxisIndex")})),r}function a(t,e,n){var o=t.componentIndex,a={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:nV+e+o};a[n]=o,r.push(a)}},lt(null==od.get(Qz)&&tV),od.set(Qz,tV);var aV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="tooltip",e.dependencies=["axisPointer"],e.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},e}(zp);function sV(t){var e=t.get("confine");return null!=e?!!e:"richText"===t.get("renderMode")}function lV(t){if(r.domSupported)for(var e=document.documentElement.style,n=0,i=t.length;n-1?(u+="top:50%",h+="translateY(-50%) rotate("+(a="left"===s?-225:-45)+"deg)"):(u+="left:50%",h+="translateX(-50%) rotate("+(a="top"===s?225:45)+"deg)");var c=a*Math.PI/180,p=l+r,d=p*Math.abs(Math.cos(c))+p*Math.abs(Math.sin(c)),f=e+" solid "+r+"px;";return'
    '}(n,i,r)),U(t))o.innerHTML=t+a;else if(t){o.innerHTML="",Y(t)||(t=[t]);for(var s=0;s=0?this._tryShow(n,i):"leave"===e&&this._hide(i))}),this))},e.prototype._keepShow=function(){var t=this._tooltipModel,e=this._ecModel,n=this._api,i=t.get("triggerOn");if(null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var r=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout((function(){!n.isDisposed()&&r.manuallyShowTip(t,e,n,{x:r._lastX,y:r._lastY,dataByCoordSys:r._lastDataByCoordSys})}))}},e.prototype.manuallyShowTip=function(t,e,n,i){if(i.from!==this.uid&&!r.node&&n.getDom()){var o=IV(i,n);this._ticket="";var a=i.dataByCoordSys,s=function(t,e,n){var i=Go(t).queryOptionMap,r=i.keys()[0];if(!r||"series"===r)return;var o=Yo(e,r,i.get(r),{useDefault:!1,enableAll:!1,enableNone:!1}),a=o.models[0];if(!a)return;var s,l=n.getViewOfComponentModel(a);if(l.group.traverse((function(e){var n=rl(e).tooltipConfig;if(n&&n.name===t.name)return s=e,!0})),s)return{componentMainType:r,componentIndex:a.componentIndex,el:s}}(i,e,n);if(s){var l=s.el.getBoundingRect().clone();l.applyTransform(s.el.transform),this._tryShow({offsetX:l.x+l.width/2,offsetY:l.y+l.height/2,target:s.el,position:i.position,positionDefault:"bottom"},o)}else if(i.tooltip&&null!=i.x&&null!=i.y){var u=wV;u.x=i.x,u.y=i.y,u.update(),rl(u).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:u},o)}else if(a)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:a,tooltipOption:i.tooltipOption},o);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(t,e,n,i))return;var h=PN(i,e),c=h.point[0],p=h.point[1];null!=c&&null!=p&&this._tryShow({offsetX:c,offsetY:p,target:h.el,position:i.position,positionDefault:"bottom"},o)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},o))}},e.prototype.manuallyHideTip=function(t,e,n,i){var r=this._tooltipContent;this._tooltipModel&&r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(IV(i,n))},e.prototype._manuallyAxisShowTip=function(t,e,n,i){var r=i.seriesIndex,o=i.dataIndex,a=e.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=o&&null!=a){var s=e.getSeriesByIndex(r);if(s)if("axis"===MV([s.getData().getItemModel(o),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:o,position:i.position}),!0}},e.prototype._tryShow=function(t,e){var n=t.target;if(this._tooltipModel){this._lastX=t.offsetX,this._lastY=t.offsetY;var i=t.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,t);else if(n){var r,o;if("legend"===rl(n).ssrType)return;this._lastDataByCoordSys=null,Oy(n,(function(t){return null!=rl(t).dataIndex?(r=t,!0):null!=rl(t).tooltipConfig?(o=t,!0):void 0}),!0),r?this._showSeriesItemTooltip(t,r,e):o?this._showComponentItemTooltip(t,o,e):this._hide(e)}else this._lastDataByCoordSys=null,this._hide(e)}},e.prototype._showOrMove=function(t,e){var n=t.get("showDelay");e=W(e,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(e,n):e()},e.prototype._showAxisTooltip=function(t,e){var n=this._ecModel,i=this._tooltipModel,r=[e.offsetX,e.offsetY],o=MV([e.tooltipOption],i),a=this._renderMode,s=[],l=og("section",{blocks:[],noHeader:!0}),u=[],h=new yg;E(t,(function(t){E(t.dataByAxis,(function(t){var e=n.getComponent(t.axisDim+"Axis",t.axisIndex),r=t.value;if(e&&null!=r){var o=fN(r,e.axis,n,t.seriesDataIndices,t.valueLabelOpt),c=og("section",{header:o,noHeader:!ut(o),sortBlocks:!0,blocks:[]});l.blocks.push(c),E(t.seriesDataIndices,(function(l){var p=n.getSeriesByIndex(l.seriesIndex),d=l.dataIndexInside,f=p.getDataParams(d);if(!(f.dataIndex<0)){f.axisDim=t.axisDim,f.axisIndex=t.axisIndex,f.axisType=t.axisType,f.axisId=t.axisId,f.axisValue=S_(e.axis,{value:r}),f.axisValueLabel=o,f.marker=h.makeTooltipMarker("item",Sp(f.color),a);var g=bf(p.formatTooltip(d,!0,null)),y=g.frag;if(y){var v=MV([p],i).get("valueFormatter");c.blocks.push(v?A({valueFormatter:v},y):y)}g.text&&u.push(g.text),s.push(f)}}))}}))})),l.blocks.reverse(),u.reverse();var c=e.position,p=o.get("order"),d=cg(l,h,a,p,n.get("useUTC"),o.get("textStyle"));d&&u.unshift(d);var f="richText"===a?"\n\n":"
    ",g=u.join(f);this._showOrMove(o,(function(){this._updateContentNotChangedOnAxis(t,s)?this._updatePosition(o,c,r[0],r[1],this._tooltipContent,s):this._showTooltipContent(o,g,s,Math.random()+"",r[0],r[1],c,null,h)}))},e.prototype._showSeriesItemTooltip=function(t,e,n){var i=this._ecModel,r=rl(e),o=r.seriesIndex,a=i.getSeriesByIndex(o),s=r.dataModel||a,l=r.dataIndex,u=r.dataType,h=s.getData(u),c=this._renderMode,p=t.positionDefault,d=MV([h.getItemModel(l),s,a&&(a.coordinateSystem||{}).model],this._tooltipModel,p?{position:p}:null),f=d.get("trigger");if(null==f||"item"===f){var g=s.getDataParams(l,u),y=new yg;g.marker=y.makeTooltipMarker("item",Sp(g.color),c);var v=bf(s.formatTooltip(l,!1,u)),m=d.get("order"),x=d.get("valueFormatter"),_=v.frag,b=_?cg(x?A({valueFormatter:x},_):_,y,c,m,i.get("useUTC"),d.get("textStyle")):v.text,w="item_"+s.name+"_"+l;this._showOrMove(d,(function(){this._showTooltipContent(d,b,g,w,t.offsetX,t.offsetY,t.position,t.target,y)})),n({type:"showTip",dataIndexInside:l,dataIndex:h.getRawIndex(l),seriesIndex:o,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,e,n){var i="html"===this._renderMode,r=rl(e),o=r.tooltipConfig.option||{},a=o.encodeHTMLContent;if(U(o)){o={content:o,formatter:o},a=!0}a&&i&&o.content&&((o=T(o)).content=re(o.content));var s=[o],l=this._ecModel.getComponent(r.componentMainType,r.componentIndex);l&&s.push(l),s.push({formatter:o.content});var u=t.positionDefault,h=MV(s,this._tooltipModel,u?{position:u}:null),c=h.get("content"),p=Math.random()+"",d=new yg;this._showOrMove(h,(function(){var n=T(h.get("formatterParams")||{});this._showTooltipContent(h,c,n,p,t.offsetX,t.offsetY,t.position,e,d)})),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,e,n,i,r,o,a,s,l){if(this._ticket="",t.get("showContent")&&t.get("show")){var u=this._tooltipContent;u.setEnterable(t.get("enterable"));var h=t.get("formatter");a=a||t.get("position");var c=e,p=this._getNearestPoint([r,o],n,t.get("trigger"),t.get("borderColor")).color;if(h)if(U(h)){var d=t.ecModel.get("useUTC"),f=Y(n)?n[0]:n;c=h,f&&f.axisType&&f.axisType.indexOf("time")>=0&&(c=Jc(f.axisValue,c,d)),c=bp(c,n,!0)}else if(X(h)){var g=W((function(e,i){e===this._ticket&&(u.setContent(i,l,t,p,a),this._updatePosition(t,a,r,o,u,n,s))}),this);this._ticket=i,c=h(n,i,g)}else c=h;u.setContent(c,l,t,p,a),u.show(t,p),this._updatePosition(t,a,r,o,u,n,s)}},e.prototype._getNearestPoint=function(t,e,n,i){return"axis"===n||Y(e)?{color:i||("html"===this._renderMode?"#fff":"none")}:Y(e)?void 0:{color:i||e.color||e.borderColor}},e.prototype._updatePosition=function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),h=t.get("align"),c=t.get("verticalAlign"),p=a&&a.getBoundingRect().clone();if(a&&p.applyTransform(a.transform),X(e)&&(e=e([n,i],o,r.el,p,{viewSize:[s,l],contentSize:u.slice()})),Y(e))n=$r(e[0],s),i=$r(e[1],l);else if(q(e)){var d=e;d.width=u[0],d.height=u[1];var f=kp(d,{width:s,height:l});n=f.x,i=f.y,h=null,c=null}else if(U(e)&&a){var g=function(t,e,n,i){var r=n[0],o=n[1],a=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,u=e.width,h=e.height;switch(t){case"inside":s=e.x+u/2-r/2,l=e.y+h/2-o/2;break;case"top":s=e.x+u/2-r/2,l=e.y-o-a;break;case"bottom":s=e.x+u/2-r/2,l=e.y+h+a;break;case"left":s=e.x-r-a,l=e.y+h/2-o/2;break;case"right":s=e.x+u+a,l=e.y+h/2-o/2}return[s,l]}(e,p,u,t.get("borderWidth"));n=g[0],i=g[1]}else{g=function(t,e,n,i,r,o,a){var s=n.getSize(),l=s[0],u=s[1];null!=o&&(t+l+o+2>i?t-=l+o:t+=o);null!=a&&(e+u+a>r?e-=u+a:e+=a);return[t,e]}(n,i,r,s,l,h?null:20,c?null:20);n=g[0],i=g[1]}if(h&&(n-=TV(h)?u[0]/2:"right"===h?u[0]:0),c&&(i-=TV(c)?u[1]/2:"bottom"===c?u[1]:0),sV(t)){g=function(t,e,n,i,r){var o=n.getSize(),a=o[0],s=o[1];return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}(n,i,r,s,l);n=g[0],i=g[1]}r.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t,e){var n=this._lastDataByCoordSys,i=this._cbParamsList,r=!!n&&n.length===t.length;return r&&E(n,(function(n,o){var a=n.dataByAxis||[],s=(t[o]||{}).dataByAxis||[];(r=r&&a.length===s.length)&&E(a,(function(t,n){var o=s[n]||{},a=t.seriesDataIndices||[],l=o.seriesDataIndices||[];(r=r&&t.value===o.value&&t.axisType===o.axisType&&t.axisId===o.axisId&&a.length===l.length)&&E(a,(function(t,e){var n=l[e];r=r&&t.seriesIndex===n.seriesIndex&&t.dataIndex===n.dataIndex})),i&&E(t.seriesDataIndices,(function(t){var n=t.seriesIndex,o=e[n],a=i[n];o&&a&&a.data!==o.data&&(r=!1)}))}))})),this._lastDataByCoordSys=t,this._cbParamsList=e,!!r},e.prototype._hide=function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,e){!r.node&&e.getDom()&&(Yg(this,"_updatePosition"),this._tooltipContent.dispose(),kN("itemTooltip",e))},e.type="tooltip",e}(Ag);function MV(t,e,n){var i,r=e.ecModel;n?(i=new Cc(n,r,r),i=new Cc(e.option,i,r)):i=e;for(var o=t.length-1;o>=0;o--){var a=t[o];a&&(a instanceof Cc&&(a=a.get("tooltip",!0)),U(a)&&(a={formatter:a}),a&&(i=new Cc(a,i,r)))}return i}function IV(t,e){return t.dispatchAction||W(e.dispatchAction,e)}function TV(t){return"center"===t||"middle"===t}var CV=["rect","polygon","keep","clear"];function DV(t,e){var n=To(t?t.brush:[]);if(n.length){var i=[];E(n,(function(t){var e=t.hasOwnProperty("toolbox")?t.toolbox:[];e instanceof Array&&(i=i.concat(e))}));var r=t&&t.toolbox;Y(r)&&(r=r[0]),r||(r={feature:{}},t.toolbox=[r]);var o=r.feature||(r.feature={}),a=o.brush||(o.brush={}),s=a.type||(a.type=[]);s.push.apply(s,i),function(t){var e={};E(t,(function(t){e[t]=1})),t.length=0,E(e,(function(e,n){t.push(n)}))}(s),e&&!s.length&&s.push.apply(s,CV)}}var AV=E;function kV(t){if(t)for(var e in t)if(t.hasOwnProperty(e))return!0}function LV(t,e,n){var i={};return AV(e,(function(e){var r,o=i[e]=((r=function(){}).prototype.__hidden=r.prototype,new r);AV(t[e],(function(t,i){if(kD.isValidType(i)){var r={type:i,visual:t};n&&n(r,e),o[i]=new kD(r),"opacity"===i&&((r=T(r)).type="colorAlpha",o.__hidden.__alphaForOpacity=new kD(r))}}))})),i}function PV(t,e,n){var i;E(n,(function(t){e.hasOwnProperty(t)&&kV(e[t])&&(i=!0)})),i&&E(n,(function(n){e.hasOwnProperty(n)&&kV(e[n])?t[n]=T(e[n]):delete t[n]}))}var OV={lineX:RV(0),lineY:RV(1),rect:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])},rect:function(t,e,n){return t&&n.boundingRect.intersect(t)}},polygon:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])&&P_(n.range,t[0],t[1])},rect:function(t,e,n){var i=n.range;if(!t||i.length<=1)return!1;var r=t.x,o=t.y,a=t.width,s=t.height,l=i[0];return!!(P_(i,r,o)||P_(i,r+a,o)||P_(i,r,o+s)||P_(i,r+a,o+s)||ze.create(t).contain(l[0],l[1])||Zh(r,o,r+a,o,i)||Zh(r,o,r,o+s,i)||Zh(r+a,o,r+a,o+s,i)||Zh(r,o+s,r+a,o+s,i))||void 0}}};function RV(t){var e=["x","y"],n=["width","height"];return{point:function(e,n,i){if(e){var r=i.range;return NV(e[t],r)}},rect:function(i,r,o){if(i){var a=o.range,s=[i[e[t]],i[e[t]]+i[n[t]]];return s[1]e[0][1]&&(e[0][1]=o[0]),o[1]e[1][1]&&(e[1][1]=o[1])}return e&&YV(e)}};function YV(t){return new ze(t[0][0],t[1][0],t[0][1]-t[0][0],t[1][1]-t[1][0])}var XV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.ecModel=t,this.api=e,this.model,(this._brushController=new lL(e.getZr())).on("brush",W(this._onBrush,this)).mount()},e.prototype.render=function(t,e,n,i){this.model=t,this._updateController(t,e,n,i)},e.prototype.updateTransform=function(t,e,n,i){BV(e),this._updateController(t,e,n,i)},e.prototype.updateVisual=function(t,e,n,i){this.updateTransform(t,e,n,i)},e.prototype.updateView=function(t,e,n,i){this._updateController(t,e,n,i)},e.prototype._updateController=function(t,e,n,i){(!i||i.$from!==t.id)&&this._brushController.setPanels(t.brushTargetManager.makePanelOpts(n)).enableBrush(t.brushOption).updateCovers(t.areas.slice())},e.prototype.dispose=function(){this._brushController.dispose()},e.prototype._onBrush=function(t){var e=this.model.id,n=this.model.brushTargetManager.setOutputRanges(t.areas,this.ecModel);(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:e,areas:T(n),$from:e}),t.isEnd&&this.api.dispatchAction({type:"brushEnd",brushId:e,areas:T(n),$from:e})},e.type="brush",e}(Ag),UV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.areas=[],n.brushOption={},n}return n(e,t),e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&PV(n,t,["inBrush","outOfBrush"]);var i=n.inBrush=n.inBrush||{};n.outOfBrush=n.outOfBrush||{color:"#ddd"},i.hasOwnProperty("liftZ")||(i.liftZ=5)},e.prototype.setAreas=function(t){t&&(this.areas=z(t,(function(t){return ZV(this.option,t)}),this))},e.prototype.setBrushOption=function(t){this.brushOption=ZV(this.option,t),this.brushType=this.brushOption.brushType},e.type="brush",e.dependencies=["geo","grid","xAxis","yAxis","parallel","series"],e.defaultOption={seriesIndex:"all",brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:"rgba(210,219,238,0.3)",borderColor:"#D2DBEE"},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4},e}(zp);function ZV(t,e){return C({brushType:t.brushType,brushMode:t.brushMode,transformable:t.transformable,brushStyle:new Cc(t.brushStyle).getItemStyle(),removeOnClick:t.removeOnClick,z:t.z},e,!0)}var jV=["rect","polygon","lineX","lineY","keep","clear"],qV=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n){var i,r,o;e.eachComponent({mainType:"brush"},(function(t){i=t.brushType,r=t.brushOption.brushMode||"single",o=o||!!t.areas.length})),this._brushType=i,this._brushMode=r,E(t.get("type",!0),(function(e){t.setIconStatus(e,("keep"===e?"multiple"===r:"clear"===e?o:e===i)?"emphasis":"normal")}))},e.prototype.updateView=function(t,e,n){this.render(t,e,n)},e.prototype.getIcons=function(){var t=this.model,e=t.get("icon",!0),n={};return E(t.get("type",!0),(function(t){e[t]&&(n[t]=e[t])})),n},e.prototype.onclick=function(t,e,n){var i=this._brushType,r=this._brushMode;"clear"===n?(e.dispatchAction({type:"axisAreaSelect",intervals:[]}),e.dispatchAction({type:"brush",command:"clear",areas:[]})):e.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:"keep"===n?i:i!==n&&n,brushMode:"keep"===n?"multiple"===r?"single":"multiple":r}})},e.getDefaultOption=function(t){return{show:!0,type:jV.slice(),icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:t.getLocaleModel().get(["toolbox","brush","title"])}},e}(mz);var KV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode={type:"box",ignoreSize:!0},n}return n(e,t),e.type="title",e.defaultOption={z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bold",color:"#464646"},subtextStyle:{fontSize:12,color:"#6E7079"}},e}(zp),$V=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){if(this.group.removeAll(),t.get("show")){var i=this.group,r=t.getModel("textStyle"),o=t.getModel("subtextStyle"),a=t.get("textAlign"),s=rt(t.get("textBaseline"),t.get("textVerticalAlign")),l=new Xs({style:oc(r,{text:t.get("text"),fill:r.getTextColor()},{disableBox:!0}),z2:10}),u=l.getBoundingRect(),h=t.get("subtext"),c=new Xs({style:oc(o,{text:h,fill:o.getTextColor(),y:u.height+t.get("itemGap"),verticalAlign:"top"},{disableBox:!0}),z2:10}),p=t.get("link"),d=t.get("sublink"),f=t.get("triggerEvent",!0);l.silent=!p&&!f,c.silent=!d&&!f,p&&l.on("click",(function(){Mp(p,"_"+t.get("target"))})),d&&c.on("click",(function(){Mp(d,"_"+t.get("subtarget"))})),rl(l).eventData=rl(c).eventData=f?{componentType:"title",componentIndex:t.componentIndex}:null,i.add(l),h&&i.add(c);var g=i.getBoundingRect(),y=t.getBoxLayoutParams();y.width=g.width,y.height=g.height;var v=kp(y,{width:n.getWidth(),height:n.getHeight()},t.get("padding"));a||("middle"===(a=t.get("left")||t.get("right"))&&(a="center"),"right"===a?v.x+=v.width:"center"===a&&(v.x+=v.width/2)),s||("center"===(s=t.get("top")||t.get("bottom"))&&(s="middle"),"bottom"===s?v.y+=v.height:"middle"===s&&(v.y+=v.height/2),s=s||"top"),i.x=v.x,i.y=v.y,i.markRedraw();var m={align:a,verticalAlign:s};l.setStyle(m),c.setStyle(m),g=i.getBoundingRect();var x=v.margin,_=t.getItemStyle(["color","opacity"]);_.fill=t.get("backgroundColor");var b=new Ws({shape:{x:g.x-x[3],y:g.y-x[0],width:g.width+x[1]+x[3],height:g.height+x[0]+x[2],r:t.get("borderRadius")},style:_,subPixelOptimize:!0,silent:!0});i.add(b)}},e.type="title",e}(Ag);var JV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode="box",n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),this._initData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this._initData()},e.prototype.setCurrentIndex=function(t){null==t&&(t=this.option.currentIndex);var e=this._data.count();this.option.loop?t=(t%e+e)%e:(t>=e&&(t=e-1),t<0&&(t=0)),this.option.currentIndex=t},e.prototype.getCurrentIndex=function(){return this.option.currentIndex},e.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},e.prototype.setPlayState=function(t){this.option.autoPlay=!!t},e.prototype.getPlayState=function(){return!!this.option.autoPlay},e.prototype._initData=function(){var t,e=this.option,n=e.data||[],i=e.axisType,r=this._names=[];"category"===i?(t=[],E(n,(function(e,n){var i,o=Ro(Ao(e),"");q(e)?(i=T(e)).value=n:i=n,t.push(i),r.push(o)}))):t=n;var o={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new cx([{name:"value",type:o}],this)).initData(t,r)},e.prototype.getData=function(){return this._data},e.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},e.type="timeline",e.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},e}(zp),QV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline.slider",e.defaultOption=kc(JV.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),e}(JV);R(QV,_f.prototype);var tB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline",e}(Ag),eB=function(t){function e(e,n,i,r){var o=t.call(this,e,n,i)||this;return o.type=r||"value",o}return n(e,t),e.prototype.getLabelModel=function(){return this.model.getModel("label")},e.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},e}(ab),nB=Math.PI,iB=Vo(),rB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.api=e},e.prototype.render=function(t,e,n){if(this.model=t,this.api=n,this.ecModel=e,this.group.removeAll(),t.get("show",!0)){var i=this._layout(t,n),r=this._createGroup("_mainGroup"),o=this._createGroup("_labelGroup"),a=this._axis=this._createAxis(i,t);t.formatTooltip=function(t){return og("nameValue",{noName:!0,value:a.scale.getLabel({value:t})})},E(["AxisLine","AxisTick","Control","CurrentPointer"],(function(e){this["_render"+e](i,r,a,t)}),this),this._renderAxisLabel(i,o,a,t),this._position(i,t)}this._doPlayStop(),this._updateTicksStatus()},e.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},e.prototype.dispose=function(){this._clearTimer()},e.prototype._layout=function(t,e){var n,i,r,o,a=t.get(["label","position"]),s=t.get("orient"),l=function(t,e){return kp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()},t.get("padding"))}(t,e),u={horizontal:"center",vertical:(n=null==a||"auto"===a?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},h={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},c={horizontal:0,vertical:nB/2},p="vertical"===s?l.height:l.width,d=t.getModel("controlStyle"),f=d.get("show",!0),g=f?d.get("itemSize"):0,y=f?d.get("itemGap"):0,v=g+y,m=t.get(["label","rotate"])||0;m=m*nB/180;var x=d.get("position",!0),_=f&&d.get("showPlayBtn",!0),b=f&&d.get("showPrevBtn",!0),w=f&&d.get("showNextBtn",!0),S=0,M=p;"left"===x||"bottom"===x?(_&&(i=[0,0],S+=v),b&&(r=[S,0],S+=v),w&&(o=[M-g,0],M-=v)):(_&&(i=[M-g,0],M-=v),b&&(r=[0,0],S+=v),w&&(o=[M-g,0],M-=v));var I=[S,M];return t.get("inverse")&&I.reverse(),{viewRect:l,mainLength:p,orient:s,rotation:c[s],labelRotation:m,labelPosOpt:n,labelAlign:t.get(["label","align"])||u[s],labelBaseline:t.get(["label","verticalAlign"])||t.get(["label","baseline"])||h[s],playPosition:i,prevBtnPosition:r,nextBtnPosition:o,axisExtent:I,controlSize:g,controlGap:y}},e.prototype._position=function(t,e){var n=this._mainGroup,i=this._labelGroup,r=t.viewRect;if("vertical"===t.orient){var o=[1,0,0,1,0,0],a=r.x,s=r.y+r.height;we(o,o,[-a,-s]),Se(o,o,-nB/2),we(o,o,[a,s]),(r=r.clone()).applyTransform(o)}var l=y(r),u=y(n.getBoundingRect()),h=y(i.getBoundingRect()),c=[n.x,n.y],p=[i.x,i.y];p[0]=c[0]=l[0][0];var d,f=t.labelPosOpt;null==f||U(f)?(v(c,u,l,1,d="+"===f?0:1),v(p,h,l,1,1-d)):(v(c,u,l,1,d=f>=0?0:1),p[1]=c[1]+f);function g(t){t.originX=l[0][0]-t.x,t.originY=l[1][0]-t.y}function y(t){return[[t.x,t.x+t.width],[t.y,t.y+t.height]]}function v(t,e,n,i,r){t[i]+=n[i][r]-e[i][r]}n.setPosition(c),i.setPosition(p),n.rotation=i.rotation=t.rotation,g(n),g(i)},e.prototype._createAxis=function(t,e){var n=e.getData(),i=e.get("axisType"),r=function(t,e){if(e=e||t.get("type"),e)switch(e){case"category":return new Rx({ordinalMeta:t.getCategories(),extent:[1/0,-1/0]});case"time":return new Kx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new Ex}}(e,i);r.getTicks=function(){return n.mapArray(["value"],(function(t){return{value:t}}))};var o=n.getDataExtent("value");r.setExtent(o[0],o[1]),r.calcNiceTicks();var a=new eB("value",r,t.axisExtent,i);return a.model=e,a},e.prototype._createGroup=function(t){var e=this[t]=new Br;return this.group.add(e),e},e.prototype._renderAxisLine=function(t,e,n,i){var r=n.getExtent();if(i.get(["lineStyle","show"])){var o=new Ku({shape:{x1:r[0],y1:0,x2:r[1],y2:0},style:A({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});e.add(o);var a=this._progressLine=new Ku({shape:{x1:r[0],x2:this._currentPointer?this._currentPointer.x:r[0],y1:0,y2:0},style:k({lineCap:"round",lineWidth:o.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});e.add(a)}},e.prototype._renderAxisTick=function(t,e,n,i){var r=this,o=i.getData(),a=n.scale.getTicks();this._tickSymbols=[],E(a,(function(t){var a=n.dataToCoord(t.value),s=o.getItemModel(t.value),l=s.getModel("itemStyle"),u=s.getModel(["emphasis","itemStyle"]),h=s.getModel(["progress","itemStyle"]),c={x:a,y:0,onclick:W(r._changeTimeline,r,t.value)},p=oB(s,l,e,c);p.ensureState("emphasis").style=u.getItemStyle(),p.ensureState("progress").style=h.getItemStyle(),Ul(p);var d=rl(p);s.get("tooltip")?(d.dataIndex=t.value,d.dataModel=i):d.dataIndex=d.dataModel=null,r._tickSymbols.push(p)}))},e.prototype._renderAxisLabel=function(t,e,n,i){var r=this;if(n.getLabelModel().get("show")){var o=i.getData(),a=n.getViewLabels();this._tickLabels=[],E(a,(function(i){var a=i.tickValue,s=o.getItemModel(a),l=s.getModel("label"),u=s.getModel(["emphasis","label"]),h=s.getModel(["progress","label"]),c=n.dataToCoord(i.tickValue),p=new Xs({x:c,y:0,rotation:t.labelRotation-t.rotation,onclick:W(r._changeTimeline,r,a),silent:!1,style:oc(l,{text:i.formattedLabel,align:t.labelAlign,verticalAlign:t.labelBaseline})});p.ensureState("emphasis").style=oc(u),p.ensureState("progress").style=oc(h),e.add(p),Ul(p),iB(p).dataIndex=a,r._tickLabels.push(p)}))}},e.prototype._renderControl=function(t,e,n,i){var r=t.controlSize,o=t.rotation,a=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),u=i.get("inverse",!0);function h(t,n,l,u){if(t){var h=Cr(rt(i.get(["controlStyle",n+"BtnSize"]),r),r),c=function(t,e,n,i){var r=i.style,o=Uh(t.get(["controlStyle",e]),i||{},new ze(n[0],n[1],n[2],n[3]));r&&o.setStyle(r);return o}(i,n+"Icon",[0,-h/2,h,h],{x:t[0],y:t[1],originX:r/2,originY:0,rotation:u?-o:0,rectHover:!0,style:a,onclick:l});c.ensureState("emphasis").style=s,e.add(c),Ul(c)}}h(t.nextBtnPosition,"next",W(this._changeTimeline,this,u?"-":"+")),h(t.prevBtnPosition,"prev",W(this._changeTimeline,this,u?"+":"-")),h(t.playPosition,l?"stop":"play",W(this._handlePlayClick,this,!l),!0)},e.prototype._renderCurrentPointer=function(t,e,n,i){var r=i.getData(),o=i.getCurrentIndex(),a=r.getItemModel(o).getModel("checkpointStyle"),s=this,l={onCreate:function(t){t.draggable=!0,t.drift=W(s._handlePointerDrag,s),t.ondragend=W(s._handlePointerDragend,s),aB(t,s._progressLine,o,n,i,!0)},onUpdate:function(t){aB(t,s._progressLine,o,n,i)}};this._currentPointer=oB(a,a,this._mainGroup,{},this._currentPointer,l)},e.prototype._handlePlayClick=function(t){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:t,from:this.uid})},e.prototype._handlePointerDrag=function(t,e,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},e.prototype._handlePointerDragend=function(t){this._pointerChangeTimeline([t.offsetX,t.offsetY],!0)},e.prototype._pointerChangeTimeline=function(t,e){var n=this._toAxisCoord(t)[0],i=Qr(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(a[o]=+a[o].toFixed(c)),[a,h]}var vB={min:H(yB,"min"),max:H(yB,"max"),average:H(yB,"average"),median:H(yB,"median")};function mB(t,e){if(e){var n=t.getData(),i=t.coordinateSystem,r=i&&i.dimensions;if(!function(t){return!isNaN(parseFloat(t.x))&&!isNaN(parseFloat(t.y))}(e)&&!Y(e.coord)&&Y(r)){var o=xB(e,n,i,t);if((e=T(e)).type&&vB[e.type]&&o.baseAxis&&o.valueAxis){var a=P(r,o.baseAxis.dim),s=P(r,o.valueAxis.dim),l=vB[e.type](n,o.baseDataDim,o.valueDataDim,a,s);e.coord=l[0],e.value=l[1]}else e.coord=[null!=e.xAxis?e.xAxis:e.radiusAxis,null!=e.yAxis?e.yAxis:e.angleAxis]}if(null!=e.coord&&Y(r))for(var u=e.coord,h=0;h<2;h++)vB[u[h]]&&(u[h]=wB(n,n.mapDimension(r[h]),u[h]));else e.coord=[];return e}}function xB(t,e,n,i){var r={};return null!=t.valueIndex||null!=t.valueDim?(r.valueDataDim=null!=t.valueIndex?e.getDimension(t.valueIndex):t.valueDim,r.valueAxis=n.getAxis(function(t,e){var n=t.getData().getDimensionInfo(e);return n&&n.coordDim}(i,r.valueDataDim)),r.baseAxis=n.getOtherAxis(r.valueAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim)):(r.baseAxis=i.getBaseAxis(),r.valueAxis=n.getOtherAxis(r.baseAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim),r.valueDataDim=e.mapDimension(r.valueAxis.dim)),r}function _B(t,e){return!(t&&t.containData&&e.coord&&!gB(e))||t.containData(e.coord)}function bB(t,e){return t?function(t,n,i,r){return If(r<2?t.coord&&t.coord[r]:t.value,e[r])}:function(t,n,i,r){return If(t.value,e[r])}}function wB(t,e,n){if("average"===n){var i=0,r=0;return t.each(e,(function(t,e){isNaN(t)||(i+=t,r++)})),i/r}return"median"===n?t.getMedian(e):t.getDataExtent(e)["max"===n?1:0]}var SB=Vo(),MB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){this.markerGroupMap=yt()},e.prototype.render=function(t,e,n){var i=this,r=this.markerGroupMap;r.each((function(t){SB(t).keep=!1})),e.eachSeries((function(t){var r=dB.getMarkerModelFromSeries(t,i.type);r&&i.renderSeries(t,r,e,n)})),r.each((function(t){!SB(t).keep&&i.group.remove(t.group)}))},e.prototype.markKeep=function(t){SB(t).keep=!0},e.prototype.toggleBlurSeries=function(t,e){var n=this;E(t,(function(t){var i=dB.getMarkerModelFromSeries(t,n.type);i&&i.getData().eachItemGraphicEl((function(t){t&&(e?Nl(t):El(t))}))}))},e.type="marker",e}(Ag);function IB(t,e,n){var i=e.coordinateSystem;t.each((function(r){var o,a=t.getItemModel(r),s=$r(a.get("x"),n.getWidth()),l=$r(a.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(e.getMarkerPosition)o=e.getMarkerPosition(t.getValues(t.dimensions,r));else if(i){var u=t.get(i.dimensions[0],r),h=t.get(i.dimensions[1],r);o=i.dataToPoint([u,h])}}else o=[s,l];isNaN(s)||(o[0]=s),isNaN(l)||(o[1]=l),t.setItemLayout(r,o)}))}var TB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=dB.getMarkerModelFromSeries(t,"markPoint");e&&(IB(e.getData(),t,n),this.markerGroupMap.get(t.id).updateLayout())}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new xS),u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new cx(i,n),o=z(n.get("data"),H(mB,e));t&&(o=B(o,H(_B,t)));var a=bB(!!t,i);return r.initData(o,null,a),r}(r,t,e);e.setData(u),IB(e.getData(),t,i),u.each((function(t){var n=u.getItemModel(t),i=n.getShallow("symbol"),r=n.getShallow("symbolSize"),o=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(X(i)||X(r)||X(o)||X(s)){var h=e.getRawValue(t),c=e.getDataParams(t);X(i)&&(i=i(h,c)),X(r)&&(r=r(h,c)),X(o)&&(o=o(h,c)),X(s)&&(s=s(h,c))}var p=n.getModel("itemStyle").getItemStyle(),d=Ay(a,"color");p.fill||(p.fill=d),u.setItemVisual(t,{symbol:i,symbolSize:r,symbolRotate:o,symbolOffset:s,symbolKeepAspect:l,style:p})})),l.updateData(u),this.group.add(l.group),u.eachItemGraphicEl((function(t){t.traverse((function(t){rl(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markPoint",e}(MB);var CB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markLine",e.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},e}(dB),DB=Vo(),AB=function(t,e,n,i){var r,o=t.getData();if(Y(i))r=i;else{var a=i.type;if("min"===a||"max"===a||"average"===a||"median"===a||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=e.getAxis(null!=i.yAxis?"y":"x"),l=it(i.yAxis,i.xAxis);else{var u=xB(i,o,e,t);s=u.valueAxis,l=wB(o,xx(o,u.valueDataDim),a)}var h="x"===s.dim?0:1,c=1-h,p=T(i),d={coord:[]};p.type=null,p.coord=[],p.coord[c]=-1/0,d.coord[c]=1/0;var f=n.get("precision");f>=0&&j(l)&&(l=+l.toFixed(Math.min(f,20))),p.coord[h]=d.coord[h]=l,r=[p,d,{type:a,valueIndex:i.valueIndex,value:l}]}else r=[]}var g=[mB(t,r[0]),mB(t,r[1]),A({},r[2])];return g[2].type=g[2].type||null,C(g[2],g[0]),C(g[2],g[1]),g};function kB(t){return!isNaN(t)&&!isFinite(t)}function LB(t,e,n,i){var r=1-t,o=i.dimensions[t];return kB(e[r])&&kB(n[r])&&e[t]===n[t]&&i.getAxis(o).containData(e[t])}function PB(t,e){if("cartesian2d"===t.type){var n=e[0].coord,i=e[1].coord;if(n&&i&&(LB(1,n,i,t)||LB(0,n,i,t)))return!0}return _B(t,e[0])&&_B(t,e[1])}function OB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=$r(s.get("x"),r.getWidth()),u=$r(s.get("y"),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(t.dimensions,e));else{var h=a.dimensions,c=t.get(h[0],e),p=t.get(h[1],e);o=a.dataToPoint([c,p])}if(OS(a,"cartesian2d")){var d=a.getAxis("x"),f=a.getAxis("y");h=a.dimensions;kB(t.get(h[0],e))?o[0]=d.toGlobalCoord(d.getExtent()[n?0:1]):kB(t.get(h[1],e))&&(o[1]=f.toGlobalCoord(f.getExtent()[n?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];t.setItemLayout(e,o)}var RB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=dB.getMarkerModelFromSeries(t,"markLine");if(e){var i=e.getData(),r=DB(e).from,o=DB(e).to;r.each((function(e){OB(r,e,!0,t,n),OB(o,e,!1,t,n)})),i.each((function(t){i.setItemLayout(t,[r.getItemLayout(t),o.getItemLayout(t)])})),this.markerGroupMap.get(t.id).updateLayout()}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new YA);this.group.add(l.group);var u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new cx(i,n),o=new cx(i,n),a=new cx([],n),s=z(n.get("data"),H(AB,e,t,n));t&&(s=B(s,H(PB,t)));var l=bB(!!t,i);return r.initData(z(s,(function(t){return t[0]})),null,l),o.initData(z(s,(function(t){return t[1]})),null,l),a.initData(z(s,(function(t){return t[2]}))),a.hasItemOption=!0,{from:r,to:o,line:a}}(r,t,e),h=u.from,c=u.to,p=u.line;DB(e).from=h,DB(e).to=c,e.setData(p);var d=e.get("symbol"),f=e.get("symbolSize"),g=e.get("symbolRotate"),y=e.get("symbolOffset");function v(e,n,r){var o=e.getItemModel(n);OB(e,n,r,t,i);var s=o.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=Ay(a,"color")),e.setItemVisual(n,{symbolKeepAspect:o.get("symbolKeepAspect"),symbolOffset:rt(o.get("symbolOffset",!0),y[r?0:1]),symbolRotate:rt(o.get("symbolRotate",!0),g[r?0:1]),symbolSize:rt(o.get("symbolSize"),f[r?0:1]),symbol:rt(o.get("symbol",!0),d[r?0:1]),style:s})}Y(d)||(d=[d,d]),Y(f)||(f=[f,f]),Y(g)||(g=[g,g]),Y(y)||(y=[y,y]),u.from.each((function(t){v(h,t,!0),v(c,t,!1)})),p.each((function(t){var e=p.getItemModel(t).getModel("lineStyle").getLineStyle();p.setItemLayout(t,[h.getItemLayout(t),c.getItemLayout(t)]),null==e.stroke&&(e.stroke=h.getItemVisual(t,"style").fill),p.setItemVisual(t,{fromSymbolKeepAspect:h.getItemVisual(t,"symbolKeepAspect"),fromSymbolOffset:h.getItemVisual(t,"symbolOffset"),fromSymbolRotate:h.getItemVisual(t,"symbolRotate"),fromSymbolSize:h.getItemVisual(t,"symbolSize"),fromSymbol:h.getItemVisual(t,"symbol"),toSymbolKeepAspect:c.getItemVisual(t,"symbolKeepAspect"),toSymbolOffset:c.getItemVisual(t,"symbolOffset"),toSymbolRotate:c.getItemVisual(t,"symbolRotate"),toSymbolSize:c.getItemVisual(t,"symbolSize"),toSymbol:c.getItemVisual(t,"symbol"),style:e})})),l.updateData(p),u.line.eachItemGraphicEl((function(t){rl(t).dataModel=e,t.traverse((function(t){rl(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markLine",e}(MB);var NB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markArea",e.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}},e}(dB),EB=Vo(),zB=function(t,e,n,i){var r=i[0],o=i[1];if(r&&o){var a=mB(t,r),s=mB(t,o),l=a.coord,u=s.coord;l[0]=it(l[0],-1/0),l[1]=it(l[1],-1/0),u[0]=it(u[0],1/0),u[1]=it(u[1],1/0);var h=D([{},a,s]);return h.coord=[a.coord,s.coord],h.x0=a.x,h.y0=a.y,h.x1=s.x,h.y1=s.y,h}};function VB(t){return!isNaN(t)&&!isFinite(t)}function BB(t,e,n,i){var r=1-t;return VB(e[r])&&VB(n[r])}function FB(t,e){var n=e.coord[0],i=e.coord[1],r={coord:n,x:e.x0,y:e.y0},o={coord:i,x:e.x1,y:e.y1};return OS(t,"cartesian2d")?!(!n||!i||!BB(1,n,i)&&!BB(0,n,i))||function(t,e,n){return!(t&&t.containZone&&e.coord&&n.coord&&!gB(e)&&!gB(n))||t.containZone(e.coord,n.coord)}(t,r,o):_B(t,r)||_B(t,o)}function GB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=$r(s.get(n[0]),r.getWidth()),u=$r(s.get(n[1]),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition){var h=t.getValues(["x0","y0"],e),c=t.getValues(["x1","y1"],e),p=a.clampData(h),d=a.clampData(c),f=[];"x0"===n[0]?f[0]=p[0]>d[0]?c[0]:h[0]:f[0]=p[0]>d[0]?h[0]:c[0],"y0"===n[1]?f[1]=p[1]>d[1]?c[1]:h[1]:f[1]=p[1]>d[1]?h[1]:c[1],o=i.getMarkerPosition(f,n,!0)}else{var g=[m=t.get(n[0],e),x=t.get(n[1],e)];a.clampData&&a.clampData(g,g),o=a.dataToPoint(g,!0)}if(OS(a,"cartesian2d")){var y=a.getAxis("x"),v=a.getAxis("y"),m=t.get(n[0],e),x=t.get(n[1],e);VB(m)?o[0]=y.toGlobalCoord(y.getExtent()["x0"===n[0]?0:1]):VB(x)&&(o[1]=v.toGlobalCoord(v.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];return o}var WB=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]],HB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=dB.getMarkerModelFromSeries(t,"markArea");if(e){var i=e.getData();i.each((function(e){var r=z(WB,(function(r){return GB(i,e,r,t,n)}));i.setItemLayout(e,r),i.getItemGraphicEl(e).setShape("points",r)}))}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,{group:new Br});this.group.add(l.group),this.markKeep(l);var u=function(t,e,n){var i,r,o=["x0","y0","x1","y1"];if(t){var a=z(t&&t.dimensions,(function(t){var n=e.getData();return A(A({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})}));r=z(o,(function(t,e){return{name:t,type:a[e%2].type}})),i=new cx(r,n)}else i=new cx(r=[{name:"value",type:"float"}],n);var s=z(n.get("data"),H(zB,e,t,n));t&&(s=B(s,H(FB,t)));var l=t?function(t,e,n,i){return If(t.coord[Math.floor(i/2)][i%2],r[i])}:function(t,e,n,i){return If(t.value,r[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(r,t,e);e.setData(u),u.each((function(e){var n=z(WB,(function(n){return GB(u,e,n,t,i)})),o=r.getAxis("x").scale,s=r.getAxis("y").scale,l=o.getExtent(),h=s.getExtent(),c=[o.parse(u.get("x0",e)),o.parse(u.get("x1",e))],p=[s.parse(u.get("y0",e)),s.parse(u.get("y1",e))];Qr(c),Qr(p);var d=!!(l[0]>c[1]||l[1]p[1]||h[1]=0},e.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},e}(zp),XB=H,UB=E,ZB=Br,jB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._contentGroup=new ZB),this.group.add(this._selectorGroup=new ZB),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,e,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),t.get("show",!0)){var r=t.get("align"),o=t.get("orient");r&&"auto"!==r||(r="right"===t.get("left")&&"vertical"===o?"right":"left");var a=t.get("selector",!0),s=t.get("selectorPosition",!0);!a||s&&"auto"!==s||(s="horizontal"===o?"end":"start"),this.renderInner(r,t,e,n,a,o,s);var l=t.getBoxLayoutParams(),u={width:n.getWidth(),height:n.getHeight()},h=t.get("padding"),c=kp(l,u,h),p=this.layoutInner(t,r,c,i,a,s),d=kp(k({width:p.width,height:p.height},l),u,h);this.group.x=d.x-p.x,this.group.y=d.y-p.y,this.group.markRedraw(),this.group.add(this._backgroundEl=Sz(p,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,e,n,i,r,o,a){var s=this.getContentGroup(),l=yt(),u=e.get("selectedMode"),h=[];n.eachRawSeries((function(t){!t.get("legendHoverLink")&&h.push(t.id)})),UB(e.getData(),(function(r,o){var a=r.get("name");if(!this.newlineDisabled&&(""===a||"\n"===a)){var c=new ZB;return c.newline=!0,void s.add(c)}var p=n.getSeriesByName(a)[0];if(!l.get(a)){if(p){var d=p.getData(),f=d.getVisual("legendLineStyle")||{},g=d.getVisual("legendIcon"),y=d.getVisual("style"),v=this._createItem(p,a,o,r,e,t,f,y,g,u,i);v.on("click",XB(qB,a,null,i,h)).on("mouseover",XB($B,p.name,null,i,h)).on("mouseout",XB(JB,p.name,null,i,h)),n.ssr&&v.eachChild((function(t){var e=rl(t);e.seriesIndex=p.seriesIndex,e.dataIndex=o,e.ssrType="legend"})),l.set(a,!0)}else n.eachRawSeries((function(s){if(!l.get(a)&&s.legendVisualProvider){var c=s.legendVisualProvider;if(!c.containName(a))return;var p=c.indexOfName(a),d=c.getItemVisual(p,"style"),f=c.getItemVisual(p,"legendIcon"),g=qn(d.fill);g&&0===g[3]&&(g[3]=.2,d=A(A({},d),{fill:ri(g,"rgba")}));var y=this._createItem(s,a,o,r,e,t,{},d,f,u,i);y.on("click",XB(qB,null,a,i,h)).on("mouseover",XB($B,null,a,i,h)).on("mouseout",XB(JB,null,a,i,h)),n.ssr&&y.eachChild((function(t){var e=rl(t);e.seriesIndex=s.seriesIndex,e.dataIndex=o,e.ssrType="legend"})),l.set(a,!0)}}),this);0}}),this),r&&this._createSelector(r,e,i,o,a)},e.prototype._createSelector=function(t,e,n,i,r){var o=this.getSelectorGroup();UB(t,(function(t){var i=t.type,r=new Xs({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});o.add(r),ic(r,{normal:e.getModel("selectorLabel"),emphasis:e.getModel(["emphasis","selectorLabel"])},{defaultText:t.title}),Ul(r)}))},e.prototype._createItem=function(t,e,n,i,r,o,a,s,l,u,h){var c=t.visualDrawType,p=r.get("itemWidth"),d=r.get("itemHeight"),f=r.isSelected(e),g=i.get("symbolRotate"),y=i.get("symbolKeepAspect"),v=i.get("icon"),m=function(t,e,n,i,r,o,a){function s(t,e){"auto"===t.lineWidth&&(t.lineWidth=e.lineWidth>0?2:0),UB(t,(function(n,i){"inherit"===t[i]&&(t[i]=e[i])}))}var l=e.getModel("itemStyle"),u=l.getItemStyle(),h=0===t.lastIndexOf("empty",0)?"fill":"stroke",c=l.getShallow("decal");u.decal=c&&"inherit"!==c?mv(c,a):i.decal,"inherit"===u.fill&&(u.fill=i[r]);"inherit"===u.stroke&&(u.stroke=i[h]);"inherit"===u.opacity&&(u.opacity=("fill"===r?i:n).opacity);s(u,i);var p=e.getModel("lineStyle"),d=p.getLineStyle();if(s(d,n),"auto"===u.fill&&(u.fill=i.fill),"auto"===u.stroke&&(u.stroke=i.fill),"auto"===d.stroke&&(d.stroke=i.fill),!o){var f=e.get("inactiveBorderWidth"),g=u[h];u.lineWidth="auto"===f?i.lineWidth>0&&g?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),d.stroke=p.get("inactiveColor"),d.lineWidth=p.get("inactiveWidth")}return{itemStyle:u,lineStyle:d}}(l=v||l||"roundRect",i,a,s,c,f,h),x=new ZB,_=i.getModel("textStyle");if(!X(t.getLegendIcon)||v&&"inherit"!==v){var b="inherit"===v&&t.getData().getVisual("symbol")?"inherit"===g?t.getData().getVisual("symbolRotate"):g:0;x.add(function(t){var e=t.icon||"roundRect",n=Xy(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill,t.symbolKeepAspect);n.setStyle(t.itemStyle),n.rotation=(t.iconRotate||0)*Math.PI/180,n.setOrigin([t.itemWidth/2,t.itemHeight/2]),e.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2);return n}({itemWidth:p,itemHeight:d,icon:l,iconRotate:b,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}))}else x.add(t.getLegendIcon({itemWidth:p,itemHeight:d,icon:l,iconRotate:g,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}));var w="left"===o?p+5:-5,S=o,M=r.get("formatter"),I=e;U(M)&&M?I=M.replace("{name}",null!=e?e:""):X(M)&&(I=M(e));var T=f?_.getTextColor():i.get("inactiveColor");x.add(new Xs({style:oc(_,{text:I,x:w,y:d/2,fill:T,align:S,verticalAlign:"middle"},{inheritColor:T})}));var C=new Ws({shape:x.getBoundingRect(),style:{fill:"transparent"}}),D=i.getModel("tooltip");return D.get("show")&&Kh({el:C,componentModel:r,itemName:e,itemTooltipOption:D.option}),x.add(C),x.eachChild((function(t){t.silent=!0})),C.silent=!u,this.getContentGroup().add(x),Ul(x),x.__legendDataIndex=n,x},e.prototype.layoutInner=function(t,e,n,i,r,o){var a=this.getContentGroup(),s=this.getSelectorGroup();Ap(t.get("orient"),a,t.get("itemGap"),n.width,n.height);var l=a.getBoundingRect(),u=[-l.x,-l.y];if(s.markRedraw(),a.markRedraw(),r){Ap("horizontal",s,t.get("selectorItemGap",!0));var h=s.getBoundingRect(),c=[-h.x,-h.y],p=t.get("selectorButtonGap",!0),d=t.getOrient().index,f=0===d?"width":"height",g=0===d?"height":"width",y=0===d?"y":"x";"end"===o?c[d]+=l[f]+p:u[d]+=h[f]+p,c[1-d]+=l[g]/2-h[g]/2,s.x=c[0],s.y=c[1],a.x=u[0],a.y=u[1];var v={x:0,y:0};return v[f]=l[f]+p+h[f],v[g]=Math.max(l[g],h[g]),v[y]=Math.min(0,h[y]+c[1-d]),v}return a.x=u[0],a.y=u[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e}(Ag);function qB(t,e,n,i){JB(t,e,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=t?t:e}),$B(t,e,n,i)}function KB(t){for(var e,n=t.getZr().storage.getDisplayList(),i=0,r=n.length;in[r],f=[-c.x,-c.y];e||(f[i]=l[s]);var g=[0,0],y=[-p.x,-p.y],v=rt(t.get("pageButtonGap",!0),t.get("itemGap",!0));d&&("end"===t.get("pageButtonPosition",!0)?y[i]+=n[r]-p[r]:g[i]+=p[r]+v);y[1-i]+=c[o]/2-p[o]/2,l.setPosition(f),u.setPosition(g),h.setPosition(y);var m={x:0,y:0};if(m[r]=d?n[r]:c[r],m[o]=Math.max(c[o],p[o]),m[a]=Math.min(0,p[a]+y[1-i]),u.__rectSize=n[r],d){var x={x:0,y:0};x[r]=Math.max(n[r]-p[r]-v,0),x[o]=m[o],u.setClipPath(new Ws({shape:x})),u.__rectSize=x[r]}else h.eachChild((function(t){t.attr({invisible:!0,silent:!0})}));var _=this._getPageInfo(t);return null!=_.pageIndex&&vh(l,{x:_.contentPosition[0],y:_.contentPosition[1]},d?t:null),this._updatePageInfoView(t,_),m},e.prototype._pageGo=function(t,e,n){var i=this._getPageInfo(e)[t];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:e.id})},e.prototype._updatePageInfoView=function(t,e){var n=this._controllerGroup;E(["pagePrev","pageNext"],(function(i){var r=null!=e[i+"DataIndex"],o=n.childOfName(i);o&&(o.setStyle("fill",r?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),o.cursor=r?"pointer":"default")}));var i=n.childOfName("pageText"),r=t.get("pageFormatter"),o=e.pageIndex,a=null!=o?o+1:0,s=e.pageCount;i&&r&&i.setStyle("text",U(r)?r.replace("{current}",null==a?"":a+"").replace("{total}",null==s?"":s+""):r({current:a,total:s}))},e.prototype._getPageInfo=function(t){var e=t.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,r=t.getOrient().index,o=oF[r],a=aF[r],s=this._findTargetItemIndex(e),l=n.children(),u=l[s],h=l.length,c=h?1:0,p={contentPosition:[n.x,n.y],pageCount:c,pageIndex:c-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!u)return p;var d=m(u);p.contentPosition[r]=-d.s;for(var f=s+1,g=d,y=d,v=null;f<=h;++f)(!(v=m(l[f]))&&y.e>g.s+i||v&&!x(v,g.s))&&(g=y.i>g.i?y:v)&&(null==p.pageNextDataIndex&&(p.pageNextDataIndex=g.i),++p.pageCount),y=v;for(f=s-1,g=d,y=d,v=null;f>=-1;--f)(v=m(l[f]))&&x(y,v.s)||!(g.i=e&&t.s<=e+i}},e.prototype._findTargetItemIndex=function(t){return this._showController?(this.getContentGroup().eachChild((function(i,r){var o=i.__legendDataIndex;null==n&&null!=o&&(n=r),o===t&&(e=r)})),null!=e?e:n):0;var e,n},e.type="legend.scroll",e}(jB);function lF(t){Vm(eF),t.registerComponentModel(nF),t.registerComponentView(sF),function(t){t.registerAction("legendScroll","legendscroll",(function(t,e){var n=t.scrollDataIndex;null!=n&&e.eachComponent({mainType:"legend",subType:"scroll",query:t},(function(t){t.setScrollDataIndex(n)}))}))}(t)}var uF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.inside",e.defaultOption=kc(az.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),e}(az),hF=Vo();function cF(t,e,n){hF(t).coordSysRecordMap.each((function(t){var i=t.dataZoomInfoMap.get(e.uid);i&&(i.getRange=n)}))}function pF(t,e){if(e){t.removeKey(e.model.uid);var n=e.controller;n&&n.dispose()}}function dF(t,e){t.isDisposed()||t.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:e})}function fF(t,e,n,i){return t.coordinateSystem.containPoint([n,i])}function gF(t){t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,(function(t,e){var n=hF(e),i=n.coordSysRecordMap||(n.coordSysRecordMap=yt());i.each((function(t){t.dataZoomInfoMap=null})),t.eachComponent({mainType:"dataZoom",subType:"inside"},(function(t){E(rz(t).infoList,(function(n){var r=n.model.uid,o=i.get(r)||i.set(r,function(t,e){var n={model:e,containsPoint:H(fF,e),dispatchAction:H(dF,t),dataZoomInfoMap:null,controller:null},i=n.controller=new nT(t.getZr());return E(["pan","zoom","scrollMove"],(function(t){i.on(t,(function(e){var i=[];n.dataZoomInfoMap.each((function(r){if(e.isAvailableBehavior(r.model.option)){var o=(r.getRange||{})[t],a=o&&o(r.dzReferCoordSysInfo,n.model.mainType,n.controller,e);!r.model.get("disabled",!0)&&a&&i.push({dataZoomId:r.model.id,start:a[0],end:a[1]})}})),i.length&&n.dispatchAction(i)}))})),n}(e,n.model));(o.dataZoomInfoMap||(o.dataZoomInfoMap=yt())).set(t.uid,{dzReferCoordSysInfo:n,model:t,getRange:null})}))})),i.each((function(t){var e,n=t.controller,r=t.dataZoomInfoMap;if(r){var o=r.keys()[0];null!=o&&(e=r.get(o))}if(e){var a=function(t){var e,n="type_",i={type_true:2,type_move:1,type_false:0,type_undefined:-1},r=!0;return t.each((function(t){var o=t.model,a=!o.get("disabled",!0)&&(!o.get("zoomLock",!0)||"move");i[n+a]>i[n+e]&&(e=a),r=r&&o.get("preventDefaultMouseMove",!0)})),{controlType:e,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!r}}}(r);n.enable(a.controlType,a.opt),n.setPointerChecker(t.containsPoint),Hg(t,"dispatchAction",e.model.get("throttle",!0),"fixRate")}else pF(i,t)}))}))}var yF=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="dataZoom.inside",e}return n(e,t),e.prototype.render=function(e,n,i){t.prototype.render.apply(this,arguments),e.noTarget()?this._clear():(this.range=e.getPercentRange(),cF(i,e,{pan:W(vF.pan,this),zoom:W(vF.zoom,this),scrollMove:W(vF.scrollMove,this)}))},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){!function(t,e){for(var n=hF(t).coordSysRecordMap,i=n.keys(),r=0;r0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(o[1]-o[0])+o[0],u=Math.max(1/i.scale,0);o[0]=(o[0]-l)*u+l,o[1]=(o[1]-l)*u+l;var h=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return zk(0,o,[0,100],0,h.minSpan,h.maxSpan),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}},pan:mF((function(t,e,n,i,r,o){var a=xF[i]([o.oldX,o.oldY],[o.newX,o.newY],e,r,n);return a.signal*(t[1]-t[0])*a.pixel/a.pixelLength})),scrollMove:mF((function(t,e,n,i,r,o){return xF[i]([0,0],[o.scrollDelta,o.scrollDelta],e,r,n).signal*(t[1]-t[0])*o.scrollDelta}))};function mF(t){return function(e,n,i,r){var o=this.range,a=o.slice(),s=e.axisModels[0];if(s)return zk(t(a,s,e,n,i,r),a,[0,100],"all"),this.range=a,o[0]!==a[0]||o[1]!==a[1]?a:void 0}}var xF={grid:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem.getRect();return t=t||[0,0],"x"===o.dim?(a.pixel=e[0]-t[0],a.pixelLength=s.width,a.pixelStart=s.x,a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=s.height,a.pixelStart=s.y,a.signal=o.inverse?-1:1),a},polar:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return t=t?s.pointToCoord(t):[0,0],e=s.pointToCoord(e),"radiusAxis"===n.mainType?(a.pixel=e[0]-t[0],a.pixelLength=l[1]-l[0],a.pixelStart=l[0],a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=u[1]-u[0],a.pixelStart=u[0],a.signal=o.inverse?-1:1),a},singleAxis:function(t,e,n,i,r){var o=n.axis,a=r.model.coordinateSystem.getRect(),s={};return t=t||[0,0],"horizontal"===o.orient?(s.pixel=e[0]-t[0],s.pixelLength=a.width,s.pixelStart=a.x,s.signal=o.inverse?1:-1):(s.pixel=e[1]-t[1],s.pixelLength=a.height,s.pixelStart=a.y,s.signal=o.inverse?-1:1),s}};function _F(t){yz(t),t.registerComponentModel(uF),t.registerComponentView(yF),gF(t)}var bF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.slider",e.layoutMode="box",e.defaultOption=kc(az.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),e}(az),wF=Ws,SF="horizontal",MF="vertical",IF=["line","bar","candlestick","scatter"],TF={easing:"cubicOut",duration:100,delay:0},CF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._displayables={},n}return n(e,t),e.prototype.init=function(t,e){this.api=e,this._onBrush=W(this._onBrush,this),this._onBrushEnd=W(this._onBrushEnd,this)},e.prototype.render=function(e,n,i,r){if(t.prototype.render.apply(this,arguments),Hg(this,"_dispatchZoomAction",e.get("throttle"),"fixRate"),this._orient=e.getOrient(),!1!==e.get("show")){if(e.noTarget())return this._clear(),void this.group.removeAll();r&&"dataZoom"===r.type&&r.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){Yg(this,"_dispatchZoomAction");var t=this.api.getZr();t.off("mousemove",this._onBrush),t.off("mouseup",this._onBrushEnd)},e.prototype._buildView=function(){var t=this.group;t.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var e=this._displayables.sliderGroup=new Br;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(e),this._positionGroup()},e.prototype._resetLocation=function(){var t=this.dataZoomModel,e=this.api,n=t.get("brushSelect")?7:0,i=this._findCoordRect(),r={width:e.getWidth(),height:e.getHeight()},o=this._orient===SF?{right:r.width-i.x-i.width,top:r.height-30-7-n,width:i.width,height:30}:{right:7,top:i.y,width:30,height:i.height},a=Rp(t.option);E(["right","top","width","height"],(function(t){"ph"===a[t]&&(a[t]=o[t])}));var s=kp(a,r);this._location={x:s.x,y:s.y},this._size=[s.width,s.height],this._orient===MF&&this._size.reverse()},e.prototype._positionGroup=function(){var t=this.group,e=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),r=i&&i.get("inverse"),o=this._displayables.sliderGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;o.attr(n!==SF||r?n===SF&&r?{scaleY:a?1:-1,scaleX:-1}:n!==MF||r?{scaleY:a?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:a?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:a?1:-1,scaleX:1});var s=t.getBoundingRect([o]);t.x=e.x-s.x,t.y=e.y-s.y,t.markRedraw()},e.prototype._getViewExtent=function(){return[0,this._size[0]]},e.prototype._renderBackground=function(){var t=this.dataZoomModel,e=this._size,n=this._displayables.sliderGroup,i=t.get("brushSelect");n.add(new wF({silent:!0,shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:t.get("backgroundColor")},z2:-40}));var r=new wF({shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:"transparent"},z2:0,onclick:W(this._onClickPanel,this)}),o=this.api.getZr();i?(r.on("mousedown",this._onBrushStart,this),r.cursor="crosshair",o.on("mousemove",this._onBrush),o.on("mouseup",this._onBrushEnd)):(o.off("mousemove",this._onBrush),o.off("mouseup",this._onBrushEnd)),n.add(r)},e.prototype._renderDataShadow=function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],t){var e=this._size,n=this._shadowSize||[],i=t.series,r=i.getRawData(),o=i.getShadowDim&&i.getShadowDim(),a=o&&r.getDimensionInfo(o)?i.getShadowDim():t.otherDim;if(null!=a){var s=this._shadowPolygonPts,l=this._shadowPolylinePts;if(r!==this._shadowData||a!==this._shadowDim||e[0]!==n[0]||e[1]!==n[1]){var u=r.getDataExtent(a),h=.3*(u[1]-u[0]);u=[u[0]-h,u[1]+h];var c,p=[0,e[1]],d=[0,e[0]],f=[[e[0],0],[0,0]],g=[],y=d[1]/(r.count()-1),v=0,m=Math.round(r.count()/e[0]);r.each([a],(function(t,e){if(m>0&&e%m)v+=y;else{var n=null==t||isNaN(t)||""===t,i=n?0:Kr(t,u,p,!0);n&&!c&&e?(f.push([f[f.length-1][0],0]),g.push([g[g.length-1][0],0])):!n&&c&&(f.push([v,0]),g.push([v,0])),f.push([v,i]),g.push([v,i]),v+=y,c=n}})),s=this._shadowPolygonPts=f,l=this._shadowPolylinePts=g}this._shadowData=r,this._shadowDim=a,this._shadowSize=[e[0],e[1]];for(var x=this.dataZoomModel,_=0;_<3;_++){var b=w(1===_);this._displayables.sliderGroup.add(b),this._displayables.dataShadowSegs.push(b)}}}function w(t){var e=x.getModel(t?"selectedDataBackground":"dataBackground"),n=new Br,i=new Xu({shape:{points:s},segmentIgnoreThreshold:1,style:e.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),r=new Zu({shape:{points:l},segmentIgnoreThreshold:1,style:e.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(r),n}},e.prototype._prepareDataShadowInfo=function(){var t=this.dataZoomModel,e=t.get("showDataShadow");if(!1!==e){var n,i=this.ecModel;return t.eachTargetAxis((function(r,o){E(t.getAxisProxy(r,o).getTargetSeriesModels(),(function(t){if(!(n||!0!==e&&P(IF,t.get("type"))<0)){var a,s=i.getComponent(nz(r),o).axis,l=function(t){var e={x:"y",y:"x",radius:"angle",angle:"radius"};return e[t]}(r),u=t.coordinateSystem;null!=l&&u.getOtherAxis&&(a=u.getOtherAxis(s).inverse),l=t.getData().mapDimension(l),n={thisAxis:s,series:t,thisDim:r,otherDim:l,otherAxisInverse:a}}}),this)}),this),n}},e.prototype._renderHandle=function(){var t=this.group,e=this._displayables,n=e.handles=[null,null],i=e.handleLabels=[null,null],r=this._displayables.sliderGroup,o=this._size,a=this.dataZoomModel,s=this.api,l=a.get("borderRadius")||0,u=a.get("brushSelect"),h=e.filler=new wF({silent:u,style:{fill:a.get("fillerColor")},textConfig:{position:"inside"}});r.add(h),r.add(new wF({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:o[0],height:o[1],r:l},style:{stroke:a.get("dataBackgroundColor")||a.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}})),E([0,1],(function(e){var o=a.get("handleIcon");!Wy[o]&&o.indexOf("path://")<0&&o.indexOf("image://")<0&&(o="path://"+o);var s=Xy(o,-1,0,2,2,null,!0);s.attr({cursor:DF(this._orient),draggable:!0,drift:W(this._onDragMove,this,e),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1),z2:5});var l=s.getBoundingRect(),u=a.get("handleSize");this._handleHeight=$r(u,this._size[1]),this._handleWidth=l.width/l.height*this._handleHeight,s.setStyle(a.getModel("handleStyle").getItemStyle()),s.style.strokeNoScale=!0,s.rectHover=!0,s.ensureState("emphasis").style=a.getModel(["emphasis","handleStyle"]).getItemStyle(),Ul(s);var h=a.get("handleColor");null!=h&&(s.style.fill=h),r.add(n[e]=s);var c=a.getModel("textStyle");t.add(i[e]=new Xs({silent:!0,invisible:!0,style:oc(c,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:c.getTextColor(),font:c.getFont()}),z2:10}))}),this);var c=h;if(u){var p=$r(a.get("moveHandleSize"),o[1]),d=e.moveHandle=new Ws({style:a.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:o[1]-.5,height:p}}),f=.8*p,g=e.moveHandleIcon=Xy(a.get("moveHandleIcon"),-f/2,-f/2,f,f,"#fff",!0);g.silent=!0,g.y=o[1]+p/2-.5,d.ensureState("emphasis").style=a.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var y=Math.min(o[1]/2,Math.max(p,10));(c=e.moveZone=new Ws({invisible:!0,shape:{y:o[1]-y,height:p+y}})).on("mouseover",(function(){s.enterEmphasis(d)})).on("mouseout",(function(){s.leaveEmphasis(d)})),r.add(d),r.add(g),r.add(c)}c.attr({draggable:!0,cursor:DF(this._orient),drift:W(this._onDragMove,this,"all"),ondragstart:W(this._showDataInfo,this,!0),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1)})},e.prototype._resetInterval=function(){var t=this._range=this.dataZoomModel.getPercentRange(),e=this._getViewExtent();this._handleEnds=[Kr(t[0],[0,100],e,!0),Kr(t[1],[0,100],e,!0)]},e.prototype._updateInterval=function(t,e){var n=this.dataZoomModel,i=this._handleEnds,r=this._getViewExtent(),o=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];zk(e,i,r,n.get("zoomLock")?"all":t,null!=o.minSpan?Kr(o.minSpan,a,r,!0):null,null!=o.maxSpan?Kr(o.maxSpan,a,r,!0):null);var s=this._range,l=this._range=Qr([Kr(i[0],r,a,!0),Kr(i[1],r,a,!0)]);return!s||s[0]!==l[0]||s[1]!==l[1]},e.prototype._updateView=function(t){var e=this._displayables,n=this._handleEnds,i=Qr(n.slice()),r=this._size;E([0,1],(function(t){var i=e.handles[t],o=this._handleHeight;i.attr({scaleX:o/2,scaleY:o/2,x:n[t]+(t?-1:1),y:r[1]/2-o/2})}),this),e.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:r[1]});var o={x:i[0],width:i[1]-i[0]};e.moveHandle&&(e.moveHandle.setShape(o),e.moveZone.setShape(o),e.moveZone.getBoundingRect(),e.moveHandleIcon&&e.moveHandleIcon.attr("x",o.x+o.width/2));for(var a=e.dataShadowSegs,s=[0,i[0],i[1],r[0]],l=0;le[0]||n[1]<0||n[1]>e[1])){var i=this._handleEnds,r=(i[0]+i[1])/2,o=this._updateInterval("all",n[0]-r);this._updateView(),o&&this._dispatchZoomAction(!1)}},e.prototype._onBrushStart=function(t){var e=t.offsetX,n=t.offsetY;this._brushStart=new De(e,n),this._brushing=!0,this._brushStartTime=+new Date},e.prototype._onBrushEnd=function(t){if(this._brushing){var e=this._displayables.brushRect;if(this._brushing=!1,e){e.attr("ignore",!0);var n=e.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),r=[0,100];this._range=Qr([Kr(n.x,i,r,!0),Kr(n.x+n.width,i,r,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},e.prototype._onBrush=function(t){this._brushing&&(de(t.event),this._updateBrushRect(t.offsetX,t.offsetY))},e.prototype._updateBrushRect=function(t,e){var n=this._displayables,i=this.dataZoomModel,r=n.brushRect;r||(r=n.brushRect=new wF({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(r)),r.attr("ignore",!1);var o=this._brushStart,a=this._displayables.sliderGroup,s=a.transformCoordToLocal(t,e),l=a.transformCoordToLocal(o.x,o.y),u=this._size;s[0]=Math.max(Math.min(u[0],s[0]),0),r.setShape({x:l[0],y:0,width:s[0]-l[0],height:u[1]})},e.prototype._dispatchZoomAction=function(t){var e=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:t?TF:null,start:e[0],end:e[1]})},e.prototype._findCoordRect=function(){var t,e=rz(this.dataZoomModel).infoList;if(!t&&e.length){var n=e[0].model.coordinateSystem;t=n.getRect&&n.getRect()}if(!t){var i=this.api.getWidth(),r=this.api.getHeight();t={x:.2*i,y:.2*r,width:.6*i,height:.6*r}}return t},e.type="dataZoom.slider",e}(uz);function DF(t){return"vertical"===t?"ns-resize":"ew-resize"}function AF(t){t.registerComponentModel(bF),t.registerComponentView(CF),yz(t)}var kF=function(t,e,n){var i=T((LF[t]||{})[e]);return n&&Y(i)?i[i.length-1]:i},LF={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},PF=kD.mapVisual,OF=kD.eachVisual,RF=Y,NF=E,EF=Qr,zF=Kr,VF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n)},e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&PV(n,t,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},e.prototype.resetVisual=function(t){var e=this.stateList;t=W(t,this),this.controllerVisuals=LV(this.option.controller,e,t),this.targetVisuals=LV(this.option.target,e,t)},e.prototype.getItemSymbol=function(){return null},e.prototype.getTargetSeriesIndices=function(){var t=this.option.seriesIndex,e=[];return null==t||"all"===t?this.ecModel.eachSeries((function(t,n){e.push(n)})):e=To(t),e},e.prototype.eachTargetSeries=function(t,e){E(this.getTargetSeriesIndices(),(function(n){var i=this.ecModel.getSeriesByIndex(n);i&&t.call(e,i)}),this)},e.prototype.isTargetSeries=function(t){var e=!1;return this.eachTargetSeries((function(n){n===t&&(e=!0)})),e},e.prototype.formatValueText=function(t,e,n){var i,r=this.option,o=r.precision,a=this.dataBound,s=r.formatter;n=n||["<",">"],Y(t)&&(t=t.slice(),i=!0);var l=e?t:i?[u(t[0]),u(t[1])]:u(t);return U(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):X(s)?i?s(t[0],t[1]):s(t):i?t[0]===a[0]?n[0]+" "+l[1]:t[1]===a[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function u(t){return t===a[0]?"min":t===a[1]?"max":(+t).toFixed(Math.min(o,20))}},e.prototype.resetExtent=function(){var t=this.option,e=EF([t.min,t.max]);this._dataExtent=e},e.prototype.getDataDimensionIndex=function(t){var e=this.option.dimension;if(null!=e)return t.getDimensionIndex(e);for(var n=t.dimensions,i=n.length-1;i>=0;i--){var r=n[i],o=t.getDimensionInfo(r);if(!o.isCalculationCoord)return o.storeDimIndex}},e.prototype.getExtent=function(){return this._dataExtent.slice()},e.prototype.completeVisualOption=function(){var t=this.ecModel,e=this.option,n={inRange:e.inRange,outOfRange:e.outOfRange},i=e.target||(e.target={}),r=e.controller||(e.controller={});C(i,n),C(r,n);var o=this.isCategory();function a(n){RF(e.color)&&!n.inRange&&(n.inRange={color:e.color.slice().reverse()}),n.inRange=n.inRange||{color:t.get("gradientColor")}}a.call(this,i),a.call(this,r),function(t,e,n){var i=t[e],r=t[n];i&&!r&&(r=t[n]={},NF(i,(function(t,e){if(kD.isValidType(e)){var n=kF(e,"inactive",o);null!=n&&(r[e]=n,"color"!==e||r.hasOwnProperty("opacity")||r.hasOwnProperty("colorAlpha")||(r.opacity=[0,0]))}})))}.call(this,i,"inRange","outOfRange"),function(t){var e=(t.inRange||{}).symbol||(t.outOfRange||{}).symbol,n=(t.inRange||{}).symbolSize||(t.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),r=this.getItemSymbol()||"roundRect";NF(this.stateList,(function(a){var s=this.itemSize,l=t[a];l||(l=t[a]={color:o?i:[i]}),null==l.symbol&&(l.symbol=e&&T(e)||(o?r:[r])),null==l.symbolSize&&(l.symbolSize=n&&T(n)||(o?s[0]:[s[0],s[0]])),l.symbol=PF(l.symbol,(function(t){return"none"===t?r:t}));var u=l.symbolSize;if(null!=u){var h=-1/0;OF(u,(function(t){t>h&&(h=t)})),l.symbolSize=PF(u,(function(t){return zF(t,[0,h],[0,s[0]],!0)}))}}),this)}.call(this,r)},e.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},e.prototype.isCategory=function(){return!!this.option.categories},e.prototype.setSelected=function(t){},e.prototype.getSelected=function(){return null},e.prototype.getValueState=function(t){return null},e.prototype.getVisualMeta=function(t){return null},e.type="visualMap",e.dependencies=["series"],e.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},e}(zp),BF=[20,140],FF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual((function(t){t.mappingMethod="linear",t.dataExtent=this.getExtent()})),this._resetRange()},e.prototype.resetItemSize=function(){t.prototype.resetItemSize.apply(this,arguments);var e=this.itemSize;(null==e[0]||isNaN(e[0]))&&(e[0]=BF[0]),(null==e[1]||isNaN(e[1]))&&(e[1]=BF[1])},e.prototype._resetRange=function(){var t=this.getExtent(),e=this.option.range;!e||e.auto?(t.auto=1,this.option.range=t):Y(e)&&(e[0]>e[1]&&e.reverse(),e[0]=Math.max(e[0],t[0]),e[1]=Math.min(e[1],t[1]))},e.prototype.completeVisualOption=function(){t.prototype.completeVisualOption.apply(this,arguments),E(this.stateList,(function(t){var e=this.option.controller[t].symbolSize;e&&e[0]!==e[1]&&(e[0]=e[1]/3)}),this)},e.prototype.setSelected=function(t){this.option.range=t.slice(),this._resetRange()},e.prototype.getSelected=function(){var t=this.getExtent(),e=Qr((this.get("range")||[]).slice());return e[0]>t[1]&&(e[0]=t[1]),e[1]>t[1]&&(e[1]=t[1]),e[0]=n[1]||t<=e[1])?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[];return this.eachTargetSeries((function(n){var i=[],r=n.getData();r.each(this.getDataDimensionIndex(r),(function(e,n){t[0]<=e&&e<=t[1]&&i.push(n)}),this),e.push({seriesId:n.id,dataIndex:i})}),this),e},e.prototype.getVisualMeta=function(t){var e=GF(this,"outOfRange",this.getExtent()),n=GF(this,"inRange",this.option.range.slice()),i=[];function r(e,n){i.push({value:e,color:t(e,n)})}for(var o=0,a=0,s=n.length,l=e.length;at[1])break;n.push({color:this.getControllerVisual(o,"color",e),offset:r/100})}return n.push({color:this.getControllerVisual(t[1],"color",e),offset:1}),n},e.prototype._createBarPoints=function(t,e){var n=this.visualMapModel.itemSize;return[[n[0]-e[0],t[0]],[n[0],t[0]],[n[0],t[1]],[n[0]-e[1],t[1]]]},e.prototype._createBarGroup=function(t){var e=this._orient,n=this.visualMapModel.get("inverse");return new Br("horizontal"!==e||n?"horizontal"===e&&n?{scaleX:"bottom"===t?-1:1,rotation:-Math.PI/2}:"vertical"!==e||n?{scaleX:"left"===t?1:-1}:{scaleX:"left"===t?1:-1,scaleY:-1}:{scaleX:"bottom"===t?1:-1,rotation:Math.PI/2})},e.prototype._updateHandle=function(t,e){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,r=n.handleThumbs,o=n.handleLabels,a=i.itemSize,s=i.getExtent();ZF([0,1],(function(l){var u=r[l];u.setStyle("fill",e.handlesColor[l]),u.y=t[l];var h=UF(t[l],[0,a[1]],s,!0),c=this.getControllerVisual(h,"symbolSize");u.scaleX=u.scaleY=c/a[0],u.x=a[0]-c/2;var p=Fh(n.handleLabelPoints[l],Bh(u,this.group));o[l].setStyle({x:p[0],y:p[1],text:i.formatValueText(this._dataInterval[l]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})}),this)}},e.prototype._showIndicator=function(t,e,n,i){var r=this.visualMapModel,o=r.getExtent(),a=r.itemSize,s=[0,a[1]],l=this._shapes,u=l.indicator;if(u){u.attr("invisible",!1);var h=this.getControllerVisual(t,"color",{convertOpacityToAlpha:!0}),c=this.getControllerVisual(t,"symbolSize"),p=UF(t,o,s,!0),d=a[0]-c/2,f={x:u.x,y:u.y};u.y=p,u.x=d;var g=Fh(l.indicatorLabelPoint,Bh(u,this.group)),y=l.indicatorLabel;y.attr("invisible",!1);var v=this._applyTransform("left",l.mainGroup),m="horizontal"===this._orient;y.setStyle({text:(n||"")+r.formatValueText(e),verticalAlign:m?v:"middle",align:m?"center":v});var x={x:d,y:p,style:{fill:h}},_={style:{x:g[0],y:g[1]}};if(r.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var b={duration:100,easing:"cubicInOut",additive:!0};u.x=f.x,u.y=f.y,u.animateTo(x,b),y.animateTo(_,b)}else u.attr(x),y.attr(_);this._firstShowIndicator=!1;var w=this._shapes.handleLabels;if(w)for(var S=0;Sr[1]&&(u[1]=1/0),e&&(u[0]===-1/0?this._showIndicator(l,u[1],"< ",a):u[1]===1/0?this._showIndicator(l,u[0],"> ",a):this._showIndicator(l,l,"≈ ",a));var h=this._hoverLinkDataIndices,c=[];(e||JF(n))&&(c=this._hoverLinkDataIndices=n.findTargetDataIndices(u));var p=function(t,e){var n={},i={};return r(t||[],n),r(e||[],i,n),[o(n),o(i)];function r(t,e,n){for(var i=0,r=t.length;i=0&&(r.dimension=o,i.push(r))}})),t.getData().setVisual("visualMeta",i)}}];function iG(t,e,n,i){for(var r=e.targetVisuals[i],o=kD.prepareVisualTypes(r),a={color:Ay(t.getData(),"color")},s=0,l=o.length;s0:t.splitNumber>0)&&!t.calculable?"piecewise":"continuous"})),t.registerAction(tG,eG),E(nG,(function(e){t.registerVisual(t.PRIORITY.VISUAL.COMPONENT,e)})),t.registerPreprocessor(oG))}function uG(t){t.registerComponentModel(FF),t.registerComponentView(KF),lG(t)}var hG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._pieceList=[],n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],cG[this._mode].call(this,this._pieceList),this._resetSelected(e,n);var r=this.option.categories;this.resetVisual((function(t,e){"categories"===i?(t.mappingMethod="category",t.categories=T(r)):(t.dataExtent=this.getExtent(),t.mappingMethod="piecewise",t.pieceList=z(this._pieceList,(function(t){return t=T(t),"inRange"!==e&&(t.visual=null),t})))}))},e.prototype.completeVisualOption=function(){var e=this.option,n={},i=kD.listVisualTypes(),r=this.isCategory();function o(t,e,n){return t&&t[e]&&t[e].hasOwnProperty(n)}E(e.pieces,(function(t){E(i,(function(e){t.hasOwnProperty(e)&&(n[e]=1)}))})),E(n,(function(t,n){var i=!1;E(this.stateList,(function(t){i=i||o(e,t,n)||o(e.target,t,n)}),this),!i&&E(this.stateList,(function(t){(e[t]||(e[t]={}))[n]=kF(n,"inRange"===t?"active":"inactive",r)}))}),this),t.prototype.completeVisualOption.apply(this,arguments)},e.prototype._resetSelected=function(t,e){var n=this.option,i=this._pieceList,r=(e?n:t).selected||{};if(n.selected=r,E(i,(function(t,e){var n=this.getSelectedMapKey(t);r.hasOwnProperty(n)||(r[n]=!0)}),this),"single"===n.selectedMode){var o=!1;E(i,(function(t,e){var n=this.getSelectedMapKey(t);r[n]&&(o?r[n]=!1:o=!0)}),this)}},e.prototype.getItemSymbol=function(){return this.get("itemSymbol")},e.prototype.getSelectedMapKey=function(t){return"categories"===this._mode?t.value+"":t.index+""},e.prototype.getPieceList=function(){return this._pieceList},e.prototype._determineMode=function(){var t=this.option;return t.pieces&&t.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},e.prototype.setSelected=function(t){this.option.selected=T(t)},e.prototype.getValueState=function(t){var e=kD.findPieceIndex(t,this._pieceList);return null!=e&&this.option.selected[this.getSelectedMapKey(this._pieceList[e])]?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[],n=this._pieceList;return this.eachTargetSeries((function(i){var r=[],o=i.getData();o.each(this.getDataDimensionIndex(o),(function(e,i){kD.findPieceIndex(e,n)===t&&r.push(i)}),this),e.push({seriesId:i.id,dataIndex:r})}),this),e},e.prototype.getRepresentValue=function(t){var e;if(this.isCategory())e=t.value;else if(null!=t.value)e=t.value;else{var n=t.interval||[];e=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return e},e.prototype.getVisualMeta=function(t){if(!this.isCategory()){var e=[],n=["",""],i=this,r=this._pieceList.slice();if(r.length){var o=r[0].interval[0];o!==-1/0&&r.unshift({interval:[-1/0,o]}),(o=r[r.length-1].interval[1])!==1/0&&r.push({interval:[o,1/0]})}else r.push({interval:[-1/0,1/0]});var a=-1/0;return E(r,(function(t){var e=t.interval;e&&(e[0]>a&&s([a,e[0]],"outOfRange"),s(e.slice()),a=e[1])}),this),{stops:e,outerColors:n}}function s(r,o){var a=i.getRepresentValue({interval:r});o||(o=i.getValueState(a));var s=t(a,o);r[0]===-1/0?n[0]=s:r[1]===1/0?n[1]=s:e.push({value:r[0],color:s},{value:r[1],color:s})}},e.type="visualMap.piecewise",e.defaultOption=kc(VF.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),e}(VF),cG={splitNumber:function(t){var e=this.option,n=Math.min(e.precision,20),i=this.getExtent(),r=e.splitNumber;r=Math.max(parseInt(r,10),1),e.splitNumber=r;for(var o=(i[1]-i[0])/r;+o.toFixed(n)!==o&&n<5;)n++;e.precision=n,o=+o.toFixed(n),e.minOpen&&t.push({interval:[-1/0,i[0]],close:[0,0]});for(var a=0,s=i[0];a","≥"][e[0]]];t.text=t.text||this.formatValueText(null!=t.value?t.value:t.interval,!1,n)}),this)}};function pG(t,e){var n=t.inverse;("vertical"===t.orient?!n:n)&&e.reverse()}var dG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.doRender=function(){var t=this.group;t.removeAll();var e=this.visualMapModel,n=e.get("textGap"),i=e.textStyleModel,r=i.getFont(),o=i.getTextColor(),a=this._getItemAlign(),s=e.itemSize,l=this._getViewData(),u=l.endsText,h=it(e.get("showLabel",!0),!u);u&&this._renderEndsText(t,u[0],s,h,a),E(l.viewPieceList,(function(i){var l=i.piece,u=new Br;u.onclick=W(this._onItemClick,this,l),this._enableHoverLink(u,i.indexInModelPieceList);var c=e.getRepresentValue(l);if(this._createItemSymbol(u,c,[0,0,s[0],s[1]]),h){var p=this.visualMapModel.getValueState(c);u.add(new Xs({style:{x:"right"===a?-n:s[0]+n,y:s[1]/2,text:l.text,verticalAlign:"middle",align:a,font:r,fill:o,opacity:"outOfRange"===p?.5:1}}))}t.add(u)}),this),u&&this._renderEndsText(t,u[1],s,h,a),Ap(e.get("orient"),t,e.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},e.prototype._enableHoverLink=function(t,e){var n=this;t.on("mouseover",(function(){return i("highlight")})).on("mouseout",(function(){return i("downplay")}));var i=function(t){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:t,batch:XF(i.findTargetDataIndices(e),i)})}},e.prototype._getItemAlign=function(){var t=this.visualMapModel,e=t.option;if("vertical"===e.orient)return YF(t,this.api,t.itemSize);var n=e.align;return n&&"auto"!==n||(n="left"),n},e.prototype._renderEndsText=function(t,e,n,i,r){if(e){var o=new Br,a=this.visualMapModel.textStyleModel;o.add(new Xs({style:oc(a,{x:i?"right"===r?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?r:"center",text:e})})),t.add(o)}},e.prototype._getViewData=function(){var t=this.visualMapModel,e=z(t.getPieceList(),(function(t,e){return{piece:t,indexInModelPieceList:e}})),n=t.get("text"),i=t.get("orient"),r=t.get("inverse");return("horizontal"===i?r:!r)?e.reverse():n&&(n=n.slice().reverse()),{viewPieceList:e,endsText:n}},e.prototype._createItemSymbol=function(t,e,n){t.add(Xy(this.getControllerVisual(e,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(e,"color")))},e.prototype._onItemClick=function(t){var e=this.visualMapModel,n=e.option,i=n.selectedMode;if(i){var r=T(n.selected),o=e.getSelectedMapKey(t);"single"===i||!0===i?(r[o]=!0,E(r,(function(t,e){r[e]=e===o}))):r[o]=!r[o],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:r})}},e.type="visualMap.piecewise",e}(WF);function fG(t){t.registerComponentModel(hG),t.registerComponentView(dG),lG(t)}var gG={label:{enabled:!0},decal:{show:!1}},yG=Vo(),vG={};function mG(t,e){var n=t.getModel("aria");if(n.get("enabled")){var i=T(gG);C(i.label,t.getLocaleModel().get("aria"),!1),C(n.option,i,!1),function(){if(n.getModel("decal").get("show")){var e=yt();t.eachSeries((function(t){if(!t.isColorBySeries()){var n=e.get(t.type);n||(n={},e.set(t.type,n)),yG(t).scope=n}})),t.eachRawSeries((function(e){if(!t.isSeriesFiltered(e))if(X(e.enableAriaDecal))e.enableAriaDecal();else{var n=e.getData();if(e.isColorBySeries()){var i=pd(e.ecModel,e.name,vG,t.getSeriesCount()),r=n.getVisual("decal");n.setVisual("decal",u(r,i))}else{var o=e.getRawData(),a={},s=yG(e).scope;n.each((function(t){var e=n.getRawIndex(t);a[e]=t}));var l=o.count();o.each((function(t){var i=a[t],r=o.getName(t)||t+"",h=pd(e.ecModel,r,s,l),c=n.getItemVisual(i,"decal");n.setItemVisual(i,"decal",u(c,h))}))}}function u(t,e){var n=t?A(A({},e),t):e;return n.dirty=!0,n}}))}}(),function(){var i=e.getZr().dom;if(!i)return;var o=t.getLocaleModel().get("aria"),a=n.getModel("label");if(a.option=k(a.option,o),!a.get("enabled"))return;if(a.get("description"))return void i.setAttribute("aria-label",a.get("description"));var s,l=t.getSeriesCount(),u=a.get(["data","maxCount"])||10,h=a.get(["series","maxCount"])||10,c=Math.min(l,h);if(l<1)return;var p=function(){var e=t.get("title");e&&e.length&&(e=e[0]);return e&&e.text}();s=p?r(a.get(["general","withTitle"]),{title:p}):a.get(["general","withoutTitle"]);var d=[];s+=r(l>1?a.get(["series","multiple","prefix"]):a.get(["series","single","prefix"]),{seriesCount:l}),t.eachSeries((function(e,n){if(n1?a.get(["series","multiple",o]):a.get(["series","single",o]),{seriesId:e.seriesIndex,seriesName:e.get("name"),seriesType:(x=e.subType,_=t.getLocaleModel().get(["series","typeNames"]),_[x]||_.chart)});var s=e.getData();if(s.count()>u)i+=r(a.get(["data","partialData"]),{displayCnt:u});else i+=a.get(["data","allData"]);for(var h=a.get(["data","separator","middle"]),p=a.get(["data","separator","end"]),f=[],g=0;g":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},bG=function(){function t(t){if(null==(this._condVal=U(t)?new RegExp(t):et(t)?t:null)){var e="";0,wo(e)}}return t.prototype.evaluate=function(t){var e=typeof t;return U(e)?this._condVal.test(t):!!j(e)&&this._condVal.test(t+"")},t}(),wG=function(){function t(){}return t.prototype.evaluate=function(){return this.value},t}(),SG=function(){function t(){}return t.prototype.evaluate=function(){for(var t=this.children,e=0;e2&&l.push(e),e=[t,n]}function f(t,n,i,r){EG(t,i)&&EG(n,r)||e.push(t,n,i,r,i,r)}function g(t,n,i,r,o,a){var s=Math.abs(n-t),l=4*Math.tan(s/4)/3,u=nM:C2&&l.push(e),l}function VG(t,e,n,i,r,o,a,s,l,u){if(EG(t,n)&&EG(e,i)&&EG(r,a)&&EG(o,s))l.push(a,s);else{var h=2/u,c=h*h,p=a-t,d=s-e,f=Math.sqrt(p*p+d*d);p/=f,d/=f;var g=n-t,y=i-e,v=r-a,m=o-s,x=g*g+y*y,_=v*v+m*m;if(x=0&&_-w*w=0)l.push(a,s);else{var S=[],M=[];wn(t,n,r,a,.5,S),wn(e,i,o,s,.5,M),VG(S[0],M[0],S[1],M[1],S[2],M[2],S[3],M[3],l,u),VG(S[4],M[4],S[5],M[5],S[6],M[6],S[7],M[7],l,u)}}}}function BG(t,e,n){var i=t[e],r=t[1-e],o=Math.abs(i/r),a=Math.ceil(Math.sqrt(o*n)),s=Math.floor(n/a);0===s&&(s=1,a=n);for(var l=[],u=0;u0)for(u=0;uMath.abs(u),c=BG([l,u],h?0:1,e),p=(h?s:u)/c.length,d=0;d1?null:new De(d*l+t,d*u+e)}function HG(t,e,n){var i=new De;De.sub(i,n,e),i.normalize();var r=new De;return De.sub(r,t,e),r.dot(i)}function YG(t,e){var n=t[t.length-1];n&&n[0]===e[0]&&n[1]===e[1]||t.push(e)}function XG(t){var e=t.points,n=[],i=[];Ba(e,n,i);var r=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]),o=r.width,a=r.height,s=r.x,l=r.y,u=new De,h=new De;return o>a?(u.x=h.x=s+o/2,u.y=l,h.y=l+a):(u.y=h.y=l+a/2,u.x=s,h.x=s+o),function(t,e,n){for(var i=t.length,r=[],o=0;or,a=BG([i,r],o?0:1,e),s=o?"width":"height",l=o?"height":"width",u=o?"x":"y",h=o?"y":"x",c=t[s]/a.length,p=0;p0)for(var b=i/n,w=-i/2;w<=i/2;w+=b){var S=Math.sin(w),M=Math.cos(w),I=0;for(x=0;x0;l/=2){var u=0,h=0;(t&l)>0&&(u=1),(e&l)>0&&(h=1),s+=l*l*(3*u^h),0===h&&(1===u&&(t=l-1-t,e=l-1-e),a=t,t=e,e=a)}return s}function lW(t){var e=1/0,n=1/0,i=-1/0,r=-1/0,o=z(t,(function(t){var o=t.getBoundingRect(),a=t.getComputedTransform(),s=o.x+o.width/2+(a?a[4]:0),l=o.y+o.height/2+(a?a[5]:0);return e=Math.min(s,e),n=Math.min(l,n),i=Math.max(s,i),r=Math.max(l,r),[s,l]}));return z(o,(function(o,a){return{cp:o,z:sW(o[0],o[1],e,n,i,r),path:t[a]}})).sort((function(t,e){return t.z-e.z})).map((function(t){return t.path}))}function uW(t){return jG(t.path,t.count)}function hW(t){return Y(t[0])}function cW(t,e){for(var n=[],i=t.length,r=0;r=0;r--)if(!n[r].many.length){var l=n[s].many;if(l.length<=1){if(!s)return n;s=0}o=l.length;var u=Math.ceil(o/2);n[r].many=l.slice(u,o),n[s].many=l.slice(0,u),s++}return n}var pW={clone:function(t){for(var e=[],n=1-Math.pow(1-t.path.style.opacity,1/t.count),i=0;i0){var s,l,u=i.getModel("universalTransition").get("delay"),h=Object.assign({setToFinal:!0},a);hW(t)&&(s=t,l=e),hW(e)&&(s=e,l=t);for(var c=s?s===t:t.length>e.length,p=s?cW(l,s):cW(c?e:t,[c?t:e]),d=0,f=0;f1e4))for(var r=n.getIndices(),o=0;o0&&i.group.traverse((function(t){t instanceof ks&&!t.animators.length&&t.animateFrom({style:{opacity:0}},r)}))}))}function SW(t){var e=t.getModel("universalTransition").get("seriesKey");return e||t.id}function MW(t){return Y(t)?t.sort().join(","):t}function IW(t){if(t.hostModel)return t.hostModel.getModel("universalTransition").get("divideShape")}function TW(t,e){for(var n=0;n=0&&r.push({dataGroupId:e.oldDataGroupIds[n],data:e.oldData[n],divide:IW(e.oldData[n]),groupIdDim:t.dimension})})),E(To(t.to),(function(t){var i=TW(n.updatedSeries,t);if(i>=0){var r=n.updatedSeries[i].getData();o.push({dataGroupId:e.oldDataGroupIds[i],data:r,divide:IW(r),groupIdDim:t.dimension})}})),r.length>0&&o.length>0&&wW(r,o,i)}(t,i,n,e)}));else{var o=function(t,e){var n=yt(),i=yt(),r=yt();return E(t.oldSeries,(function(e,n){var o=t.oldDataGroupIds[n],a=t.oldData[n],s=SW(e),l=MW(s);i.set(l,{dataGroupId:o,data:a}),Y(s)&&E(s,(function(t){r.set(t,{key:l,dataGroupId:o,data:a})}))})),E(e.updatedSeries,(function(t){if(t.isUniversalTransitionEnabled()&&t.isAnimationEnabled()){var e=t.get("dataGroupId"),o=t.getData(),a=SW(t),s=MW(a),l=i.get(s);if(l)n.set(s,{oldSeries:[{dataGroupId:l.dataGroupId,divide:IW(l.data),data:l.data}],newSeries:[{dataGroupId:e,divide:IW(o),data:o}]});else if(Y(a)){var u=[];E(a,(function(t){var e=i.get(t);e.data&&u.push({dataGroupId:e.dataGroupId,divide:IW(e.data),data:e.data})})),u.length&&n.set(s,{oldSeries:u,newSeries:[{dataGroupId:e,data:o,divide:IW(o)}]})}else{var h=r.get(a);if(h){var c=n.get(h.key);c||(c={oldSeries:[{dataGroupId:h.dataGroupId,data:h.data,divide:IW(h.data)}],newSeries:[]},n.set(h.key,c)),c.newSeries.push({dataGroupId:e,data:o,divide:IW(o)})}}}})),n}(i,n);E(o.keys(),(function(t){var n=o.get(t);wW(n.oldSeries,n.newSeries,e)}))}E(n.updatedSeries,(function(t){t[_g]&&(t[_g]=!1)}))}for(var a=t.getSeries(),s=i.oldSeries=[],l=i.oldDataGroupIds=[],u=i.oldData=[],h=0;h",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},i={weekdays:{shorthand:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],longhand:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},months:{shorthand:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],longhand:["January","February","March","April","May","June","July","August","September","October","November","December"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(e){var n=e%100;if(n>3&&n<21)return"th";switch(n%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}},rangeSeparator:" to ",weekAbbreviation:"Wk",scrollTitle:"Scroll to increment",toggleTitle:"Click to toggle",amPM:["AM","PM"],yearAriaLabel:"Year",monthAriaLabel:"Month",hourAriaLabel:"Hour",minuteAriaLabel:"Minute",time_24hr:!1},o=function(e,n){return void 0===n&&(n=2),("000"+e).slice(-1*n)},r=function(e){return!0===e?1:0};function l(e,n){var t;return function(){var a=this,i=arguments;clearTimeout(t),t=setTimeout((function(){return e.apply(a,i)}),n)}}var c=function(e){return e instanceof Array?e:[e]};function s(e,n,t){if(!0===t)return e.classList.add(n);e.classList.remove(n)}function d(e,n,t){var a=window.document.createElement(e);return n=n||"",t=t||"",a.className=n,void 0!==t&&(a.textContent=t),a}function u(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function f(e,n){return n(e)?e:e.parentNode?f(e.parentNode,n):void 0}function m(e,n){var t=d("div","numInputWrapper"),a=d("input","numInput "+e),i=d("span","arrowUp"),o=d("span","arrowDown");if(-1===navigator.userAgent.indexOf("MSIE 9.0")?a.type="number":(a.type="text",a.pattern="\\d*"),void 0!==n)for(var r in n)a.setAttribute(r,n[r]);return t.appendChild(a),t.appendChild(i),t.appendChild(o),t}function g(e){try{return"function"==typeof e.composedPath?e.composedPath()[0]:e.target}catch(n){return e.target}}var p=function(){},h=function(e,n,t){return t.months[n?"shorthand":"longhand"][e]},v={D:p,F:function(e,n,t){e.setMonth(t.months.longhand.indexOf(n))},G:function(e,n){e.setHours((e.getHours()>=12?12:0)+parseFloat(n))},H:function(e,n){e.setHours(parseFloat(n))},J:function(e,n){e.setDate(parseFloat(n))},K:function(e,n,t){e.setHours(e.getHours()%12+12*r(new RegExp(t.amPM[1],"i").test(n)))},M:function(e,n,t){e.setMonth(t.months.shorthand.indexOf(n))},S:function(e,n){e.setSeconds(parseFloat(n))},U:function(e,n){return new Date(1e3*parseFloat(n))},W:function(e,n,t){var a=parseInt(n),i=new Date(e.getFullYear(),0,2+7*(a-1),0,0,0,0);return i.setDate(i.getDate()-i.getDay()+t.firstDayOfWeek),i},Y:function(e,n){e.setFullYear(parseFloat(n))},Z:function(e,n){return new Date(n)},d:function(e,n){e.setDate(parseFloat(n))},h:function(e,n){e.setHours((e.getHours()>=12?12:0)+parseFloat(n))},i:function(e,n){e.setMinutes(parseFloat(n))},j:function(e,n){e.setDate(parseFloat(n))},l:p,m:function(e,n){e.setMonth(parseFloat(n)-1)},n:function(e,n){e.setMonth(parseFloat(n)-1)},s:function(e,n){e.setSeconds(parseFloat(n))},u:function(e,n){return new Date(parseFloat(n))},w:p,y:function(e,n){e.setFullYear(2e3+parseFloat(n))}},D={D:"",F:"",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",u:"(.+)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},w={Z:function(e){return e.toISOString()},D:function(e,n,t){return n.weekdays.shorthand[w.w(e,n,t)]},F:function(e,n,t){return h(w.n(e,n,t)-1,!1,n)},G:function(e,n,t){return o(w.h(e,n,t))},H:function(e){return o(e.getHours())},J:function(e,n){return void 0!==n.ordinal?e.getDate()+n.ordinal(e.getDate()):e.getDate()},K:function(e,n){return n.amPM[r(e.getHours()>11)]},M:function(e,n){return h(e.getMonth(),!0,n)},S:function(e){return o(e.getSeconds())},U:function(e){return e.getTime()/1e3},W:function(e,n,t){return t.getWeek(e)},Y:function(e){return o(e.getFullYear(),4)},d:function(e){return o(e.getDate())},h:function(e){return e.getHours()%12?e.getHours()%12:12},i:function(e){return o(e.getMinutes())},j:function(e){return e.getDate()},l:function(e,n){return n.weekdays.longhand[e.getDay()]},m:function(e){return o(e.getMonth()+1)},n:function(e){return e.getMonth()+1},s:function(e){return e.getSeconds()},u:function(e){return e.getTime()},w:function(e){return e.getDay()},y:function(e){return String(e.getFullYear()).substring(2)}},b=function(e){var n=e.config,t=void 0===n?a:n,o=e.l10n,r=void 0===o?i:o,l=e.isMobile,c=void 0!==l&&l;return function(e,n,a){var i=a||r;return void 0===t.formatDate||c?n.split("").map((function(n,a,o){return w[n]&&"\\"!==o[a-1]?w[n](e,i,t):"\\"!==n?n:""})).join(""):t.formatDate(e,n,i)}},C=function(e){var n=e.config,t=void 0===n?a:n,o=e.l10n,r=void 0===o?i:o;return function(e,n,i,o){if(0===e||e){var l,c=o||r,s=e;if(e instanceof Date)l=new Date(e.getTime());else if("string"!=typeof e&&void 0!==e.toFixed)l=new Date(e);else if("string"==typeof e){var d=n||(t||a).dateFormat,u=String(e).trim();if("today"===u)l=new Date,i=!0;else if(t&&t.parseDate)l=t.parseDate(e,d);else if(/Z$/.test(u)||/GMT$/.test(u))l=new Date(e);else{for(var f=void 0,m=[],g=0,p=0,h="";g=0?new Date:new Date(w.config.minDate.getTime()),t=E(w.config);n.setHours(t.hours,t.minutes,t.seconds,n.getMilliseconds()),w.selectedDates=[n],w.latestSelectedDateObj=n}void 0!==e&&"blur"!==e.type&&function(e){e.preventDefault();var n="keydown"===e.type,t=g(e),a=t;void 0!==w.amPM&&t===w.amPM&&(w.amPM.textContent=w.l10n.amPM[r(w.amPM.textContent===w.l10n.amPM[0])]);var i=parseFloat(a.getAttribute("min")),l=parseFloat(a.getAttribute("max")),c=parseFloat(a.getAttribute("step")),s=parseInt(a.value,10),d=e.delta||(n?38===e.which?1:-1:0),u=s+c*d;if(void 0!==a.value&&2===a.value.length){var f=a===w.hourElement,m=a===w.minuteElement;ul&&(u=a===w.hourElement?u-l-r(!w.amPM):i,m&&L(void 0,1,w.hourElement)),w.amPM&&f&&(1===c?u+s===23:Math.abs(u-s)>c)&&(w.amPM.textContent=w.l10n.amPM[r(w.amPM.textContent===w.l10n.amPM[0])]),a.value=o(u)}}(e);var a=w._input.value;O(),ye(),w._input.value!==a&&w._debouncedChange()}function O(){if(void 0!==w.hourElement&&void 0!==w.minuteElement){var e,n,t=(parseInt(w.hourElement.value.slice(-2),10)||0)%24,a=(parseInt(w.minuteElement.value,10)||0)%60,i=void 0!==w.secondElement?(parseInt(w.secondElement.value,10)||0)%60:0;void 0!==w.amPM&&(e=t,n=w.amPM.textContent,t=e%12+12*r(n===w.l10n.amPM[1]));var o=void 0!==w.config.minTime||w.config.minDate&&w.minDateHasTime&&w.latestSelectedDateObj&&0===M(w.latestSelectedDateObj,w.config.minDate,!0),l=void 0!==w.config.maxTime||w.config.maxDate&&w.maxDateHasTime&&w.latestSelectedDateObj&&0===M(w.latestSelectedDateObj,w.config.maxDate,!0);if(void 0!==w.config.maxTime&&void 0!==w.config.minTime&&w.config.minTime>w.config.maxTime){var c=y(w.config.minTime.getHours(),w.config.minTime.getMinutes(),w.config.minTime.getSeconds()),s=y(w.config.maxTime.getHours(),w.config.maxTime.getMinutes(),w.config.maxTime.getSeconds()),d=y(t,a,i);if(d>s&&d=12)]),void 0!==w.secondElement&&(w.secondElement.value=o(t)))}function N(e){var n=g(e),t=parseInt(n.value)+(e.delta||0);(t/1e3>1||"Enter"===e.key&&!/[^\d]/.test(t.toString()))&&ee(t)}function P(e,n,t,a){return n instanceof Array?n.forEach((function(n){return P(e,n,t,a)})):e instanceof Array?e.forEach((function(e){return P(e,n,t,a)})):(e.addEventListener(n,t,a),void w._handlers.push({remove:function(){return e.removeEventListener(n,t,a)}}))}function Y(){De("onChange")}function j(e,n){var t=void 0!==e?w.parseDate(e):w.latestSelectedDateObj||(w.config.minDate&&w.config.minDate>w.now?w.config.minDate:w.config.maxDate&&w.config.maxDate=0&&M(e,w.selectedDates[1])<=0)}(n)&&!be(n)&&o.classList.add("inRange"),w.weekNumbers&&1===w.config.showMonths&&"prevMonthDay"!==e&&a%7==6&&w.weekNumbers.insertAdjacentHTML("beforeend",""+w.config.getWeek(n)+""),De("onDayCreate",o),o}function W(e){e.focus(),"range"===w.config.mode&&oe(e)}function B(e){for(var n=e>0?0:w.config.showMonths-1,t=e>0?w.config.showMonths:-1,a=n;a!=t;a+=e)for(var i=w.daysContainer.children[a],o=e>0?0:i.children.length-1,r=e>0?i.children.length:-1,l=o;l!=r;l+=e){var c=i.children[l];if(-1===c.className.indexOf("hidden")&&ne(c.dateObj))return c}}function J(e,n){var t=k(),a=te(t||document.body),i=void 0!==e?e:a?t:void 0!==w.selectedDateElem&&te(w.selectedDateElem)?w.selectedDateElem:void 0!==w.todayDateElem&&te(w.todayDateElem)?w.todayDateElem:B(n>0?1:-1);void 0===i?w._input.focus():a?function(e,n){for(var t=-1===e.className.indexOf("Month")?e.dateObj.getMonth():w.currentMonth,a=n>0?w.config.showMonths:-1,i=n>0?1:-1,o=t-w.currentMonth;o!=a;o+=i)for(var r=w.daysContainer.children[o],l=t-w.currentMonth===o?e.$i+n:n<0?r.children.length-1:0,c=r.children.length,s=l;s>=0&&s0?c:-1);s+=i){var d=r.children[s];if(-1===d.className.indexOf("hidden")&&ne(d.dateObj)&&Math.abs(e.$i-s)>=Math.abs(n))return W(d)}w.changeMonth(i),J(B(i),0)}(i,n):W(i)}function K(e,n){for(var t=(new Date(e,n,1).getDay()-w.l10n.firstDayOfWeek+7)%7,a=w.utils.getDaysInMonth((n-1+12)%12,e),i=w.utils.getDaysInMonth(n,e),o=window.document.createDocumentFragment(),r=w.config.showMonths>1,l=r?"prevMonthDay hidden":"prevMonthDay",c=r?"nextMonthDay hidden":"nextMonthDay",s=a+1-t,u=0;s<=a;s++,u++)o.appendChild(R("flatpickr-day "+l,new Date(e,n-1,s),0,u));for(s=1;s<=i;s++,u++)o.appendChild(R("flatpickr-day",new Date(e,n,s),0,u));for(var f=i+1;f<=42-t&&(1===w.config.showMonths||u%7!=0);f++,u++)o.appendChild(R("flatpickr-day "+c,new Date(e,n+1,f%i),0,u));var m=d("div","dayContainer");return m.appendChild(o),m}function U(){if(void 0!==w.daysContainer){u(w.daysContainer),w.weekNumbers&&u(w.weekNumbers);for(var e=document.createDocumentFragment(),n=0;n1||"dropdown"!==w.config.monthSelectorType)){var e=function(e){return!(void 0!==w.config.minDate&&w.currentYear===w.config.minDate.getFullYear()&&ew.config.maxDate.getMonth())};w.monthsDropdownContainer.tabIndex=-1,w.monthsDropdownContainer.innerHTML="";for(var n=0;n<12;n++)if(e(n)){var t=d("option","flatpickr-monthDropdown-month");t.value=new Date(w.currentYear,n).getMonth().toString(),t.textContent=h(n,w.config.shorthandCurrentMonth,w.l10n),t.tabIndex=-1,w.currentMonth===n&&(t.selected=!0),w.monthsDropdownContainer.appendChild(t)}}}function $(){var e,n=d("div","flatpickr-month"),t=window.document.createDocumentFragment();w.config.showMonths>1||"static"===w.config.monthSelectorType?e=d("span","cur-month"):(w.monthsDropdownContainer=d("select","flatpickr-monthDropdown-months"),w.monthsDropdownContainer.setAttribute("aria-label",w.l10n.monthAriaLabel),P(w.monthsDropdownContainer,"change",(function(e){var n=g(e),t=parseInt(n.value,10);w.changeMonth(t-w.currentMonth),De("onMonthChange")})),q(),e=w.monthsDropdownContainer);var a=m("cur-year",{tabindex:"-1"}),i=a.getElementsByTagName("input")[0];i.setAttribute("aria-label",w.l10n.yearAriaLabel),w.config.minDate&&i.setAttribute("min",w.config.minDate.getFullYear().toString()),w.config.maxDate&&(i.setAttribute("max",w.config.maxDate.getFullYear().toString()),i.disabled=!!w.config.minDate&&w.config.minDate.getFullYear()===w.config.maxDate.getFullYear());var o=d("div","flatpickr-current-month");return o.appendChild(e),o.appendChild(a),t.appendChild(o),n.appendChild(t),{container:n,yearElement:i,monthElement:e}}function V(){u(w.monthNav),w.monthNav.appendChild(w.prevMonthNav),w.config.showMonths&&(w.yearElements=[],w.monthElements=[]);for(var e=w.config.showMonths;e--;){var n=$();w.yearElements.push(n.yearElement),w.monthElements.push(n.monthElement),w.monthNav.appendChild(n.container)}w.monthNav.appendChild(w.nextMonthNav)}function z(){w.weekdayContainer?u(w.weekdayContainer):w.weekdayContainer=d("div","flatpickr-weekdays");for(var e=w.config.showMonths;e--;){var n=d("div","flatpickr-weekdaycontainer");w.weekdayContainer.appendChild(n)}return G(),w.weekdayContainer}function G(){if(w.weekdayContainer){var e=w.l10n.firstDayOfWeek,t=n(w.l10n.weekdays.shorthand);e>0&&e\n "+t.join("")+"\n \n "}}function Z(e,n){void 0===n&&(n=!0);var t=n?e:e-w.currentMonth;t<0&&!0===w._hidePrevMonthArrow||t>0&&!0===w._hideNextMonthArrow||(w.currentMonth+=t,(w.currentMonth<0||w.currentMonth>11)&&(w.currentYear+=w.currentMonth>11?1:-1,w.currentMonth=(w.currentMonth+12)%12,De("onYearChange"),q()),U(),De("onMonthChange"),Ce())}function Q(e){return w.calendarContainer.contains(e)}function X(e){if(w.isOpen&&!w.config.inline){var n=g(e),t=Q(n),a=!(n===w.input||n===w.altInput||w.element.contains(n)||e.path&&e.path.indexOf&&(~e.path.indexOf(w.input)||~e.path.indexOf(w.altInput)))&&!t&&!Q(e.relatedTarget),i=!w.config.ignoredFocusElements.some((function(e){return e.contains(n)}));a&&i&&(w.config.allowInput&&w.setDate(w._input.value,!1,w.config.altInput?w.config.altFormat:w.config.dateFormat),void 0!==w.timeContainer&&void 0!==w.minuteElement&&void 0!==w.hourElement&&""!==w.input.value&&void 0!==w.input.value&&_(),w.close(),w.config&&"range"===w.config.mode&&1===w.selectedDates.length&&w.clear(!1))}}function ee(e){if(!(!e||w.config.minDate&&ew.config.maxDate.getFullYear())){var n=e,t=w.currentYear!==n;w.currentYear=n||w.currentYear,w.config.maxDate&&w.currentYear===w.config.maxDate.getFullYear()?w.currentMonth=Math.min(w.config.maxDate.getMonth(),w.currentMonth):w.config.minDate&&w.currentYear===w.config.minDate.getFullYear()&&(w.currentMonth=Math.max(w.config.minDate.getMonth(),w.currentMonth)),t&&(w.redraw(),De("onYearChange"),q())}}function ne(e,n){var t;void 0===n&&(n=!0);var a=w.parseDate(e,void 0,n);if(w.config.minDate&&a&&M(a,w.config.minDate,void 0!==n?n:!w.minDateHasTime)<0||w.config.maxDate&&a&&M(a,w.config.maxDate,void 0!==n?n:!w.maxDateHasTime)>0)return!1;if(!w.config.enable&&0===w.config.disable.length)return!0;if(void 0===a)return!1;for(var i=!!w.config.enable,o=null!==(t=w.config.enable)&&void 0!==t?t:w.config.disable,r=0,l=void 0;r=l.from.getTime()&&a.getTime()<=l.to.getTime())return i}return!i}function te(e){return void 0!==w.daysContainer&&(-1===e.className.indexOf("hidden")&&-1===e.className.indexOf("flatpickr-disabled")&&w.daysContainer.contains(e))}function ae(e){var n=e.target===w._input,t=w._input.value.trimEnd()!==Me();!n||!t||e.relatedTarget&&Q(e.relatedTarget)||w.setDate(w._input.value,!0,e.target===w.altInput?w.config.altFormat:w.config.dateFormat)}function ie(e){var n=g(e),t=w.config.wrap?p.contains(n):n===w._input,a=w.config.allowInput,i=w.isOpen&&(!a||!t),o=w.config.inline&&t&&!a;if(13===e.keyCode&&t){if(a)return w.setDate(w._input.value,!0,n===w.altInput?w.config.altFormat:w.config.dateFormat),w.close(),n.blur();w.open()}else if(Q(n)||i||o){var r=!!w.timeContainer&&w.timeContainer.contains(n);switch(e.keyCode){case 13:r?(e.preventDefault(),_(),fe()):me(e);break;case 27:e.preventDefault(),fe();break;case 8:case 46:t&&!w.config.allowInput&&(e.preventDefault(),w.clear());break;case 37:case 39:if(r||t)w.hourElement&&w.hourElement.focus();else{e.preventDefault();var l=k();if(void 0!==w.daysContainer&&(!1===a||l&&te(l))){var c=39===e.keyCode?1:-1;e.ctrlKey?(e.stopPropagation(),Z(c),J(B(1),0)):J(void 0,c)}}break;case 38:case 40:e.preventDefault();var s=40===e.keyCode?1:-1;w.daysContainer&&void 0!==n.$i||n===w.input||n===w.altInput?e.ctrlKey?(e.stopPropagation(),ee(w.currentYear-s),J(B(1),0)):r||J(void 0,7*s):n===w.currentYearElement?ee(w.currentYear-s):w.config.enableTime&&(!r&&w.hourElement&&w.hourElement.focus(),_(e),w._debouncedChange());break;case 9:if(r){var d=[w.hourElement,w.minuteElement,w.secondElement,w.amPM].concat(w.pluginElements).filter((function(e){return e})),u=d.indexOf(n);if(-1!==u){var f=d[u+(e.shiftKey?-1:1)];e.preventDefault(),(f||w._input).focus()}}else!w.config.noCalendar&&w.daysContainer&&w.daysContainer.contains(n)&&e.shiftKey&&(e.preventDefault(),w._input.focus())}}if(void 0!==w.amPM&&n===w.amPM)switch(e.key){case w.l10n.amPM[0].charAt(0):case w.l10n.amPM[0].charAt(0).toLowerCase():w.amPM.textContent=w.l10n.amPM[0],O(),ye();break;case w.l10n.amPM[1].charAt(0):case w.l10n.amPM[1].charAt(0).toLowerCase():w.amPM.textContent=w.l10n.amPM[1],O(),ye()}(t||Q(n))&&De("onKeyDown",e)}function oe(e,n){if(void 0===n&&(n="flatpickr-day"),1===w.selectedDates.length&&(!e||e.classList.contains(n)&&!e.classList.contains("flatpickr-disabled"))){for(var t=e?e.dateObj.getTime():w.days.firstElementChild.dateObj.getTime(),a=w.parseDate(w.selectedDates[0],void 0,!0).getTime(),i=Math.min(t,w.selectedDates[0].getTime()),o=Math.max(t,w.selectedDates[0].getTime()),r=!1,l=0,c=0,s=i;si&&sl)?l=s:s>a&&(!c||s ."+n)).forEach((function(n){var i,o,s,d=n.dateObj.getTime(),u=l>0&&d0&&d>c;if(u)return n.classList.add("notAllowed"),void["inRange","startRange","endRange"].forEach((function(e){n.classList.remove(e)}));r&&!u||(["startRange","inRange","endRange","notAllowed"].forEach((function(e){n.classList.remove(e)})),void 0!==e&&(e.classList.add(t<=w.selectedDates[0].getTime()?"startRange":"endRange"),at&&d===a&&n.classList.add("endRange"),d>=l&&(0===c||d<=c)&&(o=a,s=t,(i=d)>Math.min(o,s)&&i0||t.getMinutes()>0||t.getSeconds()>0),w.selectedDates&&(w.selectedDates=w.selectedDates.filter((function(e){return ne(e)})),w.selectedDates.length||"min"!==e||F(t),ye()),w.daysContainer&&(ue(),void 0!==t?w.currentYearElement[e]=t.getFullYear().toString():w.currentYearElement.removeAttribute(e),w.currentYearElement.disabled=!!a&&void 0!==t&&a.getFullYear()===t.getFullYear())}}function ce(){return w.config.wrap?p.querySelector("[data-input]"):p}function se(){"object"!=typeof w.config.locale&&void 0===I.l10ns[w.config.locale]&&w.config.errorHandler(new Error("flatpickr: invalid locale "+w.config.locale)),w.l10n=e(e({},I.l10ns.default),"object"==typeof w.config.locale?w.config.locale:"default"!==w.config.locale?I.l10ns[w.config.locale]:void 0),D.D="("+w.l10n.weekdays.shorthand.join("|")+")",D.l="("+w.l10n.weekdays.longhand.join("|")+")",D.M="("+w.l10n.months.shorthand.join("|")+")",D.F="("+w.l10n.months.longhand.join("|")+")",D.K="("+w.l10n.amPM[0]+"|"+w.l10n.amPM[1]+"|"+w.l10n.amPM[0].toLowerCase()+"|"+w.l10n.amPM[1].toLowerCase()+")",void 0===e(e({},v),JSON.parse(JSON.stringify(p.dataset||{}))).time_24hr&&void 0===I.defaultConfig.time_24hr&&(w.config.time_24hr=w.l10n.time_24hr),w.formatDate=b(w),w.parseDate=C({config:w.config,l10n:w.l10n})}function de(e){if("function"!=typeof w.config.position){if(void 0!==w.calendarContainer){De("onPreCalendarPosition");var n=e||w._positionElement,t=Array.prototype.reduce.call(w.calendarContainer.children,(function(e,n){return e+n.offsetHeight}),0),a=w.calendarContainer.offsetWidth,i=w.config.position.split(" "),o=i[0],r=i.length>1?i[1]:null,l=n.getBoundingClientRect(),c=window.innerHeight-l.bottom,d="above"===o||"below"!==o&&ct,u=window.pageYOffset+l.top+(d?-t-2:n.offsetHeight+2);if(s(w.calendarContainer,"arrowTop",!d),s(w.calendarContainer,"arrowBottom",d),!w.config.inline){var f=window.pageXOffset+l.left,m=!1,g=!1;"center"===r?(f-=(a-l.width)/2,m=!0):"right"===r&&(f-=a-l.width,g=!0),s(w.calendarContainer,"arrowLeft",!m&&!g),s(w.calendarContainer,"arrowCenter",m),s(w.calendarContainer,"arrowRight",g);var p=window.document.body.offsetWidth-(window.pageXOffset+l.right),h=f+a>window.document.body.offsetWidth,v=p+a>window.document.body.offsetWidth;if(s(w.calendarContainer,"rightMost",h),!w.config.static)if(w.calendarContainer.style.top=u+"px",h)if(v){var D=function(){for(var e=null,n=0;nw.currentMonth+w.config.showMonths-1)&&"range"!==w.config.mode;if(w.selectedDateElem=t,"single"===w.config.mode)w.selectedDates=[a];else if("multiple"===w.config.mode){var o=be(a);o?w.selectedDates.splice(parseInt(o),1):w.selectedDates.push(a)}else"range"===w.config.mode&&(2===w.selectedDates.length&&w.clear(!1,!1),w.latestSelectedDateObj=a,w.selectedDates.push(a),0!==M(a,w.selectedDates[0],!0)&&w.selectedDates.sort((function(e,n){return e.getTime()-n.getTime()})));if(O(),i){var r=w.currentYear!==a.getFullYear();w.currentYear=a.getFullYear(),w.currentMonth=a.getMonth(),r&&(De("onYearChange"),q()),De("onMonthChange")}if(Ce(),U(),ye(),i||"range"===w.config.mode||1!==w.config.showMonths?void 0!==w.selectedDateElem&&void 0===w.hourElement&&w.selectedDateElem&&w.selectedDateElem.focus():W(t),void 0!==w.hourElement&&void 0!==w.hourElement&&w.hourElement.focus(),w.config.closeOnSelect){var l="single"===w.config.mode&&!w.config.enableTime,c="range"===w.config.mode&&2===w.selectedDates.length&&!w.config.enableTime;(l||c)&&fe()}Y()}}w.parseDate=C({config:w.config,l10n:w.l10n}),w._handlers=[],w.pluginElements=[],w.loadedPlugins=[],w._bind=P,w._setHoursFromDate=F,w._positionCalendar=de,w.changeMonth=Z,w.changeYear=ee,w.clear=function(e,n){void 0===e&&(e=!0);void 0===n&&(n=!0);w.input.value="",void 0!==w.altInput&&(w.altInput.value="");void 0!==w.mobileInput&&(w.mobileInput.value="");w.selectedDates=[],w.latestSelectedDateObj=void 0,!0===n&&(w.currentYear=w._initialDate.getFullYear(),w.currentMonth=w._initialDate.getMonth());if(!0===w.config.enableTime){var t=E(w.config),a=t.hours,i=t.minutes,o=t.seconds;A(a,i,o)}w.redraw(),e&&De("onChange")},w.close=function(){w.isOpen=!1,w.isMobile||(void 0!==w.calendarContainer&&w.calendarContainer.classList.remove("open"),void 0!==w._input&&w._input.classList.remove("active"));De("onClose")},w.onMouseOver=oe,w._createElement=d,w.createDay=R,w.destroy=function(){void 0!==w.config&&De("onDestroy");for(var e=w._handlers.length;e--;)w._handlers[e].remove();if(w._handlers=[],w.mobileInput)w.mobileInput.parentNode&&w.mobileInput.parentNode.removeChild(w.mobileInput),w.mobileInput=void 0;else if(w.calendarContainer&&w.calendarContainer.parentNode)if(w.config.static&&w.calendarContainer.parentNode){var n=w.calendarContainer.parentNode;if(n.lastChild&&n.removeChild(n.lastChild),n.parentNode){for(;n.firstChild;)n.parentNode.insertBefore(n.firstChild,n);n.parentNode.removeChild(n)}}else w.calendarContainer.parentNode.removeChild(w.calendarContainer);w.altInput&&(w.input.type="text",w.altInput.parentNode&&w.altInput.parentNode.removeChild(w.altInput),delete w.altInput);w.input&&(w.input.type=w.input._type,w.input.classList.remove("flatpickr-input"),w.input.removeAttribute("readonly"));["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","monthsDropdownContainer","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach((function(e){try{delete w[e]}catch(e){}}))},w.isEnabled=ne,w.jumpToDate=j,w.updateValue=ye,w.open=function(e,n){void 0===n&&(n=w._positionElement);if(!0===w.isMobile){if(e){e.preventDefault();var t=g(e);t&&t.blur()}return void 0!==w.mobileInput&&(w.mobileInput.focus(),w.mobileInput.click()),void De("onOpen")}if(w._input.disabled||w.config.inline)return;var a=w.isOpen;w.isOpen=!0,a||(w.calendarContainer.classList.add("open"),w._input.classList.add("active"),De("onOpen"),de(n));!0===w.config.enableTime&&!0===w.config.noCalendar&&(!1!==w.config.allowInput||void 0!==e&&w.timeContainer.contains(e.relatedTarget)||setTimeout((function(){return w.hourElement.select()}),50))},w.redraw=ue,w.set=function(e,n){if(null!==e&&"object"==typeof e)for(var a in Object.assign(w.config,e),e)void 0!==ge[a]&&ge[a].forEach((function(e){return e()}));else w.config[e]=n,void 0!==ge[e]?ge[e].forEach((function(e){return e()})):t.indexOf(e)>-1&&(w.config[e]=c(n));w.redraw(),ye(!0)},w.setDate=function(e,n,t){void 0===n&&(n=!1);void 0===t&&(t=w.config.dateFormat);if(0!==e&&!e||e instanceof Array&&0===e.length)return w.clear(n);pe(e,t),w.latestSelectedDateObj=w.selectedDates[w.selectedDates.length-1],w.redraw(),j(void 0,n),F(),0===w.selectedDates.length&&w.clear(!1);ye(n),n&&De("onChange")},w.toggle=function(e){if(!0===w.isOpen)return w.close();w.open(e)};var ge={locale:[se,G],showMonths:[V,S,z],minDate:[j],maxDate:[j],positionElement:[ve],clickOpens:[function(){!0===w.config.clickOpens?(P(w._input,"focus",w.open),P(w._input,"click",w.open)):(w._input.removeEventListener("focus",w.open),w._input.removeEventListener("click",w.open))}]};function pe(e,n){var t=[];if(e instanceof Array)t=e.map((function(e){return w.parseDate(e,n)}));else if(e instanceof Date||"number"==typeof e)t=[w.parseDate(e,n)];else if("string"==typeof e)switch(w.config.mode){case"single":case"time":t=[w.parseDate(e,n)];break;case"multiple":t=e.split(w.config.conjunction).map((function(e){return w.parseDate(e,n)}));break;case"range":t=e.split(w.l10n.rangeSeparator).map((function(e){return w.parseDate(e,n)}))}else w.config.errorHandler(new Error("Invalid date supplied: "+JSON.stringify(e)));w.selectedDates=w.config.allowInvalidPreload?t:t.filter((function(e){return e instanceof Date&&ne(e,!1)})),"range"===w.config.mode&&w.selectedDates.sort((function(e,n){return e.getTime()-n.getTime()}))}function he(e){return e.slice().map((function(e){return"string"==typeof e||"number"==typeof e||e instanceof Date?w.parseDate(e,void 0,!0):e&&"object"==typeof e&&e.from&&e.to?{from:w.parseDate(e.from,void 0),to:w.parseDate(e.to,void 0)}:e})).filter((function(e){return e}))}function ve(){w._positionElement=w.config.positionElement||w._input}function De(e,n){if(void 0!==w.config){var t=w.config[e];if(void 0!==t&&t.length>0)for(var a=0;t[a]&&a1||"static"===w.config.monthSelectorType?w.monthElements[n].textContent=h(t.getMonth(),w.config.shorthandCurrentMonth,w.l10n)+" ":w.monthsDropdownContainer.value=t.getMonth().toString(),e.value=t.getFullYear().toString()})),w._hidePrevMonthArrow=void 0!==w.config.minDate&&(w.currentYear===w.config.minDate.getFullYear()?w.currentMonth<=w.config.minDate.getMonth():w.currentYearw.config.maxDate.getMonth():w.currentYear>w.config.maxDate.getFullYear()))}function Me(e){var n=e||(w.config.altInput?w.config.altFormat:w.config.dateFormat);return w.selectedDates.map((function(e){return w.formatDate(e,n)})).filter((function(e,n,t){return"range"!==w.config.mode||w.config.enableTime||t.indexOf(e)===n})).join("range"!==w.config.mode?w.config.conjunction:w.l10n.rangeSeparator)}function ye(e){void 0===e&&(e=!0),void 0!==w.mobileInput&&w.mobileFormatStr&&(w.mobileInput.value=void 0!==w.latestSelectedDateObj?w.formatDate(w.latestSelectedDateObj,w.mobileFormatStr):""),w.input.value=Me(w.config.dateFormat),void 0!==w.altInput&&(w.altInput.value=Me(w.config.altFormat)),!1!==e&&De("onValueUpdate")}function xe(e){var n=g(e),t=w.prevMonthNav.contains(n),a=w.nextMonthNav.contains(n);t||a?Z(t?-1:1):w.yearElements.indexOf(n)>=0?n.select():n.classList.contains("arrowUp")?w.changeYear(w.currentYear+1):n.classList.contains("arrowDown")&&w.changeYear(w.currentYear-1)}return function(){w.element=w.input=p,w.isOpen=!1,function(){var n=["wrap","weekNumbers","allowInput","allowInvalidPreload","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],i=e(e({},JSON.parse(JSON.stringify(p.dataset||{}))),v),o={};w.config.parseDate=i.parseDate,w.config.formatDate=i.formatDate,Object.defineProperty(w.config,"enable",{get:function(){return w.config._enable},set:function(e){w.config._enable=he(e)}}),Object.defineProperty(w.config,"disable",{get:function(){return w.config._disable},set:function(e){w.config._disable=he(e)}});var r="time"===i.mode;if(!i.dateFormat&&(i.enableTime||r)){var l=I.defaultConfig.dateFormat||a.dateFormat;o.dateFormat=i.noCalendar||r?"H:i"+(i.enableSeconds?":S":""):l+" H:i"+(i.enableSeconds?":S":"")}if(i.altInput&&(i.enableTime||r)&&!i.altFormat){var s=I.defaultConfig.altFormat||a.altFormat;o.altFormat=i.noCalendar||r?"h:i"+(i.enableSeconds?":S K":" K"):s+" h:i"+(i.enableSeconds?":S":"")+" K"}Object.defineProperty(w.config,"minDate",{get:function(){return w.config._minDate},set:le("min")}),Object.defineProperty(w.config,"maxDate",{get:function(){return w.config._maxDate},set:le("max")});var d=function(e){return function(n){w.config["min"===e?"_minTime":"_maxTime"]=w.parseDate(n,"H:i:S")}};Object.defineProperty(w.config,"minTime",{get:function(){return w.config._minTime},set:d("min")}),Object.defineProperty(w.config,"maxTime",{get:function(){return w.config._maxTime},set:d("max")}),"time"===i.mode&&(w.config.noCalendar=!0,w.config.enableTime=!0);Object.assign(w.config,o,i);for(var u=0;u-1?w.config[m]=c(f[m]).map(T).concat(w.config[m]):void 0===i[m]&&(w.config[m]=f[m])}i.altInputClass||(w.config.altInputClass=ce().className+" "+w.config.altInputClass);De("onParseConfig")}(),se(),function(){if(w.input=ce(),!w.input)return void w.config.errorHandler(new Error("Invalid input element specified"));w.input._type=w.input.type,w.input.type="text",w.input.classList.add("flatpickr-input"),w._input=w.input,w.config.altInput&&(w.altInput=d(w.input.nodeName,w.config.altInputClass),w._input=w.altInput,w.altInput.placeholder=w.input.placeholder,w.altInput.disabled=w.input.disabled,w.altInput.required=w.input.required,w.altInput.tabIndex=w.input.tabIndex,w.altInput.type="text",w.input.setAttribute("type","hidden"),!w.config.static&&w.input.parentNode&&w.input.parentNode.insertBefore(w.altInput,w.input.nextSibling));w.config.allowInput||w._input.setAttribute("readonly","readonly");ve()}(),function(){w.selectedDates=[],w.now=w.parseDate(w.config.now)||new Date;var e=w.config.defaultDate||("INPUT"!==w.input.nodeName&&"TEXTAREA"!==w.input.nodeName||!w.input.placeholder||w.input.value!==w.input.placeholder?w.input.value:null);e&&pe(e,w.config.dateFormat);w._initialDate=w.selectedDates.length>0?w.selectedDates[0]:w.config.minDate&&w.config.minDate.getTime()>w.now.getTime()?w.config.minDate:w.config.maxDate&&w.config.maxDate.getTime()0&&(w.latestSelectedDateObj=w.selectedDates[0]);void 0!==w.config.minTime&&(w.config.minTime=w.parseDate(w.config.minTime,"H:i"));void 0!==w.config.maxTime&&(w.config.maxTime=w.parseDate(w.config.maxTime,"H:i"));w.minDateHasTime=!!w.config.minDate&&(w.config.minDate.getHours()>0||w.config.minDate.getMinutes()>0||w.config.minDate.getSeconds()>0),w.maxDateHasTime=!!w.config.maxDate&&(w.config.maxDate.getHours()>0||w.config.maxDate.getMinutes()>0||w.config.maxDate.getSeconds()>0)}(),w.utils={getDaysInMonth:function(e,n){return void 0===e&&(e=w.currentMonth),void 0===n&&(n=w.currentYear),1===e&&(n%4==0&&n%100!=0||n%400==0)?29:w.l10n.daysInMonth[e]}},w.isMobile||function(){var e=window.document.createDocumentFragment();if(w.calendarContainer=d("div","flatpickr-calendar"),w.calendarContainer.tabIndex=-1,!w.config.noCalendar){if(e.appendChild((w.monthNav=d("div","flatpickr-months"),w.yearElements=[],w.monthElements=[],w.prevMonthNav=d("span","flatpickr-prev-month"),w.prevMonthNav.innerHTML=w.config.prevArrow,w.nextMonthNav=d("span","flatpickr-next-month"),w.nextMonthNav.innerHTML=w.config.nextArrow,V(),Object.defineProperty(w,"_hidePrevMonthArrow",{get:function(){return w.__hidePrevMonthArrow},set:function(e){w.__hidePrevMonthArrow!==e&&(s(w.prevMonthNav,"flatpickr-disabled",e),w.__hidePrevMonthArrow=e)}}),Object.defineProperty(w,"_hideNextMonthArrow",{get:function(){return w.__hideNextMonthArrow},set:function(e){w.__hideNextMonthArrow!==e&&(s(w.nextMonthNav,"flatpickr-disabled",e),w.__hideNextMonthArrow=e)}}),w.currentYearElement=w.yearElements[0],Ce(),w.monthNav)),w.innerContainer=d("div","flatpickr-innerContainer"),w.config.weekNumbers){var n=function(){w.calendarContainer.classList.add("hasWeeks");var e=d("div","flatpickr-weekwrapper");e.appendChild(d("span","flatpickr-weekday",w.l10n.weekAbbreviation));var n=d("div","flatpickr-weeks");return e.appendChild(n),{weekWrapper:e,weekNumbers:n}}(),t=n.weekWrapper,a=n.weekNumbers;w.innerContainer.appendChild(t),w.weekNumbers=a,w.weekWrapper=t}w.rContainer=d("div","flatpickr-rContainer"),w.rContainer.appendChild(z()),w.daysContainer||(w.daysContainer=d("div","flatpickr-days"),w.daysContainer.tabIndex=-1),U(),w.rContainer.appendChild(w.daysContainer),w.innerContainer.appendChild(w.rContainer),e.appendChild(w.innerContainer)}w.config.enableTime&&e.appendChild(function(){w.calendarContainer.classList.add("hasTime"),w.config.noCalendar&&w.calendarContainer.classList.add("noCalendar");var e=E(w.config);w.timeContainer=d("div","flatpickr-time"),w.timeContainer.tabIndex=-1;var n=d("span","flatpickr-time-separator",":"),t=m("flatpickr-hour",{"aria-label":w.l10n.hourAriaLabel});w.hourElement=t.getElementsByTagName("input")[0];var a=m("flatpickr-minute",{"aria-label":w.l10n.minuteAriaLabel});w.minuteElement=a.getElementsByTagName("input")[0],w.hourElement.tabIndex=w.minuteElement.tabIndex=-1,w.hourElement.value=o(w.latestSelectedDateObj?w.latestSelectedDateObj.getHours():w.config.time_24hr?e.hours:function(e){switch(e%24){case 0:case 12:return 12;default:return e%12}}(e.hours)),w.minuteElement.value=o(w.latestSelectedDateObj?w.latestSelectedDateObj.getMinutes():e.minutes),w.hourElement.setAttribute("step",w.config.hourIncrement.toString()),w.minuteElement.setAttribute("step",w.config.minuteIncrement.toString()),w.hourElement.setAttribute("min",w.config.time_24hr?"0":"1"),w.hourElement.setAttribute("max",w.config.time_24hr?"23":"12"),w.hourElement.setAttribute("maxlength","2"),w.minuteElement.setAttribute("min","0"),w.minuteElement.setAttribute("max","59"),w.minuteElement.setAttribute("maxlength","2"),w.timeContainer.appendChild(t),w.timeContainer.appendChild(n),w.timeContainer.appendChild(a),w.config.time_24hr&&w.timeContainer.classList.add("time24hr");if(w.config.enableSeconds){w.timeContainer.classList.add("hasSeconds");var i=m("flatpickr-second");w.secondElement=i.getElementsByTagName("input")[0],w.secondElement.value=o(w.latestSelectedDateObj?w.latestSelectedDateObj.getSeconds():e.seconds),w.secondElement.setAttribute("step",w.minuteElement.getAttribute("step")),w.secondElement.setAttribute("min","0"),w.secondElement.setAttribute("max","59"),w.secondElement.setAttribute("maxlength","2"),w.timeContainer.appendChild(d("span","flatpickr-time-separator",":")),w.timeContainer.appendChild(i)}w.config.time_24hr||(w.amPM=d("span","flatpickr-am-pm",w.l10n.amPM[r((w.latestSelectedDateObj?w.hourElement.value:w.config.defaultHour)>11)]),w.amPM.title=w.l10n.toggleTitle,w.amPM.tabIndex=-1,w.timeContainer.appendChild(w.amPM));return w.timeContainer}());s(w.calendarContainer,"rangeMode","range"===w.config.mode),s(w.calendarContainer,"animate",!0===w.config.animate),s(w.calendarContainer,"multiMonth",w.config.showMonths>1),w.calendarContainer.appendChild(e);var i=void 0!==w.config.appendTo&&void 0!==w.config.appendTo.nodeType;if((w.config.inline||w.config.static)&&(w.calendarContainer.classList.add(w.config.inline?"inline":"static"),w.config.inline&&(!i&&w.element.parentNode?w.element.parentNode.insertBefore(w.calendarContainer,w._input.nextSibling):void 0!==w.config.appendTo&&w.config.appendTo.appendChild(w.calendarContainer)),w.config.static)){var l=d("div","flatpickr-wrapper");w.element.parentNode&&w.element.parentNode.insertBefore(l,w.element),l.appendChild(w.element),w.altInput&&l.appendChild(w.altInput),l.appendChild(w.calendarContainer)}w.config.static||w.config.inline||(void 0!==w.config.appendTo?w.config.appendTo:window.document.body).appendChild(w.calendarContainer)}(),function(){w.config.wrap&&["open","close","toggle","clear"].forEach((function(e){Array.prototype.forEach.call(w.element.querySelectorAll("[data-"+e+"]"),(function(n){return P(n,"click",w[e])}))}));if(w.isMobile)return void function(){var e=w.config.enableTime?w.config.noCalendar?"time":"datetime-local":"date";w.mobileInput=d("input",w.input.className+" flatpickr-mobile"),w.mobileInput.tabIndex=1,w.mobileInput.type=e,w.mobileInput.disabled=w.input.disabled,w.mobileInput.required=w.input.required,w.mobileInput.placeholder=w.input.placeholder,w.mobileFormatStr="datetime-local"===e?"Y-m-d\\TH:i:S":"date"===e?"Y-m-d":"H:i:S",w.selectedDates.length>0&&(w.mobileInput.defaultValue=w.mobileInput.value=w.formatDate(w.selectedDates[0],w.mobileFormatStr));w.config.minDate&&(w.mobileInput.min=w.formatDate(w.config.minDate,"Y-m-d"));w.config.maxDate&&(w.mobileInput.max=w.formatDate(w.config.maxDate,"Y-m-d"));w.input.getAttribute("step")&&(w.mobileInput.step=String(w.input.getAttribute("step")));w.input.type="hidden",void 0!==w.altInput&&(w.altInput.type="hidden");try{w.input.parentNode&&w.input.parentNode.insertBefore(w.mobileInput,w.input.nextSibling)}catch(e){}P(w.mobileInput,"change",(function(e){w.setDate(g(e).value,!1,w.mobileFormatStr),De("onChange"),De("onClose")}))}();var e=l(re,50);w._debouncedChange=l(Y,300),w.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&P(w.daysContainer,"mouseover",(function(e){"range"===w.config.mode&&oe(g(e))}));P(w._input,"keydown",ie),void 0!==w.calendarContainer&&P(w.calendarContainer,"keydown",ie);w.config.inline||w.config.static||P(window,"resize",e);void 0!==window.ontouchstart?P(window.document,"touchstart",X):P(window.document,"mousedown",X);P(window.document,"focus",X,{capture:!0}),!0===w.config.clickOpens&&(P(w._input,"focus",w.open),P(w._input,"click",w.open));void 0!==w.daysContainer&&(P(w.monthNav,"click",xe),P(w.monthNav,["keyup","increment"],N),P(w.daysContainer,"click",me));if(void 0!==w.timeContainer&&void 0!==w.minuteElement&&void 0!==w.hourElement){var n=function(e){return g(e).select()};P(w.timeContainer,["increment"],_),P(w.timeContainer,"blur",_,{capture:!0}),P(w.timeContainer,"click",H),P([w.hourElement,w.minuteElement],["focus","click"],n),void 0!==w.secondElement&&P(w.secondElement,"focus",(function(){return w.secondElement&&w.secondElement.select()})),void 0!==w.amPM&&P(w.amPM,"click",(function(e){_(e)}))}w.config.allowInput&&P(w._input,"blur",ae)}(),(w.selectedDates.length||w.config.noCalendar)&&(w.config.enableTime&&F(w.config.noCalendar?w.latestSelectedDateObj:void 0),ye(!1)),S();var n=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);!w.isMobile&&n&&de(),De("onReady")}(),w}function T(e,n){for(var t=Array.prototype.slice.call(e).filter((function(e){return e instanceof HTMLElement})),a=[],i=0;i { + // DOM Elements + const trigger = document.getElementById('helpPanelTrigger'); + const panel = document.getElementById('help-sidebar-panel'); + const backdrop = document.getElementById('help-panel-backdrop'); + const closeBtn = document.getElementById('helpPanelClose'); + + const indexView = document.getElementById('help-index-view'); + const detailView = document.getElementById('help-detail-view'); + const backBtn = document.getElementById('help-back-btn'); + const detailContent = document.getElementById('help-detail-content'); + + const searchInput = document.getElementById('help-search-input'); + const guideLinks = document.querySelectorAll('.help-guide-link'); + const noResults = document.getElementById('help-no-results'); + + if (!trigger || !panel || !backdrop || !closeBtn) { + return; + } + + // Toggle Drawer Open/Close + function openHelpPanel() { + panel.classList.add('open'); + backdrop.classList.add('show'); + sessionStorage.setItem('helpPanelOpen', 'true'); + } + + function closeHelpPanel() { + panel.classList.remove('open'); + backdrop.classList.remove('show'); + sessionStorage.setItem('helpPanelOpen', 'false'); + sessionStorage.removeItem('helpPanelDoc'); + + // Reset to index view when closing + setTimeout(() => { + showIndexView(); + if (searchInput) { + searchInput.value = ''; + filterGuides(''); + } + }, 350); // wait for transition + } + + trigger.addEventListener('click', (e) => { + e.preventDefault(); + if (panel.classList.contains('open')) { + closeHelpPanel(); + } else { + openHelpPanel(); + } + }); + + closeBtn.addEventListener('click', closeHelpPanel); + backdrop.addEventListener('click', closeHelpPanel); + + // Escape Key to Close + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && panel.classList.contains('open')) { + closeHelpPanel(); + } + }); + + // Filtering logic (Index list) + if (searchInput) { + searchInput.addEventListener('input', function() { + const query = this.value.toLowerCase().trim(); + filterGuides(query); + }); + } + + function filterGuides(query) { + let visibleCount = 0; + + guideLinks.forEach(link => { + const titleEl = link.querySelector('.help-guide-link-title'); + const descEl = link.querySelector('.help-guide-link-desc'); + + if (!titleEl || !descEl) { + return; + } + + const title = titleEl.textContent.toLowerCase(); + const desc = descEl.textContent.toLowerCase(); + + if (title.includes(query) || desc.includes(query)) { + link.classList.remove('d-none'); + visibleCount++; + } else { + link.classList.add('d-none'); + } + }); + + if (visibleCount === 0) { + noResults.classList.remove('d-none'); + } else { + noResults.classList.add('d-none'); + } + } + + // Navigation logic (Index -> Detail) + guideLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const docName = link.getAttribute('data-doc-name'); + if (docName) { + loadHelpDocument(docName); + } + }); + }); + + if (backBtn) { + backBtn.addEventListener('click', (e) => { + e.preventDefault(); + showIndexView(); + }); + } + + function showIndexView() { + detailView.classList.add('d-none'); + indexView.classList.remove('d-none'); + detailContent.innerHTML = ''; + sessionStorage.removeItem('helpPanelDoc'); + } + + function showDetailView() { + indexView.classList.add('d-none'); + detailView.classList.remove('d-none'); + detailView.scrollTop = 0; + } + + // Dynamic AJAX loading + async function loadHelpDocument(docName) { + showDetailView(); + sessionStorage.setItem('helpPanelDoc', docName); + + // Show loading spinner + const loadingText = panel.getAttribute('data-loading-text') || 'Chargement...'; + detailContent.innerHTML = ` +
    +
    + ${loadingText} +
    + `; + + try { + const response = await fetch(`/common/help/${docName}/`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + + if (data.html) { + detailContent.innerHTML = `
    ${data.html}
    `; + } else { + showError(); + } + } catch (error) { + console.error('Error loading help document:', error); + showError(); + } + } + + function showError() { + const errorText = panel.getAttribute('data-error-text') || 'Erreur lors du chargement.'; + detailContent.innerHTML = ` +
    + +

    ${errorText}

    +
    + `; + } + + // Restore state from sessionStorage on page load + const isHelpOpen = sessionStorage.getItem('helpPanelOpen') === 'true'; + const activeDocName = sessionStorage.getItem('helpPanelDoc'); + + if (isHelpOpen) { + // Temporarily disable transition to prevent visible flicker + const originalPanelTransition = panel.style.transition; + const originalBackdropTransition = backdrop.style.transition; + panel.style.transition = 'none'; + backdrop.style.transition = 'none'; + + openHelpPanel(); + + if (activeDocName) { + loadHelpDocument(activeDocName); + } + + // Restore transition after browser renders the initial open state + setTimeout(() => { + panel.style.transition = originalPanelTransition; + backdrop.style.transition = originalBackdropTransition; + }, 50); + } +}); diff --git a/streetup/common/static/common/jquery-ui.min.css b/streetup/common/static/common/jquery-ui.min.css new file mode 100644 index 0000000..d7c0644 --- /dev/null +++ b/streetup/common/static/common/jquery-ui.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.14.1 - 2024-10-30 +* https://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit https://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;user-select:none}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;background-image:none}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:none}.ui-widget-header .ui-icon{background-image:none}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:none}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:none}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:none}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:none}.ui-button .ui-icon{background-image:none}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3}.ui-widget-shadow{box-shadow:0 0 5px #666} \ No newline at end of file diff --git a/streetup/common/static/common/jquery-ui.min.js b/streetup/common/static/common/jquery-ui.min.js new file mode 100644 index 0000000..841179e --- /dev/null +++ b/streetup/common/static/common/jquery-ui.min.js @@ -0,0 +1,6 @@ +/*! jQuery UI - v1.14.1 - 2024-10-30 +* https://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ + +(t=>{"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(V){V.ui=V.ui||{};V.ui.version="1.14.1";var n,s,x,k,o,a,r,l,h,i,E=0,N=Array.prototype.hasOwnProperty,c=Array.prototype.slice;V.cleanData=(n=V.cleanData,function(t){for(var e,i,s=0;null!=(i=t[s]);s++)(e=V._data(i,"events"))&&e.remove&&V(i).triggerHandler("remove");n(t)}),V.widget=function(t,i,e){var s,n,o,a,r={},l=t.split(".")[0];return"__proto__"===(t=t.split(".")[1])||"constructor"===t?V.error("Invalid widget name: "+t):(a=l+"-"+t,e||(e=i,i=V.Widget),Array.isArray(e)&&(e=V.extend.apply(null,[{}].concat(e))),V.expr.pseudos[a.toLowerCase()]=function(t){return!!V.data(t,a)},V[l]=V[l]||{},s=V[l][t],n=V[l][t]=function(t,e){if(!this||!this._createWidget)return new n(t,e);arguments.length&&this._createWidget(t,e)},V.extend(n,s,{version:e.version,_proto:V.extend({},e),_childConstructors:[]}),(o=new i).options=V.widget.extend({},o.options),V.each(e,function(e,s){function n(){return i.prototype[e].apply(this,arguments)}function o(t){return i.prototype[e].apply(this,t)}r[e]="function"!=typeof s?s:function(){var t,e=this._super,i=this._superApply;return this._super=n,this._superApply=o,t=s.apply(this,arguments),this._super=e,this._superApply=i,t}}),n.prototype=V.widget.extend(o,{widgetEventPrefix:s&&o.widgetEventPrefix||t},r,{constructor:n,namespace:l,widgetName:t,widgetFullName:a}),s?(V.each(s._childConstructors,function(t,e){var i=e.prototype;V.widget(i.namespace+"."+i.widgetName,n,e._proto)}),delete s._childConstructors):i._childConstructors.push(n),V.widget.bridge(t,n),n)},V.widget.extend=function(t){for(var e,i,s=c.call(arguments,1),n=0,o=s.length;n",options:{classes:{},disabled:!1,create:null},_createWidget:function(t,e){e=V(e||this.defaultElement||this)[0],this.element=V(e),this.uuid=E++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=V(),this.hoverable=V(),this.focusable=V(),this.classesElementLookup={},e!==this&&(V.data(e,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===e&&this.destroy()}}),this.document=V(e.style?e.ownerDocument:e.document||e),this.window=V(this.document[0].defaultView||this.document[0].parentWindow)),this.options=V.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:V.noop,_create:V.noop,_init:V.noop,destroy:function(){var i=this;this._destroy(),V.each(this.classesElementLookup,function(t,e){i._removeClass(e,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:V.noop,widget:function(){return this.element},option:function(t,e){var i,s,n,o=t;if(0===arguments.length)return V.widget.extend({},this.options);if("string"==typeof t)if(o={},t=(i=t.split(".")).shift(),i.length){for(s=o[t]=V.widget.extend({},this.options[t]),n=0;n{var i=[];n.element.each(function(t,e){V.map(a.classesElementLookup,function(t){return t}).some(function(t){return t.is(e)})||i.push(e)}),a._on(V(i),{remove:"_untrackClassesElement"})})(),V(V.uniqueSort(i.get().concat(n.element.get())))):V(i.not(n.element).get()),a.classesElementLookup[t[s]]=i,o.push(t[s]),e&&n.classes[t[s]]&&o.push(n.classes[t[s]])}return(n=V.extend({element:this.element,classes:this.options.classes||{}},n)).keys&&t(n.keys.match(/\S+/g)||[],!0),n.extra&&t(n.extra.match(/\S+/g)||[]),o.join(" ")},_untrackClassesElement:function(i){var s=this;V.each(s.classesElementLookup,function(t,e){-1!==V.inArray(i.target,e)&&(s.classesElementLookup[t]=V(e.not(i.target).get()))}),this._off(V(i.target))},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){var n="string"==typeof t||null===t,e={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s="boolean"==typeof s?s:i};return e.element.toggleClass(this._classes(e),s),this},_on:function(n,o,t){var a,r=this;"boolean"!=typeof n&&(t=o,o=n,n=!1),t?(o=a=V(o),this.bindings=this.bindings.add(o)):(t=o,o=this.element,a=this.widget()),V.each(t,function(t,e){function i(){if(n||!0!==r.options.disabled&&!V(this).hasClass("ui-state-disabled"))return("string"==typeof e?r[e]:e).apply(r,arguments)}"string"!=typeof e&&(i.guid=e.guid=e.guid||i.guid||V.guid++);var t=t.match(/^([\w:-]*)\s*(.*)$/),s=t[1]+r.eventNamespace,t=t[2];t?a.on(s,t,i):o.on(s,i)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.off(e),this.bindings=V(this.bindings.not(t).get()),this.focusable=V(this.focusable.not(t).get()),this.hoverable=V(this.hoverable.not(t).get())},_delay:function(t,e){var i=this;return setTimeout(function(){return("string"==typeof t?i[t]:t).apply(i,arguments)},e||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){this._addClass(V(t.currentTarget),null,"ui-state-hover")},mouseleave:function(t){this._removeClass(V(t.currentTarget),null,"ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){this._addClass(V(t.currentTarget),null,"ui-state-focus")},focusout:function(t){this._removeClass(V(t.currentTarget),null,"ui-state-focus")}})},_trigger:function(t,e,i){var s,n,o=this.options[t];if(i=i||{},(e=V.Event(e)).type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),e.target=this.element[0],n=e.originalEvent)for(s in n)s in e||(e[s]=n[s]);return this.element.trigger(e,i),!("function"==typeof o&&!1===o.apply(this.element[0],[e].concat(i))||e.isDefaultPrevented())}},V.each({show:"fadeIn",hide:"fadeOut"},function(o,a){V.Widget.prototype["_"+o]=function(e,t,i){var s,n=(t="string"==typeof t?{effect:t}:t)?!0!==t&&"number"!=typeof t&&t.effect||a:o;"number"==typeof(t=t||{})?t={duration:t}:!0===t&&(t={}),s=!V.isEmptyObject(t),t.complete=i,t.delay&&e.delay(t.delay),s&&V.effects&&V.effects.effect[n]?e[o](t):n!==o&&e[n]?e[n](t.duration,t.easing,i):e.queue(function(t){V(this)[o](),i&&i.call(e[0]),t()})}}),V.widget;function C(t,e,i){return[parseFloat(t[0])*(h.test(t[0])?e/100:1),parseFloat(t[1])*(h.test(t[1])?i/100:1)]}function D(t,e){return parseInt(V.css(t,e),10)||0}function I(t){return null!=t&&t===t.window}x=Math.max,k=Math.abs,o=/left|center|right/,a=/top|center|bottom/,r=/[\+\-]\d+(\.[\d]+)?%?/,l=/^\w+/,h=/%$/,i=V.fn.position,V.position={scrollbarWidth:function(){var t,e,i;return void 0!==s?s:(i=(e=V("
    ")).children()[0],V("body").append(e),t=i.offsetWidth,e.css("overflow","scroll"),t===(i=i.offsetWidth)&&(i=e[0].clientWidth),e.remove(),s=t-i)},getScrollInfo:function(t){var e=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),i=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),e="scroll"===e||"auto"===e&&t.widthx(k(s),k(n))?o.important="horizontal":o.important="vertical",u.using.call(this,t,o)}),a.offset(V.extend(h,{using:t}))})):i.apply(this,arguments)},V.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,s=s.width,o=t.left-e.collisionPosition.marginLeft,a=n-o,r=o+e.collisionWidth-s-n;so.mod/2?s+=o.mod:s-n>o.mod/2&&(s-=o.mod)),l[i]=_((n-s)*a+s,e)))}),this[t](l)},blend:function(t){var e,i,s;return 1===this._rgba[3]?this:(e=this._rgba.slice(),i=e.pop(),s=p(t)._rgba,p(u.map(e,function(t,e){return(1-i)*s[e]+i*t})))},toRgbaString:function(){var t="rgba(",e=u.map(this._rgba,function(t,e){return null!=t?t:2{var i,s,n={};for(i in e)s=e[i],t[i]===s||U[i]||!V.fx.step[i]&&isNaN(parseFloat(s))||(n[i]=s);return n})(this.start,this.end),this}),i.attr("class",t),e=e.map(function(){var t=this,e=V.Deferred(),i=V.extend({},o,{queue:!1,complete:function(){e.resolve(t)}});return this.el.animate(this.diff,i),e.promise()}),V.when.apply(V,e.get()).done(function(){s(),V.each(arguments,function(){var e=this.el;V.each(this.diff,function(t){e.css(t,"")})}),o.complete.call(i[0])})})},V.fn.extend({addClass:(q=V.fn.addClass,function(t,e,i,s){return e?V.effects.animateClass.call(this,{add:t},e,i,s):q.apply(this,arguments)}),removeClass:(j=V.fn.removeClass,function(t,e,i,s){return 1
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e={width:i.width(),height:i.height()},n=document.activeElement;try{n.id}catch(t){n=document.body}return i.wrap(t),i[0]!==n&&!V.contains(i[0],n)||V(n).trigger("focus"),t=i.parent(),"static"===i.css("position")?(t.css({position:"relative"}),i.css({position:"relative"})):(V.extend(s,{position:i.css("position"),zIndex:i.css("z-index")}),V.each(["top","left","bottom","right"],function(t,e){s[e]=i.css(e),isNaN(parseInt(s[e],10))&&(s[e]="auto")}),i.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),i.css(e),t.css(s).show()},removeWrapper:function(t){var e=document.activeElement;return t.parent().is(".ui-effects-wrapper")&&(t.parent().replaceWith(t),t[0]!==e&&!V.contains(t[0],e)||V(e).trigger("focus")),t}}),V.extend(V.effects,{version:"1.14.1",define:function(t,e,i){return i||(i=e,e="effect"),V.effects.effect[t]=i,V.effects.effect[t].mode=e,i},scaledDimensions:function(t,e,i){var s;return 0===e?{height:0,width:0,outerHeight:0,outerWidth:0}:(s="horizontal"!==i?(e||100)/100:1,i="vertical"!==i?(e||100)/100:1,{height:t.height()*i,width:t.width()*s,outerHeight:t.outerHeight()*i,outerWidth:t.outerWidth()*s})},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();1").insertAfter(t).css({display:/^(inline|ruby)/.test(t.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:t.css("marginTop"),marginBottom:t.css("marginBottom"),marginLeft:t.css("marginLeft"),marginRight:t.css("marginRight"),float:t.css("float")}).outerWidth(t.outerWidth()).outerHeight(t.outerHeight()).addClass("ui-effects-placeholder"),t.data(w+"placeholder",e)),t.css({position:i,left:s.left,top:s.top}),e},removePlaceholder:function(t){var e=w+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(t){V.effects.restoreStyle(t),V.effects.removePlaceholder(t)},setTransition:function(s,t,n,o){return o=o||{},V.each(t,function(t,e){var i=s.cssUnit(e);0
    ");l.appendTo("body").addClass(t.className).css({top:s.top-a,left:s.left-o,height:i.innerHeight(),width:i.innerWidth(),position:n?"fixed":"absolute"}).animate(r,t.duration,t.easing,function(){l.remove(),"function"==typeof e&&e()})}}),V.fx.step.clip=function(t){t.clipInit||(t.start=V(t.elem).cssClip(),"string"==typeof t.end&&(t.end=Z(t.end,t.elem)),t.clipInit=!0),V(t.elem).cssClip({top:t.pos*(t.end.top-t.start.top)+t.start.top,right:t.pos*(t.end.right-t.start.right)+t.start.right,bottom:t.pos*(t.end.bottom-t.start.bottom)+t.start.bottom,left:t.pos*(t.end.left-t.start.left)+t.start.left})},b={},V.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,t){b[t]=function(t){return Math.pow(t,e+2)}}),V.extend(b,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;t<((e=Math.pow(2,--i))-1)/11;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),V.each(b,function(t,e){V.easing["easeIn"+t]=e,V.easing["easeOut"+t]=function(t){return 1-e(1-t)},V.easing["easeInOut"+t]=function(t){return t<.5?e(2*t)/2:1-e(-2*t+2)/2}});var tt,et,S,it=V.effects,st=(V.effects.define("blind","hide",function(t,e){var i={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},s=V(this),n=t.direction||"up",o=s.cssClip(),a={clip:V.extend({},o)},r=V.effects.createPlaceholder(s);a.clip[i[n][0]]=a.clip[i[n][1]],"show"===t.mode&&(s.cssClip(a.clip),r&&r.css(V.effects.clipToBox(a)),a.clip=o),r&&r.animate(V.effects.clipToBox(a),t.duration,t.easing),s.animate(a,{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("bounce",function(t,e){var i,s,n=V(this),o=t.mode,a="hide"===o,o="show"===o,r=t.direction||"up",l=t.distance,h=t.times||5,c=2*h+(o||a?1:0),u=t.duration/c,d=t.easing,p="up"===r||"down"===r?"top":"left",f="up"===r||"left"===r,g=0,t=n.queue().length;for(V.effects.createPlaceholder(n),r=n.css(p),l=l||n["top"==p?"outerHeight":"outerWidth"]()/3,o&&((s={opacity:1})[p]=r,n.css("opacity",0).css(p,f?2*-l:2*l).animate(s,u,d)),a&&(l/=Math.pow(2,h-1)),(s={})[p]=r;g").css({position:"absolute",visibility:"visible",left:-s*p,top:-i*f}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:p,height:f,left:n+(u?a*p:0),top:o+(u?r*f:0),opacity:u?0:1}).animate({left:n+(u?0:a*p),top:o+(u?0:r*f),opacity:u?1:0},t.duration||500,t.easing,m)}),V.effects.define("fade","toggle",function(t,e){var i="show"===t.mode;V(this).css("opacity",i?0:1).animate({opacity:i?1:0},{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("fold","hide",function(e,t){var i=V(this),s=e.mode,n="show"===s,s="hide"===s,o=e.size||15,a=/([0-9]+)%/.exec(o),r=!!e.horizFirst?["right","bottom"]:["bottom","right"],l=e.duration/2,h=V.effects.createPlaceholder(i),c=i.cssClip(),u={clip:V.extend({},c)},d={clip:V.extend({},c)},p=[c[r[0]],c[r[1]]],f=i.queue().length;a&&(o=parseInt(a[1],10)/100*p[s?0:1]),u.clip[r[0]]=o,d.clip[r[0]]=o,d.clip[r[1]]=0,n&&(i.cssClip(d.clip),h&&h.css(V.effects.clipToBox(d)),d.clip=c),i.queue(function(t){h&&h.animate(V.effects.clipToBox(u),l,e.easing).animate(V.effects.clipToBox(d),l,e.easing),t()}).animate(u,l,e.easing).animate(d,l,e.easing).queue(t),V.effects.unshift(i,f,4)}),V.effects.define("highlight","show",function(t,e){var i=V(this),s={backgroundColor:i.css("backgroundColor")};"hide"===t.mode&&(s.opacity=0),V.effects.saveStyle(i),i.css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(s,{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("size",function(s,e){var n,i=V(this),t=["fontSize"],o=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],a=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],r=s.mode,l="effect"!==r,h=s.scale||"both",c=s.origin||["middle","center"],u=i.css("position"),d=i.position(),p=V.effects.scaledDimensions(i),f=s.from||p,g=s.to||V.effects.scaledDimensions(i,0);V.effects.createPlaceholder(i),"show"===r&&(r=f,f=g,g=r),n={from:{y:f.height/p.height,x:f.width/p.width},to:{y:g.height/p.height,x:g.width/p.width}},"box"!==h&&"both"!==h||(n.from.y!==n.to.y&&(f=V.effects.setTransition(i,o,n.from.y,f),g=V.effects.setTransition(i,o,n.to.y,g)),n.from.x!==n.to.x&&(f=V.effects.setTransition(i,a,n.from.x,f),g=V.effects.setTransition(i,a,n.to.x,g))),"content"!==h&&"both"!==h||n.from.y!==n.to.y&&(f=V.effects.setTransition(i,t,n.from.y,f),g=V.effects.setTransition(i,t,n.to.y,g)),c&&(r=V.effects.getBaseline(c,p),f.top=(p.outerHeight-f.outerHeight)*r.y+d.top,f.left=(p.outerWidth-f.outerWidth)*r.x+d.left,g.top=(p.outerHeight-g.outerHeight)*r.y+d.top,g.left=(p.outerWidth-g.outerWidth)*r.x+d.left),delete f.outerHeight,delete f.outerWidth,i.css(f),"content"!==h&&"both"!==h||(o=o.concat(["marginTop","marginBottom"]).concat(t),a=a.concat(["marginLeft","marginRight"]),i.find("*[width]").each(function(){var t=V(this),e=V.effects.scaledDimensions(t),i={height:e.height*n.from.y,width:e.width*n.from.x,outerHeight:e.outerHeight*n.from.y,outerWidth:e.outerWidth*n.from.x},e={height:e.height*n.to.y,width:e.width*n.to.x,outerHeight:e.height*n.to.y,outerWidth:e.width*n.to.x};n.from.y!==n.to.y&&(i=V.effects.setTransition(t,o,n.from.y,i),e=V.effects.setTransition(t,o,n.to.y,e)),n.from.x!==n.to.x&&(i=V.effects.setTransition(t,a,n.from.x,i),e=V.effects.setTransition(t,a,n.to.x,e)),l&&V.effects.saveStyle(t),t.css(i),t.animate(e,s.duration,s.easing,function(){l&&V.effects.restoreStyle(t)})})),i.animate(g,{queue:!1,duration:s.duration,easing:s.easing,complete:function(){var t=i.offset();0===g.opacity&&i.css("opacity",f.opacity),l||(i.css("position","static"===u?"relative":u).offset(t),V.effects.saveStyle(i)),e()}})}),V.effects.define("scale",function(t,e){var i=V(this),s=t.mode,s=parseInt(t.percent,10)||(0===parseInt(t.percent,10)||"effect"!==s?0:100),i=V.extend(!0,{from:V.effects.scaledDimensions(i),to:V.effects.scaledDimensions(i,s,t.direction||"both"),origin:t.origin||["middle","center"]},t);t.fade&&(i.from.opacity=1,i.to.opacity=0),V.effects.effect.size.call(this,i,e)}),V.effects.define("puff","hide",function(t,e){t=V.extend(!0,{},t,{fade:!0,percent:parseInt(t.percent,10)||150});V.effects.effect.scale.call(this,t,e)}),V.effects.define("pulsate","show",function(t,e){var i=V(this),s=t.mode,n="show"===s,o=2*(t.times||5)+(n||"hide"===s?1:0),a=t.duration/o,r=0,l=1,s=i.queue().length;for(!n&&i.is(":visible")||(i.css("opacity",0).show(),r=1);l li > :first-child").add(t.find("> :not(li)").filter(function(t){return t%2==0}))},heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var t=this.options;this.prevShow=this.prevHide=V(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),t.collapsible||!1!==t.active&&null!=t.active||(t.active=0),this._processPanels(),t.active<0&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():V()}},_createIcons:function(){var t,e=this.options.icons;e&&(t=V(""),this._addClass(t,"ui-accordion-header-icon","ui-icon "+e.header),t.prependTo(this.headers),t=this.active.children(".ui-accordion-header-icon"),this._removeClass(t,e.header)._addClass(t,null,e.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){"active"===t?this._activate(e):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||!1!==this.options.active||this._activate(0),"icons"===t&&(this._destroyIcons(),e)&&this._createIcons())},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var e=V.ui.keyCode,i=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case e.RIGHT:case e.DOWN:n=this.headers[(s+1)%i];break;case e.LEFT:case e.UP:n=this.headers[(s-1+i)%i];break;case e.SPACE:case e.ENTER:this._eventHandler(t);break;case e.HOME:n=this.headers[0];break;case e.END:n=this.headers[i-1]}n&&(V(t.target).attr("tabIndex",-1),V(n).attr("tabIndex",0),V(n).trigger("focus"),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===V.ui.keyCode.UP&&t.ctrlKey&&V(t.currentTarget).prev().trigger("focus")},refresh:function(){var t=this.options;this._processPanels(),!1===t.active&&!0===t.collapsible||!this.headers.length?(t.active=!1,this.active=V()):!1===t.active?this._activate(0):this.active.length&&!V.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=V()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;"function"==typeof this.options.header?this.headers=this.options.header(this.element):this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var i,t=this.options,e=t.heightStyle,s=this.element.parent();this.active=this._findActive(t.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var t=V(this),e=t.uniqueId().attr("id"),i=t.next(),s=i.uniqueId().attr("id");t.attr("aria-controls",s),i.attr("aria-labelledby",e)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(t.event),"fill"===e?(i=s.height(),this.element.siblings(":visible").each(function(){var t=V(this),e=t.css("position");"absolute"!==e&&"fixed"!==e&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=V(this).outerHeight(!0)}),this.headers.next().each(function(){V(this).height(Math.max(0,i-V(this).innerHeight()+V(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.headers.next().each(function(){var t=V(this).is(":visible");t||V(this).show(),i=Math.max(i,V(this).css("height","").height()),t||V(this).hide()}).height(i))},_activate:function(t){t=this._findActive(t)[0];t!==this.active[0]&&(t=t||this.active[0],this._eventHandler({target:t,currentTarget:t,preventDefault:V.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):V()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&V.each(t.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var e=this.options,i=this.active,s=V(t.currentTarget),n=s[0]===i[0],o=n&&e.collapsible,a=o?V():s.next(),r=i.next(),r={oldHeader:i,oldPanel:r,newHeader:o?V():s,newPanel:a};t.preventDefault(),n&&!e.collapsible||!1===this._trigger("beforeActivate",t,r)||(e.active=!o&&this.headers.index(s),this.active=n?V():s,this._toggle(r),this._removeClass(i,"ui-accordion-header-active","ui-state-active"),e.icons&&(a=i.children(".ui-accordion-header-icon"),this._removeClass(a,null,e.icons.activeHeader)._addClass(a,null,e.icons.header)),n)||(this._removeClass(s,"ui-accordion-header-collapsed")._addClass(s,"ui-accordion-header-active","ui-state-active"),e.icons&&(t=s.children(".ui-accordion-header-icon"),this._removeClass(t,null,e.icons.header)._addClass(t,null,e.icons.activeHeader)),this._addClass(s.next(),"ui-accordion-content-active"))},_toggle:function(t){var e=t.newPanel,i=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=e,this.prevHide=i,this.options.animate?this._animate(e,i,t):(i.hide(),e.show(),this._toggleComplete(t)),i.attr({"aria-hidden":"true"}),i.prev().attr({"aria-selected":"false","aria-expanded":"false"}),e.length&&i.length?i.prev().attr({tabIndex:-1,"aria-expanded":"false"}):e.length&&this.headers.filter(function(){return 0===parseInt(V(this).attr("tabIndex"),10)}).attr("tabIndex",-1),e.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,i,e){function s(){o._toggleComplete(e)}var n,o=this,a=0,r=t.css("box-sizing"),l=t.length&&(!i.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.lastMousePosition={x:null,y:null},this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault(),this._activateItem(t)},"click .ui-menu-item":function(t){var e=V(t.target),i=V(this.document[0].activeElement);!this.mouseHandled&&e.not(".ui-state-disabled").length&&(this.select(t),t.isPropagationStopped()||(this.mouseHandled=!0),e.has(".ui-menu").length?this.expand(t):!this.element.is(":focus")&&i.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active)&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer))},"mouseenter .ui-menu-item":"_activateItem","mousemove .ui-menu-item":"_activateItem",mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this._menuItems().first();e||this.focus(t,i)},blur:function(t){this._delay(function(){V.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t,!0),this.mouseHandled=!1}})},_activateItem:function(t){var e,i;this.previousFilter||t.clientX===this.lastMousePosition.x&&t.clientY===this.lastMousePosition.y||(this.lastMousePosition={x:t.clientX,y:t.clientY},e=V(t.target).closest(".ui-menu-item"),i=V(t.currentTarget),e[0]!==i[0])||i.is(".ui-state-active")||(this._removeClass(i.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(t,i))},_destroy:function(){var t=this.element.find(".ui-menu-item").removeAttr("role aria-disabled").children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),t.children().each(function(){var t=V(this);t.data("ui-menu-submenu-caret")&&t.remove()})},_keydown:function(t){var e,i,s,n=!0;switch(t.keyCode){case V.ui.keyCode.PAGE_UP:this.previousPage(t);break;case V.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case V.ui.keyCode.HOME:this._move("first","first",t);break;case V.ui.keyCode.END:this._move("last","last",t);break;case V.ui.keyCode.UP:this.previous(t);break;case V.ui.keyCode.DOWN:this.next(t);break;case V.ui.keyCode.LEFT:this.collapse(t);break;case V.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case V.ui.keyCode.ENTER:case V.ui.keyCode.SPACE:this._activate(t);break;case V.ui.keyCode.ESCAPE:this.collapse(t);break;default:e=this.previousFilter||"",s=n=!1,i=96<=t.keyCode&&t.keyCode<=105?(t.keyCode-96).toString():String.fromCharCode(t.keyCode),clearTimeout(this.filterTimer),i===e?s=!0:i=e+i,e=this._filterMenuItems(i),(e=s&&-1!==e.index(this.active.next())?this.active.nextAll(".ui-menu-item"):e).length||(i=String.fromCharCode(t.keyCode),e=this._filterMenuItems(i)),e.length?(this.focus(t,e),this.previousFilter=i,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}n&&t.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var t,e,s=this,n=this.options.icons.submenu,i=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),t=i.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=V(this),e=t.prev(),i=V("").data("ui-menu-submenu-caret",!0);s._addClass(i,"ui-menu-icon","ui-icon "+n),e.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",e.attr("id"))}),this._addClass(t,"ui-menu","ui-widget ui-widget-content ui-front"),(t=i.add(this.element).find(this.options.items)).not(".ui-menu-item").each(function(){var t=V(this);s._isDivider(t)&&s._addClass(t,"ui-menu-divider","ui-widget-content")}),e=(i=t.not(".ui-menu-item, .ui-menu-divider")).children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(i,"ui-menu-item")._addClass(e,"ui-menu-item-wrapper"),t.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!V.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){var i;"icons"===t&&(i=this.element.find(".ui-menu-icon"),this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",String(t)),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),i=this.active.children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",i.attr("id")),i=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),(i=e.children(".ui-menu")).length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(t){var e,i,s;this._hasScroll()&&(e=parseFloat(V.css(this.activeMenu[0],"borderTopWidth"))||0,i=parseFloat(V.css(this.activeMenu[0],"paddingTop"))||0,e=t.offset().top-this.activeMenu.offset().top-e-i,i=this.activeMenu.scrollTop(),s=this.activeMenu.height(),t=t.outerHeight(),e<0?this.activeMenu.scrollTop(i+e):s",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,liveRegionTimer:null,_create:function(){var i,s,n,t=this.element[0].nodeName.toLowerCase(),e="textarea"===t,t="input"===t;this.isMultiLine=e||!t&&"true"===this.element.prop("contentEditable"),this.valueMethod=this.element[e||t?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(t){if(this.element.prop("readOnly"))s=n=i=!0;else{s=n=i=!1;var e=V.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:i=!0,this._move("previousPage",t);break;case e.PAGE_DOWN:i=!0,this._move("nextPage",t);break;case e.UP:i=!0,this._keyEvent("previous",t);break;case e.DOWN:i=!0,this._keyEvent("next",t);break;case e.ENTER:this.menu.active&&(i=!0,t.preventDefault(),this.menu.select(t));break;case e.TAB:this.menu.active&&this.menu.select(t);break;case e.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(t),t.preventDefault());break;default:s=!0,this._searchTimeout(t)}}},keypress:function(t){if(i)i=!1,this.isMultiLine&&!this.menu.element.is(":visible")||t.preventDefault();else if(!s){var e=V.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:this._move("previousPage",t);break;case e.PAGE_DOWN:this._move("nextPage",t);break;case e.UP:this._keyEvent("previous",t);break;case e.DOWN:this._keyEvent("next",t)}}},input:function(t){n?(n=!1,t.preventDefault()):this._searchTimeout(t)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){clearTimeout(this.searching),this.close(t),this._change(t)}}),this._initSource(),this.menu=V("
      ").appendTo(this._appendTo()).menu({role:null}).hide().menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault()},menufocus:function(t,e){var i,s;this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent)&&/^mouse/.test(t.originalEvent.type)?(this.menu.blur(),this.document.one("mousemove",function(){V(t.target).trigger(t.originalEvent)})):(s=e.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",t,{item:s})&&t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value),(i=e.item.attr("aria-label")||s.value)&&String.prototype.trim.call(i).length&&(clearTimeout(this.liveRegionTimer),this.liveRegionTimer=this._delay(function(){this.liveRegion.html(V("
      ").text(i))},100)))},menuselect:function(t,e){var e=e.item.data("ui-autocomplete-item"),i=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.trigger("focus"),this.previous=i),!1!==this._trigger("select",t,{item:e})&&this._value(e.value),this.term=this._value(),this.close(t),this.selectedItem=e}}),this.liveRegion=V("
      ",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(t){var e=this.menu.element[0];return t.target===this.element[0]||t.target===e||V.contains(e,t.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var t=this.options.appendTo;return t=(t=(t=t&&(t.jquery||t.nodeType?V(t):this.document.find(t).eq(0)))&&t[0]?t:this.element.closest(".ui-front, dialog")).length?t:this.document[0].body},_initSource:function(){var i,s,n=this;Array.isArray(this.options.source)?(i=this.options.source,this.source=function(t,e){e(V.ui.autocomplete.filter(i,t.term))}):"string"==typeof this.options.source?(s=this.options.source,this.source=function(t,e){n.xhr&&n.xhr.abort(),n.xhr=V.ajax({url:s,data:t,dataType:"json",success:function(t){e(t)},error:function(){e([])}})}):this.source=this.options.source},_searchTimeout:function(s){clearTimeout(this.searching),this.searching=this._delay(function(){var t=this.term===this._value(),e=this.menu.element.is(":visible"),i=s.altKey||s.ctrlKey||s.metaKey||s.shiftKey;t&&(e||i)||(this.selectedItem=null,this.search(null,s))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length").append(V("
      ").text(e.label)).appendTo(t)},_move:function(t,e){this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),this.menu.blur()):this.menu[t](e):this.search(null,e)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){this.isMultiLine&&!this.menu.element.is(":visible")||(this._move(t,e),e.preventDefault())}}),V.extend(V.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,e){var i=new RegExp(V.ui.autocomplete.escapeRegex(e),"i");return V.grep(t,function(t){return i.test(t.label||t.value||t)})}}),V.widget("ui.autocomplete",V.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(1").text(e))},100))}}),V.ui.autocomplete,/ui-corner-([a-z]){2,6}/g);V.widget("ui.controlgroup",{version:"1.14.1",defaultElement:"
      ",options:{direction:"horizontal",disabled:null,onlyVisible:!0,items:{button:"input[type=button], input[type=submit], input[type=reset], button, a",controlgroupLabel:".ui-controlgroup-label",checkboxradio:"input[type='checkbox'], input[type='radio']",selectmenu:"select",spinner:".ui-spinner-input"}},_create:function(){this._enhance()},_enhance:function(){this.element.attr("role","toolbar"),this.refresh()},_destroy:function(){this._callChildMethod("destroy"),this.childWidgets.removeData("ui-controlgroup-data"),this.element.removeAttr("role"),this.options.items.controlgroupLabel&&this.element.find(this.options.items.controlgroupLabel).find(".ui-controlgroup-label-contents").contents().unwrap()},_initWidgets:function(){var o=this,a=[];V.each(this.options.items,function(s,t){var e,n={};t&&("controlgroupLabel"===s?((e=o.element.find(t)).each(function(){var t=V(this);t.children(".ui-controlgroup-label-contents").length||t.contents().wrapAll("")}),o._addClass(e,null,"ui-widget ui-widget-content ui-state-default"),a=a.concat(e.get())):V.fn[s]&&(n=o["_"+s+"Options"]?o["_"+s+"Options"]("middle"):{classes:{}},o.element.find(t).each(function(){var t=V(this),e=t[s]("instance"),i=V.widget.extend({},n);"button"===s&&t.parent(".ui-spinner").length||((e=e||t[s]()[s]("instance"))&&(i.classes=o._resolveClassesValues(i.classes,e)),t[s](i),i=t[s]("widget"),V.data(i[0],"ui-controlgroup-data",e||t[s]("instance")),a.push(i[0]))})))}),this.childWidgets=V(V.uniqueSort(a)),this._addClass(this.childWidgets,"ui-controlgroup-item")},_callChildMethod:function(e){this.childWidgets.each(function(){var t=V(this).data("ui-controlgroup-data");t&&t[e]&&t[e]()})},_updateCornerClass:function(t,e){e=this._buildSimpleOptions(e,"label").classes.label;this._removeClass(t,null,"ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-corner-all"),this._addClass(t,null,e)},_buildSimpleOptions:function(t,e){var i="vertical"===this.options.direction,s={classes:{}};return s.classes[e]={middle:"",first:"ui-corner-"+(i?"top":"left"),last:"ui-corner-"+(i?"bottom":"right"),only:"ui-corner-all"}[t],s},_spinnerOptions:function(t){t=this._buildSimpleOptions(t,"ui-spinner");return t.classes["ui-spinner-up"]="",t.classes["ui-spinner-down"]="",t},_buttonOptions:function(t){return this._buildSimpleOptions(t,"ui-button")},_checkboxradioOptions:function(t){return this._buildSimpleOptions(t,"ui-checkboxradio-label")},_selectmenuOptions:function(t){var e="vertical"===this.options.direction;return{width:e&&"auto",classes:{middle:{"ui-selectmenu-button-open":"","ui-selectmenu-button-closed":""},first:{"ui-selectmenu-button-open":"ui-corner-"+(e?"top":"tl"),"ui-selectmenu-button-closed":"ui-corner-"+(e?"top":"left")},last:{"ui-selectmenu-button-open":e?"":"ui-corner-tr","ui-selectmenu-button-closed":"ui-corner-"+(e?"bottom":"right")},only:{"ui-selectmenu-button-open":"ui-corner-top","ui-selectmenu-button-closed":"ui-corner-all"}}[t]}},_resolveClassesValues:function(i,s){var n={};return V.each(i,function(t){var e=s.options.classes[t]||"",e=String.prototype.trim.call(e.replace(st,""));n[t]=(e+" "+i[t]).replace(/\s+/g," ")}),n},_setOption:function(t,e){"direction"===t&&this._removeClass("ui-controlgroup-"+this.options.direction),this._super(t,e),"disabled"===t?this._callChildMethod(e?"disable":"enable"):this.refresh()},refresh:function(){var n,o=this;this._addClass("ui-controlgroup ui-controlgroup-"+this.options.direction),"horizontal"===this.options.direction&&this._addClass(null,"ui-helper-clearfix"),this._initWidgets(),n=this.childWidgets,(n=this.options.onlyVisible?n.filter(":visible"):n).length&&(V.each(["first","last"],function(t,e){var i,s=n[e]().data("ui-controlgroup-data");s&&o["_"+s.widgetName+"Options"]?((i=o["_"+s.widgetName+"Options"](1===n.length?"only":e)).classes=o._resolveClassesValues(i.classes,s),s.element[s.widgetName](i)):o._updateCornerClass(n[e](),e)}),this._callChildMethod("refresh"))}}),V.widget("ui.checkboxradio",[V.ui.formResetMixin,{version:"1.14.1",options:{disabled:null,label:null,icon:!0,classes:{"ui-checkboxradio-label":"ui-corner-all","ui-checkboxradio-icon":"ui-corner-all"}},_getCreateOptions:function(){var t,e=this._super()||{};return this._readType(),t=this.element.labels(),this.label=V(t[t.length-1]),this.label.length||V.error("No label found for checkboxradio widget"),this.originalLabel="",(t=this.label.contents().not(this.element[0])).length&&(this.originalLabel+=t.clone().wrapAll("
      ").parent().html()),this.originalLabel&&(e.label=this.originalLabel),null!=(t=this.element[0].disabled)&&(e.disabled=t),e},_create:function(){var t=this.element[0].checked;this._bindFormResetHandler(),null==this.options.disabled&&(this.options.disabled=this.element[0].disabled),this._setOption("disabled",this.options.disabled),this._addClass("ui-checkboxradio","ui-helper-hidden-accessible"),this._addClass(this.label,"ui-checkboxradio-label","ui-button ui-widget"),"radio"===this.type&&this._addClass(this.label,"ui-checkboxradio-radio-label"),this.options.label&&this.options.label!==this.originalLabel?this._updateLabel():this.originalLabel&&(this.options.label=this.originalLabel),this._enhance(),t&&this._addClass(this.label,"ui-checkboxradio-checked","ui-state-active"),this._on({change:"_toggleClasses",focus:function(){this._addClass(this.label,null,"ui-state-focus ui-visual-focus")},blur:function(){this._removeClass(this.label,null,"ui-state-focus ui-visual-focus")}})},_readType:function(){var t=this.element[0].nodeName.toLowerCase();this.type=this.element[0].type,"input"===t&&/radio|checkbox/.test(this.type)||V.error("Can't create checkboxradio on element.nodeName="+t+" and element.type="+this.type)},_enhance:function(){this._updateIcon(this.element[0].checked)},widget:function(){return this.label},_getRadioGroup:function(){var t=this.element[0].name,e="input[name='"+CSS.escape(t)+"']";return t?(this.form.length?V(this.form[0].elements).filter(e):V(e).filter(function(){return 0===V(V(this).prop("form")).length})).not(this.element):V([])},_toggleClasses:function(){var t=this.element[0].checked;this._toggleClass(this.label,"ui-checkboxradio-checked","ui-state-active",t),this.options.icon&&"checkbox"===this.type&&this._toggleClass(this.icon,null,"ui-icon-check ui-state-checked",t)._toggleClass(this.icon,null,"ui-icon-blank",!t),"radio"===this.type&&this._getRadioGroup().each(function(){var t=V(this).checkboxradio("instance");t&&t._removeClass(t.label,"ui-checkboxradio-checked","ui-state-active")})},_destroy:function(){this._unbindFormResetHandler(),this.icon&&(this.icon.remove(),this.iconSpace.remove())},_setOption:function(t,e){"label"===t&&!e||(this._super(t,e),"disabled"===t?(this._toggleClass(this.label,null,"ui-state-disabled",e),this.element[0].disabled=e):this.refresh())},_updateIcon:function(t){var e="ui-icon ui-icon-background ";this.options.icon?(this.icon||(this.icon=V(""),this.iconSpace=V(" "),this._addClass(this.iconSpace,"ui-checkboxradio-icon-space")),"checkbox"===this.type?(e+=t?"ui-icon-check ui-state-checked":"ui-icon-blank",this._removeClass(this.icon,null,t?"ui-icon-blank":"ui-icon-check")):e+="ui-icon-blank",this._addClass(this.icon,"ui-checkboxradio-icon",e),t||this._removeClass(this.icon,null,"ui-icon-check ui-state-checked"),this.icon.prependTo(this.label).after(this.iconSpace)):void 0!==this.icon&&(this.icon.remove(),this.iconSpace.remove(),delete this.icon)},_updateLabel:function(){var t=this.label.contents().not(this.element[0]);this.icon&&(t=t.not(this.icon[0])),(t=this.iconSpace?t.not(this.iconSpace[0]):t).remove(),this.label.append(this.options.label)},refresh:function(){var t=this.element[0].checked,e=this.element[0].disabled;this._updateIcon(t),this._toggleClass(this.label,"ui-checkboxradio-checked","ui-state-active",t),null!==this.options.label&&this._updateLabel(),e!==this.options.disabled&&this._setOptions({disabled:e})}}]),V.ui.checkboxradio,V.widget("ui.button",{version:"1.14.1",defaultElement:"
      "+(0
      ":""):"")}h+=d}return h+=T,t._keyEvent=!1,h},_generateMonthYearHeader:function(t,e,i,s,n,o,a,r){var l,h,c,u,d,p,f=this._get(t,"changeMonth"),g=this._get(t,"changeYear"),m=this._get(t,"showMonthAfterYear"),_=this._get(t,"selectMonthLabel"),v=this._get(t,"selectYearLabel"),b="
      ",y="";if(o||!f)y+=""+a[e]+"";else{for(l=s&&s.getFullYear()===i,h=n&&n.getFullYear()===i,y+=""}if(m||(b+=y+(!o&&f&&g?"":" ")),!t.yearshtml)if(t.yearshtml="",o||!g)b+=""+i+"";else{for(a=this._get(t,"yearRange").split(":"),u=(new Date).getFullYear(),d=(_=function(t){t=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?u+parseInt(t,10):parseInt(t,10);return isNaN(t)?u:t})(a[0]),p=Math.max(d,_(a[1]||"")),d=s?Math.max(d,s.getFullYear()):d,p=n?Math.min(p,n.getFullYear()):p,t.yearshtml+="",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),m&&(b+=(!o&&f&&g?"":" ")+y),b+="
      "},_adjustInstDate:function(t,e,i){var s=t.selectedYear+("Y"===i?e:0),n=t.selectedMonth+("M"===i?e:0),e=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),s=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,e)));t.selectedDay=s.getDate(),t.drawMonth=t.selectedMonth=s.getMonth(),t.drawYear=t.selectedYear=s.getFullYear(),"M"!==i&&"Y"!==i||this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),t=this._getMinMaxDate(t,"max"),i=i&&e=s.getTime())&&(!n||e.getTime()<=n.getTime())&&(!o||e.getFullYear()>=o)&&(!a||e.getFullYear()<=a)},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return{shortYearCutoff:"string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);s=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),s,this._getFormatConfig(t))}}),V.fn.datepicker=function(t){if(!this.length)return this;V.datepicker.initialized||(V(document).on("mousedown",V.datepicker._checkExternalClick),V.datepicker.initialized=!0),0===V("#"+V.datepicker._mainDivId).length&&V("body").append(V.datepicker.dpDiv);var e=Array.prototype.slice.call(arguments,1);return"string"==typeof t&&("isDisabled"===t||"getDate"===t||"widget"===t)||"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?V.datepicker["_"+t+"Datepicker"].apply(V.datepicker,[this[0]].concat(e)):this.each(function(){"string"==typeof t?V.datepicker["_"+t+"Datepicker"].apply(V.datepicker,[this].concat(e)):V.datepicker._attachDatepicker(this,t)})},V.datepicker=new nt,V.datepicker.initialized=!1,V.datepicker.uuid=(new Date).getTime(),V.datepicker.version="1.14.1";var z=!1;V(document).on("mouseup",function(){z=!1}),V.widget("ui.mouse",{version:"1.14.1",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(t){if(!0===V.data(t.target,e.widgetName+".preventClickEvent"))return V.removeData(t.target,e.widgetName+".preventClickEvent"),t.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(t){var e,i,s;if(!z)return this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(t),i=1===(this._mouseDownEvent=t).which,s="string"==typeof(e=this).options.cancel&&V(t.target).closest(this.options.cancel).length,i&&!s&&this._mouseCapture(t)&&(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){e.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=!1!==this._mouseStart(t),!this._mouseStarted)?t.preventDefault():(!0===V.data(t.target,this.widgetName+".preventClickEvent")&&V.removeData(t.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return e._mouseMove(t)},this._mouseUpDelegate=function(t){return e._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),t.preventDefault(),z=!0)),!0},_mouseMove:function(t){if(this._mouseMoved&&!t.which)if(t.originalEvent.altKey||t.originalEvent.ctrlKey||t.originalEvent.metaKey||t.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(t);return(t.which||t.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=!1!==this._mouseStart(this._mouseDownEvent,t),this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted)},_mouseUp:function(t){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&V.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,z=!1,t.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),V.ui.plugin={add:function(t,e,i){var s,n=V.ui[t].prototype;for(s in i)n.plugins[s]=n.plugins[s]||[],n.plugins[s].push([e,i[s]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;n").css("position","absolute").appendTo(t.parent()).outerWidth(t.outerWidth()).outerHeight(t.outerHeight()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(t){var e=this.document[0].activeElement;V(t.target).closest(e).length||V(e).trigger("blur")},_mouseStart:function(t){var e=this.options;return this.helper=this._createHelper(t),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),V.ui.ddmanager&&(V.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=0i[2]&&(o=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3])&&(a=i[3]+this.offset.click.top),s.grid&&(e=s.grid[1]?this.originalPageY+Math.round((a-this.originalPageY)/s.grid[1])*s.grid[1]:this.originalPageY,a=!i||e-this.offset.click.top>=i[1]||e-this.offset.click.top>i[3]?e:e-this.offset.click.top>=i[1]?e-s.grid[1]:e+s.grid[1],t=s.grid[0]?this.originalPageX+Math.round((o-this.originalPageX)/s.grid[0])*s.grid[0]:this.originalPageX,o=!i||t-this.offset.click.left>=i[0]||t-this.offset.click.left>i[2]?t:t-this.offset.click.left>=i[0]?t-s.grid[0]:t+s.grid[0]),"y"===s.axis&&(o=this.originalPageX),"x"===s.axis)?this.originalPageY:a)-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:n?0:this.offset.scroll.top),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:n?0:this.offset.scroll.left)}},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(t,e,i){return i=i||this._uiHash(),V.ui.plugin.call(this,t,[e,i,this],!0),/^(drag|start|stop)/.test(t)&&(this.positionAbs=this._convertPositionTo("absolute"),i.offset=this.positionAbs),V.Widget.prototype._trigger.call(this,t,e,i)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),V.ui.plugin.add("draggable","connectToSortable",{start:function(e,t,i){var s=V.extend({},t,{item:i.element});i.sortables=[],V(i.options.connectToSortable).each(function(){var t=V(this).sortable("instance");t&&!t.options.disabled&&(i.sortables.push(t),t.refreshPositions(),t._trigger("activate",e,s))})},stop:function(e,t,i){var s=V.extend({},t,{item:i.element});i.cancelHelperRemoval=!1,V.each(i.sortables,function(){var t=this;t.isOver?(t.isOver=0,i.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,s))})},drag:function(i,s,n){V.each(n.sortables,function(){var t=!1,e=this;e.positionAbs=n.positionAbs,e.helperProportions=n.helperProportions,e.offset.click=n.offset.click,e._intersectsWith(e.containerCache)&&(t=!0,V.each(n.sortables,function(){return this.positionAbs=n.positionAbs,this.helperProportions=n.helperProportions,this.offset.click=n.offset.click,t=this!==e&&this._intersectsWith(this.containerCache)&&V.contains(e.element[0],this.element[0])?!1:t})),t?(e.isOver||(e.isOver=1,n._parent=s.helper.parent(),e.currentItem=s.helper.appendTo(e.element).data("ui-sortable-item",!0),e.options._helper=e.options.helper,e.options.helper=function(){return s.helper[0]},i.target=e.currentItem[0],e._mouseCapture(i,!0),e._mouseStart(i,!0,!0),e.offset.click.top=n.offset.click.top,e.offset.click.left=n.offset.click.left,e.offset.parent.left-=n.offset.parent.left-e.offset.parent.left,e.offset.parent.top-=n.offset.parent.top-e.offset.parent.top,n._trigger("toSortable",i),n.dropped=e.element,V.each(n.sortables,function(){this.refreshPositions()}),n.currentItem=n.element,e.fromOutside=n),e.currentItem&&(e._mouseDrag(i),s.position=e.position)):e.isOver&&(e.isOver=0,e.cancelHelperRemoval=!0,e.options._revert=e.options.revert,e.options.revert=!1,e._trigger("out",i,e._uiHash(e)),e._mouseStop(i,!0),e.options.revert=e.options._revert,e.options.helper=e.options._helper,e.placeholder&&e.placeholder.remove(),s.helper.appendTo(n._parent),n._refreshOffsets(i),s.position=n._generatePosition(i,!0),n._trigger("fromSortable",i),n.dropped=!1,V.each(n.sortables,function(){this.refreshPositions()}))})}}),V.ui.plugin.add("draggable","cursor",{start:function(t,e,i){var s=V("body"),i=i.options;s.css("cursor")&&(i._cursor=s.css("cursor")),s.css("cursor",i.cursor)},stop:function(t,e,i){i=i.options;i._cursor&&V("body").css("cursor",i._cursor)}}),V.ui.plugin.add("draggable","opacity",{start:function(t,e,i){e=V(e.helper),i=i.options;e.css("opacity")&&(i._opacity=e.css("opacity")),e.css("opacity",i.opacity)},stop:function(t,e,i){i=i.options;i._opacity&&V(e.helper).css("opacity",i._opacity)}}),V.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(t,e,i){var s=i.options,n=!1,o=i.scrollParentNotHidden[0],a=i.document[0];o!==a&&"HTML"!==o.tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+o.offsetHeight-t.pageY
      ").css({overflow:"hidden",position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,t={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(t),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this._proportionallyResize()),this._setupHandles(),e.autoHide&&V(this.element).on("mouseenter",function(){e.disabled||(i._removeClass("ui-resizable-autohide"),i._handles.show())}).on("mouseleave",function(){e.disabled||i.resizing||(i._addClass("ui-resizable-autohide"),i._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy(),this._addedHandles.remove();function t(t){V(t).removeData("resizable").removeData("ui-resizable").off(".resizable")}var e;return this.elementIsWrapper&&(t(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),t(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;case"aspectRatio":this._aspectRatio=!!e}},_setupHandles:function(){var t,e,i,s,n,o=this.options,a=this;if(this.handles=o.handles||(V(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=V(),this._addedHandles=V(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),i=this.handles.split(","),this.handles={},e=0;e"),this._addClass(n,"ui-resizable-handle "+s),n.css({zIndex:o.zIndex}),this.handles[t]=".ui-resizable-"+t,this.element.children(this.handles[t]).length||(this.element.append(n),this._addedHandles=this._addedHandles.add(n));this._renderAxis=function(t){var e,i,s;for(e in t=t||this.element,this.handles)this.handles[e].constructor===String?this.handles[e]=this.element.children(this.handles[e]).first().show():(this.handles[e].jquery||this.handles[e].nodeType)&&(this.handles[e]=V(this.handles[e]),this._on(this.handles[e],{mousedown:a._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(s=V(this.handles[e],this.element),s=/sw|ne|nw|se|n|s/.test(e)?s.outerHeight():s.outerWidth(),i=["padding",/ne|nw|n/.test(e)?"Top":/se|sw|s/.test(e)?"Bottom":/^e$/.test(e)?"Right":"Left"].join(""),t.css(i,s),this._proportionallyResize()),this._handles=this._handles.add(this.handles[e])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){a.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),a.axis=n&&n[1]?n[1]:"se")}),o.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._addedHandles.remove()},_mouseCapture:function(t){var e,i,s=!1;for(e in this.handles)(i=V(this.handles[e])[0])!==t.target&&!V.contains(i,t.target)||(s=!0);return!this.options.disabled&&s},_mouseStart:function(t){var e,i,s,n=this.options,o=this.element;return this.resizing=!0,this._renderProxy(),e=this._num(this.helper.css("left")),i=this._num(this.helper.css("top")),n.containment&&(e+=V(n.containment).scrollLeft()||0,i+=V(n.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:e,top:i},this._helper||(s=this._calculateAdjustedElementDimensions(o)),this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:s.width,height:s.height},this.originalSize=this._helper?{width:o.outerWidth(),height:o.outerHeight()}:{width:s.width,height:s.height},this.sizeDiff={width:o.outerWidth()-o.width(),height:o.outerHeight()-o.height()},this.originalPosition={left:e,top:i},this.originalMousePosition={left:t.pageX,top:t.pageY},this.aspectRatio="number"==typeof n.aspectRatio?n.aspectRatio:this.originalSize.width/this.originalSize.height||1,s=V(".ui-resizable-"+this.axis).css("cursor"),V("body").css("cursor","auto"===s?this.axis+"-resize":s),this._addClass("ui-resizable-resizing"),this._propagate("start",t),!0},_mouseDrag:function(t){var e=this.originalMousePosition,i=this.axis,s=t.pageX-e.left||0,e=t.pageY-e.top||0,i=this._change[i];return this._updatePrevProperties(),i&&(i=i.apply(this,[t,s,e]),this._updateVirtualBoundaries(t.shiftKey),(this._aspectRatio||t.shiftKey)&&(i=this._updateRatio(i,t)),i=this._respectSize(i,t),this._updateCache(i),this._propagate("resize",t),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),V.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",t,this.ui()),this._applyChanges())),!1},_mouseStop:function(t){this.resizing=!1;var e,i,s,n=this.options,o=this;return this._helper&&(i=(e=(i=this._proportionallyResizeElements).length&&/textarea/i.test(i[0].nodeName))&&this._hasScroll(i[0],"left")?0:o.sizeDiff.height,e=e?0:o.sizeDiff.width,e={width:o.helper.width()-e,height:o.helper.height()-i},i=parseFloat(o.element.css("left"))+(o.position.left-o.originalPosition.left)||null,s=parseFloat(o.element.css("top"))+(o.position.top-o.originalPosition.top)||null,n.animate||this.element.css(V.extend(e,{top:s,left:i})),o.helper.height(o.size.height),o.helper.width(o.size.width),this._helper)&&!n.animate&&this._proportionallyResize(),V("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",t),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.helper.css(t),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px",this.helper.width(t.width)),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px",this.helper.height(t.height)),t},_updateVirtualBoundaries:function(t){var e,i,s,n=this.options,n={minWidth:this._isNumber(n.minWidth)?n.minWidth:0,maxWidth:this._isNumber(n.maxWidth)?n.maxWidth:1/0,minHeight:this._isNumber(n.minHeight)?n.minHeight:0,maxHeight:this._isNumber(n.maxHeight)?n.maxHeight:1/0};(this._aspectRatio||t)&&(t=n.minHeight*this.aspectRatio,i=n.minWidth/this.aspectRatio,e=n.maxHeight*this.aspectRatio,s=n.maxWidth/this.aspectRatio,n.minWidtht.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,l=this.originalPosition.top+this.originalSize.height,h=/sw|nw|w/.test(i),i=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&h&&(t.left=r-e.minWidth),s&&h&&(t.left=r-e.maxWidth),a&&i&&(t.top=l-e.minHeight),n&&i&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];e<4;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_calculateAdjustedElementDimensions:function(t){var e,i,s=t.get(0);return"content-box"!==t.css("box-sizing")||!this._hasScroll(s)&&!this._hasScroll(s,"left")?{height:parseFloat(t.css("height")),width:parseFloat(t.css("width"))}:(e=parseFloat(s.style.width),s=parseFloat(s.style.height),i=this._getPaddingPlusBorderDimensions(t),e=isNaN(e)?this._getElementTheoreticalSize(t,i,"width"):e,{height:isNaN(s)?this._getElementTheoreticalSize(t,i,"height"):s,width:e})},_getElementTheoreticalSize:function(t,e,i){return Math.max(0,Math.ceil(t.get(0)["offset"+i[0].toUpperCase()+i.slice(1)]-e[i]-.5))||0},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;e
      ").css({overflow:"hidden"}),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++e.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize;return{left:this.originalPosition.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize;return{top:this.originalPosition.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(t,e,i){return V.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[t,e,i]))},sw:function(t,e,i){return V.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[t,e,i]))},ne:function(t,e,i){return V.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[t,e,i]))},nw:function(t,e,i){return V.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[t,e,i]))}},_propagate:function(t,e){V.ui.plugin.call(this,t,[e,this.ui()]),"resize"!==t&&this._trigger(t,e,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),V.ui.plugin.add("resizable","animate",{stop:function(e){var i=V(this).resizable("instance"),t=i.options,s=i._proportionallyResizeElements,n=s.length&&/textarea/i.test(s[0].nodeName),o=n&&i._hasScroll(s[0],"left")?0:i.sizeDiff.height,n=n?0:i.sizeDiff.width,n={width:i.size.width-n,height:i.size.height-o},o=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,a=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(V.extend(n,a&&o?{top:a,left:o}:{}),{duration:t.animateDuration,easing:t.animateEasing,step:function(){var t={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};s&&s.length&&V(s[0]).css({width:t.width,height:t.height}),i._updateCache(t),i._propagate("resize",e)}})}}),V.ui.plugin.add("resizable","containment",{start:function(){var i,s,t,e,n=V(this).resizable("instance"),o=n.options,a=n.element,o=o.containment,a=o instanceof V?o.get(0):/parent/.test(o)?a.parent().get(0):o;a&&(n.containerElement=V(a),/document/.test(o)||o===document?(n.containerOffset={left:0,top:0},n.containerPosition={left:0,top:0},n.parentData={element:V(document),left:0,top:0,width:V(document).width(),height:V(document).height()||document.body.parentNode.scrollHeight}):(i=V(a),s=[],V(["Top","Right","Left","Bottom"]).each(function(t,e){s[t]=n._num(i.css("padding"+e))}),n.containerOffset=i.offset(),n.containerPosition=i.position(),n.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},o=n.containerOffset,e=n.containerSize.height,t=n.containerSize.width,t=n._hasScroll(a,"left")?a.scrollWidth:t,e=n._hasScroll(a)?a.scrollHeight:e,n.parentData={element:a,left:o.left,top:o.top,width:t,height:e}))},resize:function(t){var e=V(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.position,t=e._aspectRatio||t.shiftKey,o={top:0,left:0},a=e.containerElement,r=!0;a[0]!==document&&/static/.test(a.css("position"))&&(o=s),n.left<(e._helper?s.left:0)&&(e.size.width=e.size.width+(e._helper?e.position.left-s.left:e.position.left-o.left),t&&(e.size.height=e.size.width/e.aspectRatio,r=!1),e.position.left=i.helper?s.left:0),n.top<(e._helper?s.top:0)&&(e.size.height=e.size.height+(e._helper?e.position.top-s.top:e.position.top),t&&(e.size.width=e.size.height*e.aspectRatio,r=!1),e.position.top=e._helper?s.top:0),a=e.containerElement.get(0)===e.element.parent().get(0),i=/relative|absolute/.test(e.containerElement.css("position")),a&&i?(e.offset.left=e.parentData.left+e.position.left,e.offset.top=e.parentData.top+e.position.top):(e.offset.left=e.element.offset().left,e.offset.top=e.element.offset().top),n=Math.abs(e.sizeDiff.width+(e._helper?e.offset.left-o.left:e.offset.left-s.left)),a=Math.abs(e.sizeDiff.height+(e._helper?e.offset.top-o.top:e.offset.top-s.top)),n+e.size.width>=e.parentData.width&&(e.size.width=e.parentData.width-n,t)&&(e.size.height=e.size.width/e.aspectRatio,r=!1),a+e.size.height>=e.parentData.height&&(e.size.height=e.parentData.height-a,t)&&(e.size.width=e.size.height*e.aspectRatio,r=!1),r||(e.position.left=e.prevPosition.left,e.position.top=e.prevPosition.top,e.size.width=e.prevSize.width,e.size.height=e.prevSize.height)},stop:function(){var t=V(this).resizable("instance"),e=t.options,i=t.containerOffset,s=t.containerPosition,n=t.containerElement,o=V(t.helper),a=o.offset(),r=o.outerWidth()-t.sizeDiff.width,o=o.outerHeight()-t.sizeDiff.height;t._helper&&!e.animate&&/relative/.test(n.css("position"))&&V(this).css({left:a.left-s.left-i.left,width:r,height:o}),t._helper&&!e.animate&&/static/.test(n.css("position"))&&V(this).css({left:a.left-s.left-i.left,width:r,height:o})}}),V.ui.plugin.add("resizable","alsoResize",{start:function(){var i=V(this).resizable("instance"),t=i.options;V(t.alsoResize).each(function(){var t=V(this),e=i._calculateAdjustedElementDimensions(t);t.data("ui-resizable-alsoresize",{width:e.width,height:e.height,left:parseFloat(t.css("left")),top:parseFloat(t.css("top"))})})},resize:function(t,i){var e=V(this).resizable("instance"),s=e.options,n=e.originalSize,o=e.originalPosition,a={height:e.size.height-n.height||0,width:e.size.width-n.width||0,top:e.position.top-o.top||0,left:e.position.left-o.left||0};V(s.alsoResize).each(function(){var t=V(this),s=V(this).data("ui-resizable-alsoresize"),n={},e=t.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];V.each(e,function(t,e){var i=(s[e]||0)+(a[e]||0);i&&0<=i&&(n[e]=i||null)}),t.css(n)})},stop:function(){V(this).removeData("ui-resizable-alsoresize")}}),V.ui.plugin.add("resizable","ghost",{start:function(){var t=V(this).resizable("instance"),e=t.size;t.ghost=t.originalElement.clone(),t.ghost.css({opacity:.25,display:"block",position:"relative",height:e.height,width:e.width,margin:0,left:0,top:0}),t._addClass(t.ghost,"ui-resizable-ghost"),!0===V.uiBackCompat&&"string"==typeof t.options.ghost&&t.ghost.addClass(this.options.ghost),t.ghost.appendTo(t.helper)},resize:function(){var t=V(this).resizable("instance");t.ghost&&t.ghost.css({position:"relative",height:t.size.height,width:t.size.width})},stop:function(){var t=V(this).resizable("instance");t.ghost&&t.helper&&t.helper.get(0).removeChild(t.ghost.get(0))}}),V.ui.plugin.add("resizable","grid",{resize:function(){var t,e=V(this).resizable("instance"),i=e.options,s=e.size,n=e.originalSize,o=e.originalPosition,a=e.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,l=r[0]||1,h=r[1]||1,c=Math.round((s.width-n.width)/l)*l,s=Math.round((s.height-n.height)/h)*h,u=n.width+c,d=n.height+s,p=i.maxWidth&&i.maxWidthu,m=i.minHeight&&i.minHeight>d;i.grid=r,g&&(u+=l),m&&(d+=h),p&&(u-=l),f&&(d-=h),/^(se|s|e)$/.test(a)?(e.size.width=u,e.size.height=d):/^(ne)$/.test(a)?(e.size.width=u,e.size.height=d,e.position.top=o.top-s):/^(sw)$/.test(a)?(e.size.width=u,e.size.height=d,e.position.left=o.left-c):((d-h<=0||u-l<=0)&&(t=e._getPaddingPlusBorderDimensions(this)),0=+this.uiDialog.css("z-index")&&(this.uiDialog.css("z-index",s+1),i=!0),i&&!e&&this._trigger("focus",t),i},open:function(){var t=this;this._isOpen?this._moveToTop()&&this._focusTabbable():(this._isOpen=!0,this.opener=V(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this.overlay&&this.overlay.css("z-index",this.uiDialog.css("z-index")-1),this._show(this.uiDialog,this.options.show,function(){t._focusTabbable(),t._trigger("focus")}),this._makeFocusTarget(),this._trigger("open"))},_focusTabbable:function(){var t=this._focusedElement;(t=(t=(t=(t=(t=t||this.element.find("[autofocus]")).length?t:this.element.find(":tabbable")).length?t:this.uiDialogButtonPane.find(":tabbable")).length?t:this.uiDialogTitlebarClose.filter(":tabbable")).length?t:this.uiDialog).eq(0).trigger("focus")},_restoreTabbableFocus:function(){var t=this.document[0].activeElement;this.uiDialog[0]===t||V.contains(this.uiDialog[0],t)||this._focusTabbable()},_keepFocus:function(t){t.preventDefault(),this._restoreTabbableFocus()},_createWrapper:function(){this.uiDialog=V("
      ").hide().attr({tabIndex:-1,role:"dialog","aria-modal":this.options.modal?"true":null}).appendTo(this._appendTo()),this._addClass(this.uiDialog,"ui-dialog","ui-widget ui-widget-content ui-front"),this._on(this.uiDialog,{keydown:function(t){var e,i,s;this.options.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===V.ui.keyCode.ESCAPE?(t.preventDefault(),this.close(t)):t.keyCode!==V.ui.keyCode.TAB||t.isDefaultPrevented()||(e=this.uiDialog.find(":tabbable"),i=e.first(),s=e.last(),t.target!==s[0]&&t.target!==this.uiDialog[0]||t.shiftKey?t.target!==i[0]&&t.target!==this.uiDialog[0]||!t.shiftKey||(this._delay(function(){s.trigger("focus")}),t.preventDefault()):(this._delay(function(){i.trigger("focus")}),t.preventDefault()))},mousedown:function(t){this._moveToTop(t)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){this.uiDialogTitlebar=V("
      "),this._addClass(this.uiDialogTitlebar,"ui-dialog-titlebar","ui-widget-header ui-helper-clearfix"),this._on(this.uiDialogTitlebar,{mousedown:function(t){V(t.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.trigger("focus")}}),this.uiDialogTitlebarClose=V("").button({label:V("").text(this.options.closeText).html(),icon:"ui-icon-closethick",showLabel:!1}).appendTo(this.uiDialogTitlebar),this._addClass(this.uiDialogTitlebarClose,"ui-dialog-titlebar-close"),this._on(this.uiDialogTitlebarClose,{click:function(t){t.preventDefault(),this.close(t)}});var t=Number.isInteger(this.options.uiDialogTitleHeadingLevel)&&0").uniqueId().prependTo(this.uiDialogTitlebar);this._addClass(t,"ui-dialog-title"),this._title(t),this.uiDialogTitlebar.prependTo(this.uiDialog),this.uiDialog.attr({"aria-labelledby":t.attr("id")})},_title:function(t){this.options.title?t.text(this.options.title):t.html(" ")},_createButtonPane:function(){this.uiDialogButtonPane=V("
      "),this._addClass(this.uiDialogButtonPane,"ui-dialog-buttonpane","ui-widget-content ui-helper-clearfix"),this.uiButtonSet=V("
      ").appendTo(this.uiDialogButtonPane),this._addClass(this.uiButtonSet,"ui-dialog-buttonset"),this._createButtons()},_createButtons:function(){var s=this,t=this.options.buttons;this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),V.isEmptyObject(t)||Array.isArray(t)&&!t.length?this._removeClass(this.uiDialog,"ui-dialog-buttons"):(V.each(t,function(t,e){var i;e=V.extend({type:"button"},e="function"==typeof e?{click:e,text:t}:e),i=e.click,t={icon:e.icon,iconPosition:e.iconPosition,showLabel:e.showLabel,icons:e.icons,text:e.text},delete e.click,delete e.icon,delete e.iconPosition,delete e.showLabel,delete e.icons,"boolean"==typeof e.text&&delete e.text,V("",e).button(t).appendTo(s.uiButtonSet).on("click",function(){i.apply(s.element[0],arguments)})}),this._addClass(this.uiDialog,"ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog))},_makeDraggable:function(){var n=this,o=this.options;function a(t){return{position:t.position,offset:t.offset}}this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(t,e){n._addClass(V(this),"ui-dialog-dragging"),n._blockFrames(),n._trigger("dragStart",t,a(e))},drag:function(t,e){n._trigger("drag",t,a(e))},stop:function(t,e){var i=e.offset.left-n.document.scrollLeft(),s=e.offset.top-n.document.scrollTop();o.position={my:"left top",at:"left"+(0<=i?"+":"")+i+" top"+(0<=s?"+":"")+s,of:n.window},n._removeClass(V(this),"ui-dialog-dragging"),n._unblockFrames(),n._trigger("dragStop",t,a(e))}})},_makeResizable:function(){var n=this,o=this.options,t=o.resizable,e=this.uiDialog.css("position"),t="string"==typeof t?t:"n,e,s,w,se,sw,ne,nw";function a(t){return{originalPosition:t.originalPosition,originalSize:t.originalSize,position:t.position,size:t.size}}this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:o.maxWidth,maxHeight:o.maxHeight,minWidth:o.minWidth,minHeight:this._minHeight(),handles:t,start:function(t,e){n._addClass(V(this),"ui-dialog-resizing"),n._blockFrames(),n._trigger("resizeStart",t,a(e))},resize:function(t,e){n._trigger("resize",t,a(e))},stop:function(t,e){var i=n.uiDialog.offset(),s=i.left-n.document.scrollLeft(),i=i.top-n.document.scrollTop();o.height=n.uiDialog.height(),o.width=n.uiDialog.width(),o.position={my:"left top",at:"left"+(0<=s?"+":"")+s+" top"+(0<=i?"+":"")+i,of:n.window},n._removeClass(V(this),"ui-dialog-resizing"),n._unblockFrames(),n._trigger("resizeStop",t,a(e))}}).css("position",e)},_trackFocus:function(){this._on(this.widget(),{focusin:function(t){this._makeFocusTarget(),this._focusedElement=V(t.target)}})},_makeFocusTarget:function(){this._untrackInstance(),this._trackingInstances().unshift(this)},_untrackInstance:function(){var t=this._trackingInstances(),e=V.inArray(this,t);-1!==e&&t.splice(e,1)},_trackingInstances:function(){var t=this.document.data("ui-dialog-instances");return t||this.document.data("ui-dialog-instances",t=[]),t},_minHeight:function(){var t=this.options;return"auto"===t.height?t.minHeight:Math.min(t.minHeight,t.height)},_position:function(){var t=this.uiDialog.is(":visible");t||this.uiDialog.show(),this.uiDialog.position(this.options.position),t||this.uiDialog.hide()},_setOptions:function(t){var i=this,s=!1,n={};V.each(t,function(t,e){i._setOption(t,e),t in i.sizeRelatedOptions&&(s=!0),t in i.resizableRelatedOptions&&(n[t]=e)}),s&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",n)},_setOption:function(t,e){var i,s=this.uiDialog;"disabled"!==t&&(this._super(t,e),"appendTo"===t&&this.uiDialog.appendTo(this._appendTo()),"buttons"===t&&this._createButtons(),"closeText"===t&&this.uiDialogTitlebarClose.button({label:V("").text(""+this.options.closeText).html()}),"draggable"===t&&((i=s.is(":data(ui-draggable)"))&&!e&&s.draggable("destroy"),!i)&&e&&this._makeDraggable(),"position"===t&&this._position(),"resizable"===t&&((i=s.is(":data(ui-resizable)"))&&!e&&s.resizable("destroy"),i&&"string"==typeof e&&s.resizable("option","handles",e),i||!1===e||this._makeResizable()),"title"===t&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")),"modal"===t)&&s.attr("aria-modal",e?"true":null)},_size:function(){var t,e,i,s=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),s.minWidth>s.width&&(s.width=s.minWidth),t=this.uiDialog.css({height:"auto",width:s.width}).outerHeight(),e=Math.max(0,s.minHeight-t),i="number"==typeof s.maxHeight?Math.max(0,s.maxHeight-t):"none","auto"===s.height?this.element.css({minHeight:e,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,s.height-t)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var t=V(this);return V("
      ").css({position:"absolute",width:t.outerWidth(),height:t.outerHeight()}).appendTo(t.parent()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(t){return!!V(t.target).closest(".ui-dialog").length||!!V(t.target).closest(".ui-datepicker").length},_createOverlay:function(){var i;this.options.modal&&(i=!0,this._delay(function(){i=!1}),this.document.data("ui-dialog-overlays")||this.document.on("focusin.ui-dialog",function(t){var e;i||(e=this._trackingInstances()[0])._allowInteraction(t)||(t.preventDefault(),e._focusTabbable())}.bind(this)),this.overlay=V("
      ").appendTo(this._appendTo()),this._addClass(this.overlay,null,"ui-widget-overlay ui-front"),this._on(this.overlay,{mousedown:"_keepFocus"}),this.document.data("ui-dialog-overlays",(this.document.data("ui-dialog-overlays")||0)+1))},_destroyOverlay:function(){var t;this.options.modal&&this.overlay&&((t=this.document.data("ui-dialog-overlays")-1)?this.document.data("ui-dialog-overlays",t):(this.document.off("focusin.ui-dialog"),this.document.removeData("ui-dialog-overlays")),this.overlay.remove(),this.overlay=null)}}),!0===V.uiBackCompat&&V.widget("ui.dialog",V.ui.dialog,{options:{dialogClass:""},_createWrapper:function(){this._super(),this.uiDialog.addClass(this.options.dialogClass)},_setOption:function(t,e){"dialogClass"===t&&this.uiDialog.removeClass(this.options.dialogClass).addClass(e),this._superApply(arguments)}}),V.ui.dialog;function rt(t,e,i){return e<=t&&t").appendTo(this.element),this._addClass(this.valueDiv,"ui-progressbar-value","ui-widget-header"),this._refreshValue()},_destroy:function(){this.element.removeAttr("role aria-valuemin aria-valuemax aria-valuenow"),this.valueDiv.remove()},value:function(t){if(void 0===t)return this.options.value;this.options.value=this._constrainedValue(t),this._refreshValue()},_constrainedValue:function(t){return void 0===t&&(t=this.options.value),this.indeterminate=!1===t,"number"!=typeof t&&(t=0),!this.indeterminate&&Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var t=this.options.value,e=this._percentage();this.valueDiv.toggle(this.indeterminate||t>this.min).width(e.toFixed(0)+"%"),this._toggleClass(this.valueDiv,"ui-progressbar-complete",null,t===this.options.max)._toggleClass("ui-progressbar-indeterminate",null,this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=V("
      ").appendTo(this.valueDiv),this._addClass(this.overlayDiv,"ui-progressbar-overlay"))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":t}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==t&&(this.oldValue=t,this._trigger("change")),t===this.options.max&&this._trigger("complete")}}),V.widget("ui.selectable",V.ui.mouse,{version:"1.14.1",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var i=this;this._addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){i.elementPos=V(i.element[0]).offset(),i.selectees=V(i.options.filter,i.element[0]),i._addClass(i.selectees,"ui-selectee"),i.selectees.each(function(){var t=V(this),e=t.offset(),e={left:e.left-i.elementPos.left,top:e.top-i.elementPos.top};V.data(this,"selectable-item",{element:this,$element:t,left:e.left,top:e.top,right:e.left+t.outerWidth(),bottom:e.top+t.outerHeight(),startselected:!1,selected:t.hasClass("ui-selected"),selecting:t.hasClass("ui-selecting"),unselecting:t.hasClass("ui-unselecting")})})},this.refresh(),this._mouseInit(),this.helper=V("
      "),this._addClass(this.helper,"ui-selectable-helper")},_destroy:function(){this.selectees.removeData("selectable-item"),this._mouseDestroy()},_mouseStart:function(i){var s=this,t=this.options;this.opos=[i.pageX,i.pageY],this.elementPos=V(this.element[0]).offset(),this.options.disabled||(this.selectees=V(t.filter,this.element[0]),this._trigger("start",i),V(t.appendTo).append(this.helper),this.helper.css({left:i.pageX,top:i.pageY,width:0,height:0}),t.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var t=V.data(this,"selectable-item");t.startselected=!0,i.metaKey||i.ctrlKey||(s._removeClass(t.$element,"ui-selected"),t.selected=!1,s._addClass(t.$element,"ui-unselecting"),t.unselecting=!0,s._trigger("unselecting",i,{unselecting:t.element}))}),V(i.target).parents().addBack().each(function(){var t,e=V.data(this,"selectable-item");if(e)return t=!i.metaKey&&!i.ctrlKey||!e.$element.hasClass("ui-selected"),s._removeClass(e.$element,t?"ui-unselecting":"ui-selected")._addClass(e.$element,t?"ui-selecting":"ui-unselecting"),e.unselecting=!t,e.selecting=t,(e.selected=t)?s._trigger("selecting",i,{selecting:e.element}):s._trigger("unselecting",i,{unselecting:e.element}),!1}))},_mouseDrag:function(s){var t,n,o,a,r,l,h;if(this.dragged=!0,!this.options.disabled)return o=(n=this).options,a=this.opos[0],r=this.opos[1],(l=s.pageX)",options:{appendTo:null,classes:{"ui-selectmenu-button-open":"ui-corner-top","ui-selectmenu-button-closed":"ui-corner-all"},disabled:null,icons:{button:"ui-icon-triangle-1-s"},position:{my:"left top",at:"left bottom",collision:"none"},width:!1,change:null,close:null,focus:null,open:null,select:null},_create:function(){var t=this.element.uniqueId().attr("id");this.ids={element:t,button:t+"-button",menu:t+"-menu"},this._drawButton(),this._drawMenu(),this._bindFormResetHandler(),this._rendered=!1,this.menuItems=V()},_drawButton:function(){var t,e=this,i=this._parseOption(this.element.find("option:selected"),this.element[0].selectedIndex);this.labels=this.element.labels().attr("for",this.ids.button),this._on(this.labels,{click:function(t){this.button.trigger("focus"),t.preventDefault()}}),this.element.hide(),this.button=V("",{tabindex:this.options.disabled?-1:0,id:this.ids.button,role:"combobox","aria-expanded":"false","aria-autocomplete":"list","aria-owns":this.ids.menu,"aria-haspopup":"true",title:this.element.attr("title")}).insertAfter(this.element),this._addClass(this.button,"ui-selectmenu-button ui-selectmenu-button-closed","ui-button ui-widget"),t=V("").appendTo(this.button),this._addClass(t,"ui-selectmenu-icon","ui-icon "+this.options.icons.button),this.buttonItem=this._renderButtonItem(i).appendTo(this.button),!1!==this.options.width&&this._resizeButton(),this._on(this.button,this._buttonEvents),this.button.one("focusin",function(){e._rendered||e._refreshMenu()})},_drawMenu:function(){var i=this;this.menu=V("
        ",{"aria-hidden":"true","aria-labelledby":this.ids.button,id:this.ids.menu}),this.menuWrap=V("
        ").append(this.menu),this._addClass(this.menuWrap,"ui-selectmenu-menu","ui-front"),this.menuWrap.appendTo(this._appendTo()),this.menuInstance=this.menu.menu({classes:{"ui-menu":"ui-corner-bottom"},role:"listbox",select:function(t,e){t.preventDefault(),i._select(e.item.data("ui-selectmenu-item"),t)},focus:function(t,e){e=e.item.data("ui-selectmenu-item");null!=i.focusIndex&&e.index!==i.focusIndex&&(i._trigger("focus",t,{item:e}),i.isOpen||i._select(e,t)),i.focusIndex=e.index,i.button.attr("aria-activedescendant",i.menuItems.eq(e.index).attr("id"))}}).menu("instance"),this.menuInstance._off(this.menu,"mouseleave"),this.menuInstance._closeOnDocumentClick=function(){return!1},this.menuInstance._isDivider=function(){return!1}},refresh:function(){this._refreshMenu(),this.buttonItem.replaceWith(this.buttonItem=this._renderButtonItem(this._getSelectedItem().data("ui-selectmenu-item")||{})),null===this.options.width&&this._resizeButton()},_refreshMenu:function(){var t=this.element.find("option");this.menu.empty(),this._parseOptions(t),this._renderMenu(this.menu,this.items),this.menuInstance.refresh(),this.menuItems=this.menu.find("li").not(".ui-selectmenu-optgroup").find(".ui-menu-item-wrapper"),this._rendered=!0,t.length&&(t=this._getSelectedItem(),this.menuInstance.focus(null,t),this._setAria(t.data("ui-selectmenu-item")),this._setOption("disabled",this.element.prop("disabled")))},open:function(t){this.options.disabled||(this._rendered?(this._removeClass(this.menu.find(".ui-state-active"),null,"ui-state-active"),this.menuInstance.focus(null,this._getSelectedItem())):this._refreshMenu(),this.menuItems.length&&(this.isOpen=!0,this._toggleAttr(),this._resizeMenu(),this._position(),this._on(this.document,this._documentClick),this._trigger("open",t)))},_position:function(){this.menuWrap.position(V.extend({of:this.button},this.options.position))},close:function(t){this.isOpen&&(this.isOpen=!1,this._toggleAttr(),this.range=null,this._off(this.document),this._trigger("close",t))},widget:function(){return this.button},menuWidget:function(){return this.menu},_renderButtonItem:function(t){var e=V("");return this._setText(e,t.label),this._addClass(e,"ui-selectmenu-text"),e},_renderMenu:function(s,t){var n=this,o="";V.each(t,function(t,e){var i;e.optgroup!==o&&(i=V("
      • ",{text:e.optgroup}),n._addClass(i,"ui-selectmenu-optgroup","ui-menu-divider"+(e.element.parent("optgroup").prop("disabled")?" ui-state-disabled":"")),i.appendTo(s),o=e.optgroup),n._renderItemData(s,e)})},_renderItemData:function(t,e){return this._renderItem(t,e).data("ui-selectmenu-item",e)},_renderItem:function(t,e){var i=V("
      • "),s=V("
        ",{title:e.element.attr("title")});return e.disabled&&this._addClass(i,null,"ui-state-disabled"),e.hidden?i.prop("hidden",!0):this._setText(s,e.label),i.append(s).appendTo(t)},_setText:function(t,e){e?t.text(e):t.html(" ")},_move:function(t,e){var i,s=".ui-menu-item";this.isOpen?i=this.menuItems.eq(this.focusIndex).parent("li"):(i=this.menuItems.eq(this.element[0].selectedIndex).parent("li"),s+=":not(.ui-state-disabled)"),(i="first"===t||"last"===t?i["first"===t?"prevAll":"nextAll"](s).eq(-1):i[t+"All"](s).eq(0)).length&&this.menuInstance.focus(e,i)},_getSelectedItem:function(){return this.menuItems.eq(this.element[0].selectedIndex).parent("li")},_toggle:function(t){this[this.isOpen?"close":"open"](t)},_setSelection:function(){var t;this.range&&((t=window.getSelection()).removeAllRanges(),t.addRange(this.range))},_documentClick:{mousedown:function(t){!this.isOpen||V(t.target).closest(".ui-selectmenu-menu, #"+CSS.escape(this.ids.button)).length||this.close(t)}},_buttonEvents:{mousedown:function(){var t=window.getSelection();t.rangeCount&&(this.range=t.getRangeAt(0))},click:function(t){this._setSelection(),this._toggle(t)},keydown:function(t){var e=!0;switch(t.keyCode){case V.ui.keyCode.TAB:case V.ui.keyCode.ESCAPE:this.close(t),e=!1;break;case V.ui.keyCode.ENTER:this.isOpen&&this._selectFocusedItem(t);break;case V.ui.keyCode.UP:t.altKey?this._toggle(t):this._move("prev",t);break;case V.ui.keyCode.DOWN:t.altKey?this._toggle(t):this._move("next",t);break;case V.ui.keyCode.SPACE:this.isOpen?this._selectFocusedItem(t):this._toggle(t);break;case V.ui.keyCode.LEFT:this._move("prev",t);break;case V.ui.keyCode.RIGHT:this._move("next",t);break;case V.ui.keyCode.HOME:case V.ui.keyCode.PAGE_UP:this._move("first",t);break;case V.ui.keyCode.END:case V.ui.keyCode.PAGE_DOWN:this._move("last",t);break;default:this.menu.trigger(t),e=!1}e&&t.preventDefault()}},_selectFocusedItem:function(t){var e=this.menuItems.eq(this.focusIndex).parent("li");e.hasClass("ui-state-disabled")||this._select(e.data("ui-selectmenu-item"),t)},_select:function(t,e){var i=this.element[0].selectedIndex;this.element[0].selectedIndex=t.index,this.buttonItem.replaceWith(this.buttonItem=this._renderButtonItem(t)),this._setAria(t),this._trigger("select",e,{item:t}),t.index!==i&&this._trigger("change",e,{item:t}),this.close(e)},_setAria:function(t){t=this.menuItems.eq(t.index).attr("id");this.button.attr({"aria-labelledby":t,"aria-activedescendant":t}),this.menu.attr("aria-activedescendant",t)},_setOption:function(t,e){var i;"icons"===t&&(i=this.button.find("span.ui-icon"),this._removeClass(i,null,this.options.icons.button)._addClass(i,null,e.button)),this._super(t,e),"appendTo"===t&&this.menuWrap.appendTo(this._appendTo()),"width"===t&&this._resizeButton()},_setOptionDisabled:function(t){this._super(t),this.menuInstance.option("disabled",t),this.button.attr("aria-disabled",t),this._toggleClass(this.button,null,"ui-state-disabled",t),this.element.prop("disabled",t),t?(this.button.attr("tabindex",-1),this.close()):this.button.attr("tabindex",0)},_appendTo:function(){var t=this.options.appendTo;return t=(t=(t=t&&(t.jquery||t.nodeType?V(t):this.document.find(t).eq(0)))&&t[0]?t:this.element.closest(".ui-front, dialog")).length?t:this.document[0].body},_toggleAttr:function(){this.button.attr("aria-expanded",this.isOpen),this._removeClass(this.button,"ui-selectmenu-button-"+(this.isOpen?"closed":"open"))._addClass(this.button,"ui-selectmenu-button-"+(this.isOpen?"open":"closed"))._toggleClass(this.menuWrap,"ui-selectmenu-open",null,this.isOpen),this.menu.attr("aria-hidden",!this.isOpen)},_resizeButton:function(){var t=this.options.width;!1===t?this.button.css("width",""):(null===t&&(t=this.element.show().outerWidth(),this.element.hide()),this.button.outerWidth(t))},_resizeMenu:function(){this.menu.outerWidth(Math.max(this.button.outerWidth(),this.menu.width("").outerWidth()))},_getCreateOptions:function(){var t=this._super();return t.disabled=this.element.prop("disabled"),t},_parseOptions:function(t){var i=this,s=[];t.each(function(t,e){s.push(i._parseOption(V(e),t))}),this.items=s},_parseOption:function(t,e){var i=t.parent("optgroup");return{element:t,index:e,value:t.val(),label:t.text(),hidden:i.prop("hidden")||t.prop("hidden"),optgroup:i.attr("label")||"",disabled:i.prop("disabled")||t.prop("disabled")}},_destroy:function(){this._unbindFormResetHandler(),this.menuWrap.remove(),this.button.remove(),this.element.show(),this.element.removeUniqueId(),this.labels.attr("for",this.ids.element)}}]),V.widget("ui.slider",V.ui.mouse,{version:"1.14.1",widgetEventPrefix:"slide",options:{animate:!1,classes:{"ui-slider":"ui-corner-all","ui-slider-handle":"ui-corner-all","ui-slider-range":"ui-corner-all ui-widget-header"},distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null,change:null,slide:null,start:null,stop:null},numPages:5,_create:function(){this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this._calculateNewMax(),this._addClass("ui-slider ui-slider-"+this.orientation,"ui-widget ui-widget-content"),this._refresh(),this._animateOff=!1},_refresh:function(){this._createRange(),this._createHandles(),this._setupEvents(),this._refreshValue()},_createHandles:function(){var t,e=this.options,i=this.element.find(".ui-slider-handle"),s=[],n=e.values&&e.values.length||1;for(i.length>n&&(i.slice(n).remove(),i=i.slice(0,n)),t=i.length;t");this.handles=i.add(V(s.join("")).appendTo(this.element)),this._addClass(this.handles,"ui-slider-handle","ui-state-default"),this.handle=this.handles.eq(0),this.handles.each(function(t){V(this).data("ui-slider-handle-index",t).attr("tabIndex",0)})},_createRange:function(){var t=this.options;t.range?(!0===t.range&&(t.values?t.values.length&&2!==t.values.length?t.values=[t.values[0],t.values[0]]:Array.isArray(t.values)&&(t.values=t.values.slice(0)):t.values=[this._valueMin(),this._valueMin()]),this.range&&this.range.length?(this._removeClass(this.range,"ui-slider-range-min ui-slider-range-max"),this.range.css({left:"",bottom:""})):(this.range=V("
        ").appendTo(this.element),this._addClass(this.range,"ui-slider-range")),"min"!==t.range&&"max"!==t.range||this._addClass(this.range,"ui-slider-range-"+t.range)):(this.range&&this.range.remove(),this.range=null)},_setupEvents:function(){this._off(this.handles),this._on(this.handles,this._handleEvents),this._hoverable(this.handles),this._focusable(this.handles)},_destroy:function(){this.handles.remove(),this.range&&this.range.remove(),this._mouseDestroy()},_mouseCapture:function(t){var i,s,n,o,e,a,r=this,l=this.options;return!l.disabled&&(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),e={x:t.pageX,y:t.pageY},i=this._normValueFromMouse(e),s=this._valueMax()-this._valueMin()+1,this.handles.each(function(t){var e=Math.abs(i-r.values(t));(e=this._valueMax()?this._valueMax():(e=0=e&&(i+=0this.options.max&&(t-=i),this.max=parseFloat(t.toFixed(this._precision()))},_precision:function(){var t=this._precisionOf(this.options.step);return t=null!==this.options.min?Math.max(t,this._precisionOf(this.options.min)):t},_precisionOf:function(t){var t=t.toString(),e=t.indexOf(".");return-1===e?0:t.length-e-1},_valueMin:function(){return this.options.min},_valueMax:function(){return this.max},_refreshRange:function(t){"vertical"===t&&this.range.css({width:"",left:""}),"horizontal"===t&&this.range.css({height:"",bottom:""})},_refreshValue:function(){var e,i,t,s,n,o=this.options.range,a=this.options,r=this,l=!this._animateOff&&a.animate,h={};this._hasMultipleValues()?this.handles.each(function(t){i=(r.values(t)-r._valueMin())/(r._valueMax()-r._valueMin())*100,h["horizontal"===r.orientation?"left":"bottom"]=i+"%",V(this).stop(1,1)[l?"animate":"css"](h,a.animate),!0===r.options.range&&("horizontal"===r.orientation?(0===t&&r.range.stop(1,1)[l?"animate":"css"]({left:i+"%"},a.animate),1===t&&r.range[l?"animate":"css"]({width:i-e+"%"},{queue:!1,duration:a.animate})):(0===t&&r.range.stop(1,1)[l?"animate":"css"]({bottom:i+"%"},a.animate),1===t&&r.range[l?"animate":"css"]({height:i-e+"%"},{queue:!1,duration:a.animate}))),e=i}):(t=this.value(),s=this._valueMin(),n=this._valueMax(),i=n!==s?(t-s)/(n-s)*100:0,h["horizontal"===this.orientation?"left":"bottom"]=i+"%",this.handle.stop(1,1)[l?"animate":"css"](h,a.animate),"min"===o&&"horizontal"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({width:i+"%"},a.animate),"max"===o&&"horizontal"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({width:100-i+"%"},a.animate),"min"===o&&"vertical"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({height:i+"%"},a.animate),"max"===o&&"vertical"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({height:100-i+"%"},a.animate))},_handleEvents:{keydown:function(t){var e,i,s,n=V(t.target).data("ui-slider-handle-index");switch(t.keyCode){case V.ui.keyCode.HOME:case V.ui.keyCode.END:case V.ui.keyCode.PAGE_UP:case V.ui.keyCode.PAGE_DOWN:case V.ui.keyCode.UP:case V.ui.keyCode.RIGHT:case V.ui.keyCode.DOWN:case V.ui.keyCode.LEFT:if(t.preventDefault(),this._keySliding||(this._keySliding=!0,this._addClass(V(t.target),null,"ui-state-active"),!1!==this._start(t,n)))break;return}switch(s=this.options.step,e=i=this._hasMultipleValues()?this.values(n):this.value(),t.keyCode){case V.ui.keyCode.HOME:i=this._valueMin();break;case V.ui.keyCode.END:i=this._valueMax();break;case V.ui.keyCode.PAGE_UP:i=this._trimAlignValue(e+(this._valueMax()-this._valueMin())/this.numPages);break;case V.ui.keyCode.PAGE_DOWN:i=this._trimAlignValue(e-(this._valueMax()-this._valueMin())/this.numPages);break;case V.ui.keyCode.UP:case V.ui.keyCode.RIGHT:if(e===this._valueMax())return;i=this._trimAlignValue(e+s);break;case V.ui.keyCode.DOWN:case V.ui.keyCode.LEFT:if(e===this._valueMin())return;i=this._trimAlignValue(e-s)}this._slide(t,n,i)},keyup:function(t){var e=V(t.target).data("ui-slider-handle-index");this._keySliding&&(this._keySliding=!1,this._stop(t,e),this._change(t,e),this._removeClass(V(t.target),null,"ui-state-active"))}}}),V.widget("ui.sortable",V.ui.mouse,{version:"1.14.1",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return e<=t&&t*{ cursor: "+o.cursor+" !important; }").appendTo(n)),o.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",o.zIndex)),o.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",o.opacity)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",t,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!i)for(s=this.containers.length-1;0<=s;s--)this.containers[s]._trigger("activate",t,this._uiHash(this));return V.ui.ddmanager&&(V.ui.ddmanager.current=this),V.ui.ddmanager&&!o.dropBehaviour&&V.ui.ddmanager.prepareOffsets(this,t),this.dragging=!0,this._addClass(this.helper,"ui-sortable-helper"),this.helper.parent().is(this.appendTo)||(this.helper.detach().appendTo(this.appendTo),this.offset.parent=this._getParentOffset()),this.position=this.originalPosition=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,this.lastPositionAbs=this.positionAbs=this._convertPositionTo("absolute"),this._mouseDrag(t),!0},_scroll:function(t){var e=this.options,i=!1;return this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-t.pageYt[this.floating?"width":"height"]?h&&c:o",i.document[0]);return i._addClass(t,"ui-sortable-placeholder",s||i.currentItem[0].className)._removeClass(t,"ui-sortable-helper"),"tbody"===n?i._createTrPlaceholder(i.currentItem.find("tr").eq(0),V("",i.document[0]).appendTo(t)):"tr"===n?i._createTrPlaceholder(i.currentItem,t):"img"===n&&t.attr("src",i.currentItem.attr("src")),s||t.css("visibility","hidden"),t},update:function(t,e){s&&!o.forcePlaceholderSize||(e.height()&&(!o.forcePlaceholderSize||"tbody"!==n&&"tr"!==n)||e.height(i.currentItem.innerHeight()-parseInt(i.currentItem.css("paddingTop")||0,10)-parseInt(i.currentItem.css("paddingBottom")||0,10)),e.width())||e.width(i.currentItem.innerWidth()-parseInt(i.currentItem.css("paddingLeft")||0,10)-parseInt(i.currentItem.css("paddingRight")||0,10))}}),i.placeholder=V(o.placeholder.element.call(i.element,i.currentItem)),i.currentItem.after(i.placeholder),o.placeholder.update(i,i.placeholder)},_createTrPlaceholder:function(t,e){var i=this;t.children().each(function(){V(" ",i.document[0]).attr("colspan",V(this).attr("colspan")||1).appendTo(e)})},_contactContainers:function(t){for(var e,i,s,n,o,a,r,l,h,c=null,u=null,d=this.containers.length-1;0<=d;d--)V.contains(this.currentItem[0],this.containers[d].element[0])||(this._intersectsWith(this.containers[d].containerCache)?c&&V.contains(this.containers[d].element[0],c.element[0])||(c=this.containers[d],u=d):this.containers[d].containerCache.over&&(this.containers[d]._trigger("out",t,this._uiHash(this)),this.containers[d].containerCache.over=0));if(c)if(1===this.containers.length)this.containers[u].containerCache.over||(this.containers[u]._trigger("over",t,this._uiHash(this)),this.containers[u].containerCache.over=1);else{for(i=1e4,s=null,n=(l=c.floating||this._isFloating(this.currentItem))?"left":"top",o=l?"width":"height",h=l?"pageX":"pageY",e=this.items.length-1;0<=e;e--)V.contains(this.containers[u].element[0],this.items[e].item[0])&&this.items[e].item[0]!==this.currentItem[0]&&(a=this.items[e].item.offset()[n],r=!1,t[h]-a>this.items[e][o]/2&&(r=!0),Math.abs(t[h]-a)this.containment[2]&&(i=this.containment[2]+this.offset.click.left),t.pageY-this.offset.click.top>this.containment[3])&&(s=this.containment[3]+this.offset.click.top),e.grid)&&(t=this.originalPageY+Math.round((s-this.originalPageY)/e.grid[1])*e.grid[1],s=!this.containment||t-this.offset.click.top>=this.containment[1]&&t-this.offset.click.top<=this.containment[3]?t:t-this.offset.click.top>=this.containment[1]?t-e.grid[1]:t+e.grid[1],t=this.originalPageX+Math.round((i-this.originalPageX)/e.grid[0])*e.grid[0],i=!this.containment||t-this.offset.click.left>=this.containment[0]&&t-this.offset.click.left<=this.containment[2]?t:t-this.offset.click.left>=this.containment[0]?t-e.grid[0]:t+e.grid[0]),{top:s-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop()),left:i-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){this.reverting=!1;var i,s=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(i in this._storedCSS)"auto"!==this._storedCSS[i]&&"static"!==this._storedCSS[i]||(this._storedCSS[i]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();function n(e,i,s){return function(t){s._trigger(e,t,i._uiHash(i))}}for(this.fromOutside&&!e&&s.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||s.push(function(t){this._trigger("update",t,this._uiHash())}),this===this.currentContainer||e||(s.push(function(t){this._trigger("remove",t,this._uiHash())}),s.push(function(e){return function(t){e._trigger("receive",t,this._uiHash(this))}}.call(this,this.currentContainer)),s.push(function(e){return function(t){e._trigger("update",t,this._uiHash(this))}}.call(this,this.currentContainer))),i=this.containers.length-1;0<=i;i--)e||s.push(n("deactivate",this,this.containers[i])),this.containers[i].containerCache.over&&(s.push(n("out",this,this.containers[i])),this.containers[i].containerCache.over=0);if(this._storedStylesheet&&(this._storedStylesheet.remove(),this._storedStylesheet=null),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(i=0;i",widgetEventPrefix:"spin",options:{classes:{"ui-spinner":"ui-corner-all","ui-spinner-down":"ui-corner-br","ui-spinner-up":"ui-corner-tr"},culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var s=this._super(),n=this.element;return V.each(["min","max","step"],function(t,e){var i=n.attr(e);null!=i&&i.length&&(s[e]=i)}),s},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t)},mousewheel:function(t,e){var i=this.document[0].activeElement;if(this.element[0]===i&&e){if(!this.spinning&&!this._start(t))return!1;this._spin((0").parent().append("")},_draw:function(){this._enhance(),this._addClass(this.uiSpinner,"ui-spinner","ui-widget ui-widget-content"),this._addClass("ui-spinner-input"),this.element.attr("role","spinbutton"),this.buttons=this.uiSpinner.children("a").attr("tabIndex",-1).attr("aria-hidden",!0).button({classes:{"ui-button":""}}),this._removeClass(this.buttons,"ui-corner-all"),this._addClass(this.buttons.first(),"ui-spinner-button ui-spinner-up"),this._addClass(this.buttons.last(),"ui-spinner-button ui-spinner-down"),this.buttons.first().button({icon:this.options.icons.up,showLabel:!1}),this.buttons.last().button({icon:this.options.icons.down,showLabel:!1}),this.buttons.height()>Math.ceil(.5*this.uiSpinner.height())&&0e.max?e.max:null!==e.min&&t"},_buttonHtml:function(){return""}});var A;V.ui.spinner,V.widget("ui.tabs",{version:"1.14.1",delay:300,options:{active:null,classes:{"ui-tabs":"ui-corner-all","ui-tabs-nav":"ui-corner-all","ui-tabs-panel":"ui-corner-bottom","ui-tabs-tab":"ui-corner-top"},collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_isLocal:(A=/#.*$/,function(t){var e=t.href.replace(A,""),i=location.href.replace(A,"");try{e=decodeURIComponent(e)}catch(t){}try{i=decodeURIComponent(i)}catch(t){}return 1 li",function(t){V(this).is(".ui-state-disabled")&&t.preventDefault()}),this.tabs=this.tablist.find("> li:has(a[href])").attr({role:"tab",tabIndex:-1}),this._addClass(this.tabs,"ui-tabs-tab","ui-state-default"),this.anchors=this.tabs.map(function(){return V("a",this)[0]}).attr({tabIndex:-1}),this._addClass(this.anchors,"ui-tabs-anchor"),this.panels=V(),this.anchors.each(function(t,e){var i,s,n=V(e).uniqueId().attr("id"),o=V(e).closest("li"),a=o.attr("aria-controls");r._isLocal(e)?(s=decodeURIComponent(e.hash).substring(1),i=r.element.find("#"+CSS.escape(s))):(s=o.attr("aria-controls")||V({}).uniqueId()[0].id,(i=r.element.find("#"+s)).length||(i=r._createPanel(s)).insertAfter(r.panels[t-1]||r.tablist),i.attr("aria-live","polite")),i.length&&(r.panels=r.panels.add(i)),a&&o.data("ui-tabs-aria-controls",a),o.attr({"aria-controls":s,"aria-labelledby":n}),i.attr("aria-labelledby",n)}),this.panels.attr("role","tabpanel"),this._addClass(this.panels,"ui-tabs-panel","ui-widget-content"),t&&(this._off(t.not(this.tabs)),this._off(e.not(this.anchors)),this._off(i.not(this.panels)))},_getList:function(){return this.tablist||this.element.find("ol, ul").eq(0)},_createPanel:function(t){return V("
        ").attr("id",t).data("ui-tabs-destroy",!0)},_setOptionDisabled:function(t){var e,i;for(Array.isArray(t)&&(t.length?t.length===this.anchors.length&&(t=!0):t=!1),i=0;e=this.tabs[i];i++)e=V(e),!0===t||-1!==V.inArray(i,t)?(e.attr("aria-disabled","true"),this._addClass(e,null,"ui-state-disabled")):(e.removeAttr("aria-disabled"),this._removeClass(e,null,"ui-state-disabled"));this.options.disabled=t,this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!0===t)},_setupEvents:function(t){var i={};t&&V.each(t.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(!0,this.anchors,{click:function(t){t.preventDefault()}}),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(t){var i,e=this.element.parent();"fill"===t?(i=e.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var t=V(this),e=t.css("position");"absolute"!==e&&"fixed"!==e&&(i-=t.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=V(this).outerHeight(!0)}),this.panels.each(function(){V(this).height(Math.max(0,i-V(this).innerHeight()+V(this).height()))}).css("overflow","auto")):"auto"===t&&(i=0,this.panels.each(function(){i=Math.max(i,V(this).height("").height())}).height(i))},_eventHandler:function(t){var e=this.options,i=this.active,s=V(t.currentTarget).closest("li"),n=s[0]===i[0],o=n&&e.collapsible,a=o?V():this._getPanelForTab(s),r=i.length?this._getPanelForTab(i):V(),i={oldTab:i,oldPanel:r,newTab:o?V():s,newPanel:a};t.preventDefault(),s.hasClass("ui-state-disabled")||s.hasClass("ui-tabs-loading")||this.running||n&&!e.collapsible||!1===this._trigger("beforeActivate",t,i)||(e.active=!o&&this.tabs.index(s),this.active=n?V():s,this.xhr&&this.xhr.abort(),r.length||a.length||V.error("jQuery UI Tabs: Mismatching fragment identifier."),a.length&&this.load(this.tabs.index(s),t),this._toggle(t,i))},_toggle:function(t,e){var i=this,s=e.newPanel,n=e.oldPanel;function o(){i.running=!1,i._trigger("activate",t,e)}function a(){i._addClass(e.newTab.closest("li"),"ui-tabs-active","ui-state-active"),s.length&&i.options.show?i._show(s,i.options.show,o):(s.show(),o())}this.running=!0,n.length&&this.options.hide?this._hide(n,this.options.hide,function(){i._removeClass(e.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),a()}):(this._removeClass(e.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),n.hide(),a()),n.attr("aria-hidden","true"),e.oldTab.attr({"aria-selected":"false","aria-expanded":"false"}),s.length&&n.length?e.oldTab.attr("tabIndex",-1):s.length&&this.tabs.filter(function(){return 0===V(this).attr("tabIndex")}).attr("tabIndex",-1),s.attr("aria-hidden","false"),e.newTab.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_activate:function(t){var t=this._findActive(t);t[0]!==this.active[0]&&(t=(t=t.length?t:this.active).find(".ui-tabs-anchor")[0],this._eventHandler({target:t,currentTarget:t,preventDefault:V.noop}))},_findActive:function(t){return!1===t?V():this.tabs.eq(t)},_getIndex:function(t){return t="string"==typeof t?this.anchors.index(this.anchors.filter("[href$='"+CSS.escape(t)+"']")):t},_destroy:function(){this.xhr&&this.xhr.abort(),this.tablist.removeAttr("role").off(this.eventNamespace),this.anchors.removeAttr("role tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){V.data(this,"ui-tabs-destroy")?V(this).remove():V(this).removeAttr("role tabIndex aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded")}),this.tabs.each(function(){var t=V(this),e=t.data("ui-tabs-aria-controls");e?t.attr("aria-controls",e).removeData("ui-tabs-aria-controls"):t.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var t=this.options.disabled;!1!==t&&(t=void 0!==i&&(i=this._getIndex(i),Array.isArray(t)?V.map(t,function(t){return t!==i?t:null}):V.map(this.tabs,function(t,e){return e!==i?e:null})),this._setOptionDisabled(t))},disable:function(t){var e=this.options.disabled;if(!0!==e){if(void 0===t)e=!0;else{if(t=this._getIndex(t),-1!==V.inArray(t,e))return;e=Array.isArray(e)?V.merge([t],e).sort():[t]}this._setOptionDisabled(e)}},load:function(t,s){t=this._getIndex(t);function n(t,e){"abort"===e&&o.panels.stop(!1,!0),o._removeClass(i,"ui-tabs-loading"),a.removeAttr("aria-busy"),t===o.xhr&&delete o.xhr}var o=this,i=this.tabs.eq(t),t=i.find(".ui-tabs-anchor"),a=this._getPanelForTab(i),r={tab:i,panel:a};this._isLocal(t[0])||(this.xhr=V.ajax(this._ajaxSettings(t,s,r)),"canceled"!==this.xhr.statusText&&(this._addClass(i,"ui-tabs-loading"),a.attr("aria-busy","true"),this.xhr.done(function(t,e,i){a.html(t),o._trigger("load",s,r),n(i,e)}).fail(function(t,e){n(t,e)})))},_ajaxSettings:function(t,i,s){var n=this;return{url:t.attr("href"),beforeSend:function(t,e){return n._trigger("beforeLoad",i,V.extend({jqXHR:t,ajaxSettings:e},s))}}},_getPanelForTab:function(t){t=V(t).attr("aria-controls");return this.element.find("#"+CSS.escape(t))}}),!0===V.uiBackCompat&&V.widget("ui.tabs",V.ui.tabs,{_processTabs:function(){this._superApply(arguments),this._addClass(this.tabs,"ui-tab")}}),V.ui.tabs,V.widget("ui.tooltip",{version:"1.14.1",options:{classes:{"ui-tooltip":"ui-corner-all ui-widget-shadow"},content:function(){var t=V(this).attr("title");return V("").text(t).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,track:!1,close:null,open:null},_addDescribedBy:function(t,e){var i=(t.attr("aria-describedby")||"").split(/\s+/);i.push(e),t.data("ui-tooltip-id",e).attr("aria-describedby",String.prototype.trim.call(i.join(" ")))},_removeDescribedBy:function(t){var e=t.data("ui-tooltip-id"),i=(t.attr("aria-describedby")||"").split(/\s+/),e=V.inArray(e,i);-1!==e&&i.splice(e,1),t.removeData("ui-tooltip-id"),(i=String.prototype.trim.call(i.join(" ")))?t.attr("aria-describedby",i):t.removeAttr("aria-describedby")},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.liveRegion=V("
        ").attr({role:"log","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this.disabledTitles=V([])},_setOption:function(t,e){var i=this;this._super(t,e),"content"===t&&V.each(this.tooltips,function(t,e){i._updateContent(e.element)})},_setOptionDisabled:function(t){this[t?"_disable":"_enable"]()},_disable:function(){var s=this;V.each(this.tooltips,function(t,e){var i=V.Event("blur");i.target=i.currentTarget=e.element[0],s.close(i,!0)}),this.disabledTitles=this.disabledTitles.add(this.element.find(this.options.items).addBack().filter(function(){var t=V(this);if(t.is("[title]"))return t.data("ui-tooltip-title",t.attr("title")).removeAttr("title")}))},_enable:function(){this.disabledTitles.each(function(){var t=V(this);t.data("ui-tooltip-title")&&t.attr("title",t.data("ui-tooltip-title"))}),this.disabledTitles=V([])},open:function(t){var i=this,e=V(t?t.target:this.element).closest(this.options.items);e.length&&!e.data("ui-tooltip-id")&&(e.attr("title")&&e.data("ui-tooltip-title",e.attr("title")),e.data("ui-tooltip-open",!0),t&&"mouseover"===t.type&&e.parents().each(function(){var t,e=V(this);e.data("ui-tooltip-open")&&((t=V.Event("blur")).target=t.currentTarget=this,i.close(t,!0)),e.attr("title")&&(e.uniqueId(),i.parents[this.id]={element:this,title:e.attr("title")},e.attr("title",""))}),this._registerCloseHandlers(t,e),this._updateContent(e,t))},_updateContent:function(e,i){var t=this.options.content,s=this,n=i?i.type:null;if("string"==typeof t||t.nodeType||t.jquery)return this._open(i,e,t);(t=t.call(e[0],function(t){e.data("ui-tooltip-open")&&(i&&(i.type=n),s._open(i,e,t))}))&&this._open(i,e,t)},_open:function(t,e,i){var s,n,o,a=V.extend({},this.options.position);function r(t){a.of=t,s.is(":hidden")||s.position(a)}i&&((o=this._find(e))?o.tooltip.find(".ui-tooltip-content").html(i):(e.is("[title]")&&(t&&"mouseover"===t.type?e.attr("title",""):e.removeAttr("title")),o=this._tooltip(e),s=o.tooltip,this._addDescribedBy(e,s.attr("id")),s.find(".ui-tooltip-content").html(i),this.liveRegion.children().hide(),(o=V("
        ").html(s.find(".ui-tooltip-content").html())).removeAttr("name").find("[name]").removeAttr("name"),o.removeAttr("id").find("[id]").removeAttr("id"),o.appendTo(this.liveRegion),this.options.track&&t&&/^mouse/.test(t.type)?(this._on(this.document,{mousemove:r}),r(t)):s.position(V.extend({of:e},this.options.position)),s.hide(),this._show(s,this.options.show),this.options.track&&this.options.show&&this.options.show.delay&&(n=this.delayedShow=setInterval(function(){s.is(":visible")&&(r(a.of),clearInterval(n))},13)),this._trigger("open",t,{tooltip:s})))},_registerCloseHandlers:function(t,e){var i={keyup:function(t){t.keyCode===V.ui.keyCode.ESCAPE&&((t=V.Event(t)).currentTarget=e[0],this.close(t,!0))}};e[0]!==this.element[0]&&(i.remove=function(){var t=this._find(e);t&&this._removeTooltip(t.tooltip)}),t&&"mouseover"!==t.type||(i.mouseleave="close"),t&&"focusin"!==t.type||(i.focusout="close"),this._on(!0,e,i)},close:function(t){var e,i=this,s=V(t?t.currentTarget:this.element),n=this._find(s);n?(e=n.tooltip,n.closing||(clearInterval(this.delayedShow),s.data("ui-tooltip-title")&&!s.attr("title")&&s.attr("title",s.data("ui-tooltip-title")),this._removeDescribedBy(s),n.hiding=!0,e.stop(!0),this._hide(e,this.options.hide,function(){i._removeTooltip(V(this))}),s.removeData("ui-tooltip-open"),this._off(s,"mouseleave focusout keyup"),s[0]!==this.element[0]&&this._off(s,"remove"),this._off(this.document,"mousemove"),t&&"mouseleave"===t.type&&V.each(this.parents,function(t,e){V(e.element).attr("title",e.title),delete i.parents[t]}),n.closing=!0,this._trigger("close",t,{tooltip:e}),n.hiding)||(n.closing=!1)):s.removeData("ui-tooltip-open")},_tooltip:function(t){var e=V("
        ").attr("role","tooltip"),i=V("
        ").appendTo(e),s=e.uniqueId().attr("id");return this._addClass(i,"ui-tooltip-content"),this._addClass(e,"ui-tooltip","ui-widget ui-widget-content"),e.appendTo(this._appendTo(t)),this.tooltips[s]={element:t,tooltip:e}},_find:function(t){t=t.data("ui-tooltip-id");return t?this.tooltips[t]:null},_removeTooltip:function(t){clearInterval(this.delayedShow),t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){t=t.closest(".ui-front, dialog");return t=t.length?t:this.document[0].body},_destroy:function(){var s=this;V.each(this.tooltips,function(t,e){var i=V.Event("blur"),e=e.element;i.target=i.currentTarget=e[0],s.close(i,!0),V("#"+t).remove(),e.data("ui-tooltip-title")&&(e.attr("title")||e.attr("title",e.data("ui-tooltip-title")),e.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),!0===V.uiBackCompat&&V.widget("ui.tooltip",V.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),V.ui.tooltip}); \ No newline at end of file diff --git a/streetup/common/static/common/jquery.min.js b/streetup/common/static/common/jquery.min.js new file mode 100644 index 0000000..7f37b5d --- /dev/null +++ b/streetup/common/static/common/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
        "],col:[2,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
        ",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0`, + + // Map/Layers icon + layers: ``, + + // 3D/Cube icon + cube: ``, + + // Eye icon for visibility + eye: ``, + + // Grid/Tiles icon + grid: ``, + + // Tree icon for vegetation/green surfaces + tree: `` + }; + + // CSS styles for the control + const CONTROL_STYLES = ` + .layer-switcher-ctrl { + background: #fff; + border-radius: 4px; + box-shadow: 0 0 0 2px rgba(0,0,0,.1); + overflow: visible; + } + + .layer-switcher-btn { + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: 0; + outline: none; + box-sizing: border-box; + background-color: transparent; + cursor: pointer; + border-bottom: 1px solid rgba(0,0,0,.1); + } + + .layer-switcher-btn:last-child { + border-bottom: none; + } + + .layer-switcher-btn:hover { + background-color: rgba(0, 0, 0, 0.05); + } + + .layer-switcher-btn.active { + background-color: #e7f3ff; + } + + .layer-switcher-btn.active:hover { + background-color: #cce5ff; + } + + .layer-switcher-btn svg { + width: 16px; + height: 16px; + stroke: #333; + } + + .layer-switcher-btn.active svg { + stroke: #0066ff; + } + + /* Bootstrap Icons support */ + .layer-switcher-btn .bi { + font-size: 14px; + color: #333; + } + + .layer-switcher-btn.active .bi { + color: #0066ff; + } + + /* Sublayer selector styles - positioned to the right of the control */ + .layer-switcher-sublayer-selector { + position: absolute; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + padding: 6px; + z-index: 9999; + display: none; + white-space: nowrap; + } + + .layer-switcher-sublayer-selector.visible { + display: flex; + gap: 4px; + } + + .layer-switcher-sublayer-btn { + padding: 4px 8px; + border: 2px solid #ddd; + background: white; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + font-weight: 500; + transition: all 0.2s ease; + white-space: nowrap; + } + + .layer-switcher-sublayer-btn:hover { + background: #f8f9fa; + border-color: #0d6efd; + } + + .layer-switcher-sublayer-btn.active { + background: #0d6efd; + color: white; + border-color: #0d6efd; + } + `; + + /** + * LayerSwitcherControl class + * + * @param {Object} options - Configuration options + * @param {Array} options.layers - Array of layer configurations + * @param {string} options.layers[].id - Unique layer ID + * @param {string} options.layers[].name - Display name for tooltip + * @param {string} options.layers[].icon - Icon type: 'satellite', 'layers', 'cube', 'eye', 'grid' or custom SVG + * @param {Object} options.layers[].source - MapLibre source configuration (for new layers) + * @param {Object} options.layers[].layer - MapLibre layer configuration without id and source (for new layers) + * @param {string[]} options.layers[].existingLayerIds - Array of existing layer IDs to toggle (for existing layers) + * @param {string} options.layers[].beforeLayer - ID of layer to insert before (optional) + * @param {boolean} options.layers[].visible - Initial visibility (default: false for new layers, true for existing) + * @param {boolean} options.layers[].exclusive - If true, enabling this layer disables others in same group + * @param {string} options.layers[].group - Group name for exclusive layers + * @param {Array} options.layers[].sublayers - Array of sublayer configurations for multi-source layers + * @param {string} options.layers[].sublayers[].id - Unique sublayer ID + * @param {string} options.layers[].sublayers[].name - Display name for sublayer + * @param {Object} options.layers[].sublayers[].source - MapLibre source configuration + * @param {Object} options.layers[].sublayers[].layer - MapLibre layer configuration + * @param {boolean} options.layers[].sublayers[].default - If true, this sublayer is selected by default + */ + class LayerSwitcherControl { + constructor(options = {}) { + this.layers = options.layers || []; + this.map = null; + this.container = null; + this.buttons = {}; + this.sublayerSelectors = {}; // Fixed position selectors for sublayers + this.layerStates = new Map(); // Track layer visibility states + this.activeSublayers = new Map(); // Track active sublayer for each parent layer + + this._injectStyles(); + } + + /** + * Inject CSS styles + */ + _injectStyles() { + if (document.getElementById('layer-switcher-styles')) return; + + const style = document.createElement('style'); + style.id = 'layer-switcher-styles'; + style.textContent = CONTROL_STYLES; + document.head.appendChild(style); + } + + /** + * Add control to map (IControl interface) + */ + onAdd(map) { + this.map = map; + + // Create container + this.container = document.createElement('div'); + this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group layer-switcher-ctrl'; + this.container.style.position = 'relative'; + + // Create buttons for each layer + this.layers.forEach(layerConfig => { + // For layers with sublayers, set default active sublayer + if (layerConfig.sublayers && layerConfig.sublayers.length > 0) { + const defaultSublayer = layerConfig.sublayers.find(s => s.default) || layerConfig.sublayers[0]; + this.activeSublayers.set(layerConfig.id, defaultSublayer.id); + } + + const button = this._createButton(layerConfig); + this.container.appendChild(button); + this.buttons[layerConfig.id] = button; + + // Create sublayer selector for sublayers if needed + // Appended to the map container to escape the 29px maplibregl-ctrl-group constraint + if (layerConfig.sublayers && layerConfig.sublayers.length > 0) { + const selector = this._createSublayerSelector(layerConfig); + map.getContainer().appendChild(selector); + this.sublayerSelectors[layerConfig.id] = selector; + } + + // Initialize layer state + // For existing layers, default to visible (they already exist on the map) + const isExistingLayer = layerConfig.existingLayerIds && layerConfig.existingLayerIds.length > 0; + const initialVisibility = layerConfig.visible !== undefined + ? layerConfig.visible + : isExistingLayer; // existing layers are visible by default + + this.layerStates.set(layerConfig.id, { + visible: initialVisibility, + sourceAdded: isExistingLayer, // existing layers don't need source + layerAdded: isExistingLayer // existing layers are already added + }); + + // Update button state immediately for existing visible layers + if (initialVisibility) { + this._updateButtonState(layerConfig.id, true); + } + }); + + // Initialize layers if map is already loaded + if (map.loaded()) { + this._initializeLayers(); + } else { + map.on('load', () => this._initializeLayers()); + } + + return this.container; + } + + /** + * Remove control from map + */ + onRemove() { + // Remove layers and sources + this.layers.forEach(layerConfig => { + const layerId = `layer-switcher-${layerConfig.id}`; + const sourceId = `layer-switcher-source-${layerConfig.id}`; + + if (this.map.getLayer(layerId)) { + this.map.removeLayer(layerId); + } + if (this.map.getSource(sourceId)) { + this.map.removeSource(sourceId); + } + }); + + // Remove sublayer selectors from body + Object.values(this.sublayerSelectors).forEach(selector => { + if (selector && selector.parentNode) { + selector.parentNode.removeChild(selector); + } + }); + + if (this.container && this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + + this.map = null; + this.container = null; + } + + /** + * Create a button for a layer + */ + _createButton(layerConfig) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'layer-switcher-btn'; + button.title = layerConfig.name || layerConfig.id; + + // Set icon + if (layerConfig.icon) { + // Check if it's a Bootstrap Icon (format: "bi:icon-name") + if (layerConfig.icon.startsWith('bi:')) { + const iconName = layerConfig.icon.substring(3); + button.innerHTML = ``; + } + // Check if it's a built-in icon + else if (ICONS[layerConfig.icon]) { + button.innerHTML = ICONS[layerConfig.icon]; + } + // Otherwise treat as custom SVG + else { + button.innerHTML = layerConfig.icon; + } + } else { + // Default to layers icon + button.innerHTML = ICONS.layers; + } + + // Click handler - simply toggle the layer on/off + button.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.toggleLayer(layerConfig.id); + }); + + return button; + } + + /** + * Create a fixed-position sublayer selector (bottom left) + */ + _createSublayerSelector(layerConfig) { + const selector = document.createElement('div'); + selector.className = 'layer-switcher-sublayer-selector'; + selector.dataset.parentId = layerConfig.id; + + layerConfig.sublayers.forEach(sublayer => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'layer-switcher-sublayer-btn'; + btn.dataset.sublayerId = sublayer.id; + btn.textContent = sublayer.name || sublayer.id; + + // Mark default as active + const activeSublayerId = this.activeSublayers.get(layerConfig.id); + if (sublayer.id === activeSublayerId) { + btn.classList.add('active'); + } + + // Click handler + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this._selectSublayer(layerConfig.id, sublayer.id); + }); + + selector.appendChild(btn); + }); + + return selector; + } + + /** + * Show sublayer selector, positioned to the right of the control button + */ + _showSublayerSelector(layerId) { + const selector = this.sublayerSelectors[layerId]; + if (!selector) return; + + const btn = this.buttons[layerId]; + if (btn && this.map) { + const mapContainer = this.map.getContainer(); + const mapRect = mapContainer.getBoundingClientRect(); + const btnRect = btn.getBoundingClientRect(); + + // If the button is on the right half of the map, open the panel to the left + const onRightSide = (btnRect.left - mapRect.left) > mapRect.width / 2; + if (onRightSide) { + selector.style.left = ''; + selector.style.right = (mapRect.right - btnRect.left + 4) + 'px'; + } else { + selector.style.right = ''; + selector.style.left = (btnRect.right - mapRect.left + 4) + 'px'; + } + selector.style.top = (btnRect.top - mapRect.top) + 'px'; + } + + selector.classList.add('visible'); + } + + /** + * Hide sublayer selector + */ + _hideSublayerSelector(layerId) { + const selector = this.sublayerSelectors[layerId]; + if (selector) { + selector.classList.remove('visible'); + } + } + + /** + * Select a sublayer + */ + _selectSublayer(parentLayerId, sublayerId) { + const layerConfig = this.layers.find(l => l.id === parentLayerId); + if (!layerConfig || !layerConfig.sublayers) return; + + const sublayer = layerConfig.sublayers.find(s => s.id === sublayerId); + if (!sublayer) return; + + const state = this.layerStates.get(parentLayerId); + if (!state) return; + + // Store current active sublayer + this.activeSublayers.set(parentLayerId, sublayerId); + + // Update selector button states + const selector = this.sublayerSelectors[parentLayerId]; + if (selector) { + selector.querySelectorAll('.layer-switcher-sublayer-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.sublayerId === sublayerId); + }); + } + + // If layer is visible, switch the source + if (state.visible) { + const mapLayerId = `layer-switcher-${parentLayerId}`; + + // Remove old layer if exists + if (this.map.getLayer(mapLayerId)) { + this.map.removeLayer(mapLayerId); + } + + // Add new source if needed + const sourceId = `layer-switcher-source-${parentLayerId}`; + if (this.map.getSource(sourceId)) { + this.map.removeSource(sourceId); + } + + this.map.addSource(sourceId, sublayer.source); + + // Determine where to insert the layer + let beforeLayer = layerConfig.beforeLayer; + if (!beforeLayer) { + const layers = this.map.getStyle().layers; + for (const layer of layers) { + if (layer.id.startsWith('gl-draw-') || + layer.id.startsWith('custom-draw-') || + layer.id.startsWith('polygon-') || + layer.id === 'green-surfaces') { + beforeLayer = layer.id; + break; + } + } + } + + // Add layer with sublayer configuration + const mapLayerConfig = { + id: mapLayerId, + source: sourceId, + ...(sublayer.layer || layerConfig.layer) + }; + + this.map.addLayer(mapLayerConfig, beforeLayer); + } + } + + /** + * Initialize layers (add sources) + */ + _initializeLayers() { + this.layers.forEach(layerConfig => { + // Add source if not exists + const sourceId = `layer-switcher-source-${layerConfig.id}`; + if (!this.map.getSource(sourceId) && layerConfig.source) { + this.map.addSource(sourceId, layerConfig.source); + + const state = this.layerStates.get(layerConfig.id); + if (state) state.sourceAdded = true; + } + + // If layer should be visible initially, add it + if (layerConfig.visible) { + this._addLayer(layerConfig); + this._updateButtonState(layerConfig.id, true); + } + }); + } + + /** + * Add a layer to the map + */ + _addLayer(layerConfig) { + const layerId = `layer-switcher-${layerConfig.id}`; + const sourceId = `layer-switcher-source-${layerConfig.id}`; + + if (this.map.getLayer(layerId)) return; + + // For layers with sublayers, use the active sublayer's source + let sourceConfig = layerConfig.source; + let layerStyle = layerConfig.layer; + + if (layerConfig.sublayers && layerConfig.sublayers.length > 0) { + const activeSublayerId = this.activeSublayers.get(layerConfig.id); + const activeSublayer = layerConfig.sublayers.find(s => s.id === activeSublayerId) + || layerConfig.sublayers[0]; + sourceConfig = activeSublayer.source; + layerStyle = activeSublayer.layer || layerConfig.layer; + } + + // Ensure source exists before adding layer + if (!this.map.getSource(sourceId) && sourceConfig) { + this.map.addSource(sourceId, sourceConfig); + const state = this.layerStates.get(layerConfig.id); + if (state) state.sourceAdded = true; + } + + // Determine where to insert the layer + let beforeLayer = layerConfig.beforeLayer; + + // If no beforeLayer specified, try to insert before draw layers + if (!beforeLayer) { + const layers = this.map.getStyle().layers; + for (const layer of layers) { + if (layer.id.startsWith('gl-draw-') || + layer.id.startsWith('custom-draw-') || + layer.id.startsWith('polygon-') || + layer.id === 'green-surfaces') { + beforeLayer = layer.id; + break; + } + } + } + + // Build layer config + const mapLayerConfig = { + id: layerId, + source: sourceId, + ...layerStyle + }; + + this.map.addLayer(mapLayerConfig, beforeLayer); + + const state = this.layerStates.get(layerConfig.id); + if (state) { + state.layerAdded = true; + state.visible = true; + } + } + + /** + * Toggle layer visibility + */ + toggleLayer(layerId) { + const layerConfig = this.layers.find(l => l.id === layerId); + if (!layerConfig) return; + + const state = this.layerStates.get(layerId); + if (!state) return; + + // Check if there's a custom toggle callback + if (layerConfig.onToggle && typeof layerConfig.onToggle === 'function') { + // Use custom callback instead of default behavior + layerConfig.onToggle(); + // Toggle the state manually + state.visible = !state.visible; + this._updateButtonState(layerId, state.visible); + return; + } + + // Check if this is an existing layer configuration + if (layerConfig.existingLayerIds && layerConfig.existingLayerIds.length > 0) { + // Toggle existing layers + const newVisibility = state.visible ? 'none' : 'visible'; + layerConfig.existingLayerIds.forEach(existingLayerId => { + if (this.map.getLayer(existingLayerId)) { + this.map.setLayoutProperty(existingLayerId, 'visibility', newVisibility); + } + }); + state.visible = !state.visible; + } else { + // Handle new layers created by this control + const mapLayerId = `layer-switcher-${layerId}`; + + if (state.visible) { + // Hide layer + if (this.map.getLayer(mapLayerId)) { + this.map.setLayoutProperty(mapLayerId, 'visibility', 'none'); + } + state.visible = false; + + // Hide sublayer selector if exists + if (layerConfig.sublayers && layerConfig.sublayers.length > 0) { + this._hideSublayerSelector(layerId); + } + } else { + // Handle exclusive layers + if (layerConfig.exclusive && layerConfig.group) { + this._hideGroupLayers(layerConfig.group, layerId); + } + + // Show layer + if (this.map.getLayer(mapLayerId)) { + this.map.setLayoutProperty(mapLayerId, 'visibility', 'visible'); + } else { + this._addLayer(layerConfig); + } + state.visible = true; + + // Show sublayer selector if exists + if (layerConfig.sublayers && layerConfig.sublayers.length > 0) { + this._showSublayerSelector(layerId); + } + } + } + + this._updateButtonState(layerId, state.visible); + } + + /** + * Hide all layers in a group except the specified one + */ + _hideGroupLayers(group, exceptLayerId) { + this.layers.forEach(layerConfig => { + if (layerConfig.group === group && layerConfig.id !== exceptLayerId) { + const mapLayerId = `layer-switcher-${layerConfig.id}`; + const state = this.layerStates.get(layerConfig.id); + + if (state && state.visible && this.map.getLayer(mapLayerId)) { + this.map.setLayoutProperty(mapLayerId, 'visibility', 'none'); + state.visible = false; + this._updateButtonState(layerConfig.id, false); + } + } + }); + } + + /** + * Update button visual state + */ + _updateButtonState(layerId, isActive) { + const button = this.buttons[layerId]; + if (button) { + button.classList.toggle('active', isActive); + + const layerConfig = this.layers.find(l => l.id === layerId); + if (layerConfig) { + button.title = isActive + ? `Masquer ${layerConfig.name || layerConfig.id}` + : `Afficher ${layerConfig.name || layerConfig.id}`; + } + } + } + + /** + * Programmatically show a layer + */ + showLayer(layerId) { + const state = this.layerStates.get(layerId); + if (state && !state.visible) { + this.toggleLayer(layerId); + } + } + + /** + * Programmatically hide a layer + */ + hideLayer(layerId) { + const state = this.layerStates.get(layerId); + if (state && state.visible) { + this.toggleLayer(layerId); + } + } + + /** + * Check if a layer is visible + */ + isLayerVisible(layerId) { + const state = this.layerStates.get(layerId); + return state ? state.visible : false; + } + } + + // Export to global scope + window.LayerSwitcherControl = LayerSwitcherControl; + +})(window); diff --git a/streetup/common/static/common/mapbox-gl-draw.css b/streetup/common/static/common/mapbox-gl-draw.css new file mode 100644 index 0000000..0347a27 --- /dev/null +++ b/streetup/common/static/common/mapbox-gl-draw.css @@ -0,0 +1,88 @@ + +/* Override default control style */ +.mapbox-gl-draw_ctrl-bottom-left, +.mapbox-gl-draw_ctrl-top-left { + margin-left:0; + border-radius:0 4px 4px 0; +} +.mapbox-gl-draw_ctrl-top-right, +.mapbox-gl-draw_ctrl-bottom-right { + margin-right:0; + border-radius:4px 0 0 4px; +} + +.mapbox-gl-draw_ctrl-draw-btn { + border-color:rgba(0,0,0,0.9); + color:rgba(255,255,255,0.5); + width:30px; + height:30px; +} + +.mapbox-gl-draw_ctrl-draw-btn.active, +.mapbox-gl-draw_ctrl-draw-btn.active:hover { + background-color:rgb(0 0 0/5%); +} +.mapbox-gl-draw_ctrl-draw-btn { + background-repeat: no-repeat; + background-position: center; +} + +.mapbox-gl-draw_point { + background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath d="m10 2c-3.3 0-6 2.7-6 6s6 9 6 9 6-5.7 6-9-2.7-6-6-6zm0 2c2.1 0 3.8 1.7 3.8 3.8 0 1.5-1.8 3.9-2.9 5.2h-1.7c-1.1-1.4-2.9-3.8-2.9-5.2-.1-2.1 1.6-3.8 3.7-3.8z"/>%3C/svg>'); +} +.mapbox-gl-draw_polygon { + background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath d="m15 12.3v-4.6c.6-.3 1-1 1-1.7 0-1.1-.9-2-2-2-.7 0-1.4.4-1.7 1h-4.6c-.3-.6-1-1-1.7-1-1.1 0-2 .9-2 2 0 .7.4 1.4 1 1.7v4.6c-.6.3-1 1-1 1.7 0 1.1.9 2 2 2 .7 0 1.4-.4 1.7-1h4.6c.3.6 1 1 1.7 1 1.1 0 2-.9 2-2 0-.7-.4-1.4-1-1.7zm-8-.3v-4l1-1h4l1 1v4l-1 1h-4z"/>%3C/svg>'); +} +.mapbox-gl-draw_line { + background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath d="m13.5 3.5c-1.4 0-2.5 1.1-2.5 2.5 0 .3 0 .6.2.9l-3.8 3.8c-.3-.1-.6-.2-.9-.2-1.4 0-2.5 1.1-2.5 2.5s1.1 2.5 2.5 2.5 2.5-1.1 2.5-2.5c0-.3 0-.6-.2-.9l3.8-3.8c.3.1.6.2.9.2 1.4 0 2.5-1.1 2.5-2.5s-1.1-2.5-2.5-2.5z"/>%3C/svg>'); +} +.mapbox-gl-draw_trash { + background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath d="M10,3.4 c-0.8,0-1.5,0.5-1.8,1.2H5l-1,1v1h12v-1l-1-1h-3.2C11.5,3.9,10.8,3.4,10,3.4z M5,8v7c0,1,1,2,2,2h6c1,0,2-1,2-2V8h-2v5.5h-1.5V8h-3 v5.5H7V8H5z"/>%3C/svg>'); +} +.mapbox-gl-draw_uncombine { + background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath d="m12 2c-.3 0-.5.1-.7.3l-1 1c-.4.4-.4 1 0 1.4l1 1c.4.4 1 .4 1.4 0l1-1c.4-.4.4-1 0-1.4l-1-1c-.2-.2-.4-.3-.7-.3zm4 4c-.3 0-.5.1-.7.3l-1 1c-.4.4-.4 1 0 1.4l1 1c.4.4 1 .4 1.4 0l1-1c.4-.4.4-1 0-1.4l-1-1c-.2-.2-.4-.3-.7-.3zm-7 1c-1 0-1 1-.5 1.5.3.3 1 1 1 1l-1 1s-.5.5 0 1 1 0 1 0l1-1 1 1c.5.5 1.5.5 1.5-.5v-4zm-5 3c-.3 0-.5.1-.7.3l-1 1c-.4.4-.4 1 0 1.4l4.9 4.9c.4.4 1 .4 1.4 0l1-1c.4-.4.4-1 0-1.4l-4.9-4.9c-.1-.2-.4-.3-.7-.3z"/>%3C/svg>'); +} +.mapbox-gl-draw_combine { + background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath d="M12.1,2c-0.3,0-0.5,0.1-0.7,0.3l-1,1c-0.4,0.4-0.4,1,0,1.4l4.9,4.9c0.4,0.4,1,0.4,1.4,0l1-1 c0.4-0.4,0.4-1,0-1.4l-4.9-4.9C12.6,2.1,12.3,2,12.1,2z M8,8C7,8,7,9,7.5,9.5c0.3,0.3,1,1,1,1l-1,1c0,0-0.5,0.5,0,1s1,0,1,0l1-1l1,1 C11,13,12,13,12,12V8H8z M4,10c-0.3,0-0.5,0.1-0.7,0.3l-1,1c-0.4,0.4-0.4,1,0,1.4l1,1c0.4,0.4,1,0.4,1.4,0l1-1c0.4-0.4,0.4-1,0-1.4 l-1-1C4.5,10.1,4.3,10,4,10z M8,14c-0.3,0-0.5,0.1-0.7,0.3l-1,1c-0.4,0.4-0.4,1,0,1.4l1,1c0.4,0.4,1,0.4,1.4,0l1-1 c0.4-0.4,0.4-1,0-1.4l-1-1C8.5,14.1,8.3,14,8,14z"/>%3C/svg>'); +} + +.mapboxgl-map.mouse-pointer .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: pointer; +} +.mapboxgl-map.mouse-move .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: move; +} +.mapboxgl-map.mouse-add .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: crosshair; +} +.mapboxgl-map.mouse-move.mode-direct_select .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} +.mapboxgl-map.mode-direct_select.feature-vertex.mouse-move .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: move; +} +.mapboxgl-map.mode-direct_select.feature-midpoint.mouse-pointer .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: cell; +} +.mapboxgl-map.mode-direct_select.feature-feature.mouse-move .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: move; +} +.mapboxgl-map.mode-static.mouse-pointer .mapboxgl-canvas-container.mapboxgl-interactive { + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} + +.mapbox-gl-draw_boxselect { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + background: rgba(0,0,0,.1); + border: 2px dotted #fff; + opacity: 0.5; +} diff --git a/streetup/common/static/common/mapbox-gl-draw.js b/streetup/common/static/common/mapbox-gl-draw.js new file mode 100644 index 0000000..ad13030 --- /dev/null +++ b/streetup/common/static/common/mapbox-gl-draw.js @@ -0,0 +1,2 @@ +var e,t;e=this,t=function(){const e=function(e,t){const o={drag:[],click:[],mousemove:[],mousedown:[],mouseup:[],mouseout:[],keydown:[],keyup:[],touchstart:[],touchmove:[],touchend:[],tap:[]},n={on(e,t,n){if(void 0===o[e])throw new Error(`Invalid event type: ${e}`);o[e].push({selector:t,fn:n})},render(e){t.store.featureChanged(e)}},r=function(e,r){const i=o[e];let s=i.length;for(;s--;){const e=i[s];if(e.selector(r)){e.fn.call(n,r)||t.store.render(),t.ui.updateMapClasses();break}}};return e.start.call(n),{render:e.render,stop(){e.stop&&e.stop()},trash(){e.trash&&(e.trash(),t.store.render())},combineFeatures(){e.combineFeatures&&e.combineFeatures()},uncombineFeatures(){e.uncombineFeatures&&e.uncombineFeatures()},drag(e){r("drag",e)},click(e){r("click",e)},mousemove(e){r("mousemove",e)},mousedown(e){r("mousedown",e)},mouseup(e){r("mouseup",e)},mouseout(e){r("mouseout",e)},keydown(e){r("keydown",e)},keyup(e){r("keyup",e)},touchstart(e){r("touchstart",e)},touchmove(e){r("touchmove",e)},touchend(e){r("touchend",e)},tap(e){r("tap",e)}}};function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var o,n,r={},i={};function s(){return o||(o=1,i.RADIUS=6378137,i.FLATTENING=1/298.257223563,i.POLAR_RADIUS=6356752.3142),i}var a=function(){if(n)return r;n=1;var e=s();function t(e){var t=0;if(e&&e.length>0){t+=Math.abs(o(e[0]));for(var n=1;n2){for(c=0;c(e.geometry.type===h.POLYGON&&(e.area=c.geometry({type:h.FEATURE,property:{},geometry:e.geometry})),e))).sort(S).map((e=>(delete e.area,e)))}function L(e,t=0){return[[e.point.x-t,e.point.y-t],[e.point.x+t,e.point.y+t]]}function M(e){if(this._items={},this._nums={},this._length=e?e.length:0,e)for(let t=0,o=e.length;t{e.push({k:t,v:this._items[t]})})),Object.keys(this._nums).forEach((t=>{e.push({k:JSON.parse(t),v:this._nums[t]})})),e.sort(((e,t)=>e.v-t.v)).map((e=>e.k))},M.prototype.clear=function(){return this._length=0,this._items={},this._nums={},this};const N=[y.FEATURE,y.MIDPOINT,y.VERTEX];var b={click:function(e,t,o){return x(e,t,o,o.options.clickBuffer)},touch:function(e,t,o){return x(e,t,o,o.options.touchBuffer)}};function x(e,t,o,n){if(null===o.map)return[];const r=e?L(e,n):t,i={};o.options.styles&&(i.layers=o.options.styles.map((e=>e.id)).filter((e=>null!=o.map.getLayer(e))));const s=o.map.queryRenderedFeatures(r,i).filter((e=>-1!==N.indexOf(e.properties.meta))),a=new M,c=[];return s.forEach((e=>{const t=e.properties.id;a.has(t)||(a.add(t),c.push(e))})),O(c)}function A(e,t){const o=b.click(e,null,t),n={mouse:d.NONE};return o[0]&&(n.mouse=o[0].properties.active===E.ACTIVE?d.MOVE:d.POINTER,n.feature=o[0].properties.meta),-1!==t.events.currentModeName().indexOf("draw")&&(n.mouse=d.ADD),t.ui.queueMapClasses(n),t.ui.updateMapClasses(),o[0]}function P(e,t){const o=e.x-t.x,n=e.y-t.y;return Math.sqrt(o*o+n*n)}const F=4,R=12,w=500;function D(e,t,o={}){const n=null!=o.fineTolerance?o.fineTolerance:F,r=null!=o.grossTolerance?o.grossTolerance:R,i=null!=o.interval?o.interval:w;e.point=e.point||t.point,e.time=e.time||t.time;const s=P(e.point,t.point);return s(o=t)=>{let n="",r=0|o;for(;r--;)n+=e[Math.random()*e.length|0];return n})("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",32);function B(){return G()}const j=function(e,t){this.ctx=e,this.properties=t.properties||{},this.coordinates=t.geometry.coordinates,this.id=t.id||B(),this.type=t.geometry.type};j.prototype.changed=function(){this.ctx.store.featureChanged(this.id)},j.prototype.incomingCoords=function(e){this.setCoordinates(e)},j.prototype.setCoordinates=function(e){this.coordinates=e,this.changed()},j.prototype.getCoordinates=function(){return JSON.parse(JSON.stringify(this.coordinates))},j.prototype.setProperty=function(e,t){this.properties[e]=t},j.prototype.toGeoJSON=function(){return JSON.parse(JSON.stringify({id:this.id,type:h.FEATURE,properties:this.properties,geometry:{coordinates:this.getCoordinates(),type:this.type}}))},j.prototype.internal=function(e){const t={id:this.id,meta:y.FEATURE,"meta:type":this.type,active:E.INACTIVE,mode:e};if(this.ctx.options.userProperties)for(const e in this.properties)t[`user_${e}`]=this.properties[e];return{type:h.FEATURE,properties:t,geometry:{coordinates:this.getCoordinates(),type:this.type}}};const J=function(e,t){j.call(this,e,t)};(J.prototype=Object.create(j.prototype)).isValid=function(){return"number"==typeof this.coordinates[0]&&"number"==typeof this.coordinates[1]},J.prototype.updateCoordinate=function(e,t,o){this.coordinates=3===arguments.length?[t,o]:[e,t],this.changed()},J.prototype.getCoordinate=function(){return this.getCoordinates()};const $=function(e,t){j.call(this,e,t)};($.prototype=Object.create(j.prototype)).isValid=function(){return this.coordinates.length>1},$.prototype.addCoordinate=function(e,t,o){this.changed();const n=parseInt(e,10);this.coordinates.splice(n,0,[t,o])},$.prototype.getCoordinate=function(e){const t=parseInt(e,10);return JSON.parse(JSON.stringify(this.coordinates[t]))},$.prototype.removeCoordinate=function(e){this.changed(),this.coordinates.splice(parseInt(e,10),1)},$.prototype.updateCoordinate=function(e,t,o){const n=parseInt(e,10);this.coordinates[n]=[t,o],this.changed()};const Y=function(e,t){j.call(this,e,t),this.coordinates=this.coordinates.map((e=>e.slice(0,-1)))};(Y.prototype=Object.create(j.prototype)).isValid=function(){return 0!==this.coordinates.length&&this.coordinates.every((e=>e.length>2))},Y.prototype.incomingCoords=function(e){this.coordinates=e.map((e=>e.slice(0,-1))),this.changed()},Y.prototype.setCoordinates=function(e){this.coordinates=e,this.changed()},Y.prototype.addCoordinate=function(e,t,o){this.changed();const n=e.split(".").map((e=>parseInt(e,10)));this.coordinates[n[0]].splice(n[1],0,[t,o])},Y.prototype.removeCoordinate=function(e){this.changed();const t=e.split(".").map((e=>parseInt(e,10))),o=this.coordinates[t[0]];o&&(o.splice(t[1],1),o.length<3&&this.coordinates.splice(t[0],1))},Y.prototype.getCoordinate=function(e){const t=e.split(".").map((e=>parseInt(e,10))),o=this.coordinates[t[0]];return JSON.parse(JSON.stringify(o[t[1]]))},Y.prototype.getCoordinates=function(){return this.coordinates.map((e=>e.concat([e[0]])))},Y.prototype.updateCoordinate=function(e,t,o){this.changed();const n=e.split("."),r=parseInt(n[0],10),i=parseInt(n[1],10);void 0===this.coordinates[r]&&(this.coordinates[r]=[]),this.coordinates[r][i]=[t,o]};const H={MultiPoint:J,MultiLineString:$,MultiPolygon:Y},X=(e,t,o,n,r)=>{const i=o.split("."),s=parseInt(i[0],10),a=i[1]?i.slice(1).join("."):null;return e[s][t](a,n,r)},q=function(e,t){if(j.call(this,e,t),delete this.coordinates,this.model=H[t.geometry.type],void 0===this.model)throw new TypeError(`${t.geometry.type} is not a valid type`);this.features=this._coordinatesToFeatures(t.geometry.coordinates)};function Z(e){this.map=e.map,this.drawConfig=JSON.parse(JSON.stringify(e.options||{})),this._ctx=e}(q.prototype=Object.create(j.prototype))._coordinatesToFeatures=function(e){const t=this.model.bind(this);return e.map((e=>new t(this.ctx,{id:B(),type:h.FEATURE,properties:{},geometry:{coordinates:e,type:this.type.replace("Multi","")}})))},q.prototype.isValid=function(){return this.features.every((e=>e.isValid()))},q.prototype.setCoordinates=function(e){this.features=this._coordinatesToFeatures(e),this.changed()},q.prototype.getCoordinate=function(e){return X(this.features,"getCoordinate",e)},q.prototype.getCoordinates=function(){return JSON.parse(JSON.stringify(this.features.map((e=>e.type===h.POLYGON?e.getCoordinates():e.coordinates))))},q.prototype.updateCoordinate=function(e,t,o){X(this.features,"updateCoordinate",e,t,o),this.changed()},q.prototype.addCoordinate=function(e,t,o){X(this.features,"addCoordinate",e,t,o),this.changed()},q.prototype.removeCoordinate=function(e){X(this.features,"removeCoordinate",e),this.changed()},q.prototype.getFeatures=function(){return this.features},Z.prototype.setSelected=function(e){return this._ctx.store.setSelected(e)},Z.prototype.setSelectedCoordinates=function(e){this._ctx.store.setSelectedCoordinates(e),e.reduce(((e,t)=>(void 0===e[t.feature_id]&&(e[t.feature_id]=!0,this._ctx.store.get(t.feature_id).changed()),e)),{})},Z.prototype.getSelected=function(){return this._ctx.store.getSelected()},Z.prototype.getSelectedIds=function(){return this._ctx.store.getSelectedIds()},Z.prototype.isSelected=function(e){return this._ctx.store.isSelected(e)},Z.prototype.getFeature=function(e){return this._ctx.store.get(e)},Z.prototype.select=function(e){return this._ctx.store.select(e)},Z.prototype.deselect=function(e){return this._ctx.store.deselect(e)},Z.prototype.deleteFeature=function(e,t={}){return this._ctx.store.delete(e,t)},Z.prototype.addFeature=function(e,t={}){return this._ctx.store.add(e,t)},Z.prototype.clearSelectedFeatures=function(){return this._ctx.store.clearSelected()},Z.prototype.clearSelectedCoordinates=function(){return this._ctx.store.clearSelectedCoordinates()},Z.prototype.setActionableState=function(e={}){const t={trash:e.trash||!1,combineFeatures:e.combineFeatures||!1,uncombineFeatures:e.uncombineFeatures||!1};return this._ctx.events.actionable(t)},Z.prototype.changeMode=function(e,t={},o={}){return this._ctx.events.changeMode(e,t,o)},Z.prototype.fire=function(e,t){return this._ctx.events.fire(e,t)},Z.prototype.updateUIClasses=function(e){return this._ctx.ui.queueMapClasses(e)},Z.prototype.activateUIButton=function(e){return this._ctx.ui.setActiveButton(e)},Z.prototype.featuresAt=function(e,t,o="click"){if("click"!==o&&"touch"!==o)throw new Error("invalid buffer type");return b[o](e,t,this._ctx)},Z.prototype.newFeature=function(e){const t=e.geometry.type;return t===h.POINT?new J(this._ctx,e):t===h.LINE_STRING?new $(this._ctx,e):t===h.POLYGON?new Y(this._ctx,e):new q(this._ctx,e)},Z.prototype.isInstanceOf=function(e,t){if(e===h.POINT)return t instanceof J;if(e===h.LINE_STRING)return t instanceof $;if(e===h.POLYGON)return t instanceof Y;if("MultiFeature"===e)return t instanceof q;throw new Error(`Unknown feature class: ${e}`)},Z.prototype.doRender=function(e){return this._ctx.store.featureChanged(e)},Z.prototype.onSetup=function(){},Z.prototype.onDrag=function(){},Z.prototype.onClick=function(){},Z.prototype.onMouseMove=function(){},Z.prototype.onMouseDown=function(){},Z.prototype.onMouseUp=function(){},Z.prototype.onMouseOut=function(){},Z.prototype.onKeyUp=function(){},Z.prototype.onKeyDown=function(){},Z.prototype.onTouchStart=function(){},Z.prototype.onTouchMove=function(){},Z.prototype.onTouchEnd=function(){},Z.prototype.onTap=function(){},Z.prototype.onStop=function(){},Z.prototype.onTrash=function(){},Z.prototype.onCombineFeature=function(){},Z.prototype.onUncombineFeature=function(){},Z.prototype.toDisplayFeatures=function(){throw new Error("You must overwrite toDisplayFeatures")};const W={drag:"onDrag",click:"onClick",mousemove:"onMouseMove",mousedown:"onMouseDown",mouseup:"onMouseUp",mouseout:"onMouseOut",keyup:"onKeyUp",keydown:"onKeyDown",touchstart:"onTouchStart",touchmove:"onTouchMove",touchend:"onTouchEnd",tap:"onTap"},K=Object.keys(W);function z(e){const t=Object.keys(e);return function(o,n={}){let r={};const i=t.reduce(((t,o)=>(t[o]=e[o],t)),new Z(o));return{start(){r=i.onSetup(n),K.forEach((t=>{const o=W[t];let n=()=>!1;var s;e[o]&&(n=()=>!0),this.on(t,n,(s=o,e=>i[s](r,e)))}))},stop(){i.onStop(r)},trash(){i.onTrash(r)},combineFeatures(){i.onCombineFeatures(r)},uncombineFeatures(){i.onUncombineFeatures(r)},render(e,t){i.toDisplayFeatures(r,e,t)}}}}function Q(e){return[].concat(e).filter((e=>void 0!==e))}function ee(){const e=this;if(!e.ctx.map||void 0===e.ctx.map.getSource(l.HOT))return a();const t=e.ctx.events.currentModeName();e.ctx.ui.queueMapClasses({mode:t});let o=[],n=[];e.isDirty?n=e.getAllIds():(o=e.getChangedIds().filter((t=>void 0!==e.get(t))),n=e.sources.hot.filter((t=>t.properties.id&&-1===o.indexOf(t.properties.id)&&void 0!==e.get(t.properties.id))).map((e=>e.properties.id))),e.sources.hot=[];const r=e.sources.cold.length;e.sources.cold=e.isDirty?[]:e.sources.cold.filter((e=>{const t=e.properties.id||e.properties.parent;return-1===o.indexOf(t)}));const i=r!==e.sources.cold.length||n.length>0;function s(o,n){const r=e.get(o).internal(t);e.ctx.events.currentModeRender(r,(o=>{o.properties.mode=t,e.sources[n].push(o)}))}function a(){e.isDirty=!1,e.clearChangedIds()}o.forEach((e=>s(e,"hot"))),n.forEach((e=>s(e,"cold"))),i&&e.ctx.map.getSource(l.COLD).setData({type:h.FEATURE_COLLECTION,features:e.sources.cold}),e.ctx.map.getSource(l.HOT).setData({type:h.FEATURE_COLLECTION,features:e.sources.hot}),a()}function te(e){let t;this._features={},this._featureIds=new M,this._selectedFeatureIds=new M,this._selectedCoordinates=[],this._changedFeatureIds=new M,this._emitSelectionChange=!1,this._mapInitialConfig={},this.ctx=e,this.sources={hot:[],cold:[]},this.render=()=>{t||(t=requestAnimationFrame((()=>{t=null,ee.call(this),this._emitSelectionChange&&(this.ctx.events.fire(g.SELECTION_CHANGE,{features:this.getSelected().map((e=>e.toGeoJSON())),points:this.getSelectedCoordinates().map((e=>({type:h.FEATURE,properties:{},geometry:{type:h.POINT,coordinates:e.coordinates}})))}),this._emitSelectionChange=!1),this.ctx.events.fire(g.RENDER,{})})))},this.isDirty=!1}function oe(e,t={}){const o=e._selectedCoordinates.filter((t=>e._selectedFeatureIds.has(t.feature_id)));e._selectedCoordinates.length===o.length||t.silent||(e._emitSelectionChange=!0),e._selectedCoordinates=o}te.prototype.createRenderBatch=function(){const e=this.render;let t=0;return this.render=function(){t++},()=>{this.render=e,t>0&&this.render()}},te.prototype.setDirty=function(){return this.isDirty=!0,this},te.prototype.featureCreated=function(e,t={}){if(this._changedFeatureIds.add(e),!0!==(null!=t.silent?t.silent:this.ctx.options.suppressAPIEvents)){const t=this.get(e);this.ctx.events.fire(g.CREATE,{features:[t.toGeoJSON()]})}return this},te.prototype.featureChanged=function(e,t={}){return this._changedFeatureIds.add(e),!0!==(null!=t.silent?t.silent:this.ctx.options.suppressAPIEvents)&&this.ctx.events.fire(g.UPDATE,{action:t.action?t.action:m.CHANGE_COORDINATES,features:[this.get(e).toGeoJSON()]}),this},te.prototype.getChangedIds=function(){return this._changedFeatureIds.values()},te.prototype.clearChangedIds=function(){return this._changedFeatureIds.clear(),this},te.prototype.getAllIds=function(){return this._featureIds.values()},te.prototype.add=function(e,t={}){return this._features[e.id]=e,this._featureIds.add(e.id),this.featureCreated(e.id,{silent:t.silent}),this},te.prototype.delete=function(e,t={}){const o=[];return Q(e).forEach((e=>{this._featureIds.has(e)&&(this._featureIds.delete(e),this._selectedFeatureIds.delete(e),t.silent||-1===o.indexOf(this._features[e])&&o.push(this._features[e].toGeoJSON()),delete this._features[e],this.isDirty=!0)})),o.length&&this.ctx.events.fire(g.DELETE,{features:o}),oe(this,t),this},te.prototype.get=function(e){return this._features[e]},te.prototype.getAll=function(){return Object.keys(this._features).map((e=>this._features[e]))},te.prototype.select=function(e,t={}){return Q(e).forEach((e=>{this._selectedFeatureIds.has(e)||(this._selectedFeatureIds.add(e),this._changedFeatureIds.add(e),t.silent||(this._emitSelectionChange=!0))})),this},te.prototype.deselect=function(e,t={}){return Q(e).forEach((e=>{this._selectedFeatureIds.has(e)&&(this._selectedFeatureIds.delete(e),this._changedFeatureIds.add(e),t.silent||(this._emitSelectionChange=!0))})),oe(this,t),this},te.prototype.clearSelected=function(e={}){return this.deselect(this._selectedFeatureIds.values(),{silent:e.silent}),this},te.prototype.setSelected=function(e,t={}){return e=Q(e),this.deselect(this._selectedFeatureIds.values().filter((t=>-1===e.indexOf(t))),{silent:t.silent}),this.select(e.filter((e=>!this._selectedFeatureIds.has(e))),{silent:t.silent}),this},te.prototype.setSelectedCoordinates=function(e){return this._selectedCoordinates=e,this._emitSelectionChange=!0,this},te.prototype.clearSelectedCoordinates=function(){return this._selectedCoordinates=[],this._emitSelectionChange=!0,this},te.prototype.getSelectedIds=function(){return this._selectedFeatureIds.values()},te.prototype.getSelected=function(){return this.getSelectedIds().map((e=>this.get(e)))},te.prototype.getSelectedCoordinates=function(){return this._selectedCoordinates.map((e=>({coordinates:this.get(e.feature_id).getCoordinate(e.coord_path)})))},te.prototype.isSelected=function(e){return this._selectedFeatureIds.has(e)},te.prototype.setFeatureProperty=function(e,t,o,n={}){this.get(e).setProperty(t,o),this.featureChanged(e,{silent:n.silent,action:m.CHANGE_PROPERTIES})},te.prototype.storeMapConfig=function(){T.forEach((e=>{this.ctx.map[e]&&(this._mapInitialConfig[e]=this.ctx.map[e].isEnabled())}))},te.prototype.restoreMapConfig=function(){Object.keys(this._mapInitialConfig).forEach((e=>{this._mapInitialConfig[e]?this.ctx.map[e].enable():this.ctx.map[e].disable()}))},te.prototype.getInitialConfigValue=function(e){return void 0===this._mapInitialConfig[e]||this._mapInitialConfig[e]};const ne=["mode","feature","mouse"];function re(t){let o=null,n=null;const r={onRemove(){return t.map.off("load",r.connect),clearInterval(n),r.removeLayers(),t.store.restoreMapConfig(),t.ui.removeButtons(),t.events.removeEventListeners(),t.ui.clearMapClasses(),t.boxZoomInitial&&t.map.boxZoom.enable(),t.map=null,t.container=null,t.store=null,o&&o.parentNode&&o.parentNode.removeChild(o),o=null,this},connect(){t.map.off("load",r.connect),clearInterval(n),r.addLayers(),t.store.storeMapConfig(),t.events.addEventListeners()},onAdd(i){if(t.map=i,t.events=function(t){const o=Object.keys(t.options.modes).reduce(((e,o)=>(e[o]=z(t.options.modes[o]),e)),{});let n={},r={};const i={};let s=null,a=null;i.drag=function(e,o){o({point:e.point,time:(new Date).getTime()})?(t.ui.queueMapClasses({mouse:d.DRAG}),a.drag(e)):e.originalEvent.stopPropagation()},i.mousedrag=function(e){i.drag(e,(e=>!D(n,e)))},i.touchdrag=function(e){i.drag(e,(e=>!V(r,e)))},i.mousemove=function(e){if(1===(void 0!==e.originalEvent.buttons?e.originalEvent.buttons:e.originalEvent.which))return i.mousedrag(e);const o=A(e,t);e.featureTarget=o,a.mousemove(e)},i.mousedown=function(e){n={time:(new Date).getTime(),point:e.point};const o=A(e,t);e.featureTarget=o,a.mousedown(e)},i.mouseup=function(e){const o=A(e,t);e.featureTarget=o,D(n,{point:e.point,time:(new Date).getTime()})?a.click(e):a.mouseup(e)},i.mouseout=function(e){a.mouseout(e)},i.touchstart=function(e){if(!t.options.touchEnabled)return;r={time:(new Date).getTime(),point:e.point};const o=b.touch(e,null,t)[0];e.featureTarget=o,a.touchstart(e)},i.touchmove=function(e){if(t.options.touchEnabled)return a.touchmove(e),i.touchdrag(e)},i.touchend=function(e){if(e.originalEvent.preventDefault(),!t.options.touchEnabled)return;const o=b.touch(e,null,t)[0];e.featureTarget=o,V(r,{time:(new Date).getTime(),point:e.point})?a.tap(e):a.touchend(e)};const c=e=>!(8===e||46===e||e>=48&&e<=57);function l(n,r,i={}){a.stop();const c=o[n];if(void 0===c)throw new Error(`${n} is not valid`);s=n;const u=c(t,r);a=e(u,t),i.silent||t.map.fire(g.MODE_CHANGE,{mode:n}),t.store.setDirty(),t.store.render()}i.keydown=function(e){(e.srcElement||e.target).classList.contains(u.CANVAS)&&(8!==e.keyCode&&46!==e.keyCode||!t.options.controls.trash?c(e.keyCode)?a.keydown(e):49===e.keyCode&&t.options.controls.point?l(f.DRAW_POINT):50===e.keyCode&&t.options.controls.line_string?l(f.DRAW_LINE_STRING):51===e.keyCode&&t.options.controls.polygon&&l(f.DRAW_POLYGON):(e.preventDefault(),a.trash()))},i.keyup=function(e){c(e.keyCode)&&a.keyup(e)},i.zoomend=function(){t.store.changeZoom()},i.data=function(e){if("style"===e.dataType){const{setup:e,map:o,options:n,store:r}=t;n.styles.some((e=>o.getLayer(e.id)))||(e.addLayers(),r.setDirty(),r.render())}};const p={trash:!1,combineFeatures:!1,uncombineFeatures:!1};return{start(){s=t.options.defaultMode,a=e(o[s](t),t)},changeMode:l,actionable:function(e){let o=!1;Object.keys(e).forEach((t=>{if(void 0===p[t])throw new Error("Invalid action type");p[t]!==e[t]&&(o=!0),p[t]=e[t]})),o&&t.map.fire(g.ACTIONABLE,{actions:p})},currentModeName:()=>s,currentModeRender:(e,t)=>a.render(e,t),fire(e,o){t.map&&t.map.fire(e,o)},addEventListeners(){t.map.on("mousemove",i.mousemove),t.map.on("mousedown",i.mousedown),t.map.on("mouseup",i.mouseup),t.map.on("data",i.data),t.map.on("touchmove",i.touchmove),t.map.on("touchstart",i.touchstart),t.map.on("touchend",i.touchend),t.container.addEventListener("mouseout",i.mouseout),t.options.keybindings&&(t.container.addEventListener("keydown",i.keydown),t.container.addEventListener("keyup",i.keyup))},removeEventListeners(){t.map.off("mousemove",i.mousemove),t.map.off("mousedown",i.mousedown),t.map.off("mouseup",i.mouseup),t.map.off("data",i.data),t.map.off("touchmove",i.touchmove),t.map.off("touchstart",i.touchstart),t.map.off("touchend",i.touchend),t.container.removeEventListener("mouseout",i.mouseout),t.options.keybindings&&(t.container.removeEventListener("keydown",i.keydown),t.container.removeEventListener("keyup",i.keyup))},trash(e){a.trash(e)},combineFeatures(){a.combineFeatures()},uncombineFeatures(){a.uncombineFeatures()},getMode:()=>s}}(t),t.ui=function(e){const t={};let o=null,n={mode:null,feature:null,mouse:null},r={mode:null,feature:null,mouse:null};function i(e){r=Object.assign(r,e)}function s(){if(!e.container)return;const t=[],o=[];ne.forEach((e=>{r[e]!==n[e]&&(t.push(`${e}-${n[e]}`),null!==r[e]&&o.push(`${e}-${r[e]}`))})),t.length>0&&e.container.classList.remove(...t),o.length>0&&e.container.classList.add(...o),n=Object.assign(n,r)}function a(e,t={}){const n=document.createElement("button");return n.className=`${u.CONTROL_BUTTON} ${t.className}`,n.setAttribute("title",t.title),t.container.appendChild(n),n.addEventListener("click",(n=>{if(n.preventDefault(),n.stopPropagation(),n.target===o)return c(),void t.onDeactivate();l(e),t.onActivate()}),!0),n}function c(){o&&(o.classList.remove(u.ACTIVE_BUTTON),o=null)}function l(e){c();const n=t[e];n&&n&&"trash"!==e&&(n.classList.add(u.ACTIVE_BUTTON),o=n)}return{setActiveButton:l,queueMapClasses:i,updateMapClasses:s,clearMapClasses:function(){i({mode:null,feature:null,mouse:null}),s()},addButtons:function(){const o=e.options.controls,n=document.createElement("div");return n.className=`${u.CONTROL_GROUP} ${u.CONTROL_BASE}`,o?(o[p.LINE]&&(t[p.LINE]=a(p.LINE,{container:n,className:u.CONTROL_BUTTON_LINE,title:"LineString tool "+(e.options.keybindings?"(l)":""),onActivate:()=>e.events.changeMode(f.DRAW_LINE_STRING),onDeactivate:()=>e.events.trash()})),o[p.POLYGON]&&(t[p.POLYGON]=a(p.POLYGON,{container:n,className:u.CONTROL_BUTTON_POLYGON,title:"Polygon tool "+(e.options.keybindings?"(p)":""),onActivate:()=>e.events.changeMode(f.DRAW_POLYGON),onDeactivate:()=>e.events.trash()})),o[p.POINT]&&(t[p.POINT]=a(p.POINT,{container:n,className:u.CONTROL_BUTTON_POINT,title:"Marker tool "+(e.options.keybindings?"(m)":""),onActivate:()=>e.events.changeMode(f.DRAW_POINT),onDeactivate:()=>e.events.trash()})),o.trash&&(t.trash=a("trash",{container:n,className:u.CONTROL_BUTTON_TRASH,title:"Delete",onActivate:()=>{e.events.trash()}})),o.combine_features&&(t.combine_features=a("combineFeatures",{container:n,className:u.CONTROL_BUTTON_COMBINE_FEATURES,title:"Combine",onActivate:()=>{e.events.combineFeatures()}})),o.uncombine_features&&(t.uncombine_features=a("uncombineFeatures",{container:n,className:u.CONTROL_BUTTON_UNCOMBINE_FEATURES,title:"Uncombine",onActivate:()=>{e.events.uncombineFeatures()}})),n):n},removeButtons:function(){Object.keys(t).forEach((e=>{const o=t[e];o.parentNode&&o.parentNode.removeChild(o),delete t[e]}))}}}(t),t.container=i.getContainer(),t.store=new te(t),o=t.ui.addButtons(),t.options.boxSelect){t.boxZoomInitial=i.boxZoom.isEnabled(),i.boxZoom.disable();const e=i.dragPan.isEnabled();i.dragPan.disable(),i.dragPan.enable(),e||i.dragPan.disable()}return i.loaded()?r.connect():(i.on("load",r.connect),n=setInterval((()=>{i.loaded()&&r.connect()}),16)),t.events.start(),o},addLayers(){t.map.addSource(l.COLD,{data:{type:h.FEATURE_COLLECTION,features:[]},type:"geojson"}),t.map.addSource(l.HOT,{data:{type:h.FEATURE_COLLECTION,features:[]},type:"geojson"}),t.options.styles.forEach((e=>{t.map.addLayer(e)})),t.store.setDirty(!0),t.store.render()},removeLayers(){t.options.styles.forEach((e=>{t.map.getLayer(e.id)&&t.map.removeLayer(e.id)})),t.map.getSource(l.COLD)&&t.map.removeSource(l.COLD),t.map.getSource(l.HOT)&&t.map.removeSource(l.HOT)}};return t.setup=r,r}const ie="#3bb2d0",se="#fbb03b",ae="#fff";var ce=[{id:"gl-draw-polygon-fill",type:"fill",filter:["all",["==","$type","Polygon"]],paint:{"fill-color":["case",["==",["get","active"],"true"],se,ie],"fill-opacity":.1}},{id:"gl-draw-lines",type:"line",filter:["any",["==","$type","LineString"],["==","$type","Polygon"]],layout:{"line-cap":"round","line-join":"round"},paint:{"line-color":["case",["==",["get","active"],"true"],se,ie],"line-dasharray":["case",["==",["get","active"],"true"],[.2,2],[2,0]],"line-width":2}},{id:"gl-draw-point-outer",type:"circle",filter:["all",["==","$type","Point"],["==","meta","feature"]],paint:{"circle-radius":["case",["==",["get","active"],"true"],7,5],"circle-color":ae}},{id:"gl-draw-point-inner",type:"circle",filter:["all",["==","$type","Point"],["==","meta","feature"]],paint:{"circle-radius":["case",["==",["get","active"],"true"],5,3],"circle-color":["case",["==",["get","active"],"true"],se,ie]}},{id:"gl-draw-vertex-outer",type:"circle",filter:["all",["==","$type","Point"],["==","meta","vertex"],["!=","mode","simple_select"]],paint:{"circle-radius":["case",["==",["get","active"],"true"],7,5],"circle-color":ae}},{id:"gl-draw-vertex-inner",type:"circle",filter:["all",["==","$type","Point"],["==","meta","vertex"],["!=","mode","simple_select"]],paint:{"circle-radius":["case",["==",["get","active"],"true"],5,3],"circle-color":se}},{id:"gl-draw-midpoint",type:"circle",filter:["all",["==","meta","midpoint"]],paint:{"circle-radius":3,"circle-color":se}}];function ue(e){return function(t){const o=t.featureTarget;return!!o&&!!o.properties&&o.properties.meta===e}}function le(e){return!!e.originalEvent&&!!e.originalEvent.shiftKey&&0===e.originalEvent.button}function de(e){return!!e.featureTarget&&!!e.featureTarget.properties&&e.featureTarget.properties.active===E.ACTIVE&&e.featureTarget.properties.meta===y.FEATURE}function pe(e){return!!e.featureTarget&&!!e.featureTarget.properties&&e.featureTarget.properties.active===E.INACTIVE&&e.featureTarget.properties.meta===y.FEATURE}function he(e){return void 0===e.featureTarget}function fe(e){return!!e.featureTarget&&!!e.featureTarget.properties&&e.featureTarget.properties.meta===y.FEATURE}function ge(e){const t=e.featureTarget;return!!t&&!!t.properties&&t.properties.meta===y.VERTEX}function me(e){return!!e.originalEvent&&!0===e.originalEvent.shiftKey}function ye(e){return 27===e.keyCode}function Ee(e){return 13===e.keyCode}var Te=Object.freeze({__proto__:null,isActiveFeature:de,isEnterKey:Ee,isEscapeKey:ye,isFeature:fe,isInactiveFeature:pe,isOfMetaType:ue,isShiftDown:me,isShiftMousedown:le,isTrue:function(){return!0},isVertex:ge,noTarget:he});function Ce(e,t){this.x=e,this.y=t}function _e(e,t){const o=t.getBoundingClientRect();return new Ce(e.clientX-o.left-(t.clientLeft||0),e.clientY-o.top-(t.clientTop||0))}function ve(e,t,o,n){return{type:h.FEATURE,properties:{meta:y.VERTEX,parent:e,coord_path:o,active:n?E.ACTIVE:E.INACTIVE},geometry:{type:h.POINT,coordinates:t}}}function Ie(e,t,o){const n=t.geometry.coordinates,r=o.geometry.coordinates;if(n[1]>_||n[1]_||r[1]{const u=null!=o?`${o}.${a}`:String(a),l=ve(i,e,u,c(u));if(t.midpoints&&r){const e=Ie(i,r,l);e&&s.push(e)}r=l;const d=JSON.stringify(e);n!==d&&s.push(l),0===a&&(n=d)}))}function c(e){return!!t.selectedPaths&&-1!==t.selectedPaths.indexOf(e)}return n===h.POINT?s.push(ve(i,r,o,c(o))):n===h.POLYGON?r.forEach(((e,t)=>{a(e,null!==o?`${o}.${t}`:String(t))})):n===h.LINE_STRING?a(r,o):0===n.indexOf(h.MULTI_PREFIX)&&function(){const o=n.replace(h.MULTI_PREFIX,"");r.forEach(((n,r)=>{const i={type:h.FEATURE,properties:e.properties,geometry:{type:o,coordinates:n}};s=s.concat(Se(i,t,r))}))}(),s}Ce.prototype={clone(){return new Ce(this.x,this.y)},add(e){return this.clone()._add(e)},sub(e){return this.clone()._sub(e)},multByPoint(e){return this.clone()._multByPoint(e)},divByPoint(e){return this.clone()._divByPoint(e)},mult(e){return this.clone()._mult(e)},div(e){return this.clone()._div(e)},rotate(e){return this.clone()._rotate(e)},rotateAround(e,t){return this.clone()._rotateAround(e,t)},matMult(e){return this.clone()._matMult(e)},unit(){return this.clone()._unit()},perp(){return this.clone()._perp()},round(){return this.clone()._round()},mag(){return Math.sqrt(this.x*this.x+this.y*this.y)},equals(e){return this.x===e.x&&this.y===e.y},dist(e){return Math.sqrt(this.distSqr(e))},distSqr(e){const t=e.x-this.x,o=e.y-this.y;return t*t+o*o},angle(){return Math.atan2(this.y,this.x)},angleTo(e){return Math.atan2(this.y-e.y,this.x-e.x)},angleWith(e){return this.angleWithSep(e.x,e.y)},angleWithSep(e,t){return Math.atan2(this.x*t-this.y*e,this.x*e+this.y*t)},_matMult(e){const t=e[0]*this.x+e[1]*this.y,o=e[2]*this.x+e[3]*this.y;return this.x=t,this.y=o,this},_add(e){return this.x+=e.x,this.y+=e.y,this},_sub(e){return this.x-=e.x,this.y-=e.y,this},_mult(e){return this.x*=e,this.y*=e,this},_div(e){return this.x/=e,this.y/=e,this},_multByPoint(e){return this.x*=e.x,this.y*=e.y,this},_divByPoint(e){return this.x/=e.x,this.y/=e.y,this},_unit(){return this._div(this.mag()),this},_perp(){const e=this.y;return this.y=this.x,this.x=-e,this},_rotate(e){const t=Math.cos(e),o=Math.sin(e),n=t*this.x-o*this.y,r=o*this.x+t*this.y;return this.x=n,this.y=r,this},_rotateAround(e,t){const o=Math.cos(e),n=Math.sin(e),r=t.x+o*(this.x-t.x)-n*(this.y-t.y),i=t.y+n*(this.x-t.x)+o*(this.y-t.y);return this.x=r,this.y=i,this},_round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},constructor:Ce},Ce.convert=function(e){if(e instanceof Ce)return e;if(Array.isArray(e))return new Ce(+e[0],+e[1]);if(void 0!==e.x&&void 0!==e.y)return new Ce(+e.x,+e.y);throw new Error("Expected [x, y] or {x, y} point format")};var Oe={enable(e){setTimeout((()=>{e.map&&e.map.doubleClickZoom&&e._ctx&&e._ctx.store&&e._ctx.store.getInitialConfigValue&&e._ctx.store.getInitialConfigValue("doubleClickZoom")&&e.map.doubleClickZoom.enable()}),0)},disable(e){setTimeout((()=>{e.map&&e.map.doubleClickZoom&&e.map.doubleClickZoom.disable()}),0)}};const{LAT_MIN:Le,LAT_MAX:Me,LAT_RENDERED_MIN:Ne,LAT_RENDERED_MAX:be,LNG_MIN:xe,LNG_MAX:Ae}=v;function Pe(e,t){let o=Le,n=Me,r=Le,i=Me,s=Ae,a=xe;e.forEach((e=>{const t=function(e){const t={Point:0,LineString:1,Polygon:2,MultiPoint:1,MultiLineString:2,MultiPolygon:3}[e.geometry.type],o=[e.geometry.coordinates].flat(t),n=o.map((e=>e[0])),r=o.map((e=>e[1])),i=e=>Math.min.apply(null,e),s=e=>Math.max.apply(null,e);return[i(n),i(r),s(n),s(r)]}(e),c=t[1],u=t[3],l=t[0],d=t[2];c>o&&(o=c),ur&&(r=u),ca&&(a=d)}));const c=t;return o+c.lat>be&&(c.lat=be-o),r+c.lat>Me&&(c.lat=Me-r),n+c.lat=Ae&&(c.lng-=360*Math.ceil(Math.abs(c.lng)/360)),c}function Fe(e,t){const o=Pe(e.map((e=>e.toGeoJSON())),t);e.forEach((e=>{const t=e.getCoordinates(),n=e=>{const t={lng:e[0]+o.lng,lat:e[1]+o.lat};return[t.lng,t.lat]},r=e=>e.map((e=>n(e))),i=e=>e.map((e=>r(e)));let s;e.type===h.POINT?s=n(t):e.type===h.LINE_STRING||e.type===h.MULTI_POINT?s=t.map(n):e.type===h.POLYGON||e.type===h.MULTI_LINE_STRING?s=t.map(r):e.type===h.MULTI_POLYGON&&(s=t.map(i)),e.incomingCoords(s)}))}const Re={onSetup:function(e){const t={dragMoveLocation:null,boxSelectStartLocation:null,boxSelectElement:void 0,boxSelecting:!1,canBoxSelect:!1,dragMoving:!1,canDragMove:!1,initialDragPanState:this.map.dragPan.isEnabled(),initiallySelectedFeatureIds:e.featureIds||[]};return this.setSelected(t.initiallySelectedFeatureIds.filter((e=>void 0!==this.getFeature(e)))),this.fireActionable(),this.setActionableState({combineFeatures:!0,uncombineFeatures:!0,trash:!0}),t},fireUpdate:function(){this.fire(g.UPDATE,{action:m.MOVE,features:this.getSelected().map((e=>e.toGeoJSON()))})},fireActionable:function(){const e=this.getSelected(),t=e.filter((e=>this.isInstanceOf("MultiFeature",e)));let o=!1;if(e.length>1){o=!0;const t=e[0].type.replace("Multi","");e.forEach((e=>{e.type.replace("Multi","")!==t&&(o=!1)}))}const n=t.length>0,r=e.length>0;this.setActionableState({combineFeatures:o,uncombineFeatures:n,trash:r})},getUniqueIds:function(e){return e.length?e.map((e=>e.properties.id)).filter((e=>void 0!==e)).reduce(((e,t)=>(e.add(t),e)),new M).values():[]},stopExtendedInteractions:function(e){e.boxSelectElement&&(e.boxSelectElement.parentNode&&e.boxSelectElement.parentNode.removeChild(e.boxSelectElement),e.boxSelectElement=null),(e.canDragMove||e.canBoxSelect)&&!0===e.initialDragPanState&&this.map.dragPan.enable(),e.boxSelecting=!1,e.canBoxSelect=!1,e.dragMoving=!1,e.canDragMove=!1},onStop:function(){Oe.enable(this)},onMouseMove:function(e,t){return fe(t)&&e.dragMoving&&this.fireUpdate(),this.stopExtendedInteractions(e),!0},onMouseOut:function(e){return!e.dragMoving||this.fireUpdate()}};Re.onTap=Re.onClick=function(e,t){return he(t)?this.clickAnywhere(e,t):ue(y.VERTEX)(t)?this.clickOnVertex(e,t):fe(t)?this.clickOnFeature(e,t):void 0},Re.clickAnywhere=function(e){const t=this.getSelectedIds();t.length&&(this.clearSelectedFeatures(),t.forEach((e=>this.doRender(e)))),Oe.enable(this),this.stopExtendedInteractions(e)},Re.clickOnVertex=function(e,t){this.changeMode(f.DIRECT_SELECT,{featureId:t.featureTarget.properties.parent,coordPath:t.featureTarget.properties.coord_path,startPos:t.lngLat}),this.updateUIClasses({mouse:d.MOVE})},Re.startOnActiveFeature=function(e,t){this.stopExtendedInteractions(e),this.map.dragPan.disable(),this.doRender(t.featureTarget.properties.id),e.canDragMove=!0,e.dragMoveLocation=t.lngLat},Re.clickOnFeature=function(e,t){Oe.disable(this),this.stopExtendedInteractions(e);const o=me(t),n=this.getSelectedIds(),r=t.featureTarget.properties.id,i=this.isSelected(r);if(!o&&i&&this.getFeature(r).type!==h.POINT)return this.changeMode(f.DIRECT_SELECT,{featureId:r});i&&o?(this.deselect(r),this.updateUIClasses({mouse:d.POINTER}),1===n.length&&Oe.enable(this)):!i&&o?(this.select(r),this.updateUIClasses({mouse:d.MOVE})):i||o||(n.forEach((e=>this.doRender(e))),this.setSelected(r),this.updateUIClasses({mouse:d.MOVE})),this.doRender(r)},Re.onMouseDown=function(e,t){return e.initialDragPanState=this.map.dragPan.isEnabled(),de(t)?this.startOnActiveFeature(e,t):this.drawConfig.boxSelect&&le(t)?this.startBoxSelect(e,t):void 0},Re.startBoxSelect=function(e,t){this.stopExtendedInteractions(e),this.map.dragPan.disable(),e.boxSelectStartLocation=_e(t.originalEvent,this.map.getContainer()),e.canBoxSelect=!0},Re.onTouchStart=function(e,t){if(de(t))return this.startOnActiveFeature(e,t)},Re.onDrag=function(e,t){return e.canDragMove?this.dragMove(e,t):this.drawConfig.boxSelect&&e.canBoxSelect?this.whileBoxSelect(e,t):void 0},Re.whileBoxSelect=function(e,t){e.boxSelecting=!0,this.updateUIClasses({mouse:d.ADD}),e.boxSelectElement||(e.boxSelectElement=document.createElement("div"),e.boxSelectElement.classList.add(u.BOX_SELECT),this.map.getContainer().appendChild(e.boxSelectElement));const o=_e(t.originalEvent,this.map.getContainer()),n=Math.min(e.boxSelectStartLocation.x,o.x),r=Math.max(e.boxSelectStartLocation.x,o.x),i=Math.min(e.boxSelectStartLocation.y,o.y),s=Math.max(e.boxSelectStartLocation.y,o.y),a=`translate(${n}px, ${i}px)`;e.boxSelectElement.style.transform=a,e.boxSelectElement.style.WebkitTransform=a,e.boxSelectElement.style.width=r-n+"px",e.boxSelectElement.style.height=s-i+"px"},Re.dragMove=function(e,t){e.dragMoving=!0,t.originalEvent.stopPropagation();const o={lng:t.lngLat.lng-e.dragMoveLocation.lng,lat:t.lngLat.lat-e.dragMoveLocation.lat};Fe(this.getSelected(),o),e.dragMoveLocation=t.lngLat},Re.onTouchEnd=Re.onMouseUp=function(e,t){if(e.dragMoving)this.fireUpdate();else if(e.boxSelecting){const o=[e.boxSelectStartLocation,_e(t.originalEvent,this.map.getContainer())],n=this.featuresAt(null,o,"click"),r=this.getUniqueIds(n).filter((e=>!this.isSelected(e)));r.length&&(this.select(r),r.forEach((e=>this.doRender(e))),this.updateUIClasses({mouse:d.MOVE}))}this.stopExtendedInteractions(e)},Re.toDisplayFeatures=function(e,t,o){t.properties.active=this.isSelected(t.properties.id)?E.ACTIVE:E.INACTIVE,o(t),this.fireActionable(),t.properties.active===E.ACTIVE&&t.geometry.type!==h.POINT&&Se(t).forEach(o)},Re.onTrash=function(){this.deleteFeature(this.getSelectedIds()),this.fireActionable()},Re.onCombineFeatures=function(){const e=this.getSelected();if(0===e.length||e.length<2)return;const t=[],o=[],n=e[0].type.replace("Multi","");for(let r=0;r{t.push(e)})):t.push(i.getCoordinates()),o.push(i.toGeoJSON())}if(o.length>1){const e=this.newFeature({type:h.FEATURE,properties:o[0].properties,geometry:{type:`Multi${n}`,coordinates:t}});this.addFeature(e),this.deleteFeature(this.getSelectedIds(),{silent:!0}),this.setSelected([e.id]),this.fire(g.COMBINE_FEATURES,{createdFeatures:[e.toGeoJSON()],deletedFeatures:o})}this.fireActionable()},Re.onUncombineFeatures=function(){const e=this.getSelected();if(0===e.length)return;const t=[],o=[];for(let n=0;n{this.addFeature(e),e.properties=r.properties,t.push(e.toGeoJSON()),this.select([e.id])})),this.deleteFeature(r.id,{silent:!0}),o.push(r.toGeoJSON()))}t.length>1&&this.fire(g.UNCOMBINE_FEATURES,{createdFeatures:t,deletedFeatures:o}),this.fireActionable()};const we=ue(y.VERTEX),De=ue(y.MIDPOINT),Ue={fireUpdate:function(){this.fire(g.UPDATE,{action:m.CHANGE_COORDINATES,features:this.getSelected().map((e=>e.toGeoJSON()))})},fireActionable:function(e){this.setActionableState({combineFeatures:!1,uncombineFeatures:!1,trash:e.selectedCoordPaths.length>0})},startDragging:function(e,t){e.initialDragPanState=this.map.dragPan.isEnabled(),this.map.dragPan.disable(),e.canDragMove=!0,e.dragMoveLocation=t.lngLat},stopDragging:function(e){e.canDragMove&&!0===e.initialDragPanState&&this.map.dragPan.enable(),e.dragMoving=!1,e.canDragMove=!1,e.dragMoveLocation=null},onVertex:function(e,t){this.startDragging(e,t);const o=t.featureTarget.properties,n=e.selectedCoordPaths.indexOf(o.coord_path);me(t)||-1!==n?me(t)&&-1===n&&e.selectedCoordPaths.push(o.coord_path):e.selectedCoordPaths=[o.coord_path];const r=this.pathsToCoordinates(e.featureId,e.selectedCoordPaths);this.setSelectedCoordinates(r)},onMidpoint:function(e,t){this.startDragging(e,t);const o=t.featureTarget.properties;e.feature.addCoordinate(o.coord_path,o.lng,o.lat),this.fireUpdate(),e.selectedCoordPaths=[o.coord_path]},pathsToCoordinates:function(e,t){return t.map((t=>({feature_id:e,coord_path:t})))},onFeature:function(e,t){0===e.selectedCoordPaths.length?this.startDragging(e,t):this.stopDragging(e)},dragFeature:function(e,t,o){Fe(this.getSelected(),o),e.dragMoveLocation=t.lngLat},dragVertex:function(e,t,o){const n=e.selectedCoordPaths.map((t=>e.feature.getCoordinate(t))),r=Pe(n.map((e=>({type:h.FEATURE,properties:{},geometry:{type:h.POINT,coordinates:e}}))),o);for(let t=0;tt.localeCompare(e,"en",{numeric:!0}))).forEach((t=>e.feature.removeCoordinate(t))),this.fireUpdate(),e.selectedCoordPaths=[],this.clearSelectedCoordinates(),this.fireActionable(e),!1===e.feature.isValid()&&(this.deleteFeature([e.featureId]),this.changeMode(f.SIMPLE_SELECT,{}))},onMouseMove:function(e,t){const o=de(t),n=we(t),r=De(t),i=0===e.selectedCoordPaths.length;return o&&i||n&&!i?this.updateUIClasses({mouse:d.MOVE}):this.updateUIClasses({mouse:d.NONE}),(n||o||r)&&e.dragMoving&&this.fireUpdate(),this.stopDragging(e),!0},onMouseOut:function(e){return e.dragMoving&&this.fireUpdate(),!0}};Ue.onTouchStart=Ue.onMouseDown=function(e,t){return we(t)?this.onVertex(e,t):de(t)?this.onFeature(e,t):De(t)?this.onMidpoint(e,t):void 0},Ue.onDrag=function(e,t){if(!0!==e.canDragMove)return;e.dragMoving=!0,t.originalEvent.stopPropagation();const o={lng:t.lngLat.lng-e.dragMoveLocation.lng,lat:t.lngLat.lat-e.dragMoveLocation.lat};e.selectedCoordPaths.length>0?this.dragVertex(e,t,o):this.dragFeature(e,t,o),e.dragMoveLocation=t.lngLat},Ue.onClick=function(e,t){return he(t)?this.clickNoTarget(e,t):de(t)?this.clickActiveFeature(e,t):pe(t)?this.clickInactive(e,t):void this.stopDragging(e)},Ue.onTap=function(e,t){return he(t)?this.clickNoTarget(e,t):de(t)?this.clickActiveFeature(e,t):pe(t)?this.clickInactive(e,t):void 0},Ue.onTouchEnd=Ue.onMouseUp=function(e){e.dragMoving&&this.fireUpdate(),this.stopDragging(e)};const ke={};function Ve(e,t){return!!e.lngLat&&e.lngLat.lng===t[0]&&e.lngLat.lat===t[1]}ke.onSetup=function(){const e=this.newFeature({type:h.FEATURE,properties:{},geometry:{type:h.POINT,coordinates:[]}});return this.addFeature(e),this.clearSelectedFeatures(),this.updateUIClasses({mouse:d.ADD}),this.activateUIButton(p.POINT),this.setActionableState({trash:!0}),{point:e}},ke.stopDrawingAndRemove=function(e){this.deleteFeature([e.point.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT)},ke.onTap=ke.onClick=function(e,t){this.updateUIClasses({mouse:d.MOVE}),e.point.updateCoordinate("",t.lngLat.lng,t.lngLat.lat),this.fire(g.CREATE,{features:[e.point.toGeoJSON()]}),this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.point.id]})},ke.onStop=function(e){this.activateUIButton(),e.point.getCoordinate().length||this.deleteFeature([e.point.id],{silent:!0})},ke.toDisplayFeatures=function(e,t,o){const n=t.properties.id===e.point.id;if(t.properties.active=n?E.ACTIVE:E.INACTIVE,!n)return o(t)},ke.onTrash=ke.stopDrawingAndRemove,ke.onKeyUp=function(e,t){if(ye(t)||Ee(t))return this.stopDrawingAndRemove(e,t)};const Ge={onSetup:function(){const e=this.newFeature({type:h.FEATURE,properties:{},geometry:{type:h.POLYGON,coordinates:[[]]}});return this.addFeature(e),this.clearSelectedFeatures(),Oe.disable(this),this.updateUIClasses({mouse:d.ADD}),this.activateUIButton(p.POLYGON),this.setActionableState({trash:!0}),{polygon:e,currentVertexPosition:0}},clickAnywhere:function(e,t){if(e.currentVertexPosition>0&&Ve(t,e.polygon.coordinates[0][e.currentVertexPosition-1]))return this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.polygon.id]});this.updateUIClasses({mouse:d.ADD}),e.polygon.updateCoordinate(`0.${e.currentVertexPosition}`,t.lngLat.lng,t.lngLat.lat),e.currentVertexPosition++,e.polygon.updateCoordinate(`0.${e.currentVertexPosition}`,t.lngLat.lng,t.lngLat.lat)},clickOnVertex:function(e){return this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.polygon.id]})},onMouseMove:function(e,t){e.polygon.updateCoordinate(`0.${e.currentVertexPosition}`,t.lngLat.lng,t.lngLat.lat),ge(t)&&this.updateUIClasses({mouse:d.POINTER})}};Ge.onTap=Ge.onClick=function(e,t){return ge(t)?this.clickOnVertex(e,t):this.clickAnywhere(e,t)},Ge.onKeyUp=function(e,t){ye(t)?(this.deleteFeature([e.polygon.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT)):Ee(t)&&this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.polygon.id]})},Ge.onStop=function(e){this.updateUIClasses({mouse:d.NONE}),Oe.enable(this),this.activateUIButton(),void 0!==this.getFeature(e.polygon.id)&&(e.polygon.removeCoordinate(`0.${e.currentVertexPosition}`),e.polygon.isValid()?this.fire(g.CREATE,{features:[e.polygon.toGeoJSON()]}):(this.deleteFeature([e.polygon.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT,{},{silent:!0})))},Ge.toDisplayFeatures=function(e,t,o){const n=t.properties.id===e.polygon.id;if(t.properties.active=n?E.ACTIVE:E.INACTIVE,!n)return o(t);if(0===t.geometry.coordinates.length)return;const r=t.geometry.coordinates[0].length;if(!(r<3)){if(t.properties.meta=y.FEATURE,o(ve(e.polygon.id,t.geometry.coordinates[0][0],"0.0",!1)),r>3){const n=t.geometry.coordinates[0].length-3;o(ve(e.polygon.id,t.geometry.coordinates[0][n],`0.${n}`,!1))}if(r<=4){const e=[[t.geometry.coordinates[0][0][0],t.geometry.coordinates[0][0][1]],[t.geometry.coordinates[0][1][0],t.geometry.coordinates[0][1][1]]];if(o({type:h.FEATURE,properties:t.properties,geometry:{coordinates:e,type:h.LINE_STRING}}),3===r)return}return o(t)}},Ge.onTrash=function(e){this.deleteFeature([e.polygon.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT)};const Be={onSetup:function(e){const t=(e=e||{}).featureId;let o,n,r="forward";if(t){if(o=this.getFeature(t),!o)throw new Error("Could not find a feature with the provided featureId");let i=e.from;if(i&&"Feature"===i.type&&i.geometry&&"Point"===i.geometry.type&&(i=i.geometry),i&&"Point"===i.type&&i.coordinates&&2===i.coordinates.length&&(i=i.coordinates),!i||!Array.isArray(i))throw new Error("Please use the `from` property to indicate which point to continue the line from");const s=o.coordinates.length-1;if(o.coordinates[s][0]===i[0]&&o.coordinates[s][1]===i[1])n=s+1,o.addCoordinate(n,...o.coordinates[s]);else{if(o.coordinates[0][0]!==i[0]||o.coordinates[0][1]!==i[1])throw new Error("`from` should match the point at either the start or the end of the provided LineString");r="backwards",n=0,o.addCoordinate(n,...o.coordinates[0])}}else o=this.newFeature({type:h.FEATURE,properties:{},geometry:{type:h.LINE_STRING,coordinates:[]}}),n=0,this.addFeature(o);return this.clearSelectedFeatures(),Oe.disable(this),this.updateUIClasses({mouse:d.ADD}),this.activateUIButton(p.LINE),this.setActionableState({trash:!0}),{line:o,currentVertexPosition:n,direction:r}},clickAnywhere:function(e,t){if(e.currentVertexPosition>0&&Ve(t,e.line.coordinates[e.currentVertexPosition-1])||"backwards"===e.direction&&Ve(t,e.line.coordinates[e.currentVertexPosition+1]))return this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.line.id]});this.updateUIClasses({mouse:d.ADD}),e.line.updateCoordinate(e.currentVertexPosition,t.lngLat.lng,t.lngLat.lat),"forward"===e.direction?(e.currentVertexPosition++,e.line.updateCoordinate(e.currentVertexPosition,t.lngLat.lng,t.lngLat.lat)):e.line.addCoordinate(0,t.lngLat.lng,t.lngLat.lat)},clickOnVertex:function(e){return this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.line.id]})},onMouseMove:function(e,t){e.line.updateCoordinate(e.currentVertexPosition,t.lngLat.lng,t.lngLat.lat),ge(t)&&this.updateUIClasses({mouse:d.POINTER})}};Be.onTap=Be.onClick=function(e,t){if(ge(t))return this.clickOnVertex(e,t);this.clickAnywhere(e,t)},Be.onKeyUp=function(e,t){Ee(t)?this.changeMode(f.SIMPLE_SELECT,{featureIds:[e.line.id]}):ye(t)&&(this.deleteFeature([e.line.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT))},Be.onStop=function(e){Oe.enable(this),this.activateUIButton(),void 0!==this.getFeature(e.line.id)&&(e.line.removeCoordinate(`${e.currentVertexPosition}`),e.line.isValid()?this.fire(g.CREATE,{features:[e.line.toGeoJSON()]}):(this.deleteFeature([e.line.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT,{},{silent:!0})))},Be.onTrash=function(e){this.deleteFeature([e.line.id],{silent:!0}),this.changeMode(f.SIMPLE_SELECT)},Be.toDisplayFeatures=function(e,t,o){const n=t.properties.id===e.line.id;if(t.properties.active=n?E.ACTIVE:E.INACTIVE,!n)return o(t);t.geometry.coordinates.length<2||(t.properties.meta=y.FEATURE,o(ve(e.line.id,t.geometry.coordinates["forward"===e.direction?t.geometry.coordinates.length-2:1],""+("forward"===e.direction?t.geometry.coordinates.length-2:1),!1)),o(t))};var je={simple_select:Re,direct_select:Ue,draw_point:ke,draw_polygon:Ge,draw_line_string:Be};const Je={defaultMode:f.SIMPLE_SELECT,keybindings:!0,touchEnabled:!0,clickBuffer:2,touchBuffer:25,boxSelect:!0,displayControlsDefault:!0,styles:ce,modes:je,controls:{},userProperties:!1,suppressAPIEvents:!0},$e={point:!0,line_string:!0,polygon:!0,trash:!0,combine_features:!0,uncombine_features:!0},Ye={point:!1,line_string:!1,polygon:!1,trash:!1,combine_features:!1,uncombine_features:!1};function He(e,t){return e.map((e=>e.source?e:Object.assign({},e,{id:`${e.id}.${t}`,source:"hot"===t?l.HOT:l.COLD})))}var Xe,qe,Ze,We,Ke=t(qe?Xe:(qe=1,Xe=function e(t,o){if(t===o)return!0;if(t&&o&&"object"==typeof t&&"object"==typeof o){if(t.constructor!==o.constructor)return!1;var n,r,i;if(Array.isArray(t)){if((n=t.length)!=o.length)return!1;for(r=n;0!=r--;)if(!e(t[r],o[r]))return!1;return!0}if(t.constructor===RegExp)return t.source===o.source&&t.flags===o.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===o.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===o.toString();if((n=(i=Object.keys(t)).length)!==Object.keys(o).length)return!1;for(r=n;0!=r--;)if(!Object.prototype.hasOwnProperty.call(o,i[r]))return!1;for(r=n;0!=r--;){var s=i[r];if(!e(t[s],o[s]))return!1}return!0}return t!=t&&o!=o})),ze=function(){if(We)return Ze;We=1,Ze=function(t){if(!t||!t.type)return null;var o=e[t.type];return o?"geometry"===o?{type:"FeatureCollection",features:[{type:"Feature",properties:{},geometry:t}]}:"feature"===o?{type:"FeatureCollection",features:[t]}:"featurecollection"===o?t:void 0:null};var e={Point:"geometry",MultiPoint:"geometry",LineString:"geometry",MultiLineString:"geometry",Polygon:"geometry",MultiPolygon:"geometry",GeometryCollection:"geometry",Feature:"feature",FeatureCollection:"featurecollection"};return Ze}(),Qe=t(ze);function et(e,t){return e.length===t.length&&JSON.stringify(e.map((e=>e)).sort())===JSON.stringify(t.map((e=>e)).sort())}const tt={Polygon:Y,LineString:$,Point:J,MultiPolygon:q,MultiLineString:q,MultiPoint:q};var ot=Object.freeze({__proto__:null,CommonSelectors:Te,ModeHandler:e,StringSet:M,constrainFeatureMovement:Pe,createMidPoint:Ie,createSupplementaryPoints:Se,createVertex:ve,doubleClickZoom:Oe,euclideanDistance:P,featuresAt:b,getFeatureAtAndSetCursors:A,isClick:D,isEventAtCoordinates:Ve,isTap:V,mapEventToBoundingBox:L,moveFeatures:Fe,sortFeatures:O,stringSetsAreEqual:et,theme:ce,toDenseArray:Q});const nt=function(e,t){const o={options:e=function(e={}){let t=Object.assign({},e);return e.controls||(t.controls={}),!1===e.displayControlsDefault?t.controls=Object.assign({},Ye,e.controls):t.controls=Object.assign({},$e,e.controls),t=Object.assign({},Je,t),t.styles=He(t.styles,"cold").concat(He(t.styles,"hot")),t}(e)};t=function(e,t){t.modes=f;const o=void 0===e.options.suppressAPIEvents||!!e.options.suppressAPIEvents;return t.getFeatureIdsAt=function(t){return b.click({point:t},null,e).map((e=>e.properties.id))},t.getSelectedIds=function(){return e.store.getSelectedIds()},t.getSelected=function(){return{type:h.FEATURE_COLLECTION,features:e.store.getSelectedIds().map((t=>e.store.get(t))).map((e=>e.toGeoJSON()))}},t.getSelectedPoints=function(){return{type:h.FEATURE_COLLECTION,features:e.store.getSelectedCoordinates().map((e=>({type:h.FEATURE,properties:{},geometry:{type:h.POINT,coordinates:e.coordinates}})))}},t.set=function(o){if(void 0===o.type||o.type!==h.FEATURE_COLLECTION||!Array.isArray(o.features))throw new Error("Invalid FeatureCollection");const n=e.store.createRenderBatch();let r=e.store.getAllIds().slice();const i=t.add(o),s=new M(i);return r=r.filter((e=>!s.has(e))),r.length&&t.delete(r),n(),i},t.add=function(t){const n=JSON.parse(JSON.stringify(Qe(t))).features.map((t=>{if(t.id=t.id||B(),null===t.geometry)throw new Error("Invalid geometry: null");if(void 0===e.store.get(t.id)||e.store.get(t.id).type!==t.geometry.type){const n=tt[t.geometry.type];if(void 0===n)throw new Error(`Invalid geometry type: ${t.geometry.type}.`);const r=new n(e,t);e.store.add(r,{silent:o})}else{const n=e.store.get(t.id),r=n.properties;n.properties=t.properties,Ke(r,t.properties)||e.store.featureChanged(n.id,{silent:o}),Ke(n.getCoordinates(),t.geometry.coordinates)||n.incomingCoords(t.geometry.coordinates)}return t.id}));return e.store.render(),n},t.get=function(t){const o=e.store.get(t);if(o)return o.toGeoJSON()},t.getAll=function(){return{type:h.FEATURE_COLLECTION,features:e.store.getAll().map((e=>e.toGeoJSON()))}},t.delete=function(n){return e.store.delete(n,{silent:o}),t.getMode()!==f.DIRECT_SELECT||e.store.getSelectedIds().length?e.store.render():e.events.changeMode(f.SIMPLE_SELECT,void 0,{silent:o}),t},t.deleteAll=function(){return e.store.delete(e.store.getAllIds(),{silent:o}),t.getMode()===f.DIRECT_SELECT?e.events.changeMode(f.SIMPLE_SELECT,void 0,{silent:o}):e.store.render(),t},t.changeMode=function(n,r={}){return n===f.SIMPLE_SELECT&&t.getMode()===f.SIMPLE_SELECT?(et(r.featureIds||[],e.store.getSelectedIds())||(e.store.setSelected(r.featureIds,{silent:o}),e.store.render()),t):(n===f.DIRECT_SELECT&&t.getMode()===f.DIRECT_SELECT&&r.featureId===e.store.getSelectedIds()[0]||e.events.changeMode(n,r,{silent:o}),t)},t.getMode=function(){return e.events.getMode()},t.trash=function(){return e.events.trash({silent:o}),t},t.combineFeatures=function(){return e.events.combineFeatures({silent:o}),t},t.uncombineFeatures=function(){return e.events.uncombineFeatures({silent:o}),t},t.setFeatureProperty=function(n,r,i){return e.store.setFeatureProperty(n,r,i,{silent:o}),t},t}(o,t),o.api=t;const n=re(o);return t.onAdd=n.onAdd,t.onRemove=n.onRemove,t.types=p,t.options=e,t};function rt(e){nt(e,this)}return rt.modes=je,rt.constants=v,rt.lib=ot,rt},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).MapboxDraw=t(); + diff --git a/streetup/common/static/common/maplibre-gl-geocoder.js b/streetup/common/static/common/maplibre-gl-geocoder.js new file mode 100644 index 0000000..7e39332 --- /dev/null +++ b/streetup/common/static/common/maplibre-gl-geocoder.js @@ -0,0 +1,2779 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.MaplibreGeocoder = factory()); +})(this, (function () { 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + /* global Reflect, Promise */ + + + function __awaiter(thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + var immutable; + var hasRequiredImmutable; + + function requireImmutable () { + if (hasRequiredImmutable) return immutable; + hasRequiredImmutable = 1; + immutable = extend; + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + function extend() { + var target = {}; + + for (var i = 0; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target + } + return immutable; + } + + var fuzzy = {exports: {}}; + + /* + * Fuzzy + * https://github.com/myork/fuzzy + * + * Copyright (c) 2012 Matt York + * Licensed under the MIT license. + */ + + var hasRequiredFuzzy; + + function requireFuzzy () { + if (hasRequiredFuzzy) return fuzzy.exports; + hasRequiredFuzzy = 1; + (function (module, exports) { + (function() { + + var fuzzy = {}; + + // Use in node or in browser + { + module.exports = fuzzy; + } + + // Return all elements of `array` that have a fuzzy + // match against `pattern`. + fuzzy.simpleFilter = function(pattern, array) { + return array.filter(function(str) { + return fuzzy.test(pattern, str); + }); + }; + + // Does `pattern` fuzzy match `str`? + fuzzy.test = function(pattern, str) { + return fuzzy.match(pattern, str) !== null; + }; + + // If `pattern` matches `str`, wrap each matching character + // in `opts.pre` and `opts.post`. If no match, return null + fuzzy.match = function(pattern, str, opts) { + opts = opts || {}; + var patternIdx = 0 + , result = [] + , len = str.length + , totalScore = 0 + , currScore = 0 + // prefix + , pre = opts.pre || '' + // suffix + , post = opts.post || '' + // String to compare against. This might be a lowercase version of the + // raw string + , compareString = opts.caseSensitive && str || str.toLowerCase() + , ch; + + pattern = opts.caseSensitive && pattern || pattern.toLowerCase(); + + // For each character in the string, either add it to the result + // or wrap in template if it's the next string in the pattern + for(var idx = 0; idx < len; idx++) { + ch = str[idx]; + if(compareString[idx] === pattern[patternIdx]) { + ch = pre + ch + post; + patternIdx += 1; + + // consecutive characters should increase the score more than linearly + currScore += 1 + currScore; + } else { + currScore = 0; + } + totalScore += currScore; + result[result.length] = ch; + } + + // return rendered string if we have a match for every char + if(patternIdx === pattern.length) { + // if the string is an exact match with pattern, totalScore should be maxed + totalScore = (compareString === pattern) ? Infinity : totalScore; + return {rendered: result.join(''), score: totalScore}; + } + + return null; + }; + + // The normal entry point. Filters `arr` for matches against `pattern`. + // It returns an array with matching values of the type: + // + // [{ + // string: 'lah' // The rendered string + // , index: 2 // The index of the element in `arr` + // , original: 'blah' // The original element in `arr` + // }] + // + // `opts` is an optional argument bag. Details: + // + // opts = { + // // string to put before a matching character + // pre: '' + // + // // string to put after matching character + // , post: '' + // + // // Optional function. Input is an entry in the given arr`, + // // output should be the string to test `pattern` against. + // // In this example, if `arr = [{crying: 'koala'}]` we would return + // // 'koala'. + // , extract: function(arg) { return arg.crying; } + // } + fuzzy.filter = function(pattern, arr, opts) { + if(!arr || arr.length === 0) { + return []; + } + if (typeof pattern !== 'string') { + return arr; + } + opts = opts || {}; + return arr + .reduce(function(prev, element, idx, arr) { + var str = element; + if(opts.extract) { + str = opts.extract(element); + } + var rendered = fuzzy.match(pattern, str, opts); + if(rendered != null) { + prev[prev.length] = { + string: rendered.rendered + , score: rendered.score + , index: idx + , original: element + }; + } + return prev; + }, []) + + // Sort by score. Browsers are inconsistent wrt stable/unstable + // sorting, so force stable by using the index in the case of tie. + // See http://ofb.net/~sethml/is-sort-stable.html + .sort(function(a,b) { + var compare = b.score - a.score; + if(compare) return compare; + return a.index - b.index; + }); + }; + + + }()); + } (fuzzy)); + return fuzzy.exports; + } + + var list; + var hasRequiredList; + + function requireList () { + if (hasRequiredList) return list; + hasRequiredList = 1; + + var List = function(component) { + this.component = component; + this.items = []; + this.active = component.options.noInitialSelection ? -1 : 0; + this.wrapper = document.createElement('div'); + this.wrapper.className = 'suggestions-wrapper'; + this.element = document.createElement('ul'); + this.element.className = 'suggestions'; + this.wrapper.appendChild(this.element); + + // selectingListItem is set to true in the time between the mousedown and mouseup when clicking an item in the list + // mousedown on a list item will cause the input to blur which normally hides the list, so this flag is used to keep + // the list open until the mouseup + this.selectingListItem = false; + + component.el.parentNode.insertBefore(this.wrapper, component.el.nextSibling); + return this; + }; + + List.prototype.show = function() { + this.element.style.display = 'block'; + }; + + List.prototype.hide = function() { + this.element.style.display = 'none'; + }; + + List.prototype.add = function(item) { + this.items.push(item); + }; + + List.prototype.clear = function() { + this.items = []; + this.active = this.component.options.noInitialSelection ? -1 : 0; + }; + + List.prototype.isEmpty = function() { + return !this.items.length; + }; + + List.prototype.isVisible = function() { + return this.element.style.display === 'block'; + }; + + List.prototype.draw = function() { + this.element.innerHTML = ''; + + if (this.items.length === 0) { + this.hide(); + return; + } + + for (var i = 0; i < this.items.length; i++) { + this.drawItem(this.items[i], this.active === i); + } + + this.show(); + }; + + List.prototype.drawItem = function(item, active) { + var li = document.createElement('li'), + a = document.createElement('a'); + + if (active) li.className += ' active'; + + a.innerHTML = item.string; + + li.appendChild(a); + this.element.appendChild(li); + + li.addEventListener('mousedown', function() { + this.selectingListItem = true; + }.bind(this)); + + li.addEventListener('mouseup', function() { + this.handleMouseUp.call(this, item); + }.bind(this)); + }; + + List.prototype.handleMouseUp = function(item) { + this.selectingListItem = false; + this.component.value(item.original); + this.clear(); + this.draw(); + }; + + List.prototype.move = function(index) { + this.active = index; + this.draw(); + }; + + List.prototype.previous = function() { + this.move(this.active <= 0 ? this.items.length - 1 : this.active - 1); + }; + + List.prototype.next = function() { + this.move(this.active >= this.items.length - 1 ? 0 : this.active + 1); + }; + + List.prototype.drawError = function(msg){ + var li = document.createElement('li'); + + li.innerHTML = msg; + + this.element.appendChild(li); + this.show(); + }; + + list = List; + return list; + } + + var suggestions; + var hasRequiredSuggestions; + + function requireSuggestions () { + if (hasRequiredSuggestions) return suggestions; + hasRequiredSuggestions = 1; + + var extend = requireImmutable(); + var fuzzy = requireFuzzy(); + var List = requireList(); + + var Suggestions = function(el, data, options) { + options = options || {}; + + this.options = extend({ + minLength: 2, + limit: 5, + filter: true, + hideOnBlur: true, + noInitialSelection: true + }, options); + + this.el = el; + this.data = data || []; + this.list = new List(this); + + this.query = ''; + this.selected = null; + + this.list.draw(); + + this.el.addEventListener('keyup', function(e) { + this.handleKeyUp(e.keyCode, e); + }.bind(this), false); + + this.el.addEventListener('keydown', function(e) { + this.handleKeyDown(e); + }.bind(this)); + + this.el.addEventListener('focus', function() { + this.handleFocus(); + }.bind(this)); + + this.el.addEventListener('blur', function() { + this.handleBlur(); + }.bind(this)); + + this.el.addEventListener('paste', function(e) { + this.handlePaste(e); + }.bind(this)); + + // use user-provided render function if given, otherwise just use the default + this.render = (this.options.render) ? this.options.render.bind(this) : this.render.bind(this); + + this.getItemValue = (this.options.getItemValue) ? this.options.getItemValue.bind(this) : this.getItemValue.bind(this); + + return this; + }; + + Suggestions.prototype.handleKeyUp = function(keyCode, e) { + // 40 - DOWN + // 38 - UP + // 27 - ESC + // 13 - ENTER + // 9 - TAB + + if (keyCode === 40 || + keyCode === 38 || + keyCode === 27 || + keyCode === 9) return; + + if (keyCode === 13) { + if (this.list.items[this.list.active]) { + this.list.handleMouseUp(this.list.items[this.list.active]); + e.stopPropagation(); + } + return; + } + + this.handleInputChange(this.el.value); + + }; + + Suggestions.prototype.handleKeyDown = function(e) { + switch (e.keyCode) { + case 13: // ENTER + if (this.list.active >= 0) { + this.list.selectingListItem = true; + } + break; + case 9: // TAB + if (!this.list.isEmpty()) { + if (this.list.isVisible()) { + e.preventDefault(); + } + this.value(this.list.active >= 0 ? this.list.items[this.list.active].original : null); + this.list.hide(); + } + break; + case 27: // ESC + if (!this.list.isEmpty()) this.list.hide(); + break; + case 38: // UP + this.list.previous(); + break; + case 40: // DOWN + this.list.next(); + break; + } + }; + + Suggestions.prototype.handleBlur = function() { + if (!this.list.selectingListItem && this.options.hideOnBlur) { + this.list.hide(); + } + }; + + Suggestions.prototype.handlePaste = function(e) { + if (e.clipboardData) { + this.handleInputChange(e.clipboardData.getData('Text')); + } else { + var self = this; + setTimeout(function () { + self.handleInputChange(e.target.value); + }, 100); + } + }; + + Suggestions.prototype.handleInputChange = function(query) { + this.query = this.normalize(query); + + this.list.clear(); + + if (this.query.length < this.options.minLength) { + this.list.draw(); + return; + } + + this.getCandidates(function(data) { + for (var i = 0; i < data.length; i++) { + this.list.add(data[i]); + if (i === (this.options.limit - 1)) break; + } + this.list.draw(); + }.bind(this)); + }; + + Suggestions.prototype.handleFocus = function() { + if (!this.list.isEmpty()) this.list.show(); + this.list.selectingListItem = false; + }; + + /** + * Update data previously passed + * + * @param {Array} revisedData + */ + Suggestions.prototype.update = function(revisedData) { + this.data = revisedData; + this.handleKeyUp(); + }; + + /** + * Clears data + */ + Suggestions.prototype.clear = function() { + this.data = []; + this.list.clear(); + }; + + /** + * Normalize the results list and input value for matching + * + * @param {String} value + * @return {String} + */ + Suggestions.prototype.normalize = function(value) { + value = value.toLowerCase(); + return value; + }; + + /** + * Evaluates whether an array item qualifies as a match with the current query + * + * @param {String} candidate a possible item from the array passed + * @param {String} query the current query + * @return {Boolean} + */ + Suggestions.prototype.match = function(candidate, query) { + return candidate.indexOf(query) > -1; + }; + + Suggestions.prototype.value = function(value) { + this.selected = value; + this.el.value = this.getItemValue(value || { place_name: this.query }); + + if (document.createEvent) { + var e = document.createEvent('HTMLEvents'); + e.initEvent('change', true, false); + this.el.dispatchEvent(e); + } else { + this.el.fireEvent('onchange'); + } + }; + + Suggestions.prototype.getCandidates = function(callback) { + var options = { + pre: '', + post: '', + extract: function(d) { return this.getItemValue(d); }.bind(this) + }; + var results; + if(this.options.filter){ + results = fuzzy.filter(this.query, this.data, options); + + results = results.map(function(item){ + return { + original: item.original, + string: this.render(item.original, item.string) + }; + }.bind(this)); + }else { + results = this.data.map(function(d) { + var renderedString = this.render(d); + return { + original: d, + string: renderedString + }; + }.bind(this)); + } + callback(results); + }; + + /** + * For a given item in the data array, return what should be used as the candidate string + * + * @param {Object|String} item an item from the data array + * @return {String} item + */ + Suggestions.prototype.getItemValue = function(item) { + return item; + }; + + /** + * For a given item in the data array, return a string of html that should be rendered in the dropdown + * @param {Object|String} item an item from the data array + * @param {String} sourceFormatting a string that has pre-formatted html that should be passed directly through the render function + * @return {String} html + */ + Suggestions.prototype.render = function(item, sourceFormatting) { + if (sourceFormatting){ + // use existing formatting on the source string + return sourceFormatting; + } + var boldString = (item.original) ? this.getItemValue(item.original) : this.getItemValue(item); + var indexString = this.normalize(boldString); + var indexOfQuery = indexString.lastIndexOf(this.query); + while (indexOfQuery > -1) { + var endIndexOfQuery = indexOfQuery + this.query.length; + boldString = boldString.slice(0, indexOfQuery) + '' + boldString.slice(indexOfQuery, endIndexOfQuery) + '' + boldString.slice(endIndexOfQuery); + indexOfQuery = indexString.slice(0, indexOfQuery).lastIndexOf(this.query); + } + return boldString + }; + + /** + * Render an custom error message in the suggestions list + * @param {String} msg An html string to render as an error message + */ + Suggestions.prototype.renderError = function(msg){ + this.list.drawError(msg); + }; + + suggestions = Suggestions; + return suggestions; + } + + var suggestionsList; + var hasRequiredSuggestionsList; + + function requireSuggestionsList () { + if (hasRequiredSuggestionsList) return suggestionsList; + hasRequiredSuggestionsList = 1; + + /** + * A typeahead component for inputs + * @class Suggestions + * + * @param {HTMLInputElement} el A valid HTML input element + * @param {Array} data An array of data used for results + * @param {Object} options + * @param {Number} [options.limit=5] Max number of results to display in the auto suggest list. + * @param {Number} [options.minLength=2] Number of characters typed into an input to trigger suggestions. + * @param {Boolean} [options.hideOnBlur=true] If `true`, hides the suggestions when focus is lost. + * @return {Suggestions} `this` + * @example + * // in the browser + * var input = document.querySelector('input'); + * var data = [ + * 'Roy Eldridge', + * 'Roy Hargrove', + * 'Rex Stewart' + * ]; + * + * new Suggestions(input, data); + * + * // with options + * var input = document.querySelector('input'); + * var data = [{ + * name: 'Roy Eldridge', + * year: 1911 + * }, { + * name: 'Roy Hargrove', + * year: 1969 + * }, { + * name: 'Rex Stewart', + * year: 1907 + * }]; + * + * var typeahead = new Suggestions(input, data, { + * filter: false, // Disable filtering + * minLength: 3, // Number of characters typed into an input to trigger suggestions. + * limit: 3, // Max number of results to display. + * hideOnBlur: false // Don't hide results when input loses focus + * }); + * + * // As we're passing an object of an arrays as data, override + * // `getItemValue` by specifying the specific property to search on. + * typeahead.getItemValue = function(item) { return item.name }; + * + * input.addEventListener('change', function() { + * console.log(typeahead.selected); // Current selected item. + * }); + * + * // With browserify + * var Suggestions = require('suggestions'); + * + * new Suggestions(input, data); + */ + var Suggestions = requireSuggestions(); + suggestionsList = Suggestions; + + if (typeof window !== 'undefined') { + window.Suggestions = Suggestions; + } + return suggestionsList; + } + + var suggestionsListExports = requireSuggestionsList(); + var Typeahead = /*@__PURE__*/getDefaultExportFromCjs(suggestionsListExports); + + var subtag$2 = {exports: {}}; + + var subtag$1 = subtag$2.exports; + + var hasRequiredSubtag; + + function requireSubtag () { + if (hasRequiredSubtag) return subtag$2.exports; + hasRequiredSubtag = 1; + (function (module) { + !function(root, name, make) { + if (module.exports) module.exports = make(); + else root[name] = make(); + }(subtag$1, 'subtag', function() { + + var empty = ''; + var pattern = /^([a-zA-Z]{2,3})(?:[_-]+([a-zA-Z]{3})(?=$|[_-]+))?(?:[_-]+([a-zA-Z]{4})(?=$|[_-]+))?(?:[_-]+([a-zA-Z]{2}|[0-9]{3})(?=$|[_-]+))?/; + + function match(tag) { + return tag.match(pattern) || [] + } + + function split(tag) { + return match(tag).filter(function(v, i) { return v && i }) + } + + function api(tag) { + tag = match(tag); + return { + language: tag[1] || empty, + extlang: tag[2] || empty, + script: tag[3] || empty, + region: tag[4] || empty + } + } + + function expose(target, key, value) { + Object.defineProperty(target, key, { + value: value, + enumerable: true + }); + } + + function part(position, pattern, type) { + function method(tag) { + return match(tag)[position] || empty + } + expose(method, 'pattern', pattern); + expose(api, type, method); + } + + part(1, /^[a-zA-Z]{2,3}$/, 'language'); + part(2, /^[a-zA-Z]{3}$/, 'extlang'); + part(3, /^[a-zA-Z]{4}$/, 'script'); + part(4, /^[a-zA-Z]{2}$|^[0-9]{3}$/, 'region'); + + expose(api, 'split', split); + + return api + }); + } (subtag$2)); + return subtag$2.exports; + } + + var subtagExports = requireSubtag(); + var subtag = /*@__PURE__*/getDefaultExportFromCjs(subtagExports); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + var lodash_debounce; + var hasRequiredLodash_debounce; + + function requireLodash_debounce () { + if (hasRequiredLodash_debounce) return lodash_debounce; + hasRequiredLodash_debounce = 1; + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + + /** Used as references for various `Number` constants. */ + var NAN = 0 / 0; + + /** `Object#toString` result references. */ + var symbolTag = '[object Symbol]'; + + /** Used to match leading and trailing whitespace. */ + var reTrim = /^\s+|\s+$/g; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary = /^0b[01]+$/i; + + /** Used to detect octal string values. */ + var reIsOctal = /^0o[0-7]+$/i; + + /** Built-in method references without a dependency on `root`. */ + var freeParseInt = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Used for built-in method references. */ + var objectProto = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString = objectProto.toString; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeMax = Math.max, + nativeMin = Math.min; + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now = function() { + return root.Date.now(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && objectToString.call(value) == symbolTag); + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); + } + + lodash_debounce = debounce; + return lodash_debounce; + } + + var lodash_debounceExports = requireLodash_debounce(); + var debounce = /*@__PURE__*/getDefaultExportFromCjs(lodash_debounceExports); + + var immutableExports = requireImmutable(); + var extend = /*@__PURE__*/getDefaultExportFromCjs(immutableExports); + + var events = {exports: {}}; + + var hasRequiredEvents; + + function requireEvents () { + if (hasRequiredEvents) return events.exports; + hasRequiredEvents = 1; + + var R = typeof Reflect === 'object' ? Reflect : null; + var ReflectApply = R && typeof R.apply === 'function' + ? R.apply + : function ReflectApply(target, receiver, args) { + return Function.prototype.apply.call(target, receiver, args); + }; + + var ReflectOwnKeys; + if (R && typeof R.ownKeys === 'function') { + ReflectOwnKeys = R.ownKeys; + } else if (Object.getOwnPropertySymbols) { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target) + .concat(Object.getOwnPropertySymbols(target)); + }; + } else { + ReflectOwnKeys = function ReflectOwnKeys(target) { + return Object.getOwnPropertyNames(target); + }; + } + + function ProcessEmitWarning(warning) { + if (console && console.warn) console.warn(warning); + } + + var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) { + return value !== value; + }; + + function EventEmitter() { + EventEmitter.init.call(this); + } + events.exports = EventEmitter; + events.exports.once = once; + + // Backwards-compat with node 0.10.x + EventEmitter.EventEmitter = EventEmitter; + + EventEmitter.prototype._events = undefined; + EventEmitter.prototype._eventsCount = 0; + EventEmitter.prototype._maxListeners = undefined; + + // By default EventEmitters will print a warning if more than 10 listeners are + // added to it. This is a useful default which helps finding memory leaks. + var defaultMaxListeners = 10; + + function checkListener(listener) { + if (typeof listener !== 'function') { + throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener); + } + } + + Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: true, + get: function() { + return defaultMaxListeners; + }, + set: function(arg) { + if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { + throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.'); + } + defaultMaxListeners = arg; + } + }); + + EventEmitter.init = function() { + + if (this._events === undefined || + this._events === Object.getPrototypeOf(this)._events) { + this._events = Object.create(null); + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; + }; + + // Obviously not all Emitters should be limited to 10. This function allows + // that to be increased. Set to zero for unlimited. + EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { + throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.'); + } + this._maxListeners = n; + return this; + }; + + function _getMaxListeners(that) { + if (that._maxListeners === undefined) + return EventEmitter.defaultMaxListeners; + return that._maxListeners; + } + + EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return _getMaxListeners(this); + }; + + EventEmitter.prototype.emit = function emit(type) { + var args = []; + for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); + var doError = (type === 'error'); + + var events = this._events; + if (events !== undefined) + doError = (doError && events.error === undefined); + else if (!doError) + return false; + + // If there is no 'error' event listener then throw. + if (doError) { + var er; + if (args.length > 0) + er = args[0]; + if (er instanceof Error) { + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event + } + // At least give some kind of context to the user + var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : '')); + err.context = er; + throw err; // Unhandled 'error' event + } + + var handler = events[type]; + + if (handler === undefined) + return false; + + if (typeof handler === 'function') { + ReflectApply(handler, this, args); + } else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + ReflectApply(listeners[i], this, args); + } + + return true; + }; + + function _addListener(target, type, listener, prepend) { + var m; + var events; + var existing; + + checkListener(listener); + + events = target._events; + if (events === undefined) { + events = target._events = Object.create(null); + target._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + target.emit('newListener', type, + listener.listener ? listener.listener : listener); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = target._events; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events[type] = listener; + ++target._eventsCount; + } else { + if (typeof existing === 'function') { + // Adding the second element, need to change to array. + existing = events[type] = + prepend ? [listener, existing] : [existing, listener]; + // If we've already got an array, just append. + } else if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + + // Check for listener leak + m = _getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + // eslint-disable-next-line no-restricted-syntax + var w = new Error('Possible EventEmitter memory leak detected. ' + + existing.length + ' ' + String(type) + ' listeners ' + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit'); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = existing.length; + ProcessEmitWarning(w); + } + } + + return target; + } + + EventEmitter.prototype.addListener = function addListener(type, listener) { + return _addListener(this, type, listener, false); + }; + + EventEmitter.prototype.on = EventEmitter.prototype.addListener; + + EventEmitter.prototype.prependListener = + function prependListener(type, listener) { + return _addListener(this, type, listener, true); + }; + + function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + if (arguments.length === 0) + return this.listener.call(this.target); + return this.listener.apply(this.target, arguments); + } + } + + function _onceWrap(target, type, listener) { + var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener }; + var wrapped = onceWrapper.bind(state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; + } + + EventEmitter.prototype.once = function once(type, listener) { + checkListener(listener); + this.on(type, _onceWrap(this, type, listener)); + return this; + }; + + EventEmitter.prototype.prependOnceListener = + function prependOnceListener(type, listener) { + checkListener(listener); + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; + + // Emits a 'removeListener' event if and only if the listener was removed. + EventEmitter.prototype.removeListener = + function removeListener(type, listener) { + var list, events, position, i, originalListener; + + checkListener(listener); + + events = this._events; + if (events === undefined) + return this; + + list = events[type]; + if (list === undefined) + return this; + + if (list === listener || list.listener === listener) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + position = -1; + + for (i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + originalListener = list[i].listener; + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + else { + spliceOne(list, position); + } + + if (list.length === 1) + events[type] = list[0]; + + if (events.removeListener !== undefined) + this.emit('removeListener', type, originalListener || listener); + } + + return this; + }; + + EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + + EventEmitter.prototype.removeAllListeners = + function removeAllListeners(type) { + var listeners, events, i; + + events = this._events; + if (events === undefined) + return this; + + // not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = Object.create(null); + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = Object.create(null); + else + delete events[type]; + } + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + var keys = Object.keys(events); + var key; + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = Object.create(null); + this._eventsCount = 0; + return this; + } + + listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return this; + }; + + function _listeners(target, type, unwrap) { + var events = target._events; + + if (events === undefined) + return []; + + var evlistener = events[type]; + if (evlistener === undefined) + return []; + + if (typeof evlistener === 'function') + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + + return unwrap ? + unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length); + } + + EventEmitter.prototype.listeners = function listeners(type) { + return _listeners(this, type, true); + }; + + EventEmitter.prototype.rawListeners = function rawListeners(type) { + return _listeners(this, type, false); + }; + + EventEmitter.listenerCount = function(emitter, type) { + if (typeof emitter.listenerCount === 'function') { + return emitter.listenerCount(type); + } else { + return listenerCount.call(emitter, type); + } + }; + + EventEmitter.prototype.listenerCount = listenerCount; + function listenerCount(type) { + var events = this._events; + + if (events !== undefined) { + var evlistener = events[type]; + + if (typeof evlistener === 'function') { + return 1; + } else if (evlistener !== undefined) { + return evlistener.length; + } + } + + return 0; + } + + EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; + }; + + function arrayClone(arr, n) { + var copy = new Array(n); + for (var i = 0; i < n; ++i) + copy[i] = arr[i]; + return copy; + } + + function spliceOne(list, index) { + for (; index + 1 < list.length; index++) + list[index] = list[index + 1]; + list.pop(); + } + + function unwrapListeners(arr) { + var ret = new Array(arr.length); + for (var i = 0; i < ret.length; ++i) { + ret[i] = arr[i].listener || arr[i]; + } + return ret; + } + + function once(emitter, name) { + return new Promise(function (resolve, reject) { + function errorListener(err) { + emitter.removeListener(name, resolver); + reject(err); + } + + function resolver() { + if (typeof emitter.removeListener === 'function') { + emitter.removeListener('error', errorListener); + } + resolve([].slice.call(arguments)); + } + eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); + if (name !== 'error') { + addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); + } + }); + } + + function addErrorHandlerIfEventEmitter(emitter, handler, flags) { + if (typeof emitter.on === 'function') { + eventTargetAgnosticAddListener(emitter, 'error', handler, flags); + } + } + + function eventTargetAgnosticAddListener(emitter, name, listener, flags) { + if (typeof emitter.on === 'function') { + if (flags.once) { + emitter.once(name, listener); + } else { + emitter.on(name, listener); + } + } else if (typeof emitter.addEventListener === 'function') { + // EventTarget does not have `error` event semantics like Node + // EventEmitters, we do not listen for `error` events here. + emitter.addEventListener(name, function wrapListener(arg) { + // IE does not have builtin `{ once: true }` support so we + // have to do it manually. + if (flags.once) { + emitter.removeEventListener(name, wrapListener); + } + listener(arg); + }); + } else { + throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter); + } + } + return events.exports; + } + + var eventsExports = requireEvents(); + + const exceptions = { + 'fr': { + 'name': 'France', + 'bbox': [[-4.59235, 41.380007], [9.560016, 51.148506]] + }, + 'us': { + 'name': 'United States', + 'bbox': [[-171.791111, 18.91619], [-66.96466, 71.357764]] + }, + 'ru': { + 'name': 'Russia', + 'bbox': [[19.66064, 41.151416], [190.10042, 81.2504]] + }, + 'ca': { + 'name': 'Canada', + 'bbox': [[-140.99778, 41.675105], [-52.648099, 83.23324]] + } + }; + + /** + * Localized values for the placeholder string + * + * @private + */ + const placeholder = { + // list drawn from https://docs.mapbox.com/api/search/#language-coverage + 'de': 'Suche', // german + 'it': 'Ricerca', //italian + 'en': 'Search', // english + 'nl': 'Zoeken', //dutch + 'fr': 'Chercher', //french + 'ca': 'Cerca', //catalan + 'he': 'לחפש', //hebrew + 'ja': 'サーチ', //japanese + 'lv': 'Meklēt', //latvian + 'pt': 'Procurar', //portuguese + 'sr': 'Претрага', //serbian + 'zh': '搜索', //chinese-simplified + 'cs': 'Vyhledávání', //czech + 'hu': 'Keresés', //hungarian + 'ka': 'ძიება', // georgian + 'nb': 'Søke', //norwegian + 'sk': 'Vyhľadávanie', //slovak + 'th': 'ค้นหา', //thai + 'fi': 'Hae', //finnish + 'is': 'Leita', //icelandic + 'ko': '수색', //korean + 'pl': 'Szukaj', //polish + 'sl': 'Iskanje', //slovenian + 'fa': 'جستجو', //persian(aka farsi) + 'ru': 'Поиск' //russian + }; + + /** + * A regular expression to match coordinates. + */ + const COORDINATES_REGEXP = /(-?\d+\.?\d*)[, ]+(-?\d+\.?\d*)[ ]*$/; + /** + * A geocoder component that works with maplibre + */ + class MaplibreGeocoder { + constructor(geocoderApi, options) { + this.options = { + zoom: 16, + flyTo: true, + trackProximity: true, + showResultsWhileTyping: false, + minLength: 2, + reverseGeocode: false, + limit: 5, + enableEventLogging: true, + marker: true, + popup: false, + maplibregl: undefined, + collapsed: false, + clearAndBlurOnEsc: false, + clearOnBlur: false, + proximityMinZoom: 9, + getItemValue: (item) => { + return item.text !== undefined ? item.text : item.place_name; + }, + render: function (item) { + // Render as a suggestion + if (!item.geometry) { + const suggestionString = item.text; + const indexOfMatch = suggestionString + .toLowerCase() + .indexOf(this.query.toLowerCase()); + const lengthOfMatch = this.query.length; + const beforeMatch = suggestionString.substring(0, indexOfMatch); + const match = suggestionString.substring(indexOfMatch, indexOfMatch + lengthOfMatch); + const afterMatch = suggestionString.substring(indexOfMatch + lengthOfMatch); + return ('
        ' + + '' + + '
        ' + + '
        ' + + beforeMatch + + '' + + match + + "" + + afterMatch + + "
        " + + "
        " + + "
        "); + } + // render as a search result + const placeName = item.place_name.split(","); + return ('
        ' + + '' + + "
        " + + '
        ' + + placeName[0] + + "
        " + + '
        ' + + placeName.splice(1, placeName.length).join(",") + + "
        " + + "
        " + + "
        "); + }, + popupRender: (item) => { + const placeName = item.place_name.split(","); + return ('"); + }, + showResultMarkers: true, + debounceSearch: 200, + }; + this._eventEmitter = new eventsExports.EventEmitter(); + this.options = extend({}, this.options, options); + this.fresh = true; + this.lastSelected = null; + this.geocoderApi = geocoderApi; + } + /** + * Add the geocoder to a container. The container can be either a `Map`, an `HTMLElement` or a CSS selector string. + * + * If the container is a [`Map`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map), this function will behave identically to [`Map.addControl(geocoder)`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map#addcontrol). + * If the container is an instance of [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement), then the geocoder will be appended as a child of that [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). + * If the container is a [CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), the geocoder will be appended to the element returned from the query. + * + * This function will throw an error if the container is none of the above. + * It will also throw an error if the referenced HTML element cannot be found in the `document.body`. + * + * For example, if the HTML body contains the element `
        `, the following script will append the geocoder to `#geocoder-container`: + * @example + * ```js + * const GeoApi = { + * forwardGeocode: (config) => { return { features: [] } }, + * reverseGeocode: (config) => { return { features: [] } } + * } + * const geocoder = new MaplibreGeocoder(GeoAPI, {}); + * geocoder.addTo('#geocoder-container'); + * ``` + * @param container - A reference to the container to which to add the geocoder + */ + addTo(container) { + function addToExistingContainer(geocoder, container) { + if (!document.body.contains(container)) { + throw new Error("Element provided to #addTo() exists, but is not in the DOM"); + } + const el = geocoder.onAdd(); //returns the input elements, which are then added to the requested html container + container.appendChild(el); + } + // if the container is an HTMLElement, then set the parent to be that element + if (container instanceof HTMLElement) { + addToExistingContainer(this, container); + } + // if the container is a string, treat it as a CSS query + else if (typeof container == "string") { + const parent = document.querySelectorAll(container); + if (parent.length === 0) { + throw new Error("Element " + container + "not found."); + } + if (parent.length > 1) { + throw new Error("Geocoder can only be added to a single html element"); + } + addToExistingContainer(this, parent[0]); + } + // if the container is a map, add the control like normal + else if ('addControl' in container) { + // it's a maplibre-gl map, add like normal + container.addControl(this); + } + else { + throw new Error("Error: addTo must be a maplibre-gl-js map, an html element, or a CSS selector query for a single html element"); + } + } + onAdd(map) { + if (map && typeof map != "string") { + this._map = map; + } + this.setLanguage(); + if (this.options.localGeocoderOnly && !this.options.localGeocoder) { + throw new Error("A localGeocoder function must be specified to use localGeocoderOnly mode"); + } + this._onChange = this._onChange.bind(this); + this._onKeyDown = this._onKeyDown.bind(this); + this._onPaste = this._onPaste.bind(this); + this._onBlur = this._onBlur.bind(this); + this._showButton = this._showButton.bind(this); + this._hideButton = this._hideButton.bind(this); + this._onQueryResult = this._onQueryResult.bind(this); + this.clear = this.clear.bind(this); + this._updateProximity = this._updateProximity.bind(this); + this._collapse = this._collapse.bind(this); + this._unCollapse = this._unCollapse.bind(this); + this._clear = this._clear.bind(this); + this._clearOnBlur = this._clearOnBlur.bind(this); + const el = (this.container = document.createElement("div")); + el.className = + "maplibregl-ctrl-geocoder maplibregl-ctrl maplibregl-ctrl-geocoder maplibregl-ctrl"; + const searchIcon = this.createIcon("search", ''); + this._inputEl = document.createElement("input"); + this._inputEl.type = "text"; + this._inputEl.className = + "maplibregl-ctrl-geocoder--input"; + this.setPlaceholder(); + if (this.options.collapsed) { + this._collapse(); + this.container.addEventListener("mouseenter", this._unCollapse); + this.container.addEventListener("mouseleave", this._collapse); + this._inputEl.addEventListener("focus", this._unCollapse); + } + if (this.options.collapsed || this.options.clearOnBlur) { + this._inputEl.addEventListener("blur", this._onBlur); + } + this._inputEl.addEventListener("keydown", debounce(this._onKeyDown, this.options.debounceSearch)); + this._inputEl.addEventListener("paste", this._onPaste); + this._inputEl.addEventListener("change", this._onChange); + this.container.addEventListener("mouseenter", this._showButton); + this.container.addEventListener("mouseleave", this._hideButton); + const actions = document.createElement("div"); + actions.classList.add("maplibregl-ctrl-geocoder--pin-right"); + this._clearEl = document.createElement("button"); + this._clearEl.setAttribute("type", "button"); + this._clearEl.setAttribute("aria-label", "Clear"); + this._clearEl.addEventListener("click", this.clear); + this._clearEl.className = "maplibregl-ctrl-geocoder--button"; + const buttonIcon = this.createIcon("close", ''); + this._clearEl.appendChild(buttonIcon); + this._loadingEl = this.createIcon("loading", ''); + actions.appendChild(this._clearEl); + actions.appendChild(this._loadingEl); + el.appendChild(searchIcon); + el.appendChild(this._inputEl); + el.appendChild(actions); + this._typeahead = new Typeahead(this._inputEl, [], { + filter: false, + minLength: this.options.minLength, + limit: this.options.limit, + noInitialSelection: true, + }); + this.setRenderFunction(this.options.render); + this._typeahead.getItemValue = this.options.getItemValue; + this.mapMarker = null; + this.resultMarkers = []; + this._handleMarker = this._handleMarker.bind(this); + this._handleResultMarkers = this._handleResultMarkers.bind(this); + if (this._map) { + if (this.options.trackProximity) { + this._updateProximity(); + this._map.on("moveend", this._updateProximity); + } + this._maplibregl = this.options.maplibregl; + if (!this._maplibregl && this.options.marker) { + console.error("No maplibregl detected in options. Map markers are disabled. Please set options.maplibregl."); + this.options.marker = false; + } + } + return el; + } + createIcon(name, path) { + const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + icon.setAttribute("class", "maplibregl-ctrl-geocoder--icon maplibregl-ctrl-geocoder--icon-" + name); + icon.setAttribute("viewBox", "0 0 18 18"); + icon.setAttribute("xml:space", "preserve"); + icon.setAttribute("width", "18"); + icon.setAttribute("height", "18"); + // IE does not have innerHTML for SVG nodes + if (!("innerHTML" in icon)) { + const SVGNodeContainer = document.createElement("div"); + SVGNodeContainer.innerHTML = + "" + path.valueOf().toString() + ""; + const SVGNode = SVGNodeContainer.firstChild, SVGPath = SVGNode.firstChild; + icon.appendChild(SVGPath); + } + else { + icon.innerHTML = path; + } + return icon; + } + onRemove() { + this.container.remove(); + if (this.options.trackProximity && this._map) { + this._map.off("moveend", this._updateProximity); + } + this._removeMarker(); + this._map = null; + return this; + } + _onPaste(e) { + const value = (e.clipboardData || window.clipboardData).getData("text"); + if (value.length >= this.options.minLength && + this.options.showResultsWhileTyping) { + this._geocode(value); + } + } + _onKeyDown(e) { + const ESC_KEY_CODE = 27; + const TAB_KEY_CODE = 9; + const ENTER_KEY_CODE = 13; + if (e.keyCode === ESC_KEY_CODE && this.options.clearAndBlurOnEsc) { + this._clear(e); + return this._inputEl.blur(); + } + // if target has shadowRoot, then get the actual active element inside the shadowRoot + const target = e.target && e.target.shadowRoot + ? e.target.shadowRoot.activeElement + : e.target; + const value = target ? target.value : ""; + if (!value) { + this.fresh = true; + // the user has removed all the text + if (e.keyCode !== TAB_KEY_CODE) + this.clear(e); + return (this._clearEl.style.display = "none"); + } + // TAB, ESC, LEFT, RIGHT, UP, DOWN + if (e.metaKey || + [TAB_KEY_CODE, ESC_KEY_CODE, 37, 39, 38, 40].indexOf(e.keyCode) !== -1) + return; + // ENTER + if (e.keyCode === ENTER_KEY_CODE) { + if (!this.options.showResultsWhileTyping) { + if (!this._typeahead.selected) { + this._geocode(target.value); + } + } + else { + // Pressing enter on the search box will do a search for the currently string input + if (this._typeahead.selected == null && + this.geocoderApi.getSuggestions) { + this._geocode(target.value, true); + // If suggestions API is not defined pressing enter while the input box is selected will try to fit the results into the current map view + } + else if (this._typeahead.selected == null) { + if (this.options.showResultMarkers) { + this._fitBoundsForMarkers(); + } + } + return; + } + } + // Show results while typing and greater than min length + if (target.value.length >= this.options.minLength && + this.options.showResultsWhileTyping) { + this._geocode(target.value); + } + } + _showButton() { + if (this._inputEl.value.length > 0) + this._clearEl.style.display = "block"; + } + _hideButton() { + if (this._typeahead.selected) + this._clearEl.style.display = "none"; + } + _onBlur(e) { + if (this.options.clearOnBlur) { + this._clearOnBlur(e); + } + if (this.options.collapsed) { + this._collapse(); + } + } + // Change events are fire by suggestions library whenever the enter key is pressed or input is blurred + // This can sometimes cause strange behavior as this function is called before our own onKeyDown handler and thus + // we cannot depend on some internal values of the suggestion state like `selected` as those will change or before + // our onKeyDown handler. + _onChange() { + const selected = this._typeahead.selected; + // If a suggestion was selected + if (selected && !selected.geometry) { + if (selected.placeId) + this._geocode(selected.placeId, true, true); + else + this._geocode(selected.text, true); + } + else if (selected && JSON.stringify(selected) !== this.lastSelected) { + this._clearEl.style.display = "none"; + if (this.options.flyTo) { + let flyOptions; + this._removeResultMarkers(); + if (selected.properties && exceptions[selected.properties.short_code]) { + // Certain geocoder search results return (and therefore zoom to fit) + // an unexpectedly large bounding box: for example, both Russia and the + // USA span both sides of -180/180, or France includes the island of + // Reunion in the Indian Ocean. An incomplete list of these exceptions + // at ./exceptions.json provides "reasonable" bounding boxes as a + // short-term solution; this may be amended as necessary. + flyOptions = extend({}, this.options.flyTo); + if (this._map) { + this._map.fitBounds(exceptions[selected.properties.short_code].bbox, flyOptions); + } + } + else if (selected.bbox) { + const bbox = selected.bbox; + flyOptions = extend({}, this.options.flyTo); + if (this._map) { + this._map.fitBounds([ + [bbox[0], bbox[1]], + [bbox[2], bbox[3]], + ], flyOptions); + } + } + else { + const defaultFlyOptions = { + zoom: this.options.zoom, + }; + flyOptions = extend({}, defaultFlyOptions, this.options.flyTo); + // ensure that center is not overriden by custom options + if (selected.center) { + flyOptions.center = selected.center; + } + else if (selected.geometry && + selected.geometry.type && + selected.geometry.type === "Point" && + selected.geometry.coordinates) { + flyOptions.center = selected.geometry.coordinates; + } + if (this._map) { + this._map.flyTo(flyOptions); + } + } + } + if (this.options.marker && this._maplibregl) { + this._handleMarker(selected); + } + // After selecting a feature, re-focus the textarea and set + // cursor at start, and reset the selected feature. + this._inputEl.focus(); + this._inputEl.scrollLeft = 0; + this._inputEl.setSelectionRange(0, 0); + this.lastSelected = JSON.stringify(selected); + this._typeahead.selected = null; // reset selection current selection value and set it to last selected + this._eventEmitter.emit("result", { result: selected }); + } + } + _getConfigForRequest() { + // Possible config proprerties to pass to client + const keys = [ + "bbox", + "limit", + "proximity", + "countries", + "types", + "language", + "reverseMode", + ]; + // Create config object + const config = keys.reduce((config, key) => { + if (this.options[key]) { + if (["countries", "types", "language"].indexOf(key) > -1) { + (config[key] = this.options[key].split(/[\s,]+/)); + } + else { + (config[key] = this.options[key]); + } + if (key === "proximity" && + this.options[key] && + typeof this.options[key].longitude === "number" && + typeof this.options[key].latitude === "number") { + config[key] = [ + this.options[key].longitude, + this.options[key].latitude, + ]; + } + } + return config; + }, {}); + return config; + } + _geocode(searchInput_1) { + return __awaiter(this, arguments, undefined, function* (searchInput, isSuggestion = false, isPlaceId = false) { + this._loadingEl.style.display = "block"; + this._eventEmitter.emit("loading", { query: searchInput }); + const config = this._getConfigForRequest(); + const request = this._createGeocodeRequest(config, searchInput, isSuggestion, isPlaceId); + const localGeocoderResults = this.options.localGeocoder + ? (this.options.localGeocoder(searchInput) || []) + : []; + try { + const response = yield request; + yield this._handleGeocodeResponse(response, config, searchInput, isSuggestion, localGeocoderResults); + } + catch (err) { + this._handleGeocodeErrorResponse(err, localGeocoderResults); + } + return request; + }); + } + _createGeocodeRequest(config, searchInput, isSuggestion, isPlaceId) { + if (this.options.localGeocoderOnly) { + return Promise.resolve({}); + } + if (this.options.reverseGeocode && COORDINATES_REGEXP.test(searchInput)) { + // searchInput resembles coordinates, make the request a reverseGeocode + return this._createReverseGeocodeRequest(searchInput, config); + } + config.query = searchInput; + if (!this.geocoderApi.getSuggestions) { + return this.geocoderApi.forwardGeocode(config); + } + if (!isSuggestion) { + // user typed in text and should receive suggestions + return this.geocoderApi.getSuggestions(config); + } + // user clicked on a suggestion + if (this.geocoderApi.searchByPlaceId && isPlaceId) { + // suggestion has place Id + return this.geocoderApi.searchByPlaceId(config); + } + return this.geocoderApi.forwardGeocode(config); + } + _createReverseGeocodeRequest(searchInput, config) { + // parse coordinates + const coords = searchInput + .split(/[\s(,)?]+/) + .map((c) => parseFloat(c)) + .reverse(); + // client only accepts one type for reverseGeocode, so + // use first config type if one, if not default to poi + config.query = coords; + config.limit = 1; + // drop proximity which may have been set by trackProximity since it's not supported by the reverseGeocoder + if ("proximity" in config) { + delete config.proximity; + } + return this.geocoderApi.reverseGeocode(config); + } + _handleGeocodeResponse(response, config, searchInput, isSuggestion, localGeocoderResults) { + return __awaiter(this, undefined, undefined, function* () { + this._loadingEl.style.display = "none"; + let res = {}; + if (!response) { + res = { + type: "FeatureCollection", + features: [], + }; + } + else { + res = response; + } + res.config = config; + if (this.fresh) { + this.fresh = false; + } + // supplement Maplibre Geocoding API results with locally populated results + res.features = res.features + ? localGeocoderResults.concat(res.features) + : localGeocoderResults; + const externalGeocoderResultsPromise = this.options.externalGeocoder + ? (this.options.externalGeocoder(searchInput, res.features, config) || Promise.resolve([])) + : Promise.resolve([]); + // supplement Geocoding API results with features returned by a promise + try { + const features = yield externalGeocoderResultsPromise; + res.features = res.features + ? features.concat(res.features) + : features; + } + catch (_a) { + // on error, display the original result + } + // apply results filter if provided + if (this.options.filter && res.features.length) { + res.features = res.features.filter(this.options.filter); + } + let results = []; + if ('suggestions' in res) { + results = res.suggestions; + } + else if ('place' in res) { + results = [res.place]; + } + else { + results = res.features; + } + if (results.length) { + this._clearEl.style.display = "block"; + this._typeahead.update(results); + if ((!this.options.showResultsWhileTyping || isSuggestion) && + this.options.showResultMarkers && + (res.features.length > 0 || 'place' in res)) { + this._fitBoundsForMarkers(); + } + this._eventEmitter.emit("results", res); + } + else { + this._clearEl.style.display = "none"; + this._typeahead.selected = null; + this._renderNoResults(); + this._eventEmitter.emit("results", res); + } + }); + } + _handleGeocodeErrorResponse(error, localGeocoderResults) { + this._loadingEl.style.display = "none"; + // in the event of an error in the Geocoding API still display results from the localGeocoder + if (localGeocoderResults.length && this.options.localGeocoder) { + this._clearEl.style.display = "block"; + this._typeahead.update(localGeocoderResults); + } + else { + this._clearEl.style.display = "none"; + this._typeahead.selected = null; + this._renderError(); + } + this._eventEmitter.emit("results", { features: localGeocoderResults }); + this._eventEmitter.emit("error", { error }); + } + /** + * Shared logic for clearing input + * @param ev - the event that triggered the clear, if available + */ + _clear(ev) { + if (ev) + ev.preventDefault(); + this._inputEl.value = ""; + this._typeahead.selected = null; + this._typeahead.clear(); + this._onChange(); + this._clearEl.style.display = "none"; + this._removeMarker(); + this._removeResultMarkers(); + this.lastSelected = null; + this._eventEmitter.emit("clear"); + this.fresh = true; + } + /** + * Clear and then focus the input. + * @param ev - the event that triggered the clear, if available + * + */ + clear(ev) { + this._clear(ev); + this._inputEl.focus(); + } + /** + * Clear the input, without refocusing it. Used to implement clearOnBlur + * constructor option. + * @param ev - the blur event + */ + _clearOnBlur(ev) { + /* + * If relatedTarget is not found, assume user targeted the suggestions list. + * In that case, do not clear on blur. There are other edge cases where + * ev.relatedTarget could be null. Clicking on list always results in null + * relatedtarget because of upstream behavior in `suggestions`. + * + * The ideal solution would be to check if ev.relatedTarget is a child of + * the list. See issue #258 for details on why we can't do that yet. + */ + if (ev.relatedTarget) { + this._clear(ev); + } + } + _onQueryResult(results) { + if (!('features' in results)) { + return; + } + if (!results.features.length) + return; + const result = results.features[0]; + this._typeahead.selected = result; + this._inputEl.value = result.place_name; + this._onChange(); + } + _updateProximity() { + // proximity is designed for local scale, if the user is looking at the whole world, + // it doesn't make sense to factor in the arbitrary centre of the map + if (!this._map) { + return; + } + if (this._map.getZoom() > this.options.proximityMinZoom) { + const center = this._map.getCenter().wrap(); + this.setProximity({ longitude: center.lng, latitude: center.lat }); + } + else { + this.setProximity(null); + } + } + _collapse() { + // do not collapse if input is in focus + if (!this._inputEl.value && this._inputEl !== document.activeElement) + this.container.classList.add("maplibregl-ctrl-geocoder--collapsed"); + } + _unCollapse() { + this.container.classList.remove("maplibregl-ctrl-geocoder--collapsed"); + } + /** + * Set & query the input + * @param searchInput - location name or other search input + */ + query(searchInput) { + return __awaiter(this, undefined, undefined, function* () { + const results = yield this._geocode(searchInput); + this._onQueryResult(results); + }); + } + _renderError() { + const errorMessage = "
        There was an error reaching the server
        "; + this._renderMessage(errorMessage); + } + _renderNoResults() { + const errorMessage = "
        No results found
        "; + this._renderMessage(errorMessage); + } + _renderMessage(msg) { + this._typeahead.update([]); + this._typeahead.selected = null; + this._typeahead.clear(); + this._typeahead.renderError(msg); + } + /** + * Get the text to use as the search bar placeholder + * + * If placeholder is provided in options, then use options.placeholder + * Otherwise, if language is provided in options, then use the localized string of the first language if available + * Otherwise use the default + * + * @returns the value to use as the search bar placeholder + */ + _getPlaceholderText() { + if (this.options.placeholder) + return this.options.placeholder; + if (this.options.language) { + const firstLanguage = this.options.language.split(",")[0]; + const language = subtag.language(firstLanguage); + const localizedValue = placeholder[language]; + if (localizedValue) + return localizedValue; + } + return "Search"; + } + /** + * Fits the map to the current bounds for the searched results + */ + _fitBoundsForMarkers() { + if (this._typeahead.data.length < 1) + return; + // Filter out suggestions and restrict to limit + const results = this._typeahead.data + .filter((result) => { + return typeof result === "string" ? false : true; + }) + .slice(0, this.options.limit); + this._clearEl.style.display = "none"; + if (this.options.flyTo && this._maplibregl) { + if (this._map) { + const defaultFlyOptions = { padding: 100 }; + const flyOptions = extend({}, defaultFlyOptions, this.options.flyTo); + const bounds = new this._maplibregl.LngLatBounds(); + for (const feature of results) { + bounds.extend(feature.geometry.coordinates); + } + this._map.fitBounds(bounds, flyOptions); + } + } + if (results.length > 0 && this._maplibregl) { + this._handleResultMarkers(results); + } + return this; + } + /** + * Set input + * @param searchInput - location name or other search input + */ + setInput(searchInput) { + // Set input value to passed value and clear everything else. + this._inputEl.value = searchInput; + this._typeahead.selected = null; + this._typeahead.clear(); + if (searchInput.length >= this.options.minLength && + this.options.showResultsWhileTyping) { + this._geocode(searchInput); + } + return this; + } + /** + * Set proximity + * @param proximity - The new `options.proximity` value. This is a geographical point given as an object with `latitude` and `longitude` properties. + */ + setProximity(proximity) { + this.options.proximity = proximity; + return this; + } + /** + * Get proximity + * @returns The geocoder proximity + */ + getProximity() { + return this.options.proximity; + } + /** + * Set the render function used in the results dropdown + * @param fn - The function to use as a render function. This function accepts a single {@link CarmenGeojsonFeature} object as input and returns a string. + */ + setRenderFunction(fn) { + if (fn && typeof fn == "function") { + this._typeahead.render = fn; + } + return this; + } + /** + * Get the function used to render the results dropdown + * + * @returns the render function + */ + getRenderFunction() { + return this._typeahead.render; + } + /** + * Get the language to use in UI elements and when making search requests + * + * Look first at the explicitly set options otherwise use the browser's language settings + * @param language - Specify the language to use for response text and query result weighting. Options are IETF language tags comprised of a mandatory ISO 639-1 language code and optionally one or more IETF subtags for country or script. More than one value can also be specified, separated by commas. + */ + setLanguage(language) { + this.options.language = language || this.options.language || navigator.language; + return this; + } + /** + * Get the language to use in UI elements and when making search requests + * @returns The language(s) used by the plugin, if any + */ + getLanguage() { + return this.options.language; + } + /** + * Get the zoom level the map will move to when there is no bounding box on the selected result + * @returns the map zoom + */ + getZoom() { + return this.options.zoom; + } + /** + * Set the zoom level + * @param zoom - The zoom level that the map should animate to when a `bbox` isn't found in the response. If a `bbox` is found the map will fit to the `bbox`. + * @returns this + */ + setZoom(zoom) { + this.options.zoom = zoom; + return this; + } + /** + * Get the parameters used to fly to the selected response, if any + * @returns The `flyTo` option + */ + getFlyTo() { + return this.options.flyTo; + } + /** + * Set the flyTo options + * @param flyTo - If false, animating the map to a selected result is disabled. If true, animating the map will use the default animation parameters. If an object, it will be passed as `options` to the map [`flyTo`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map#flyto) or [`fitBounds`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map#fitbounds) method providing control over the animation of the transition. + */ + setFlyTo(flyTo) { + this.options.flyTo = flyTo; + return this; + } + /** + * Get the value of the placeholder string + * @returns The input element's placeholder value + */ + getPlaceholder() { + return this.options.placeholder; + } + /** + * Set the value of the input element's placeholder + * @param placeholder - the text to use as the input element's placeholder + */ + setPlaceholder(placeholder) { + this.placeholder = placeholder ? placeholder : this._getPlaceholderText(); + this._inputEl.placeholder = this.placeholder; + this._inputEl.setAttribute("aria-label", this.placeholder); + return this; + } + /** + * Get the bounding box used by the plugin + * @returns the bounding box, if any + */ + getBbox() { + return this.options.bbox; + } + /** + * Set the bounding box to limit search results to + * @param bbox - a bounding box given as an array in the format [minX, minY, maxX, maxY]. + */ + setBbox(bbox) { + this.options.bbox = bbox; + return this; + } + /** + * Get a list of the countries to limit search results to + * @returns a comma separated list of countries to limit to, if any + */ + getCountries() { + return this.options.countries; + } + /** + * Set the countries to limit search results to + * @param countries - a comma separated list of countries to limit to + */ + setCountries(countries) { + this.options.countries = countries; + return this; + } + /** + * Get a list of the types to limit search results to + * @returns a comma separated list of types to limit to + */ + getTypes() { + return this.options.types; + } + /** + * Set the types to limit search results to + * @param types - a comma separated list of types to limit to + */ + setTypes(types) { + this.options.types = types; + return this; + } + /** + * Get the minimum number of characters typed to trigger results used in the plugin + * @returns The minimum length in characters before a search is triggered + */ + getMinLength() { + return this.options.minLength; + } + /** + * Set the minimum number of characters typed to trigger results used by the plugin + * @param minLength - the minimum length in characters + */ + setMinLength(minLength) { + this.options.minLength = minLength; + if (this._typeahead) + this._typeahead.options.minLength = minLength; + return this; + } + /** + * Get the limit value for the number of results to display used by the plugin + * @returns The limit value for the number of results to display used by the plugin + */ + getLimit() { + return this.options.limit; + } + /** + * Set the limit value for the number of results to display used by the plugin + * @param limit - the number of search results to return + */ + setLimit(limit) { + this.options.limit = limit; + if (this._typeahead) + this._typeahead.options.limit = limit; + return this; + } + /** + * Get the filter function used by the plugin + * @returns the filter function + */ + getFilter() { + return this.options.filter; + } + /** + * Set the filter function used by the plugin. + * @param filter - A function which accepts a {@link CarmenGeojsonFeature} to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise. + */ + setFilter(filter) { + this.options.filter = filter; + return this; + } + /** + * Set the geocoding api used by the plugin. + */ + setGeocoderApi(geocoderApi) { + this.geocoderApi = geocoderApi; + return this; + } + /** + * Get the geocoding endpoint the plugin is currently set to + * @returns the geocoding API + */ + getGeocoderApi() { + return this.geocoderApi; + } + /** + * Handle the placement of a result marking the selected result + * @param selected - the selected geojson feature + */ + _handleMarker(selected) { + // clean up any old marker that might be present + if (!this._map) { + return; + } + this._removeMarker(); + const defaultMarkerOptions = { + color: "#4668F2", + }; + const markerOptions = extend({}, defaultMarkerOptions, this.options.marker); + this.mapMarker = new this._maplibregl.Marker(markerOptions); + let popup; + if (this.options.popup) { + const defaultPopupOptions = {}; + const popupOptions = extend({}, defaultPopupOptions, this.options.popup); + popup = new this._maplibregl.Popup(popupOptions).setHTML(this.options.popupRender(selected)); + } + if (selected.center) { + this.mapMarker.setLngLat(selected.center).addTo(this._map); + if (this.options.popup) + this.mapMarker.setPopup(popup); + } + else if (selected.geometry && + selected.geometry.type && + selected.geometry.type === "Point" && + selected.geometry.coordinates) { + this.mapMarker.setLngLat(selected.geometry.coordinates).addTo(this._map); + if (this.options.popup) + this.mapMarker.setPopup(popup); + } + return this; + } + /** + * Handle the removal of a result marker + */ + _removeMarker() { + if (this.mapMarker) { + this.mapMarker.remove(); + this.mapMarker = null; + } + } + /** + * Handle the placement of a result marking the selected result + * @param results - the top results to display on the map + */ + _handleResultMarkers(results) { + // clean up any old marker that might be present + if (!this._map) { + return; + } + this._removeResultMarkers(); + const defaultMarkerOptions = { + color: "#4668F2", + }; + let markerOptions = extend({}, defaultMarkerOptions, this.options.showResultMarkers); + for (const result of results) { + let el; + if (this.options.showResultMarkers) { + if (this.options.showResultMarkers && + this.options.showResultMarkers.element) { + el = this.options.showResultMarkers.element.cloneNode(true); + markerOptions = extend(markerOptions, { element: el }); + } + const marker = new this._maplibregl.Marker(extend({}, markerOptions, { element: el })); + let popup; + if (this.options.popup) { + const defaultPopupOptions = {}; + const popupOptions = extend({}, defaultPopupOptions, this.options.popup); + popup = new this._maplibregl.Popup(popupOptions).setHTML(this.options.popupRender(result)); + } + if (result.center) { + marker.setLngLat(result.center).addTo(this._map); + if (this.options.popup) + marker.setPopup(popup); + } + else if (result.geometry && + result.geometry.type && + result.geometry.type === "Point" && + result.geometry.coordinates) { + marker.setLngLat(result.geometry.coordinates).addTo(this._map); + if (this.options.popup) + marker.setPopup(popup); + } + this.resultMarkers.push(marker); + } + } + return this; + } + /** + * Handle the removal of a result marker + */ + _removeResultMarkers() { + if (this.resultMarkers && this.resultMarkers.length > 0) { + this.resultMarkers.forEach(function (marker) { + marker.remove(); + }); + this.resultMarkers = []; + } + } + /** + * Subscribe to events that happen within the plugin. + * @param type - name of event. Available events and the data passed into their respective event objects are: + * + * - __clear__ `Emitted when the input is cleared` + * - __loading__ `{ query } Emitted when the geocoder is looking up a query` + * - __results__ `{ results } Fired when the geocoder returns a response` + * - __result__ `{ result } Fired when input is set` + * - __error__ `{ error } Error as string` + * @param fn - function that's called when the event is emitted. + */ + on(type, fn) { + this._eventEmitter.on(type, fn); + return this; + } + /** + * Subscribe to events that happen within the plugin only once. + * @param type - Event name. + * Available events and the data passed into their respective event objects are: + * + * - __clear__ `Emitted when the input is cleared` + * - __loading__ `{ query } Emitted when the geocoder is looking up a query` + * - __results__ `{ results } Fired when the geocoder returns a response` + * - __result__ `{ result } Fired when input is set` + * - __error__ `{ error } Error as string` + * @returns a Promise that resolves when the event is emitted. + */ + once(type) { + return new Promise((resolve) => { + this._eventEmitter.once(type, resolve); + }); + } + /** + * Remove an event + * @param type - Event name. + * @param fn - Function that should unsubscribe to the event emitted. + */ + off(type, fn) { + this._eventEmitter.removeListener(type, fn); + return this; + } + } + + return MaplibreGeocoder; + +})); diff --git a/streetup/common/static/common/maplibre-gl.css b/streetup/common/static/common/maplibre-gl.css new file mode 100644 index 0000000..3f86747 --- /dev/null +++ b/streetup/common/static/common/maplibre-gl.css @@ -0,0 +1 @@ +.maplibregl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgb(0,0,0,0)}.maplibregl-canvas{left:0;position:absolute;top:0}.maplibregl-map:fullscreen{height:100%;width:100%}.maplibregl-ctrl-group button.maplibregl-ctrl-compass{touch-action:none}.maplibregl-canvas-container.maplibregl-interactive,.maplibregl-ctrl-group button.maplibregl-ctrl-compass{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-canvas-container.maplibregl-interactive.maplibregl-track-pointer{cursor:pointer}.maplibregl-canvas-container.maplibregl-interactive:active,.maplibregl-ctrl-group button.maplibregl-ctrl-compass:active{cursor:grabbing}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-canvas-container.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:pinch-zoom}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:none}.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures,.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-top-left,.maplibregl-ctrl-top-right{pointer-events:none;position:absolute;z-index:2}.maplibregl-ctrl-top-left{left:0;top:0}.maplibregl-ctrl-top-right{right:0;top:0}.maplibregl-ctrl-bottom-left{bottom:0;left:0}.maplibregl-ctrl-bottom-right{bottom:0;right:0}.maplibregl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.maplibregl-ctrl-top-left .maplibregl-ctrl{float:left;margin:10px 0 0 10px}.maplibregl-ctrl-top-right .maplibregl-ctrl{float:right;margin:10px 10px 0 0}.maplibregl-ctrl-bottom-left .maplibregl-ctrl{float:left;margin:0 0 10px 10px}.maplibregl-ctrl-bottom-right .maplibregl-ctrl{float:right;margin:0 10px 10px 0}.maplibregl-ctrl-group{background:#fff;border-radius:4px}.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (forced-colors:active){.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.maplibregl-ctrl-group button{background-color:transparent;border:0;box-sizing:border-box;cursor:pointer;display:block;height:29px;outline:none;padding:0;width:29px}.maplibregl-ctrl-group button+button{border-top:1px solid #ddd}.maplibregl-ctrl button .maplibregl-ctrl-icon{background-position:50%;background-repeat:no-repeat;display:block;height:100%;width:100%}@media (forced-colors:active){.maplibregl-ctrl-icon{background-color:transparent}.maplibregl-ctrl-group button+button{border-top:1px solid ButtonText}}.maplibregl-ctrl button::-moz-focus-inner{border:0;padding:0}.maplibregl-ctrl-attrib-button:focus,.maplibregl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl button:disabled{cursor:not-allowed}.maplibregl-ctrl button:disabled .maplibregl-ctrl-icon{opacity:.25}@media (hover:hover){.maplibregl-ctrl button:not(:disabled):hover{background-color:rgba(0,0,0,.05)}}.maplibregl-ctrl button:not(:disabled):active{background-color:rgba(0,0,0,.05)}.maplibregl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.maplibregl-ctrl-group button:focus:first-child{border-radius:4px 4px 0 0}.maplibregl-ctrl-group button:focus:last-child{border-radius:0 0 4px 4px}.maplibregl-ctrl-group button:focus:only-child{border-radius:inherit}.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-globe .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='none' stroke='%23333' viewBox='0 0 22 22'%3E%3Ccircle cx='11' cy='11' r='8.5'/%3E%3Cpath d='M17.5 11c0 4.819-3.02 8.5-6.5 8.5S4.5 15.819 4.5 11 7.52 2.5 11 2.5s6.5 3.681 6.5 8.5Z'/%3E%3Cpath d='M13.5 11c0 2.447-.331 4.64-.853 6.206-.262.785-.562 1.384-.872 1.777-.314.399-.58.517-.775.517s-.461-.118-.775-.517c-.31-.393-.61-.992-.872-1.777C8.831 15.64 8.5 13.446 8.5 11s.331-4.64.853-6.206c.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517s.461.118.775.517c.31.393.61.992.872 1.777.522 1.565.853 3.76.853 6.206Z'/%3E%3Cpath d='M11 7.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138q.07-.058.224-.138c.299-.151.763-.302 1.379-.434C7.378 5.666 9.091 5.5 11 5.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138q-.07.058-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428ZM4.486 6.436ZM11 16.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138 1.3 1.3 0 0 1 .224-.138c.299-.151.763-.302 1.379-.434C7.378 14.666 9.091 14.5 11 14.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138a1.3 1.3 0 0 1-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm-6.514-1.064ZM11 12.5c-2.46 0-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.5 1.5 0 0 1-.39-.272.3.3 0 0 1-.047-.064.3.3 0 0 1 .048-.064c.066-.073.189-.167.389-.272.399-.21 1.009-.413 1.805-.59C6.328 9.722 8.54 9.5 11 9.5s4.672.222 6.256.574c.795.177 1.405.38 1.804.59.2.105.323.2.39.272a.3.3 0 0 1 .047.064.3.3 0 0 1-.048.064 1.4 1.4 0 0 1-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002zm0 .018v.002zm17.002.002v-.002zm0-.018v-.002z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-globe-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='none' stroke='%2333b5e5' viewBox='0 0 22 22'%3E%3Ccircle cx='11' cy='11' r='8.5'/%3E%3Cpath d='M17.5 11c0 4.819-3.02 8.5-6.5 8.5S4.5 15.819 4.5 11 7.52 2.5 11 2.5s6.5 3.681 6.5 8.5Z'/%3E%3Cpath d='M13.5 11c0 2.447-.331 4.64-.853 6.206-.262.785-.562 1.384-.872 1.777-.314.399-.58.517-.775.517s-.461-.118-.775-.517c-.31-.393-.61-.992-.872-1.777C8.831 15.64 8.5 13.446 8.5 11s.331-4.64.853-6.206c.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517s.461.118.775.517c.31.393.61.992.872 1.777.522 1.565.853 3.76.853 6.206Z'/%3E%3Cpath d='M11 7.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138q.07-.058.224-.138c.299-.151.763-.302 1.379-.434C7.378 5.666 9.091 5.5 11 5.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138q-.07.058-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428ZM4.486 6.436ZM11 16.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138 1.3 1.3 0 0 1 .224-.138c.299-.151.763-.302 1.379-.434C7.378 14.666 9.091 14.5 11 14.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138a1.3 1.3 0 0 1-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm-6.514-1.064ZM11 12.5c-2.46 0-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.5 1.5 0 0 1-.39-.272.3.3 0 0 1-.047-.064.3.3 0 0 1 .048-.064c.066-.073.189-.167.389-.272.399-.21 1.009-.413 1.805-.59C6.328 9.722 8.54 9.5 11 9.5s4.672.222 6.256.574c.795.177 1.405.38 1.804.59.2.105.323.2.39.272a.3.3 0 0 1 .047.064.3.3 0 0 1-.048.064 1.4 1.4 0 0 1-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002zm0 .018v.002zm17.002.002v-.002zm0-.018v-.002z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%23333' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%2333b5e5' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23aaa' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e58978' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e54e33' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon{animation:maplibregl-spin 2s linear infinite}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23999' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e58978' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e54e33' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23666' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}}@keyframes maplibregl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.maplibregl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;cursor:pointer;display:block;height:23px;margin:0 0 -4px -4px;overflow:hidden;width:88px}a.maplibregl-ctrl-logo.maplibregl-compact{width:14px}@media (forced-colors:active){a.maplibregl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){a.maplibregl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}}.maplibregl-ctrl.maplibregl-ctrl-attrib{background-color:hsla(0,0%,100%,.5);margin:0;padding:0 5px}@media screen{.maplibregl-ctrl-attrib.maplibregl-compact{background-color:#fff;border-radius:12px;box-sizing:content-box;color:#000;margin:10px;min-height:20px;padding:2px 24px 2px 0;position:relative}.maplibregl-ctrl-attrib.maplibregl-compact-show{padding:2px 28px 2px 8px;visibility:visible}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact-show,.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact-show{border-radius:12px;padding:2px 8px 2px 28px}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-inner{display:none}.maplibregl-ctrl-attrib-button{background-color:hsla(0,0%,100%,.5);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");border:0;border-radius:12px;box-sizing:border-box;cursor:pointer;display:none;height:24px;outline:none;position:absolute;right:0;top:0;width:24px}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;list-style:none}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button::-webkit-details-marker{display:none}.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-top-left .maplibregl-ctrl-attrib-button{left:0}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-inner{display:block}.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button{background-color:rgba(0,0,0,.05)}.maplibregl-ctrl-bottom-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;right:0}.maplibregl-ctrl-top-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{right:0;top:0}.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{left:0;top:0}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;left:0}}@media screen and (forced-colors:active){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}@media screen and (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}.maplibregl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.maplibregl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.maplibregl-attrib-empty{display:none}.maplibregl-ctrl-scale{background-color:hsla(0,0%,100%,.75);border:2px solid #333;border-top:#333;box-sizing:border-box;color:#333;font-size:10px;padding:0 5px}.maplibregl-popup{display:flex;left:0;pointer-events:none;position:absolute;top:0;will-change:transform}.maplibregl-popup-anchor-top,.maplibregl-popup-anchor-top-left,.maplibregl-popup-anchor-top-right{flex-direction:column}.maplibregl-popup-anchor-bottom,.maplibregl-popup-anchor-bottom-left,.maplibregl-popup-anchor-bottom-right{flex-direction:column-reverse}.maplibregl-popup-anchor-left{flex-direction:row}.maplibregl-popup-anchor-right{flex-direction:row-reverse}.maplibregl-popup-tip{border:10px solid transparent;height:0;width:0;z-index:1}.maplibregl-popup-anchor-top .maplibregl-popup-tip{align-self:center;border-bottom-color:#fff;border-top:none}.maplibregl-popup-anchor-top-left .maplibregl-popup-tip{align-self:flex-start;border-bottom-color:#fff;border-left:none;border-top:none}.maplibregl-popup-anchor-top-right .maplibregl-popup-tip{align-self:flex-end;border-bottom-color:#fff;border-right:none;border-top:none}.maplibregl-popup-anchor-bottom .maplibregl-popup-tip{align-self:center;border-bottom:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip{align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip{align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.maplibregl-popup-anchor-left .maplibregl-popup-tip{align-self:center;border-left:none;border-right-color:#fff}.maplibregl-popup-anchor-right .maplibregl-popup-tip{align-self:center;border-left-color:#fff;border-right:none}.maplibregl-popup-close-button{background-color:transparent;border:0;border-radius:0 3px 0 0;cursor:pointer;position:absolute;right:0;top:0}.maplibregl-popup-close-button:hover{background-color:rgba(0,0,0,.05)}.maplibregl-popup-content{background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:15px 10px;pointer-events:auto;position:relative}.maplibregl-popup-anchor-top-left .maplibregl-popup-content{border-top-left-radius:0}.maplibregl-popup-anchor-top-right .maplibregl-popup-content{border-top-right-radius:0}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-content{border-bottom-left-radius:0}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-content{border-bottom-right-radius:0}.maplibregl-popup-track-pointer{display:none}.maplibregl-popup-track-pointer *{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-map:hover .maplibregl-popup-track-pointer{display:flex}.maplibregl-map:active .maplibregl-popup-track-pointer{display:none}.maplibregl-marker{left:0;position:absolute;top:0;transition:opacity .2s;will-change:transform}.maplibregl-user-location-dot,.maplibregl-user-location-dot:before{background-color:#1da1f2;border-radius:50%;height:15px;width:15px}.maplibregl-user-location-dot:before{animation:maplibregl-user-location-dot-pulse 2s infinite;content:"";position:absolute}.maplibregl-user-location-dot:after{border:2px solid #fff;border-radius:50%;box-shadow:0 0 3px rgba(0,0,0,.35);box-sizing:border-box;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px}@keyframes maplibregl-user-location-dot-pulse{0%{opacity:1;transform:scale(1)}70%{opacity:0;transform:scale(3)}to{opacity:0;transform:scale(1)}}.maplibregl-user-location-dot-stale{background-color:#aaa}.maplibregl-user-location-dot-stale:after{display:none}.maplibregl-user-location-accuracy-circle{background-color:#1da1f233;border-radius:100%;height:1px;width:1px}.maplibregl-crosshair,.maplibregl-crosshair .maplibregl-interactive,.maplibregl-crosshair .maplibregl-interactive:active{cursor:crosshair}.maplibregl-boxzoom{background:#fff;border:2px dotted #202020;height:0;left:0;opacity:.5;position:absolute;top:0;width:0}.maplibregl-cooperative-gesture-screen{align-items:center;background:rgba(0,0,0,.4);color:#fff;display:flex;font-size:1.4em;inset:0;justify-content:center;line-height:1.2;opacity:0;padding:1rem;pointer-events:none;position:absolute;transition:opacity 1s ease 1s;z-index:99999}.maplibregl-cooperative-gesture-screen.maplibregl-show{opacity:1;transition:opacity .05s}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:none}@media (hover:none),(pointer:coarse){.maplibregl-cooperative-gesture-screen .maplibregl-desktop-message{display:none}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:block}}.maplibregl-pseudo-fullscreen{height:100%!important;left:0!important;position:fixed!important;top:0!important;width:100%!important;z-index:99999} \ No newline at end of file diff --git a/streetup/common/static/common/maplibre-gl.js b/streetup/common/static/common/maplibre-gl.js new file mode 100644 index 0000000..501632f --- /dev/null +++ b/streetup/common/static/common/maplibre-gl.js @@ -0,0 +1,58 @@ +/** + * MapLibre GL JS + * @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v5.7.0/LICENSE.txt + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : +typeof define === 'function' && define.amd ? define(factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.maplibregl = factory()); +})(this, (function () { 'use strict'; + +/* eslint-disable */ + +var maplibregl = {}; +var modules = {}; +function define(moduleName, _dependencies, moduleFactory) { + modules[moduleName] = moduleFactory; + + // to get the list of modules see generated dist/maplibre-gl-dev.js file (look for `define(` calls) + if (moduleName !== 'index') { + return; + } + + // we assume that when an index module is initializing then other modules are loaded already + var workerBundleString = 'var sharedModule = {}; (' + modules.shared + ')(sharedModule); (' + modules.worker + ')(sharedModule);' + + var sharedModule = {}; + // the order of arguments of a module factory depends on rollup (it decides who is whose dependency) + // to check the correct order, see dist/maplibre-gl-dev.js file (look for `define(` calls) + // we assume that for our 3 chunks it will generate 3 modules and their order is predefined like the following + modules.shared(sharedModule); + modules.index(maplibregl, sharedModule); + + if (typeof window !== 'undefined') { + maplibregl.setWorkerUrl(window.URL.createObjectURL(new Blob([workerBundleString], { type: 'text/javascript' }))); + } + + return maplibregl; +}; + + + +define("shared",["exports"],(function(t){"use strict";function e(t,e,r,n){return new(r||(r=Promise))((function(i,s){function a(t){try{l(n.next(t));}catch(t){s(t);}}function o(t){try{l(n.throw(t));}catch(t){s(t);}}function l(t){var e;t.done?i(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e);}))).then(a,o);}l((n=n.apply(t,e||[])).next());}))}function r(t,e){this.x=t,this.y=e;}function n(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var i,s;"function"==typeof SuppressedError&&SuppressedError,r.prototype={clone(){return new r(this.x,this.y)},add(t){return this.clone()._add(t)},sub(t){return this.clone()._sub(t)},multByPoint(t){return this.clone()._multByPoint(t)},divByPoint(t){return this.clone()._divByPoint(t)},mult(t){return this.clone()._mult(t)},div(t){return this.clone()._div(t)},rotate(t){return this.clone()._rotate(t)},rotateAround(t,e){return this.clone()._rotateAround(t,e)},matMult(t){return this.clone()._matMult(t)},unit(){return this.clone()._unit()},perp(){return this.clone()._perp()},round(){return this.clone()._round()},mag(){return Math.sqrt(this.x*this.x+this.y*this.y)},equals(t){return this.x===t.x&&this.y===t.y},dist(t){return Math.sqrt(this.distSqr(t))},distSqr(t){const e=t.x-this.x,r=t.y-this.y;return e*e+r*r},angle(){return Math.atan2(this.y,this.x)},angleTo(t){return Math.atan2(this.y-t.y,this.x-t.x)},angleWith(t){return this.angleWithSep(t.x,t.y)},angleWithSep(t,e){return Math.atan2(this.x*e-this.y*t,this.x*t+this.y*e)},_matMult(t){const e=t[2]*this.x+t[3]*this.y;return this.x=t[0]*this.x+t[1]*this.y,this.y=e,this},_add(t){return this.x+=t.x,this.y+=t.y,this},_sub(t){return this.x-=t.x,this.y-=t.y,this},_mult(t){return this.x*=t,this.y*=t,this},_div(t){return this.x/=t,this.y/=t,this},_multByPoint(t){return this.x*=t.x,this.y*=t.y,this},_divByPoint(t){return this.x/=t.x,this.y/=t.y,this},_unit(){return this._div(this.mag()),this},_perp(){const t=this.y;return this.y=this.x,this.x=-t,this},_rotate(t){const e=Math.cos(t),r=Math.sin(t),n=r*this.x+e*this.y;return this.x=e*this.x-r*this.y,this.y=n,this},_rotateAround(t,e){const r=Math.cos(t),n=Math.sin(t),i=e.y+n*(this.x-e.x)+r*(this.y-e.y);return this.x=e.x+r*(this.x-e.x)-n*(this.y-e.y),this.y=i,this},_round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},constructor:r},r.convert=function(t){if(t instanceof r)return t;if(Array.isArray(t))return new r(+t[0],+t[1]);if(void 0!==t.x&&void 0!==t.y)return new r(+t.x,+t.y);throw new Error("Expected [x, y] or {x, y} point format")};var a=function(){if(s)return i;function t(t,e,r,n){this.cx=3*t,this.bx=3*(r-t)-this.cx,this.ax=1-this.cx-this.bx,this.cy=3*e,this.by=3*(n-e)-this.cy,this.ay=1-this.cy-this.by,this.p1x=t,this.p1y=e,this.p2x=r,this.p2y=n;}return s=1,i=t,t.prototype={sampleCurveX:function(t){return ((this.ax*t+this.bx)*t+this.cx)*t},sampleCurveY:function(t){return ((this.ay*t+this.by)*t+this.cy)*t},sampleCurveDerivativeX:function(t){return (3*this.ax*t+2*this.bx)*t+this.cx},solveCurveX:function(t,e){if(void 0===e&&(e=1e-6),t<0)return 0;if(t>1)return 1;for(var r=t,n=0;n<8;n++){var i=this.sampleCurveX(r)-t;if(Math.abs(i)i?a=r:o=r,r=.5*(o-a)+a;return r},solve:function(t,e){return this.sampleCurveY(this.solveCurveX(t,e))}},i}(),o=n(a);let l,u;function c(){return null==l&&(l="undefined"!=typeof OffscreenCanvas&&new OffscreenCanvas(1,1).getContext("2d")&&"function"==typeof createImageBitmap),l}function h(){if(null==u&&(u=!1,c())){const t=5,e=new OffscreenCanvas(t,t).getContext("2d",{willReadFrequently:!0});if(e){for(let r=0;r4&&void 0!==arguments[4]?arguments[4]:"zyx",s=Math.PI/360;e*=s,n*=s,r*=s;var a=Math.sin(e),o=Math.cos(e),l=Math.sin(r),u=Math.cos(r),c=Math.sin(n),h=Math.cos(n);switch(i){case "xyz":t[0]=a*u*h+o*l*c,t[1]=o*l*h-a*u*c,t[2]=o*u*c+a*l*h,t[3]=o*u*h-a*l*c;break;case "xzy":t[0]=a*u*h-o*l*c,t[1]=o*l*h-a*u*c,t[2]=o*u*c+a*l*h,t[3]=o*u*h+a*l*c;break;case "yxz":t[0]=a*u*h+o*l*c,t[1]=o*l*h-a*u*c,t[2]=o*u*c-a*l*h,t[3]=o*u*h+a*l*c;break;case "yzx":t[0]=a*u*h+o*l*c,t[1]=o*l*h+a*u*c,t[2]=o*u*c-a*l*h,t[3]=o*u*h-a*l*c;break;case "zxy":t[0]=a*u*h-o*l*c,t[1]=o*l*h+a*u*c,t[2]=o*u*c+a*l*h,t[3]=o*u*h-a*l*c;break;case "zyx":t[0]=a*u*h-o*l*c,t[1]=o*l*h+a*u*c,t[2]=o*u*c-a*l*h,t[3]=o*u*h+a*l*c;break;default:throw new Error("Unknown angle order "+i)}return t}function I(){var t=new f(2);return f!=Float32Array&&(t[0]=0,t[1]=0),t}function z(t,e){var r=new f(2);return r[0]=t,r[1]=e,r}m(),_=new f(4),f!=Float32Array&&(_[0]=0,_[1]=0,_[2]=0,_[3]=0),m(),x(1,0,0),x(0,1,0),k(),k(),d(),I();const P=8192;function C(t,e,r){return e*(P/(t.tileSize*Math.pow(2,r-t.tileID.overscaledZ)))}function E(t,e){return (t%e+e)%e}function B(t,e,r){return t*(1-r)+e*r}function V(t){if(t<=0)return 0;if(t>=1)return 1;const e=t*t,r=e*t;return 4*(t<.5?r:3*(t-e)+r-.75)}function T(t,e,r,n){const i=new o(t,e,r,n);return t=>i.solve(t)}const F=T(.25,.1,.25,1);function $(t,e,r){return Math.min(r,Math.max(e,t))}function L(t,e,r){const n=r-e,i=((t-e)%n+n)%n+e;return i===e?r:i}function O(t,...e){for(const r of e)for(const e in r)t[e]=r[e];return t}let D=1;function R(t,e,r){const n={};for(const r in t)n[r]=e.call(this,t[r],r,t);return n}function U(t,e,r){const n={};for(const r in t)e.call(this,t[r],r,t)&&(n[r]=t[r]);return n}function j(t){return Array.isArray(t)?t.map(j):"object"==typeof t&&t?R(t,j):t}const N={};function q(t){N[t]||("undefined"!=typeof console&&console.warn(t),N[t]=!0);}function G(t,e,r){return (r.y-t.y)*(e.x-t.x)>(e.y-t.y)*(r.x-t.x)}function X(t){return "undefined"!=typeof WorkerGlobalScope&&void 0!==t&&t instanceof WorkerGlobalScope}let Z=null;function Y(t){return "undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap}const H="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=";function K(t,r,n,i,s){return e(this,void 0,void 0,(function*(){if("undefined"==typeof VideoFrame)throw new Error("VideoFrame not supported");const e=new VideoFrame(t,{timestamp:0});try{const a=null==e?void 0:e.format;if(!a||!a.startsWith("BGR")&&!a.startsWith("RGB"))throw new Error(`Unrecognized format ${a}`);const o=a.startsWith("BGR"),l=new Uint8ClampedArray(i*s*4);if(yield e.copyTo(l,function(t,e,r,n,i){const s=4*Math.max(-e,0),a=(Math.max(0,r)-r)*n*4+s,o=4*n,l=Math.max(0,e),u=Math.max(0,r);return {rect:{x:l,y:u,width:Math.min(t.width,e+n)-l,height:Math.min(t.height,r+i)-u},layout:[{offset:a,stride:o}]}}(t,r,n,i,s)),o)for(let t=0;t{t.removeEventListener(e,r,n);}}}function tt(t){return t*Math.PI/180}function et(t){return t/Math.PI*180}const rt={touchstart:!0,touchmove:!0,touchmoveWindow:!0,touchend:!0,touchcancel:!0},nt={dblclick:!0,click:!0,mouseover:!0,mouseout:!0,mousedown:!0,mousemove:!0,mousemoveWindow:!0,mouseup:!0,mouseupWindow:!0,contextmenu:!0,wheel:!0},it="AbortError";function st(){return new Error(it)}const at={MAX_PARALLEL_IMAGE_REQUESTS:16,MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME:8,MAX_TILE_CACHE_ZOOM_LEVELS:5,REGISTERED_PROTOCOLS:{},WORKER_URL:""};function ot(t){return at.REGISTERED_PROTOCOLS[t.substring(0,t.indexOf("://"))]}const lt="global-dispatcher";class ut extends Error{constructor(t,e,r,n){super(`AJAXError: ${e} (${t}): ${r}`),this.status=t,this.statusText=e,this.url=r,this.body=n;}}const ct=()=>X(self)?self.worker&&self.worker.referrer:("blob:"===window.location.protocol?window.parent:window).location.href,ht=function(t,r){if(/:\/\//.test(t.url)&&!/^https?:|^file:/.test(t.url)){const e=ot(t.url);if(e)return e(t,r);if(X(self)&&self.worker&&self.worker.actor)return self.worker.actor.sendAsync({type:"GR",data:t,targetMapId:lt},r)}if(!(/^file:/.test(n=t.url)||/^file:/.test(ct())&&!/^\w+:/.test(n))){if(fetch&&Request&&AbortController&&Object.prototype.hasOwnProperty.call(Request.prototype,"signal"))return function(t,r){return e(this,void 0,void 0,(function*(){const e=new Request(t.url,{method:t.method||"GET",body:t.body,credentials:t.credentials,headers:t.headers,cache:t.cache,referrer:ct(),signal:r.signal});let n,i;"json"!==t.type||e.headers.has("Accept")||e.headers.set("Accept","application/json");try{n=yield fetch(e);}catch(e){throw new ut(0,e.message,t.url,new Blob)}if(!n.ok){const e=yield n.blob();throw new ut(n.status,n.statusText,t.url,e)}i="arrayBuffer"===t.type||"image"===t.type?n.arrayBuffer():"json"===t.type?n.json():n.text();const s=yield i;if(r.signal.aborted)throw st();return {data:s,cacheControl:n.headers.get("Cache-Control"),expires:n.headers.get("Expires")}}))}(t,r);if(X(self)&&self.worker&&self.worker.actor)return self.worker.actor.sendAsync({type:"GR",data:t,mustQueue:!0,targetMapId:lt},r)}var n;return function(t,e){return new Promise(((r,n)=>{var i;const s=new XMLHttpRequest;s.open(t.method||"GET",t.url,!0),"arrayBuffer"!==t.type&&"image"!==t.type||(s.responseType="arraybuffer");for(const e in t.headers)s.setRequestHeader(e,t.headers[e]);"json"===t.type&&(s.responseType="text",(null===(i=t.headers)||void 0===i?void 0:i.Accept)||s.setRequestHeader("Accept","application/json")),s.withCredentials="include"===t.credentials,s.onerror=()=>{n(new Error(s.statusText));},s.onload=()=>{if(!e.signal.aborted)if((s.status>=200&&s.status<300||0===s.status)&&null!==s.response){let e=s.response;if("json"===t.type)try{e=JSON.parse(s.response);}catch(t){return void n(t)}r({data:e,cacheControl:s.getResponseHeader("Cache-Control"),expires:s.getResponseHeader("Expires")});}else {const e=new Blob([s.response],{type:s.getResponseHeader("Content-Type")});n(new ut(s.status,s.statusText,t.url,e));}},e.signal.addEventListener("abort",(()=>{s.abort(),n(st());})),s.send(t.body);}))}(t,r)};function pt(t){if(!t||t.indexOf("://")<=0||0===t.indexOf("data:image/")||0===t.indexOf("blob:"))return !0;const e=new URL(t),r=window.location;return e.protocol===r.protocol&&e.host===r.host}function ft(t,e,r){r[t]&&-1!==r[t].indexOf(e)||(r[t]=r[t]||[],r[t].push(e));}function dt(t,e,r){if(r&&r[t]){const n=r[t].indexOf(e);-1!==n&&r[t].splice(n,1);}}class yt{constructor(t,e={}){O(this,e),this.type=t;}}class mt extends yt{constructor(t,e={}){super("error",O({error:t},e));}}class gt{on(t,e){return this._listeners=this._listeners||{},ft(t,e,this._listeners),{unsubscribe:()=>{this.off(t,e);}}}off(t,e){return dt(t,e,this._listeners),dt(t,e,this._oneTimeListeners),this}once(t,e){return e?(this._oneTimeListeners=this._oneTimeListeners||{},ft(t,e,this._oneTimeListeners),this):new Promise((e=>this.once(t,e)))}fire(t,e){"string"==typeof t&&(t=new yt(t,e||{}));const r=t.type;if(this.listens(r)){t.target=this;const e=this._listeners&&this._listeners[r]?this._listeners[r].slice():[];for(const r of e)r.call(this,t);const n=this._oneTimeListeners&&this._oneTimeListeners[r]?this._oneTimeListeners[r].slice():[];for(const e of n)dt(r,e,this._oneTimeListeners),e.call(this,t);const i=this._eventedParent;i&&(O(t,"function"==typeof this._eventedParentData?this._eventedParentData():this._eventedParentData),i.fire(t));}else t instanceof mt&&console.error(t.error);return this}listens(t){return this._listeners&&this._listeners[t]&&this._listeners[t].length>0||this._oneTimeListeners&&this._oneTimeListeners[t]&&this._oneTimeListeners[t].length>0||this._eventedParent&&this._eventedParent.listens(t)}setEventedParent(t,e){return this._eventedParent=t,this._eventedParentData=e,this}}var xt={$version:8,$root:{version:{required:!0,type:"enum",values:[8]},name:{type:"string"},metadata:{type:"*"},center:{type:"array",value:"number"},centerAltitude:{type:"number"},zoom:{type:"number"},bearing:{type:"number",default:0,period:360,units:"degrees"},pitch:{type:"number",default:0,units:"degrees"},roll:{type:"number",default:0,units:"degrees"},state:{type:"state",default:{}},light:{type:"light"},sky:{type:"sky"},projection:{type:"projection"},terrain:{type:"terrain"},sources:{required:!0,type:"sources"},sprite:{type:"sprite"},glyphs:{type:"string"},transition:{type:"transition"},layers:{required:!0,type:"array",value:"layer"}},sources:{"*":{type:"source"}},source:["source_vector","source_raster","source_raster_dem","source_geojson","source_video","source_image"],source_vector:{type:{required:!0,type:"enum",values:{vector:{}}},url:{type:"string"},tiles:{type:"array",value:"string"},bounds:{type:"array",value:"number",length:4,default:[-180,-85.051129,180,85.051129]},scheme:{type:"enum",values:{xyz:{},tms:{}},default:"xyz"},minzoom:{type:"number",default:0},maxzoom:{type:"number",default:22},attribution:{type:"string"},promoteId:{type:"promoteId"},volatile:{type:"boolean",default:!1},"*":{type:"*"}},source_raster:{type:{required:!0,type:"enum",values:{raster:{}}},url:{type:"string"},tiles:{type:"array",value:"string"},bounds:{type:"array",value:"number",length:4,default:[-180,-85.051129,180,85.051129]},minzoom:{type:"number",default:0},maxzoom:{type:"number",default:22},tileSize:{type:"number",default:512,units:"pixels"},scheme:{type:"enum",values:{xyz:{},tms:{}},default:"xyz"},attribution:{type:"string"},volatile:{type:"boolean",default:!1},"*":{type:"*"}},source_raster_dem:{type:{required:!0,type:"enum",values:{"raster-dem":{}}},url:{type:"string"},tiles:{type:"array",value:"string"},bounds:{type:"array",value:"number",length:4,default:[-180,-85.051129,180,85.051129]},minzoom:{type:"number",default:0},maxzoom:{type:"number",default:22},tileSize:{type:"number",default:512,units:"pixels"},attribution:{type:"string"},encoding:{type:"enum",values:{terrarium:{},mapbox:{},custom:{}},default:"mapbox"},redFactor:{type:"number",default:1},blueFactor:{type:"number",default:1},greenFactor:{type:"number",default:1},baseShift:{type:"number",default:0},volatile:{type:"boolean",default:!1},"*":{type:"*"}},source_geojson:{type:{required:!0,type:"enum",values:{geojson:{}}},data:{required:!0,type:"*"},maxzoom:{type:"number",default:18},attribution:{type:"string"},buffer:{type:"number",default:128,maximum:512,minimum:0},filter:{type:"*"},tolerance:{type:"number",default:.375},cluster:{type:"boolean",default:!1},clusterRadius:{type:"number",default:50,minimum:0},clusterMaxZoom:{type:"number"},clusterMinPoints:{type:"number"},clusterProperties:{type:"*"},lineMetrics:{type:"boolean",default:!1},generateId:{type:"boolean",default:!1},promoteId:{type:"promoteId"}},source_video:{type:{required:!0,type:"enum",values:{video:{}}},urls:{required:!0,type:"array",value:"string"},coordinates:{required:!0,type:"array",length:4,value:{type:"array",length:2,value:"number"}}},source_image:{type:{required:!0,type:"enum",values:{image:{}}},url:{required:!0,type:"string"},coordinates:{required:!0,type:"array",length:4,value:{type:"array",length:2,value:"number"}}},layer:{id:{type:"string",required:!0},type:{type:"enum",values:{fill:{},line:{},symbol:{},circle:{},heatmap:{},"fill-extrusion":{},raster:{},hillshade:{},"color-relief":{},background:{}},required:!0},metadata:{type:"*"},source:{type:"string"},"source-layer":{type:"string"},minzoom:{type:"number",minimum:0,maximum:24},maxzoom:{type:"number",minimum:0,maximum:24},filter:{type:"filter"},layout:{type:"layout"},paint:{type:"paint"}},layout:["layout_fill","layout_line","layout_circle","layout_heatmap","layout_fill-extrusion","layout_symbol","layout_raster","layout_hillshade","layout_color-relief","layout_background"],layout_background:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_fill:{"fill-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_circle:{"circle-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_heatmap:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},"layout_fill-extrusion":{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_line:{"line-cap":{type:"enum",values:{butt:{},round:{},square:{}},default:"butt",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"line-join":{type:"enum",values:{bevel:{},round:{},miter:{}},default:"miter",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"line-miter-limit":{type:"number",default:2,requires:[{"line-join":"miter"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"line-round-limit":{type:"number",default:1.05,requires:[{"line-join":"round"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"line-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_symbol:{"symbol-placement":{type:"enum",values:{point:{},line:{},"line-center":{}},default:"point",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"symbol-spacing":{type:"number",default:250,minimum:1,units:"pixels",requires:[{"symbol-placement":"line"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"symbol-avoid-edges":{type:"boolean",default:!1,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"symbol-sort-key":{type:"number",expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"symbol-z-order":{type:"enum",values:{auto:{},"viewport-y":{},source:{}},default:"auto",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-allow-overlap":{type:"boolean",default:!1,requires:["icon-image",{"!":"icon-overlap"}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-overlap":{type:"enum",values:{never:{},always:{},cooperative:{}},requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-ignore-placement":{type:"boolean",default:!1,requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-optional":{type:"boolean",default:!1,requires:["icon-image","text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-rotation-alignment":{type:"enum",values:{map:{},viewport:{},auto:{}},default:"auto",requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-size":{type:"number",default:1,minimum:0,units:"factor of the original icon size",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-text-fit":{type:"enum",values:{none:{},width:{},height:{},both:{}},default:"none",requires:["icon-image","text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-text-fit-padding":{type:"array",value:"number",length:4,default:[0,0,0,0],units:"pixels",requires:["icon-image","text-field",{"icon-text-fit":["both","width","height"]}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"icon-image":{type:"resolvedImage",tokens:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-rotate":{type:"number",default:0,period:360,units:"degrees",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-padding":{type:"padding",default:[2],units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-keep-upright":{type:"boolean",default:!1,requires:["icon-image",{"icon-rotation-alignment":"map"},{"symbol-placement":["line","line-center"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"icon-offset":{type:"array",value:"number",length:2,default:[0,0],requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-anchor":{type:"enum",values:{center:{},left:{},right:{},top:{},bottom:{},"top-left":{},"top-right":{},"bottom-left":{},"bottom-right":{}},default:"center",requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"icon-pitch-alignment":{type:"enum",values:{map:{},viewport:{},auto:{}},default:"auto",requires:["icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-pitch-alignment":{type:"enum",values:{map:{},viewport:{},auto:{}},default:"auto",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-rotation-alignment":{type:"enum",values:{map:{},viewport:{},"viewport-glyph":{},auto:{}},default:"auto",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-field":{type:"formatted",default:"",tokens:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-font":{type:"array",value:"string",default:["Open Sans Regular","Arial Unicode MS Regular"],requires:["text-field"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-size":{type:"number",default:16,minimum:0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-max-width":{type:"number",default:10,minimum:0,units:"ems",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-line-height":{type:"number",default:1.2,units:"ems",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-letter-spacing":{type:"number",default:0,units:"ems",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-justify":{type:"enum",values:{auto:{},left:{},center:{},right:{}},default:"center",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-radial-offset":{type:"number",units:"ems",default:0,requires:["text-field"],"property-type":"data-driven",expression:{interpolated:!0,parameters:["zoom","feature"]}},"text-variable-anchor":{type:"array",value:"enum",values:{center:{},left:{},right:{},top:{},bottom:{},"top-left":{},"top-right":{},"bottom-left":{},"bottom-right":{}},requires:["text-field",{"symbol-placement":["point"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-variable-anchor-offset":{type:"variableAnchorOffsetCollection",requires:["text-field",{"symbol-placement":["point"]}],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-anchor":{type:"enum",values:{center:{},left:{},right:{},top:{},bottom:{},"top-left":{},"top-right":{},"bottom-left":{},"bottom-right":{}},default:"center",requires:["text-field",{"!":"text-variable-anchor"}],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-max-angle":{type:"number",default:45,units:"degrees",requires:["text-field",{"symbol-placement":["line","line-center"]}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-writing-mode":{type:"array",value:"enum",values:{horizontal:{},vertical:{}},requires:["text-field",{"symbol-placement":["point"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-rotate":{type:"number",default:0,period:360,units:"degrees",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-padding":{type:"number",default:2,minimum:0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-keep-upright":{type:"boolean",default:!0,requires:["text-field",{"text-rotation-alignment":"map"},{"symbol-placement":["line","line-center"]}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-transform":{type:"enum",values:{none:{},uppercase:{},lowercase:{}},default:"none",requires:["text-field"],expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-offset":{type:"array",value:"number",units:"ems",length:2,default:[0,0],requires:["text-field",{"!":"text-radial-offset"}],expression:{interpolated:!0,parameters:["zoom","feature"]},"property-type":"data-driven"},"text-allow-overlap":{type:"boolean",default:!1,requires:["text-field",{"!":"text-overlap"}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-overlap":{type:"enum",values:{never:{},always:{},cooperative:{}},requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-ignore-placement":{type:"boolean",default:!1,requires:["text-field"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-optional":{type:"boolean",default:!1,requires:["text-field","icon-image"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_raster:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},layout_hillshade:{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},"layout_color-relief":{visibility:{type:"enum",values:{visible:{},none:{}},default:"visible","property-type":"constant"}},filter:{type:"array",value:"*"},filter_operator:{type:"enum",values:{"==":{},"!=":{},">":{},">=":{},"<":{},"<=":{},in:{},"!in":{},all:{},any:{},none:{},has:{},"!has":{}}},geometry_type:{type:"enum",values:{Point:{},LineString:{},Polygon:{}}},function:{expression:{type:"expression"},stops:{type:"array",value:"function_stop"},base:{type:"number",default:1,minimum:0},property:{type:"string",default:"$zoom"},type:{type:"enum",values:{identity:{},exponential:{},interval:{},categorical:{}},default:"exponential"},colorSpace:{type:"enum",values:{rgb:{},lab:{},hcl:{}},default:"rgb"},default:{type:"*",required:!1}},function_stop:{type:"array",minimum:0,maximum:24,value:["number","color"],length:2},expression:{type:"array",value:"*",minimum:1},light:{anchor:{type:"enum",default:"viewport",values:{map:{},viewport:{}},"property-type":"data-constant",transition:!1,expression:{interpolated:!1,parameters:["zoom"]}},position:{type:"array",default:[1.15,210,30],length:3,value:"number","property-type":"data-constant",transition:!0,expression:{interpolated:!0,parameters:["zoom"]}},color:{type:"color","property-type":"data-constant",default:"#ffffff",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},intensity:{type:"number","property-type":"data-constant",default:.5,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0}},sky:{"sky-color":{type:"color","property-type":"data-constant",default:"#88C6FC",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"horizon-color":{type:"color","property-type":"data-constant",default:"#ffffff",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"fog-color":{type:"color","property-type":"data-constant",default:"#ffffff",expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"fog-ground-blend":{type:"number","property-type":"data-constant",default:.5,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"horizon-fog-blend":{type:"number","property-type":"data-constant",default:.8,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"sky-horizon-blend":{type:"number","property-type":"data-constant",default:.8,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0},"atmosphere-blend":{type:"number","property-type":"data-constant",default:.8,minimum:0,maximum:1,expression:{interpolated:!0,parameters:["zoom"]},transition:!0}},terrain:{source:{type:"string",required:!0},exaggeration:{type:"number",minimum:0,default:1}},projection:{type:{type:"projectionDefinition",default:"mercator","property-type":"data-constant",transition:!1,expression:{interpolated:!0,parameters:["zoom"]}}},paint:["paint_fill","paint_line","paint_circle","paint_heatmap","paint_fill-extrusion","paint_symbol","paint_raster","paint_hillshade","paint_color-relief","paint_background"],paint_fill:{"fill-antialias":{type:"boolean",default:!0,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"fill-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"fill-pattern"}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-outline-color":{type:"color",transition:!0,requires:[{"!":"fill-pattern"},{"fill-antialias":!0}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"fill-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["fill-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"fill-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"cross-faded-data-driven"}},"paint_fill-extrusion":{"fill-extrusion-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"fill-extrusion-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"fill-extrusion-pattern"}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-extrusion-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"fill-extrusion-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["fill-extrusion-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"fill-extrusion-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"cross-faded-data-driven"},"fill-extrusion-height":{type:"number",default:0,minimum:0,units:"meters",transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-extrusion-base":{type:"number",default:0,minimum:0,units:"meters",transition:!0,requires:["fill-extrusion-height"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"fill-extrusion-vertical-gradient":{type:"boolean",default:!0,transition:!1,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"}},paint_line:{"line-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"line-pattern"}],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"line-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["line-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"line-width":{type:"number",default:1,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-gap-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-offset":{type:"number",default:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-blur":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"line-dasharray":{type:"array",value:"number",minimum:0,transition:!0,units:"line widths",requires:[{"!":"line-pattern"}],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"cross-faded"},"line-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom","feature"]},"property-type":"cross-faded-data-driven"},"line-gradient":{type:"color",transition:!1,requires:[{"!":"line-dasharray"},{"!":"line-pattern"},{source:"geojson",has:{lineMetrics:!0}}],expression:{interpolated:!0,parameters:["line-progress"]},"property-type":"color-ramp"}},paint_circle:{"circle-radius":{type:"number",default:5,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-blur":{type:"number",default:0,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"circle-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["circle-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"circle-pitch-scale":{type:"enum",values:{map:{},viewport:{}},default:"map",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"circle-pitch-alignment":{type:"enum",values:{map:{},viewport:{}},default:"viewport",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"circle-stroke-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-stroke-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"circle-stroke-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"}},paint_heatmap:{"heatmap-radius":{type:"number",default:30,minimum:1,transition:!0,units:"pixels",expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"heatmap-weight":{type:"number",default:1,minimum:0,transition:!1,expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"heatmap-intensity":{type:"number",default:1,minimum:0,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"heatmap-color":{type:"color",default:["interpolate",["linear"],["heatmap-density"],0,"rgba(0, 0, 255, 0)",.1,"royalblue",.3,"cyan",.5,"lime",.7,"yellow",1,"red"],transition:!1,expression:{interpolated:!0,parameters:["heatmap-density"]},"property-type":"color-ramp"},"heatmap-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},paint_symbol:{"icon-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-color":{type:"color",default:"#000000",transition:!0,requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-halo-color":{type:"color",default:"rgba(0, 0, 0, 0)",transition:!0,requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-halo-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-halo-blur":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"icon-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",requires:["icon-image"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"icon-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["icon-image","icon-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"text-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-color":{type:"color",default:"#000000",transition:!0,overridable:!0,requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-halo-color":{type:"color",default:"rgba(0, 0, 0, 0)",transition:!0,requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-halo-width":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-halo-blur":{type:"number",default:0,minimum:0,transition:!0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom","feature","feature-state"]},"property-type":"data-driven"},"text-translate":{type:"array",value:"number",length:2,default:[0,0],transition:!0,units:"pixels",requires:["text-field"],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"text-translate-anchor":{type:"enum",values:{map:{},viewport:{}},default:"map",requires:["text-field","text-translate"],expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"}},paint_raster:{"raster-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-hue-rotate":{type:"number",default:0,period:360,transition:!0,units:"degrees",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-brightness-min":{type:"number",default:0,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-brightness-max":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-saturation":{type:"number",default:0,minimum:-1,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-contrast":{type:"number",default:0,minimum:-1,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"raster-resampling":{type:"enum",values:{linear:{},nearest:{}},default:"linear",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"raster-fade-duration":{type:"number",default:300,minimum:0,transition:!1,units:"milliseconds",expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},paint_hillshade:{"hillshade-illumination-direction":{type:"numberArray",default:335,minimum:0,maximum:359,transition:!1,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-illumination-altitude":{type:"numberArray",default:45,minimum:0,maximum:90,transition:!1,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-illumination-anchor":{type:"enum",values:{map:{},viewport:{}},default:"viewport",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-exaggeration":{type:"number",default:.5,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-shadow-color":{type:"colorArray",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-highlight-color":{type:"colorArray",default:"#FFFFFF",transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-accent-color":{type:"color",default:"#000000",transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"hillshade-method":{type:"enum",values:{standard:{},basic:{},combined:{},igor:{},multidirectional:{}},default:"standard",expression:{interpolated:!1,parameters:["zoom"]},"property-type":"data-constant"}},"paint_color-relief":{"color-relief-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"color-relief-color":{type:"color",transition:!1,expression:{interpolated:!0,parameters:["elevation"]},"property-type":"color-ramp"}},paint_background:{"background-color":{type:"color",default:"#000000",transition:!0,requires:[{"!":"background-pattern"}],expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"},"background-pattern":{type:"resolvedImage",transition:!0,expression:{interpolated:!1,parameters:["zoom"]},"property-type":"cross-faded"},"background-opacity":{type:"number",default:1,minimum:0,maximum:1,transition:!0,expression:{interpolated:!0,parameters:["zoom"]},"property-type":"data-constant"}},transition:{duration:{type:"number",default:300,minimum:0,units:"milliseconds"},delay:{type:"number",default:0,minimum:0,units:"milliseconds"}},"property-type":{"data-driven":{type:"property-type"},"cross-faded":{type:"property-type"},"cross-faded-data-driven":{type:"property-type"},"color-ramp":{type:"property-type"},"data-constant":{type:"property-type"},constant:{type:"property-type"}},promoteId:{"*":{type:"string"}}};const vt=["type","source","source-layer","minzoom","maxzoom","filter","layout"];function bt(t,e){const r={};for(const e in t)"ref"!==e&&(r[e]=t[e]);return vt.forEach((t=>{t in e&&(r[t]=e[t]);})),r}function wt(t,e){if(Array.isArray(t)){if(!Array.isArray(e)||t.length!==e.length)return !1;for(let r=0;r`:"value"===t.itemType.kind?"array":`array<${e}>`}return t.kind}const Jt=[Tt,Ft,$t,Lt,Ot,Dt,Nt,Rt,Ht(Ut),qt,Xt,Gt,Zt,Yt];function Wt(t,e){if("error"===e.kind)return null;if("array"===t.kind){if("array"===e.kind&&(0===e.N&&"value"===e.itemType.kind||!Wt(t.itemType,e.itemType))&&("number"!=typeof t.N||t.N===e.N))return null}else {if(t.kind===e.kind)return null;if("value"===t.kind)for(const t of Jt)if(!Wt(t,e))return null}return `Expected ${Kt(t)} but found ${Kt(e)} instead.`}function Qt(t,e){return e.some((e=>e.kind===t.kind))}function te(t,e){return e.some((e=>"null"===e?null===t:"array"===e?Array.isArray(t):"object"===e?t&&!Array.isArray(t)&&"object"==typeof t:e===typeof t))}function ee(t,e){return "array"===t.kind&&"array"===e.kind?t.itemType.kind===e.itemType.kind&&"number"==typeof t.N:t.kind===e.kind}const re=.96422,ne=.82521,ie=4/29,se=6/29,ae=3*se*se,oe=se*se*se,le=Math.PI/180,ue=180/Math.PI;function ce(t){return (t%=360)<0&&(t+=360),t}function he([t,e,r,n]){let i,s;const a=fe((.2225045*(t=pe(t))+.7168786*(e=pe(e))+.0606169*(r=pe(r)))/1);t===e&&e===r?i=s=a:(i=fe((.4360747*t+.3850649*e+.1430804*r)/re),s=fe((.0139322*t+.0971045*e+.7141733*r)/ne));const o=116*a-16;return [o<0?0:o,500*(i-a),200*(a-s),n]}function pe(t){return t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function fe(t){return t>oe?Math.pow(t,1/3):t/ae+ie}function de([t,e,r,n]){let i=(t+16)/116,s=isNaN(e)?i:i+e/500,a=isNaN(r)?i:i-r/200;return i=1*me(i),s=re*me(s),a=ne*me(a),[ye(3.1338561*s-1.6168667*i-.4906146*a),ye(-.9787684*s+1.9161415*i+.033454*a),ye(.0719453*s-.2289914*i+1.4052427*a),n]}function ye(t){return (t=t<=.00304?12.92*t:1.055*Math.pow(t,1/2.4)-.055)<0?0:t>1?1:t}function me(t){return t>se?t*t*t:ae*(t-ie)}const ge=Object.hasOwn||function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};function xe(t,e){return ge(t,e)?t[e]:void 0}function ve(t){return parseInt(t.padEnd(2,t),16)/255}function be(t,e){return we(e?t/100:t,0,1)}function we(t,e,r){return Math.min(Math.max(e,t),r)}function _e(t){return !t.some(Number.isNaN)}const Se={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]};function Ae(t,e,r){return t+r*(e-t)}function ke(t,e,r){return t.map(((t,n)=>Ae(t,e[n],r)))}class Me{constructor(t,e,r,n=1,i=!0){this.r=t,this.g=e,this.b=r,this.a=n,i||(this.r*=n,this.g*=n,this.b*=n,n||this.overwriteGetter("rgb",[t,e,r,n]));}static parse(t){if(t instanceof Me)return t;if("string"!=typeof t)return;const e=function(t){if("transparent"===(t=t.toLowerCase().trim()))return [0,0,0,0];const e=xe(Se,t);if(e){const[t,r,n]=e;return [t/255,r/255,n/255,1]}if(t.startsWith("#")&&/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(t)){const e=t.length<6?1:2;let r=1;return [ve(t.slice(r,r+=e)),ve(t.slice(r,r+=e)),ve(t.slice(r,r+=e)),ve(t.slice(r,r+e)||"ff")]}if(t.startsWith("rgb")){const e=t.match(/^rgba?\(\s*([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/);if(e){const[t,r,n,i,s,a,o,l,u,c,h,p]=e,f=[i||" ",o||" ",c].join("");if(" "===f||" /"===f||",,"===f||",,,"===f){const t=[n,a,u].join(""),e="%%%"===t?100:""===t?255:0;if(e){const t=[we(+r/e,0,1),we(+s/e,0,1),we(+l/e,0,1),h?be(+h,p):1];if(_e(t))return t}}return}}const r=t.match(/^hsla?\(\s*([\de.+-]+)(?:deg)?(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/);if(r){const[t,e,n,i,s,a,o,l,u]=r,c=[n||" ",s||" ",o].join("");if(" "===c||" /"===c||",,"===c||",,,"===c){const t=[+e,we(+i,0,100),we(+a,0,100),l?be(+l,u):1];if(_e(t))return function([t,e,r,n]){function i(n){const i=(n+t/30)%12,s=e*Math.min(r,1-r);return r-s*Math.max(-1,Math.min(i-3,9-i,1))}return t=ce(t),e/=100,r/=100,[i(0),i(8),i(4),n]}(t)}}}(t);return e?new Me(...e,!1):void 0}get rgb(){const{r:t,g:e,b:r,a:n}=this,i=n||1/0;return this.overwriteGetter("rgb",[t/i,e/i,r/i,n])}get hcl(){return this.overwriteGetter("hcl",function(t){const[e,r,n,i]=he(t),s=Math.sqrt(r*r+n*n);return [Math.round(1e4*s)?ce(Math.atan2(n,r)*ue):NaN,s,e,i]}(this.rgb))}get lab(){return this.overwriteGetter("lab",he(this.rgb))}overwriteGetter(t,e){return Object.defineProperty(this,t,{value:e}),e}toString(){const[t,e,r,n]=this.rgb;return `rgba(${[t,e,r].map((t=>Math.round(255*t))).join(",")},${n})`}static interpolate(t,e,r,n="rgb"){switch(n){case "rgb":{const[n,i,s,a]=ke(t.rgb,e.rgb,r);return new Me(n,i,s,a,!1)}case "hcl":{const[n,i,s,a]=t.hcl,[o,l,u,c]=e.hcl;let h,p;if(isNaN(n)||isNaN(o))isNaN(n)?isNaN(o)?h=NaN:(h=o,1!==s&&0!==s||(p=l)):(h=n,1!==u&&0!==u||(p=i));else {let t=o-n;o>n&&t>180?t-=360:o180&&(t+=360),h=n+r*t;}const[f,d,y,m]=function([t,e,r,n]){return t=isNaN(t)?0:t*le,de([r,Math.cos(t)*e,Math.sin(t)*e,n])}([h,null!=p?p:Ae(i,l,r),Ae(s,u,r),Ae(a,c,r)]);return new Me(f,d,y,m,!1)}case "lab":{const[n,i,s,a]=de(ke(t.lab,e.lab,r));return new Me(n,i,s,a,!1)}}}}Me.black=new Me(0,0,0,1),Me.white=new Me(1,1,1,1),Me.transparent=new Me(0,0,0,0),Me.red=new Me(1,0,0,1);class Ie{constructor(t,e,r){this.sensitivity=t?e?"variant":"case":e?"accent":"base",this.locale=r,this.collator=new Intl.Collator(this.locale?this.locale:[],{sensitivity:this.sensitivity,usage:"search"});}compare(t,e){return this.collator.compare(t,e)}resolvedLocale(){return new Intl.Collator(this.locale?this.locale:[]).resolvedOptions().locale}}const ze=["bottom","center","top"];class Pe{constructor(t,e,r,n,i,s){this.text=t,this.image=e,this.scale=r,this.fontStack=n,this.textColor=i,this.verticalAlign=s;}}class Ce{constructor(t){this.sections=t;}static fromString(t){return new Ce([new Pe(t,null,null,null,null,null)])}isEmpty(){return 0===this.sections.length||!this.sections.some((t=>0!==t.text.length||t.image&&0!==t.image.name.length))}static factory(t){return t instanceof Ce?t:Ce.fromString(t)}toString(){return 0===this.sections.length?"":this.sections.map((t=>t.text)).join("")}}class Ee{constructor(t){this.values=t.slice();}static parse(t){if(t instanceof Ee)return t;if("number"==typeof t)return new Ee([t,t,t,t]);if(Array.isArray(t)&&!(t.length<1||t.length>4)){for(const e of t)if("number"!=typeof e)return;switch(t.length){case 1:t=[t[0],t[0],t[0],t[0]];break;case 2:t=[t[0],t[1],t[0],t[1]];break;case 3:t=[t[0],t[1],t[2],t[1]];}return new Ee(t)}}toString(){return JSON.stringify(this.values)}static interpolate(t,e,r){return new Ee(ke(t.values,e.values,r))}}class Be{constructor(t){this.values=t.slice();}static parse(t){if(t instanceof Be)return t;if("number"==typeof t)return new Be([t]);if(Array.isArray(t)){for(const e of t)if("number"!=typeof e)return;return new Be(t)}}toString(){return JSON.stringify(this.values)}static interpolate(t,e,r){return new Be(ke(t.values,e.values,r))}}class Ve{constructor(t){this.values=t.slice();}static parse(t){if(t instanceof Ve)return t;if("string"==typeof t){const e=Me.parse(t);if(!e)return;return new Ve([e])}if(!Array.isArray(t))return;const e=[];for(const r of t){if("string"!=typeof r)return;const t=Me.parse(r);if(!t)return;e.push(t);}return new Ve(e)}toString(){return JSON.stringify(this.values)}static interpolate(t,e,r,n="rgb"){const i=[];if(t.values.length!=e.values.length)throw new Error(`colorArray: Arrays have mismatched length (${t.values.length} vs. ${e.values.length}), cannot interpolate.`);for(let s=0;s=0&&t<=255&&"number"==typeof e&&e>=0&&e<=255&&"number"==typeof r&&r>=0&&r<=255?void 0===n||"number"==typeof n&&n>=0&&n<=1?null:`Invalid rgba value [${[t,e,r,n].join(", ")}]: 'a' must be between 0 and 1.`:`Invalid rgba value [${("number"==typeof n?[t,e,r,n]:[t,e,r]).join(", ")}]: 'r', 'g', and 'b' must be between 0 and 255.`}function Re(t){if(null===t||"string"==typeof t||"boolean"==typeof t||"number"==typeof t||t instanceof Oe||t instanceof Me||t instanceof Ie||t instanceof Ce||t instanceof Ee||t instanceof Be||t instanceof Ve||t instanceof $e||t instanceof Le)return !0;if(Array.isArray(t)){for(const e of t)if(!Re(e))return !1;return !0}if("object"==typeof t){for(const e in t)if(!Re(t[e]))return !1;return !0}return !1}function Ue(t){if(null===t)return Tt;if("string"==typeof t)return $t;if("boolean"==typeof t)return Lt;if("number"==typeof t)return Ft;if(t instanceof Me)return Ot;if(t instanceof Oe)return Dt;if(t instanceof Ie)return jt;if(t instanceof Ce)return Nt;if(t instanceof Ee)return qt;if(t instanceof Be)return Xt;if(t instanceof Ve)return Gt;if(t instanceof $e)return Yt;if(t instanceof Le)return Zt;if(Array.isArray(t)){const e=t.length;let r;for(const e of t){const t=Ue(e);if(r){if(r===t)continue;r=Ut;break}r=t;}return Ht(r||Ut,e)}return Rt}function je(t){const e=typeof t;return null===t?"":"string"===e||"number"===e||"boolean"===e?String(t):t instanceof Me||t instanceof Oe||t instanceof Ce||t instanceof Ee||t instanceof Be||t instanceof Ve||t instanceof $e||t instanceof Le?t.toString():JSON.stringify(t)}class Ne{constructor(t,e){this.type=t,this.value=e;}static parse(t,e){if(2!==t.length)return e.error(`'literal' expression requires exactly one argument, but found ${t.length-1} instead.`);if(!Re(t[1]))return e.error("invalid value");const r=t[1];let n=Ue(r);const i=e.expectedType;return "array"!==n.kind||0!==n.N||!i||"array"!==i.kind||"number"==typeof i.N&&0!==i.N||(n=i),new Ne(n,r)}evaluate(){return this.value}eachChild(){}outputDefined(){return !0}}const qe={string:$t,number:Ft,boolean:Lt,object:Rt};class Ge{constructor(t,e){this.type=t,this.args=e;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");let r,n=1;const i=t[0];if("array"===i){let i,s;if(t.length>2){const r=t[1];if("string"!=typeof r||!(r in qe)||"object"===r)return e.error('The item type argument of "array" must be one of string, number, boolean',1);i=qe[r],n++;}else i=Ut;if(t.length>3){if(null!==t[2]&&("number"!=typeof t[2]||t[2]<0||t[2]!==Math.floor(t[2])))return e.error('The length argument to "array" must be a positive integer literal',2);s=t[2],n++;}r=Ht(i,s);}else {if(!qe[i])throw new Error(`Types doesn't contain name = ${i}`);r=qe[i];}const s=[];for(;nt.outputDefined()))}}const Xe={"to-boolean":Lt,"to-color":Ot,"to-number":Ft,"to-string":$t};class Ze{constructor(t,e){this.type=t,this.args=e;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");const r=t[0];if(!Xe[r])throw new Error(`Can't parse ${r} as it is not part of the known types`);if(("to-boolean"===r||"to-string"===r)&&2!==t.length)return e.error("Expected one argument.");const n=Xe[r],i=[];for(let r=1;r4?`Invalid rgba value ${JSON.stringify(e)}: expected an array containing either three or four numeric values.`:De(e[0],e[1],e[2],e[3]),!r))return new Me(e[0]/255,e[1]/255,e[2]/255,e[3])}throw new Te(r||`Could not parse color from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case "padding":{let e;for(const r of this.args){e=r.evaluate(t);const n=Ee.parse(e);if(n)return n}throw new Te(`Could not parse padding from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case "numberArray":{let e;for(const r of this.args){e=r.evaluate(t);const n=Be.parse(e);if(n)return n}throw new Te(`Could not parse numberArray from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case "colorArray":{let e;for(const r of this.args){e=r.evaluate(t);const n=Ve.parse(e);if(n)return n}throw new Te(`Could not parse colorArray from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case "variableAnchorOffsetCollection":{let e;for(const r of this.args){e=r.evaluate(t);const n=$e.parse(e);if(n)return n}throw new Te(`Could not parse variableAnchorOffsetCollection from value '${"string"==typeof e?e:JSON.stringify(e)}'`)}case "number":{let e=null;for(const r of this.args){if(e=r.evaluate(t),null===e)return 0;const n=Number(e);if(!isNaN(n))return n}throw new Te(`Could not convert ${JSON.stringify(e)} to number.`)}case "formatted":return Ce.fromString(je(this.args[0].evaluate(t)));case "resolvedImage":return Le.fromString(je(this.args[0].evaluate(t)));case "projectionDefinition":return this.args[0].evaluate(t);default:return je(this.args[0].evaluate(t))}}eachChild(t){this.args.forEach(t);}outputDefined(){return this.args.every((t=>t.outputDefined()))}}const Ye=["Unknown","Point","LineString","Polygon"];class He{constructor(){this.globals=null,this.feature=null,this.featureState=null,this.formattedSection=null,this._parseColorCache=new Map,this.availableImages=null,this.canonical=null;}id(){return this.feature&&"id"in this.feature?this.feature.id:null}geometryType(){return this.feature?"number"==typeof this.feature.type?Ye[this.feature.type]:this.feature.type:null}geometry(){return this.feature&&"geometry"in this.feature?this.feature.geometry:null}canonicalID(){return this.canonical}properties(){return this.feature&&this.feature.properties||{}}parseColor(t){let e=this._parseColorCache.get(t);return e||(e=Me.parse(t),this._parseColorCache.set(t,e)),e}}class Ke{constructor(t,e,r=[],n,i=new Vt,s=[]){this.registry=t,this.path=r,this.key=r.map((t=>`[${t}]`)).join(""),this.scope=i,this.errors=s,this.expectedType=n,this._isConstant=e;}parse(t,e,r,n,i={}){return e?this.concat(e,r,n)._parse(t,i):this._parse(t,i)}_parse(t,e){function r(t,e,r){return "assert"===r?new Ge(e,[t]):"coerce"===r?new Ze(e,[t]):t}if(null!==t&&"string"!=typeof t&&"boolean"!=typeof t&&"number"!=typeof t||(t=["literal",t]),Array.isArray(t)){if(0===t.length)return this.error('Expected an array with at least one element. If you wanted a literal array, use ["literal", []].');const n=t[0];if("string"!=typeof n)return this.error(`Expression name must be a string, but found ${typeof n} instead. If you wanted a literal array, use ["literal", [...]].`,0),null;const i=this.registry[n];if(i){let n=i.parse(t,this);if(!n)return null;if(this.expectedType){const t=this.expectedType,i=n.type;if("string"!==t.kind&&"number"!==t.kind&&"boolean"!==t.kind&&"object"!==t.kind&&"array"!==t.kind||"value"!==i.kind){if("projectionDefinition"===t.kind&&["string","array"].includes(i.kind)||["color","formatted","resolvedImage"].includes(t.kind)&&["value","string"].includes(i.kind)||["padding","numberArray"].includes(t.kind)&&["value","number","array"].includes(i.kind)||"colorArray"===t.kind&&["value","string","array"].includes(i.kind)||"variableAnchorOffsetCollection"===t.kind&&["value","array"].includes(i.kind))n=r(n,t,e.typeAnnotation||"coerce");else if(this.checkSubtype(t,i))return null}else n=r(n,t,e.typeAnnotation||"assert");}if(!(n instanceof Ne)&&"resolvedImage"!==n.type.kind&&this._isConstant(n)){const t=new He;try{n=new Ne(n.type,n.evaluate(t));}catch(t){return this.error(t.message),null}}return n}return this.error(`Unknown expression "${n}". If you wanted a literal array, use ["literal", [...]].`,0)}return this.error(void 0===t?"'undefined' value invalid. Use null instead.":"object"==typeof t?'Bare objects invalid. Use ["literal", {...}] instead.':`Expected an array, but found ${typeof t} instead.`)}concat(t,e,r){const n="number"==typeof t?this.path.concat(t):this.path,i=r?this.scope.concat(r):this.scope;return new Ke(this.registry,this._isConstant,n,e||null,i,this.errors)}error(t,...e){const r=`${this.key}${e.map((t=>`[${t}]`)).join("")}`;this.errors.push(new Bt(r,t));}checkSubtype(t,e){const r=Wt(t,e);return r&&this.error(r),r}}class Je{constructor(t,e){this.type=e.type,this.bindings=[].concat(t),this.result=e;}evaluate(t){return this.result.evaluate(t)}eachChild(t){for(const e of this.bindings)t(e[1]);t(this.result);}static parse(t,e){if(t.length<4)return e.error(`Expected at least 3 arguments, but found ${t.length-1} instead.`);const r=[];for(let n=1;n=r.length)throw new Te(`Array index out of bounds: ${e} > ${r.length-1}.`);if(e!==Math.floor(e))throw new Te(`Array index must be an integer, but found ${e} instead.`);return r[e]}eachChild(t){t(this.index),t(this.input);}outputDefined(){return !1}}class tr{constructor(t,e){this.type=Lt,this.needle=t,this.haystack=e;}static parse(t,e){if(3!==t.length)return e.error(`Expected 2 arguments, but found ${t.length-1} instead.`);const r=e.parse(t[1],1,Ut),n=e.parse(t[2],2,Ut);return r&&n?Qt(r.type,[Lt,$t,Ft,Tt,Ut])?new tr(r,n):e.error(`Expected first argument to be of type boolean, string, number or null, but found ${Kt(r.type)} instead`):null}evaluate(t){const e=this.needle.evaluate(t),r=this.haystack.evaluate(t);if(!r)return !1;if(!te(e,["boolean","string","number","null"]))throw new Te(`Expected first argument to be of type boolean, string, number or null, but found ${Kt(Ue(e))} instead.`);if(!te(r,["string","array"]))throw new Te(`Expected second argument to be of type array or string, but found ${Kt(Ue(r))} instead.`);return r.indexOf(e)>=0}eachChild(t){t(this.needle),t(this.haystack);}outputDefined(){return !0}}class er{constructor(t,e,r){this.type=Ft,this.needle=t,this.haystack=e,this.fromIndex=r;}static parse(t,e){if(t.length<=2||t.length>=5)return e.error(`Expected 3 or 4 arguments, but found ${t.length-1} instead.`);const r=e.parse(t[1],1,Ut),n=e.parse(t[2],2,Ut);if(!r||!n)return null;if(!Qt(r.type,[Lt,$t,Ft,Tt,Ut]))return e.error(`Expected first argument to be of type boolean, string, number or null, but found ${Kt(r.type)} instead`);if(4===t.length){const i=e.parse(t[3],3,Ft);return i?new er(r,n,i):null}return new er(r,n)}evaluate(t){const e=this.needle.evaluate(t),r=this.haystack.evaluate(t);if(!te(e,["boolean","string","number","null"]))throw new Te(`Expected first argument to be of type boolean, string, number or null, but found ${Kt(Ue(e))} instead.`);let n;if(this.fromIndex&&(n=this.fromIndex.evaluate(t)),te(r,["string"])){const t=r.indexOf(e,n);return -1===t?-1:[...r.slice(0,t)].length}if(te(r,["array"]))return r.indexOf(e,n);throw new Te(`Expected second argument to be of type array or string, but found ${Kt(Ue(r))} instead.`)}eachChild(t){t(this.needle),t(this.haystack),this.fromIndex&&t(this.fromIndex);}outputDefined(){return !1}}class rr{constructor(t,e,r,n,i,s){this.inputType=t,this.type=e,this.input=r,this.cases=n,this.outputs=i,this.otherwise=s;}static parse(t,e){if(t.length<5)return e.error(`Expected at least 4 arguments, but found only ${t.length-1}.`);if(t.length%2!=1)return e.error("Expected an even number of arguments.");let r,n;e.expectedType&&"value"!==e.expectedType.kind&&(n=e.expectedType);const i={},s=[];for(let a=2;aNumber.MAX_SAFE_INTEGER)return u.error(`Branch labels must be integers no larger than ${Number.MAX_SAFE_INTEGER}.`);if("number"==typeof t&&Math.floor(t)!==t)return u.error("Numeric branch labels must be integer values.");if(r){if(u.checkSubtype(r,Ue(t)))return null}else r=Ue(t);if(void 0!==i[String(t)])return u.error("Branch labels must be unique.");i[String(t)]=s.length;}const c=e.parse(l,a,n);if(!c)return null;n=n||c.type,s.push(c);}const a=e.parse(t[1],1,Ut);if(!a)return null;const o=e.parse(t[t.length-1],t.length-1,n);return o?"value"!==a.type.kind&&e.concat(1).checkSubtype(r,a.type)?null:new rr(r,n,a,i,s,o):null}evaluate(t){const e=this.input.evaluate(t);return (Ue(e)===this.inputType&&this.outputs[this.cases[e]]||this.otherwise).evaluate(t)}eachChild(t){t(this.input),this.outputs.forEach(t),t(this.otherwise);}outputDefined(){return this.outputs.every((t=>t.outputDefined()))&&this.otherwise.outputDefined()}}class nr{constructor(t,e,r){this.type=t,this.branches=e,this.otherwise=r;}static parse(t,e){if(t.length<4)return e.error(`Expected at least 3 arguments, but found only ${t.length-1}.`);if(t.length%2!=0)return e.error("Expected an odd number of arguments.");let r;e.expectedType&&"value"!==e.expectedType.kind&&(r=e.expectedType);const n=[];for(let i=1;ie.outputDefined()))&&this.otherwise.outputDefined()}}class ir{constructor(t,e,r,n){this.type=t,this.input=e,this.beginIndex=r,this.endIndex=n;}static parse(t,e){if(t.length<=2||t.length>=5)return e.error(`Expected 3 or 4 arguments, but found ${t.length-1} instead.`);const r=e.parse(t[1],1,Ut),n=e.parse(t[2],2,Ft);if(!r||!n)return null;if(!Qt(r.type,[Ht(Ut),$t,Ut]))return e.error(`Expected first argument to be of type array or string, but found ${Kt(r.type)} instead`);if(4===t.length){const i=e.parse(t[3],3,Ft);return i?new ir(r.type,r,n,i):null}return new ir(r.type,r,n)}evaluate(t){const e=this.input.evaluate(t),r=this.beginIndex.evaluate(t);let n;if(this.endIndex&&(n=this.endIndex.evaluate(t)),te(e,["string"]))return [...e].slice(r,n).join("");if(te(e,["array"]))return e.slice(r,n);throw new Te(`Expected first argument to be of type array or string, but found ${Kt(Ue(e))} instead.`)}eachChild(t){t(this.input),t(this.beginIndex),this.endIndex&&t(this.endIndex);}outputDefined(){return !1}}function sr(t,e){const r=t.length-1;let n,i,s=0,a=r,o=0;for(;s<=a;)if(o=Math.floor((s+a)/2),n=t[o],i=t[o+1],n<=e){if(o===r||ee))throw new Te("Input is not a number.");a=o-1;}return 0}class ar{constructor(t,e,r){this.type=t,this.input=e,this.labels=[],this.outputs=[];for(const[t,e]of r)this.labels.push(t),this.outputs.push(e);}static parse(t,e){if(t.length-1<4)return e.error(`Expected at least 4 arguments, but found only ${t.length-1}.`);if((t.length-1)%2!=0)return e.error("Expected an even number of arguments.");const r=e.parse(t[1],1,Ft);if(!r)return null;const n=[];let i=null;e.expectedType&&"value"!==e.expectedType.kind&&(i=e.expectedType);for(let r=1;r=s)return e.error('Input/output pairs for "step" expressions must be arranged with input values in strictly ascending order.',o);const u=e.parse(a,l,i);if(!u)return null;i=i||u.type,n.push([s,u]);}return new ar(i,r,n)}evaluate(t){const e=this.labels,r=this.outputs;if(1===e.length)return r[0].evaluate(t);const n=this.input.evaluate(t);if(n<=e[0])return r[0].evaluate(t);const i=e.length;return n>=e[i-1]?r[i-1].evaluate(t):r[sr(e,n)].evaluate(t)}eachChild(t){t(this.input);for(const e of this.outputs)t(e);}outputDefined(){return this.outputs.every((t=>t.outputDefined()))}}function or(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var lr,ur,cr=function(){if(ur)return lr;function t(t,e,r,n){this.cx=3*t,this.bx=3*(r-t)-this.cx,this.ax=1-this.cx-this.bx,this.cy=3*e,this.by=3*(n-e)-this.cy,this.ay=1-this.cy-this.by,this.p1x=t,this.p1y=e,this.p2x=r,this.p2y=n;}return ur=1,lr=t,t.prototype={sampleCurveX:function(t){return ((this.ax*t+this.bx)*t+this.cx)*t},sampleCurveY:function(t){return ((this.ay*t+this.by)*t+this.cy)*t},sampleCurveDerivativeX:function(t){return (3*this.ax*t+2*this.bx)*t+this.cx},solveCurveX:function(t,e){if(void 0===e&&(e=1e-6),t<0)return 0;if(t>1)return 1;for(var r=t,n=0;n<8;n++){var i=this.sampleCurveX(r)-t;if(Math.abs(i)i?a=r:o=r,r=.5*(o-a)+a;return r},solve:function(t,e){return this.sampleCurveY(this.solveCurveX(t,e))}},lr}(),hr=or(cr);class pr{constructor(t,e,r,n,i){this.type=t,this.operator=e,this.interpolation=r,this.input=n,this.labels=[],this.outputs=[];for(const[t,e]of i)this.labels.push(t),this.outputs.push(e);}static interpolationFactor(t,e,r,n){let i=0;if("exponential"===t.name)i=fr(e,t.base,r,n);else if("linear"===t.name)i=fr(e,1,r,n);else if("cubic-bezier"===t.name){const s=t.controlPoints;i=new hr(s[0],s[1],s[2],s[3]).solve(fr(e,1,r,n));}return i}static parse(t,e){let[r,n,i,...s]=t;if(!Array.isArray(n)||0===n.length)return e.error("Expected an interpolation type expression.",1);if("linear"===n[0])n={name:"linear"};else if("exponential"===n[0]){const t=n[1];if("number"!=typeof t)return e.error("Exponential interpolation requires a numeric base.",1,1);n={name:"exponential",base:t};}else {if("cubic-bezier"!==n[0])return e.error(`Unknown interpolation type ${String(n[0])}`,1,0);{const t=n.slice(1);if(4!==t.length||t.some((t=>"number"!=typeof t||t<0||t>1)))return e.error("Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.",1);n={name:"cubic-bezier",controlPoints:t};}}if(t.length-1<4)return e.error(`Expected at least 4 arguments, but found only ${t.length-1}.`);if((t.length-1)%2!=0)return e.error("Expected an even number of arguments.");if(i=e.parse(i,2,Ft),!i)return null;const a=[];let o=null;"interpolate-hcl"!==r&&"interpolate-lab"!==r||e.expectedType==Gt?e.expectedType&&"value"!==e.expectedType.kind&&(o=e.expectedType):o=Ot;for(let t=0;t=r)return e.error('Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.',i);const u=e.parse(n,l,o);if(!u)return null;o=o||u.type,a.push([r,u]);}return ee(o,Ft)||ee(o,Dt)||ee(o,Ot)||ee(o,qt)||ee(o,Xt)||ee(o,Gt)||ee(o,Yt)||ee(o,Ht(Ft))?new pr(o,r,n,i,a):e.error(`Type ${Kt(o)} is not interpolatable.`)}evaluate(t){const e=this.labels,r=this.outputs;if(1===e.length)return r[0].evaluate(t);const n=this.input.evaluate(t);if(n<=e[0])return r[0].evaluate(t);const i=e.length;if(n>=e[i-1])return r[i-1].evaluate(t);const s=sr(e,n),a=pr.interpolationFactor(this.interpolation,n,e[s],e[s+1]),o=r[s].evaluate(t),l=r[s+1].evaluate(t);switch(this.operator){case "interpolate":switch(this.type.kind){case "number":return Ae(o,l,a);case "color":return Me.interpolate(o,l,a);case "padding":return Ee.interpolate(o,l,a);case "colorArray":return Ve.interpolate(o,l,a);case "numberArray":return Be.interpolate(o,l,a);case "variableAnchorOffsetCollection":return $e.interpolate(o,l,a);case "array":return ke(o,l,a);case "projectionDefinition":return Oe.interpolate(o,l,a)}case "interpolate-hcl":switch(this.type.kind){case "color":return Me.interpolate(o,l,a,"hcl");case "colorArray":return Ve.interpolate(o,l,a,"hcl")}case "interpolate-lab":switch(this.type.kind){case "color":return Me.interpolate(o,l,a,"lab");case "colorArray":return Ve.interpolate(o,l,a,"lab")}}}eachChild(t){t(this.input);for(const e of this.outputs)t(e);}outputDefined(){return this.outputs.every((t=>t.outputDefined()))}}function fr(t,e,r,n){const i=n-r,s=t-r;return 0===i?0:1===e?s/i:(Math.pow(e,s)-1)/(Math.pow(e,i)-1)}const dr={color:Me.interpolate,number:Ae,padding:Ee.interpolate,numberArray:Be.interpolate,colorArray:Ve.interpolate,variableAnchorOffsetCollection:$e.interpolate,array:ke};class yr{constructor(t,e){this.type=t,this.args=e;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");let r=null;const n=e.expectedType;n&&"value"!==n.kind&&(r=n);const i=[];for(const n of t.slice(1)){const t=e.parse(n,1+i.length,r,void 0,{typeAnnotation:"omit"});if(!t)return null;r=r||t.type,i.push(t);}if(!r)throw new Error("No output type");const s=n&&i.some((t=>Wt(n,t.type)));return new yr(s?Ut:r,i)}evaluate(t){let e,r=null,n=0;for(const i of this.args)if(n++,r=i.evaluate(t),r&&r instanceof Le&&!r.available&&(e||(e=r.name),r=null,n===this.args.length&&(r=e)),null!==r)break;return r}eachChild(t){this.args.forEach(t);}outputDefined(){return this.args.every((t=>t.outputDefined()))}}function mr(t,e){return "=="===t||"!="===t?"boolean"===e.kind||"string"===e.kind||"number"===e.kind||"null"===e.kind||"value"===e.kind:"string"===e.kind||"number"===e.kind||"value"===e.kind}function gr(t,e,r,n){return 0===n.compare(e,r)}function xr(t,e,r){const n="=="!==t&&"!="!==t;return class i{constructor(t,e,r){this.type=Lt,this.lhs=t,this.rhs=e,this.collator=r,this.hasUntypedArgument="value"===t.type.kind||"value"===e.type.kind;}static parse(t,e){if(3!==t.length&&4!==t.length)return e.error("Expected two or three arguments.");const r=t[0];let s=e.parse(t[1],1,Ut);if(!s)return null;if(!mr(r,s.type))return e.concat(1).error(`"${r}" comparisons are not supported for type '${Kt(s.type)}'.`);let a=e.parse(t[2],2,Ut);if(!a)return null;if(!mr(r,a.type))return e.concat(2).error(`"${r}" comparisons are not supported for type '${Kt(a.type)}'.`);if(s.type.kind!==a.type.kind&&"value"!==s.type.kind&&"value"!==a.type.kind)return e.error(`Cannot compare types '${Kt(s.type)}' and '${Kt(a.type)}'.`);n&&("value"===s.type.kind&&"value"!==a.type.kind?s=new Ge(a.type,[s]):"value"!==s.type.kind&&"value"===a.type.kind&&(a=new Ge(s.type,[a])));let o=null;if(4===t.length){if("string"!==s.type.kind&&"string"!==a.type.kind&&"value"!==s.type.kind&&"value"!==a.type.kind)return e.error("Cannot use collator to compare non-string types.");if(o=e.parse(t[3],3,jt),!o)return null}return new i(s,a,o)}evaluate(i){const s=this.lhs.evaluate(i),a=this.rhs.evaluate(i);if(n&&this.hasUntypedArgument){const e=Ue(s),r=Ue(a);if(e.kind!==r.kind||"string"!==e.kind&&"number"!==e.kind)throw new Te(`Expected arguments for "${t}" to be (string, string) or (number, number), but found (${e.kind}, ${r.kind}) instead.`)}if(this.collator&&!n&&this.hasUntypedArgument){const t=Ue(s),r=Ue(a);if("string"!==t.kind||"string"!==r.kind)return e(i,s,a)}return this.collator?r(i,s,a,this.collator.evaluate(i)):e(i,s,a)}eachChild(t){t(this.lhs),t(this.rhs),this.collator&&t(this.collator);}outputDefined(){return !0}}}const vr=xr("==",(function(t,e,r){return e===r}),gr),br=xr("!=",(function(t,e,r){return e!==r}),(function(t,e,r,n){return !gr(0,e,r,n)})),wr=xr("<",(function(t,e,r){return e",(function(t,e,r){return e>r}),(function(t,e,r,n){return n.compare(e,r)>0})),Sr=xr("<=",(function(t,e,r){return e<=r}),(function(t,e,r,n){return n.compare(e,r)<=0})),Ar=xr(">=",(function(t,e,r){return e>=r}),(function(t,e,r,n){return n.compare(e,r)>=0}));class kr{constructor(t,e,r){this.type=jt,this.locale=r,this.caseSensitive=t,this.diacriticSensitive=e;}static parse(t,e){if(2!==t.length)return e.error("Expected one argument.");const r=t[1];if("object"!=typeof r||Array.isArray(r))return e.error("Collator options argument must be an object.");const n=e.parse(void 0!==r["case-sensitive"]&&r["case-sensitive"],1,Lt);if(!n)return null;const i=e.parse(void 0!==r["diacritic-sensitive"]&&r["diacritic-sensitive"],1,Lt);if(!i)return null;let s=null;return r.locale&&(s=e.parse(r.locale,1,$t),!s)?null:new kr(n,i,s)}evaluate(t){return new Ie(this.caseSensitive.evaluate(t),this.diacriticSensitive.evaluate(t),this.locale?this.locale.evaluate(t):null)}eachChild(t){t(this.caseSensitive),t(this.diacriticSensitive),this.locale&&t(this.locale);}outputDefined(){return !1}}class Mr{constructor(t,e,r,n,i){this.type=$t,this.number=t,this.locale=e,this.currency=r,this.minFractionDigits=n,this.maxFractionDigits=i;}static parse(t,e){if(3!==t.length)return e.error("Expected two arguments.");const r=e.parse(t[1],1,Ft);if(!r)return null;const n=t[2];if("object"!=typeof n||Array.isArray(n))return e.error("NumberFormat options argument must be an object.");let i=null;if(n.locale&&(i=e.parse(n.locale,1,$t),!i))return null;let s=null;if(n.currency&&(s=e.parse(n.currency,1,$t),!s))return null;let a=null;if(n["min-fraction-digits"]&&(a=e.parse(n["min-fraction-digits"],1,Ft),!a))return null;let o=null;return n["max-fraction-digits"]&&(o=e.parse(n["max-fraction-digits"],1,Ft),!o)?null:new Mr(r,i,s,a,o)}evaluate(t){return new Intl.NumberFormat(this.locale?this.locale.evaluate(t):[],{style:this.currency?"currency":"decimal",currency:this.currency?this.currency.evaluate(t):void 0,minimumFractionDigits:this.minFractionDigits?this.minFractionDigits.evaluate(t):void 0,maximumFractionDigits:this.maxFractionDigits?this.maxFractionDigits.evaluate(t):void 0}).format(this.number.evaluate(t))}eachChild(t){t(this.number),this.locale&&t(this.locale),this.currency&&t(this.currency),this.minFractionDigits&&t(this.minFractionDigits),this.maxFractionDigits&&t(this.maxFractionDigits);}outputDefined(){return !1}}class Ir{constructor(t){this.type=Nt,this.sections=t;}static parse(t,e){if(t.length<2)return e.error("Expected at least one argument.");const r=t[1];if(!Array.isArray(r)&&"object"==typeof r)return e.error("First argument must be an image or text section.");const n=[];let i=!1;for(let r=1;r<=t.length-1;++r){const s=t[r];if(i&&"object"==typeof s&&!Array.isArray(s)){i=!1;let t=null;if(s["font-scale"]&&(t=e.parse(s["font-scale"],1,Ft),!t))return null;let r=null;if(s["text-font"]&&(r=e.parse(s["text-font"],1,Ht($t)),!r))return null;let a=null;if(s["text-color"]&&(a=e.parse(s["text-color"],1,Ot),!a))return null;let o=null;if(s["vertical-align"]){if("string"==typeof s["vertical-align"]&&!ze.includes(s["vertical-align"]))return e.error(`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${s["vertical-align"]}' instead.`);if(o=e.parse(s["vertical-align"],1,$t),!o)return null}const l=n[n.length-1];l.scale=t,l.font=r,l.textColor=a,l.verticalAlign=o;}else {const s=e.parse(t[r],1,Ut);if(!s)return null;const a=s.type.kind;if("string"!==a&&"value"!==a&&"null"!==a&&"resolvedImage"!==a)return e.error("Formatted text type must be 'string', 'value', 'image' or 'null'.");i=!0,n.push({content:s,scale:null,font:null,textColor:null,verticalAlign:null});}}return new Ir(n)}evaluate(t){return new Ce(this.sections.map((e=>{const r=e.content.evaluate(t);return Ue(r)===Zt?new Pe("",r,null,null,null,e.verticalAlign?e.verticalAlign.evaluate(t):null):new Pe(je(r),null,e.scale?e.scale.evaluate(t):null,e.font?e.font.evaluate(t).join(","):null,e.textColor?e.textColor.evaluate(t):null,e.verticalAlign?e.verticalAlign.evaluate(t):null)})))}eachChild(t){for(const e of this.sections)t(e.content),e.scale&&t(e.scale),e.font&&t(e.font),e.textColor&&t(e.textColor),e.verticalAlign&&t(e.verticalAlign);}outputDefined(){return !1}}class zr{constructor(t){this.type=Zt,this.input=t;}static parse(t,e){if(2!==t.length)return e.error("Expected two arguments.");const r=e.parse(t[1],1,$t);return r?new zr(r):e.error("No image name provided.")}evaluate(t){const e=this.input.evaluate(t),r=Le.fromString(e);return r&&t.availableImages&&(r.available=t.availableImages.indexOf(e)>-1),r}eachChild(t){t(this.input);}outputDefined(){return !1}}class Pr{constructor(t){this.type=Ft,this.input=t;}static parse(t,e){if(2!==t.length)return e.error(`Expected 1 argument, but found ${t.length-1} instead.`);const r=e.parse(t[1],1);return r?"array"!==r.type.kind&&"string"!==r.type.kind&&"value"!==r.type.kind?e.error(`Expected argument of type string or array, but found ${Kt(r.type)} instead.`):new Pr(r):null}evaluate(t){const e=this.input.evaluate(t);if("string"==typeof e)return [...e].length;if(Array.isArray(e))return e.length;throw new Te(`Expected value to be of type string or array, but found ${Kt(Ue(e))} instead.`)}eachChild(t){t(this.input);}outputDefined(){return !1}}const Cr=8192;function Er(t,e){const r=(180+t[0])/360,n=(180-180/Math.PI*Math.log(Math.tan(Math.PI/4+t[1]*Math.PI/360)))/360,i=Math.pow(2,e.z);return [Math.round(r*i*Cr),Math.round(n*i*Cr)]}function Br(t,e){const r=Math.pow(2,e.z);return [(i=(t[0]/Cr+e.x)/r,360*i-180),(n=(t[1]/Cr+e.y)/r,360/Math.PI*Math.atan(Math.exp((180-360*n)*Math.PI/180))-90)];var n,i;}function Vr(t,e){t[0]=Math.min(t[0],e[0]),t[1]=Math.min(t[1],e[1]),t[2]=Math.max(t[2],e[0]),t[3]=Math.max(t[3],e[1]);}function Tr(t,e){return !(t[0]<=e[0]||t[2]>=e[2]||t[1]<=e[1]||t[3]>=e[3])}function Fr(t,e,r){const n=t[0]-e[0],i=t[1]-e[1],s=t[0]-r[0],a=t[1]-r[1];return n*a-s*i==0&&n*s<=0&&i*a<=0}function $r(t,e,r,n){return 0!=(i=[n[0]-r[0],n[1]-r[1]])[0]*(s=[e[0]-t[0],e[1]-t[1]])[1]-i[1]*s[0]&&!(!jr(t,e,r,n)||!jr(r,n,t,e));var i,s;}function Lr(t,e,r){for(const n of r)for(let r=0;r(i=t)[1]!=(a=o[e+1])[1]>i[1]&&i[0]<(a[0]-s[0])*(i[1]-s[1])/(a[1]-s[1])+s[0]&&(n=!n);}var i,s,a;return n}function Dr(t,e){for(const r of e)if(Or(t,r))return !0;return !1}function Rr(t,e){for(const r of t)if(!Or(r,e))return !1;for(let r=0;r0&&o<0||a<0&&o>0}function Nr(t,e,r){const n=[];for(let i=0;ir[2]){const e=.5*n;let i=t[0]-r[0]>e?-n:r[0]-t[0]>e?n:0;0===i&&(i=t[0]-r[2]>e?-n:r[2]-t[0]>e?n:0),t[0]+=i;}Vr(e,t);}function Xr(t,e,r,n){const i=Math.pow(2,n.z)*Cr,s=[n.x*Cr,n.y*Cr],a=[];for(const n of t)for(const t of n){const n=[t.x+s[0],t.y+s[1]];Gr(n,e,r,i),a.push(n);}return a}function Zr(t,e,r,n){const i=Math.pow(2,n.z)*Cr,s=[n.x*Cr,n.y*Cr],a=[];for(const r of t){const t=[];for(const n of r){const r=[n.x+s[0],n.y+s[1]];Vr(e,r),t.push(r);}a.push(t);}if(e[2]-e[0]<=i/2){(o=e)[0]=o[1]=1/0,o[2]=o[3]=-1/0;for(const t of a)for(const n of t)Gr(n,e,r,i);}var o;return a}class Yr{constructor(t,e){this.type=Lt,this.geojson=t,this.geometries=e;}static parse(t,e){if(2!==t.length)return e.error(`'within' expression requires exactly one argument, but found ${t.length-1} instead.`);if(Re(t[1])){const e=t[1];if("FeatureCollection"===e.type){const t=[];for(const r of e.features){const{type:e,coordinates:n}=r.geometry;"Polygon"===e&&t.push(n),"MultiPolygon"===e&&t.push(...n);}if(t.length)return new Yr(e,{type:"MultiPolygon",coordinates:t})}else if("Feature"===e.type){const t=e.geometry.type;if("Polygon"===t||"MultiPolygon"===t)return new Yr(e,e.geometry)}else if("Polygon"===e.type||"MultiPolygon"===e.type)return new Yr(e,e)}return e.error("'within' expression requires valid geojson object that contains polygon geometry type.")}evaluate(t){if(null!=t.geometry()&&null!=t.canonicalID()){if("Point"===t.geometryType())return function(t,e){const r=[1/0,1/0,-1/0,-1/0],n=[1/0,1/0,-1/0,-1/0],i=t.canonicalID();if("Polygon"===e.type){const s=Nr(e.coordinates,n,i),a=Xr(t.geometry(),r,n,i);if(!Tr(r,n))return !1;for(const t of a)if(!Or(t,s))return !1}if("MultiPolygon"===e.type){const s=qr(e.coordinates,n,i),a=Xr(t.geometry(),r,n,i);if(!Tr(r,n))return !1;for(const t of a)if(!Dr(t,s))return !1}return !0}(t,this.geometries);if("LineString"===t.geometryType())return function(t,e){const r=[1/0,1/0,-1/0,-1/0],n=[1/0,1/0,-1/0,-1/0],i=t.canonicalID();if("Polygon"===e.type){const s=Nr(e.coordinates,n,i),a=Zr(t.geometry(),r,n,i);if(!Tr(r,n))return !1;for(const t of a)if(!Rr(t,s))return !1}if("MultiPolygon"===e.type){const s=qr(e.coordinates,n,i),a=Zr(t.geometry(),r,n,i);if(!Tr(r,n))return !1;for(const t of a)if(!Ur(t,s))return !1}return !0}(t,this.geometries)}return !1}eachChild(){}outputDefined(){return !0}}let Hr=class{constructor(t=[],e=(t,e)=>te?1:0){if(this.data=t,this.length=this.data.length,this.compare=e,this.length>0)for(let t=(this.length>>1)-1;t>=0;t--)this._down(t);}push(t){this.data.push(t),this._up(this.length++);}pop(){if(0===this.length)return;const t=this.data[0],e=this.data.pop();return --this.length>0&&(this.data[0]=e,this._down(0)),t}peek(){return this.data[0]}_up(t){const{data:e,compare:r}=this,n=e[t];for(;t>0;){const i=t-1>>1,s=e[i];if(r(n,s)>=0)break;e[t]=s,t=i;}e[t]=n;}_down(t){const{data:e,compare:r}=this,n=this.length>>1,i=e[t];for(;t=0)break;e[t]=e[n],t=n;}e[t]=i;}};function Kr(t,e,r=0,n=t.length-1,i=Wr){for(;n>r;){if(n-r>600){const s=n-r+1,a=e-r+1,o=Math.log(s),l=.5*Math.exp(2*o/3),u=.5*Math.sqrt(o*l*(s-l)/s)*(a-s/2<0?-1:1);Kr(t,e,Math.max(r,Math.floor(e-a*l/s+u)),Math.min(n,Math.floor(e+(s-a)*l/s+u)),i);}const s=t[e];let a=r,o=n;for(Jr(t,r,e),i(t[n],s)>0&&Jr(t,r,n);a0;)o--;}0===i(t[r],s)?Jr(t,r,o):(o++,Jr(t,o,n)),o<=e&&(r=o+1),e<=o&&(n=o-1);}}function Jr(t,e,r){const n=t[e];t[e]=t[r],t[r]=n;}function Wr(t,e){return te?1:0}function Qr(t,e){if(t.length<=1)return [t];const r=[];let n,i;for(const e of t){const t=en(e);0!==t&&(e.area=Math.abs(t),void 0===i&&(i=t<0),i===t<0?(n&&r.push(n),n=[e]):n.push(e));}if(n&&r.push(n),e>1)for(let t=0;t1?(l=t[o+1][0],u=t[o+1][1]):p>0&&(l+=c/this.kx*p,u+=h/this.ky*p)),c=this.wrap(e[0]-l)*this.kx,h=(e[1]-u)*this.ky;const f=c*c+h*h;f180;)t-=360;return t}}function on(t,e){return e[0]-t[0]}function ln(t){return t[1]-t[0]+1}function un(t,e){return t[1]>=t[0]&&t[1]t[1])return [null,null];const r=ln(t);if(e){if(2===r)return [t,null];const e=Math.floor(r/2);return [[t[0],t[0]+e],[t[0]+e,t[1]]]}if(1===r)return [t,null];const n=Math.floor(r/2)-1;return [[t[0],t[0]+n],[t[0]+n+1,t[1]]]}function hn(t,e){if(!un(e,t.length))return [1/0,1/0,-1/0,-1/0];const r=[1/0,1/0,-1/0,-1/0];for(let n=e[0];n<=e[1];++n)Vr(r,t[n]);return r}function pn(t){const e=[1/0,1/0,-1/0,-1/0];for(const r of t)for(const t of r)Vr(e,t);return e}function fn(t){return t[0]!==-1/0&&t[1]!==-1/0&&t[2]!==1/0&&t[3]!==1/0}function dn(t,e,r){if(!fn(t)||!fn(e))return NaN;let n=0,i=0;return t[2]e[2]&&(n=t[0]-e[2]),t[1]>e[3]&&(i=t[1]-e[3]),t[3]=n)return n;if(Tr(i,s)){if(wn(t,e))return 0}else if(wn(e,t))return 0;let a=1/0;for(const n of t)for(let t=0,i=n.length,s=i-1;t0;){const i=a.pop();if(i[0]>=s)continue;const l=i[1],u=e?50:100;if(ln(l)<=u){if(!un(l,t.length))return NaN;if(e){const e=bn(t,l,r,n);if(isNaN(e)||0===e)return e;s=Math.min(s,e);}else for(let e=l[0];e<=l[1];++e){const i=vn(t[e],r,n);if(s=Math.min(s,i),0===s)return 0}}else {const r=cn(l,e);Sn(a,s,n,t,o,r[0]),Sn(a,s,n,t,o,r[1]);}}return s}function Mn(t,e,r,n,i,s=1/0){let a=Math.min(s,i.distance(t[0],r[0]));if(0===a)return a;const o=new Hr([[0,[0,t.length-1],[0,r.length-1]]],on);for(;o.length>0;){const s=o.pop();if(s[0]>=a)continue;const l=s[1],u=s[2],c=e?50:100,h=n?50:100;if(ln(l)<=c&&ln(u)<=h){if(!un(l,t.length)&&un(u,r.length))return NaN;let s;if(e&&n)s=gn(t,l,r,u,i),a=Math.min(a,s);else if(e&&!n){const e=t.slice(l[0],l[1]+1);for(let t=u[0];t<=u[1];++t)if(s=yn(r[t],e,i),a=Math.min(a,s),0===a)return a}else if(!e&&n){const e=r.slice(u[0],u[1]+1);for(let r=l[0];r<=l[1];++r)if(s=yn(t[r],e,i),a=Math.min(a,s),0===a)return a}else s=xn(t,l,r,u,i),a=Math.min(a,s);}else {const s=cn(l,e),c=cn(u,n);An(o,a,i,t,r,s[0],c[0]),An(o,a,i,t,r,s[0],c[1]),An(o,a,i,t,r,s[1],c[0]),An(o,a,i,t,r,s[1],c[1]);}}return a}function In(t){return "MultiPolygon"===t.type?t.coordinates.map((t=>({type:"Polygon",coordinates:t}))):"MultiLineString"===t.type?t.coordinates.map((t=>({type:"LineString",coordinates:t}))):"MultiPoint"===t.type?t.coordinates.map((t=>({type:"Point",coordinates:t}))):[t]}class zn{constructor(t,e){this.type=Ft,this.geojson=t,this.geometries=e;}static parse(t,e){if(2!==t.length)return e.error(`'distance' expression requires exactly one argument, but found ${t.length-1} instead.`);if(Re(t[1])){const e=t[1];if("FeatureCollection"===e.type)return new zn(e,e.features.map((t=>In(t.geometry))).flat());if("Feature"===e.type)return new zn(e,In(e.geometry));if("type"in e&&"coordinates"in e)return new zn(e,In(e))}return e.error("'distance' expression requires valid geojson object that contains polygon geometry type.")}evaluate(t){if(null!=t.geometry()&&null!=t.canonicalID()){if("Point"===t.geometryType())return function(t,e){const r=t.geometry(),n=r.flat().map((e=>Br([e.x,e.y],t.canonical)));if(0===r.length)return NaN;const i=new an(n[0][1]);let s=1/0;for(const t of e){switch(t.type){case "Point":s=Math.min(s,Mn(n,!1,[t.coordinates],!1,i,s));break;case "LineString":s=Math.min(s,Mn(n,!1,t.coordinates,!0,i,s));break;case "Polygon":s=Math.min(s,kn(n,!1,t.coordinates,i,s));}if(0===s)return s}return s}(t,this.geometries);if("LineString"===t.geometryType())return function(t,e){const r=t.geometry(),n=r.flat().map((e=>Br([e.x,e.y],t.canonical)));if(0===r.length)return NaN;const i=new an(n[0][1]);let s=1/0;for(const t of e){switch(t.type){case "Point":s=Math.min(s,Mn(n,!0,[t.coordinates],!1,i,s));break;case "LineString":s=Math.min(s,Mn(n,!0,t.coordinates,!0,i,s));break;case "Polygon":s=Math.min(s,kn(n,!0,t.coordinates,i,s));}if(0===s)return s}return s}(t,this.geometries);if("Polygon"===t.geometryType())return function(t,e){const r=t.geometry();if(0===r.length||0===r[0].length)return NaN;const n=Qr(r,0).map((e=>e.map((e=>e.map((e=>Br([e.x,e.y],t.canonical))))))),i=new an(n[0][0][0][1]);let s=1/0;for(const t of e)for(const e of n){switch(t.type){case "Point":s=Math.min(s,kn([t.coordinates],!1,e,i,s));break;case "LineString":s=Math.min(s,kn(t.coordinates,!0,e,i,s));break;case "Polygon":s=Math.min(s,_n(e,t.coordinates,i,s));}if(0===s)return s}return s}(t,this.geometries)}return NaN}eachChild(){}outputDefined(){return !0}}class Pn{constructor(t){this.type=Ut,this.key=t;}static parse(t,e){if(2!==t.length)return e.error(`Expected 1 argument, but found ${t.length-1} instead.`);const r=t[1];return null==r?e.error("Global state property must be defined."):"string"!=typeof r?e.error(`Global state property must be string, but found ${typeof t[1]} instead.`):new Pn(r)}evaluate(t){var e;const r=null===(e=t.globals)||void 0===e?void 0:e.globalState;return r&&0!==Object.keys(r).length?xe(r,this.key):null}eachChild(){}outputDefined(){return !1}}const Cn={"==":vr,"!=":br,">":_r,"<":wr,">=":Ar,"<=":Sr,array:Ge,at:Qe,boolean:Ge,case:nr,coalesce:yr,collator:kr,format:Ir,image:zr,in:tr,"index-of":er,interpolate:pr,"interpolate-hcl":pr,"interpolate-lab":pr,length:Pr,let:Je,literal:Ne,match:rr,number:Ge,"number-format":Mr,object:Ge,slice:ir,step:ar,string:Ge,"to-boolean":Ze,"to-color":Ze,"to-number":Ze,"to-string":Ze,var:We,within:Yr,distance:zn,"global-state":Pn};class En{constructor(t,e,r,n){this.name=t,this.type=e,this._evaluate=r,this.args=n;}evaluate(t){return this._evaluate(t,this.args)}eachChild(t){this.args.forEach(t);}outputDefined(){return !1}static parse(t,e){const r=t[0],n=En.definitions[r];if(!n)return e.error(`Unknown expression "${r}". If you wanted a literal array, use ["literal", [...]].`,0);const i=Array.isArray(n)?n[0]:n.type,s=Array.isArray(n)?[[n[1],n[2]]]:n.overloads,a=s.filter((([e])=>!Array.isArray(e)||e.length===t.length-1));let o=null;for(const[n,s]of a){o=new Ke(e.registry,$n,e.path,null,e.scope);const a=[];let l=!1;for(let e=1;e{return e=t,Array.isArray(e)?`(${e.map(Kt).join(", ")})`:`(${Kt(e.type)}...)`;var e;})).join(" | "),n=[];for(let r=1;r{r=e?r&&$n(t):r&&t instanceof Ne;})),!!r&&Ln(t)&&Dn(t,["zoom","heatmap-density","elevation","line-progress","accumulated","is-supported-script"])}function Ln(t){if(t instanceof En){if("get"===t.name&&1===t.args.length)return !1;if("feature-state"===t.name)return !1;if("has"===t.name&&1===t.args.length)return !1;if("properties"===t.name||"geometry-type"===t.name||"id"===t.name)return !1;if(/^filter-/.test(t.name))return !1}if(t instanceof Yr)return !1;if(t instanceof zn)return !1;let e=!0;return t.eachChild((t=>{e&&!Ln(t)&&(e=!1);})),e}function On(t){if(t instanceof En&&"feature-state"===t.name)return !1;let e=!0;return t.eachChild((t=>{e&&!On(t)&&(e=!1);})),e}function Dn(t,e){if(t instanceof En&&e.indexOf(t.name)>=0)return !1;let r=!0;return t.eachChild((t=>{r&&!Dn(t,e)&&(r=!1);})),r}function Rn(t){return {result:"success",value:t}}function Un(t){return {result:"error",value:t}}function jn(t){return "data-driven"===t["property-type"]||"cross-faded-data-driven"===t["property-type"]}function Nn(t){return !!t.expression&&t.expression.parameters.indexOf("zoom")>-1}function qn(t){return !!t.expression&&t.expression.interpolated}function Gn(t){return t instanceof Number?"number":t instanceof String?"string":t instanceof Boolean?"boolean":Array.isArray(t)?"array":null===t?"null":typeof t}function Xn(t){return "object"==typeof t&&null!==t&&!Array.isArray(t)&&Ue(t)===Rt}function Zn(t){return t}function Yn(t,e){const r=t.stops&&"object"==typeof t.stops[0][0],n=r||!(r||void 0!==t.property),i=t.type||(qn(e)?"exponential":"interval"),s=function(t){switch(t.type){case "color":return Me.parse;case "padding":return Ee.parse;case "numberArray":return Be.parse;case "colorArray":return Ve.parse;default:return null}}(e);if(s&&((t=Et({},t)).stops&&(t.stops=t.stops.map((t=>[t[0],s(t[1])]))),t.default=s(t.default?t.default:e.default)),t.colorSpace&&"rgb"!==(a=t.colorSpace)&&"hcl"!==a&&"lab"!==a)throw new Error(`Unknown color space: "${t.colorSpace}"`);var a;const o=function(t){switch(t){case "exponential":return Wn;case "interval":return Jn;case "categorical":return Kn;case "identity":return Qn;default:throw new Error(`Unknown function type "${t}"`)}}(i);let l,u;if("categorical"===i){l=Object.create(null);for(const e of t.stops)l[e[0]]=e[1];u=typeof t.stops[0][0];}if(r){const r={},n=[];for(let e=0;et[0])),evaluate:({zoom:r},n)=>Wn({stops:i,base:t.base},e,r).evaluate(r,n)}}if(n){const r="exponential"===i?{name:"exponential",base:void 0!==t.base?t.base:1}:null;return {kind:"camera",interpolationType:r,interpolationFactor:pr.interpolationFactor.bind(void 0,r),zoomStops:t.stops.map((t=>t[0])),evaluate:({zoom:r})=>o(t,e,r,l,u)}}return {kind:"source",evaluate(r,n){const i=n&&n.properties?n.properties[t.property]:void 0;return void 0===i?Hn(t.default,e.default):o(t,e,i,l,u)}}}function Hn(t,e,r){return void 0!==t?t:void 0!==e?e:void 0!==r?r:void 0}function Kn(t,e,r,n,i){return Hn(typeof r===i?n[r]:void 0,t.default,e.default)}function Jn(t,e,r){if("number"!==Gn(r))return Hn(t.default,e.default);const n=t.stops.length;if(1===n)return t.stops[0][1];if(r<=t.stops[0][0])return t.stops[0][1];if(r>=t.stops[n-1][0])return t.stops[n-1][1];const i=sr(t.stops.map((t=>t[0])),r);return t.stops[i][1]}function Wn(t,e,r){const n=void 0!==t.base?t.base:1;if("number"!==Gn(r))return Hn(t.default,e.default);const i=t.stops.length;if(1===i)return t.stops[0][1];if(r<=t.stops[0][0])return t.stops[0][1];if(r>=t.stops[i-1][0])return t.stops[i-1][1];const s=sr(t.stops.map((t=>t[0])),r),a=function(t,e,r,n){const i=n-r,s=t-r;return 0===i?0:1===e?s/i:(Math.pow(e,s)-1)/(Math.pow(e,i)-1)}(r,n,t.stops[s][0],t.stops[s+1][0]),o=t.stops[s][1],l=t.stops[s+1][1],u=dr[e.type]||Zn;return "function"==typeof o.evaluate?{evaluate(...e){const r=o.evaluate.apply(void 0,e),n=l.evaluate.apply(void 0,e);if(void 0!==r&&void 0!==n)return u(r,n,a,t.colorSpace)}}:u(o,l,a,t.colorSpace)}function Qn(t,e,r){switch(e.type){case "color":r=Me.parse(r);break;case "formatted":r=Ce.fromString(r.toString());break;case "resolvedImage":r=Le.fromString(r.toString());break;case "padding":r=Ee.parse(r);break;case "colorArray":r=Ve.parse(r);break;case "numberArray":r=Be.parse(r);break;default:Gn(r)===e.type||"enum"===e.type&&e.values[r]||(r=void 0);}return Hn(r,t.default,e.default)}En.register(Cn,{error:[{kind:"error"},[$t],(t,[e])=>{throw new Te(e.evaluate(t))}],typeof:[$t,[Ut],(t,[e])=>Kt(Ue(e.evaluate(t)))],"to-rgba":[Ht(Ft,4),[Ot],(t,[e])=>{const[r,n,i,s]=e.evaluate(t).rgb;return [255*r,255*n,255*i,s]}],rgb:[Ot,[Ft,Ft,Ft],Bn],rgba:[Ot,[Ft,Ft,Ft,Ft],Bn],has:{type:Lt,overloads:[[[$t],(t,[e])=>Vn(e.evaluate(t),t.properties())],[[$t,Rt],(t,[e,r])=>Vn(e.evaluate(t),r.evaluate(t))]]},get:{type:Ut,overloads:[[[$t],(t,[e])=>Tn(e.evaluate(t),t.properties())],[[$t,Rt],(t,[e,r])=>Tn(e.evaluate(t),r.evaluate(t))]]},"feature-state":[Ut,[$t],(t,[e])=>Tn(e.evaluate(t),t.featureState||{})],properties:[Rt,[],t=>t.properties()],"geometry-type":[$t,[],t=>t.geometryType()],id:[Ut,[],t=>t.id()],zoom:[Ft,[],t=>t.globals.zoom],"heatmap-density":[Ft,[],t=>t.globals.heatmapDensity||0],elevation:[Ft,[],t=>t.globals.elevation||0],"line-progress":[Ft,[],t=>t.globals.lineProgress||0],accumulated:[Ut,[],t=>void 0===t.globals.accumulated?null:t.globals.accumulated],"+":[Ft,Fn(Ft),(t,e)=>{let r=0;for(const n of e)r+=n.evaluate(t);return r}],"*":[Ft,Fn(Ft),(t,e)=>{let r=1;for(const n of e)r*=n.evaluate(t);return r}],"-":{type:Ft,overloads:[[[Ft,Ft],(t,[e,r])=>e.evaluate(t)-r.evaluate(t)],[[Ft],(t,[e])=>-e.evaluate(t)]]},"/":[Ft,[Ft,Ft],(t,[e,r])=>e.evaluate(t)/r.evaluate(t)],"%":[Ft,[Ft,Ft],(t,[e,r])=>e.evaluate(t)%r.evaluate(t)],ln2:[Ft,[],()=>Math.LN2],pi:[Ft,[],()=>Math.PI],e:[Ft,[],()=>Math.E],"^":[Ft,[Ft,Ft],(t,[e,r])=>Math.pow(e.evaluate(t),r.evaluate(t))],sqrt:[Ft,[Ft],(t,[e])=>Math.sqrt(e.evaluate(t))],log10:[Ft,[Ft],(t,[e])=>Math.log(e.evaluate(t))/Math.LN10],ln:[Ft,[Ft],(t,[e])=>Math.log(e.evaluate(t))],log2:[Ft,[Ft],(t,[e])=>Math.log(e.evaluate(t))/Math.LN2],sin:[Ft,[Ft],(t,[e])=>Math.sin(e.evaluate(t))],cos:[Ft,[Ft],(t,[e])=>Math.cos(e.evaluate(t))],tan:[Ft,[Ft],(t,[e])=>Math.tan(e.evaluate(t))],asin:[Ft,[Ft],(t,[e])=>Math.asin(e.evaluate(t))],acos:[Ft,[Ft],(t,[e])=>Math.acos(e.evaluate(t))],atan:[Ft,[Ft],(t,[e])=>Math.atan(e.evaluate(t))],min:[Ft,Fn(Ft),(t,e)=>Math.min(...e.map((e=>e.evaluate(t))))],max:[Ft,Fn(Ft),(t,e)=>Math.max(...e.map((e=>e.evaluate(t))))],abs:[Ft,[Ft],(t,[e])=>Math.abs(e.evaluate(t))],round:[Ft,[Ft],(t,[e])=>{const r=e.evaluate(t);return r<0?-Math.round(-r):Math.round(r)}],floor:[Ft,[Ft],(t,[e])=>Math.floor(e.evaluate(t))],ceil:[Ft,[Ft],(t,[e])=>Math.ceil(e.evaluate(t))],"filter-==":[Lt,[$t,Ut],(t,[e,r])=>t.properties()[e.value]===r.value],"filter-id-==":[Lt,[Ut],(t,[e])=>t.id()===e.value],"filter-type-==":[Lt,[$t],(t,[e])=>t.geometryType()===e.value],"filter-<":[Lt,[$t,Ut],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n{const r=t.id(),n=e.value;return typeof r==typeof n&&r":[Lt,[$t,Ut],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n>i}],"filter-id->":[Lt,[Ut],(t,[e])=>{const r=t.id(),n=e.value;return typeof r==typeof n&&r>n}],"filter-<=":[Lt,[$t,Ut],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n<=i}],"filter-id-<=":[Lt,[Ut],(t,[e])=>{const r=t.id(),n=e.value;return typeof r==typeof n&&r<=n}],"filter->=":[Lt,[$t,Ut],(t,[e,r])=>{const n=t.properties()[e.value],i=r.value;return typeof n==typeof i&&n>=i}],"filter-id->=":[Lt,[Ut],(t,[e])=>{const r=t.id(),n=e.value;return typeof r==typeof n&&r>=n}],"filter-has":[Lt,[Ut],(t,[e])=>e.value in t.properties()],"filter-has-id":[Lt,[],t=>null!==t.id()&&void 0!==t.id()],"filter-type-in":[Lt,[Ht($t)],(t,[e])=>e.value.indexOf(t.geometryType())>=0],"filter-id-in":[Lt,[Ht(Ut)],(t,[e])=>e.value.indexOf(t.id())>=0],"filter-in-small":[Lt,[$t,Ht(Ut)],(t,[e,r])=>r.value.indexOf(t.properties()[e.value])>=0],"filter-in-large":[Lt,[$t,Ht(Ut)],(t,[e,r])=>function(t,e,r,n){for(;r<=n;){const i=r+n>>1;if(e[i]===t)return !0;e[i]>t?n=i-1:r=i+1;}return !1}(t.properties()[e.value],r.value,0,r.value.length-1)],all:{type:Lt,overloads:[[[Lt,Lt],(t,[e,r])=>e.evaluate(t)&&r.evaluate(t)],[Fn(Lt),(t,e)=>{for(const r of e)if(!r.evaluate(t))return !1;return !0}]]},any:{type:Lt,overloads:[[[Lt,Lt],(t,[e,r])=>e.evaluate(t)||r.evaluate(t)],[Fn(Lt),(t,e)=>{for(const r of e)if(r.evaluate(t))return !0;return !1}]]},"!":[Lt,[Lt],(t,[e])=>!e.evaluate(t)],"is-supported-script":[Lt,[$t],(t,[e])=>{const r=t.globals&&t.globals.isSupportedScript;return !r||r(e.evaluate(t))}],upcase:[$t,[$t],(t,[e])=>e.evaluate(t).toUpperCase()],downcase:[$t,[$t],(t,[e])=>e.evaluate(t).toLowerCase()],concat:[$t,Fn(Ut),(t,e)=>e.map((e=>je(e.evaluate(t)))).join("")],"resolved-locale":[$t,[jt],(t,[e])=>e.evaluate(t).resolvedLocale()]});class ti{constructor(t,e){this.expression=t,this._warningHistory={},this._evaluator=new He,this._defaultValue=e?function(t){if("color"===t.type&&Xn(t.default))return new Me(0,0,0,0);switch(t.type){case "color":return Me.parse(t.default)||null;case "padding":return Ee.parse(t.default)||null;case "numberArray":return Be.parse(t.default)||null;case "colorArray":return Ve.parse(t.default)||null;case "variableAnchorOffsetCollection":return $e.parse(t.default)||null;case "projectionDefinition":return Oe.parse(t.default)||null;default:return void 0===t.default?null:t.default}}(e):null,this._enumValues=e&&"enum"===e.type?e.values:null;}evaluateWithoutErrorHandling(t,e,r,n,i,s){return this._evaluator.globals=t,this._evaluator.feature=e,this._evaluator.featureState=r,this._evaluator.canonical=n,this._evaluator.availableImages=i||null,this._evaluator.formattedSection=s,this.expression.evaluate(this._evaluator)}evaluate(t,e,r,n,i,s){this._evaluator.globals=t,this._evaluator.feature=e||null,this._evaluator.featureState=r||null,this._evaluator.canonical=n,this._evaluator.availableImages=i||null,this._evaluator.formattedSection=s||null;try{const t=this.expression.evaluate(this._evaluator);if(null==t||"number"==typeof t&&t!=t)return this._defaultValue;if(this._enumValues&&!(t in this._enumValues))throw new Te(`Expected value to be one of ${Object.keys(this._enumValues).map((t=>JSON.stringify(t))).join(", ")}, but found ${JSON.stringify(t)} instead.`);return t}catch(t){return this._warningHistory[t.message]||(this._warningHistory[t.message]=!0,"undefined"!=typeof console&&console.warn(t.message)),this._defaultValue}}}function ei(t){return Array.isArray(t)&&t.length>0&&"string"==typeof t[0]&&t[0]in Cn}function ri(t,e){const r=new Ke(Cn,$n,[],e?function(t){const e={color:Ot,string:$t,number:Ft,enum:$t,boolean:Lt,formatted:Nt,padding:qt,numberArray:Xt,colorArray:Gt,projectionDefinition:Dt,resolvedImage:Zt,variableAnchorOffsetCollection:Yt};return "array"===t.type?Ht(e[t.value]||Ut,t.length):e[t.type]}(e):void 0),n=r.parse(t,void 0,void 0,void 0,e&&"string"===e.type?{typeAnnotation:"coerce"}:void 0);return n?Rn(new ti(n,e)):Un(r.errors)}class ni{constructor(t,e){this.kind=t,this._styleExpression=e,this.isStateDependent="constant"!==t&&!On(e.expression),this.globalStateRefs=li(e.expression);}evaluateWithoutErrorHandling(t,e,r,n,i,s){return this._styleExpression.evaluateWithoutErrorHandling(t,e,r,n,i,s)}evaluate(t,e,r,n,i,s){return this._styleExpression.evaluate(t,e,r,n,i,s)}}class ii{constructor(t,e,r,n){this.kind=t,this.zoomStops=r,this._styleExpression=e,this.isStateDependent="camera"!==t&&!On(e.expression),this.globalStateRefs=li(e.expression),this.interpolationType=n;}evaluateWithoutErrorHandling(t,e,r,n,i,s){return this._styleExpression.evaluateWithoutErrorHandling(t,e,r,n,i,s)}evaluate(t,e,r,n,i,s){return this._styleExpression.evaluate(t,e,r,n,i,s)}interpolationFactor(t,e,r){return this.interpolationType?pr.interpolationFactor(this.interpolationType,t,e,r):0}}function si(t,e){const r=ri(t,e);if("error"===r.result)return r;const n=r.value.expression,i=Ln(n);if(!i&&!jn(e))return Un([new Bt("","data expressions not supported")]);const s=Dn(n,["zoom"]);if(!s&&!Nn(e))return Un([new Bt("","zoom expressions not supported")]);const a=oi(n);return a||s?a instanceof Bt?Un([a]):a instanceof pr&&!qn(e)?Un([new Bt("",'"interpolate" expressions cannot be used with this property')]):Rn(a?new ii(i?"camera":"composite",r.value,a.labels,a instanceof pr?a.interpolation:void 0):new ni(i?"constant":"source",r.value)):Un([new Bt("",'"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.')])}class ai{constructor(t,e){this._parameters=t,this._specification=e,Et(this,Yn(this._parameters,this._specification));}static deserialize(t){return new ai(t._parameters,t._specification)}static serialize(t){return {_parameters:t._parameters,_specification:t._specification}}}function oi(t){let e=null;if(t instanceof Je)e=oi(t.result);else if(t instanceof yr){for(const r of t.args)if(e=oi(r),e)break}else (t instanceof ar||t instanceof pr)&&t.input instanceof En&&"zoom"===t.input.name&&(e=t);return e instanceof Bt||t.eachChild((t=>{const r=oi(t);r instanceof Bt?e=r:!e&&r?e=new Bt("",'"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.'):e&&r&&e!==r&&(e=new Bt("",'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.'));})),e}function li(t,e=new Set){return t instanceof Pn&&e.add(t.key),t.eachChild((t=>{li(t,e);})),e}function ui(t){if(!0===t||!1===t)return !0;if(!Array.isArray(t)||0===t.length)return !1;switch(t[0]){case "has":return t.length>=2&&"$id"!==t[1]&&"$type"!==t[1];case "in":return t.length>=3&&("string"!=typeof t[1]||Array.isArray(t[2]));case "!in":case "!has":case "none":return !1;case "==":case "!=":case ">":case ">=":case "<":case "<=":return 3!==t.length||Array.isArray(t[1])||Array.isArray(t[2]);case "any":case "all":for(const e of t.slice(1))if(!ui(e)&&"boolean"!=typeof e)return !1;return !0;default:return !0}}const ci={type:"boolean",default:!1,transition:!1,"property-type":"data-driven",expression:{interpolated:!1,parameters:["zoom","feature"]}};function hi(t){if(null==t)return {filter:()=>!0,needGeometry:!1,getGlobalStateRefs:()=>new Set};ui(t)||(t=di(t));const e=ri(t,ci);if("error"===e.result)throw new Error(e.value.map((t=>`${t.key}: ${t.message}`)).join(", "));return {filter:(t,r,n)=>e.value.evaluate(t,r,{},n),needGeometry:fi(t),getGlobalStateRefs:()=>li(e.value.expression)}}function pi(t,e){return te?1:0}function fi(t){if(!Array.isArray(t))return !1;if("within"===t[0]||"distance"===t[0])return !0;for(let e=1;e"===e||"<="===e||">="===e?yi(t[1],t[2],e):"any"===e?(r=t.slice(1),["any"].concat(r.map(di))):"all"===e?["all"].concat(t.slice(1).map(di)):"none"===e?["all"].concat(t.slice(1).map(di).map(xi)):"in"===e?mi(t[1],t.slice(2)):"!in"===e?xi(mi(t[1],t.slice(2))):"has"===e?gi(t[1]):"!has"!==e||xi(gi(t[1]));var r;}function yi(t,e,r){switch(t){case "$type":return [`filter-type-${r}`,e];case "$id":return [`filter-id-${r}`,e];default:return [`filter-${r}`,t,e]}}function mi(t,e){if(0===e.length)return !1;switch(t){case "$type":return ["filter-type-in",["literal",e]];case "$id":return ["filter-id-in",["literal",e]];default:return e.length>200&&!e.some((t=>typeof t!=typeof e[0]))?["filter-in-large",t,["literal",e.sort(pi)]]:["filter-in-small",t,["literal",e]]}}function gi(t){switch(t){case "$type":return !0;case "$id":return ["filter-has-id"];default:return ["filter-has",t]}}function xi(t){return ["!",t]}function vi(t){const e=typeof t;if("number"===e||"boolean"===e||"string"===e||null==t)return JSON.stringify(t);if(Array.isArray(t)){let e="[";for(const r of t)e+=`${vi(r)},`;return `${e}]`}const r=Object.keys(t).sort();let n="{";for(let e=0;en.maximum?[new Ct(e,r,`${r} is greater than the maximum value ${n.maximum}`)]:[]}function Ii(t){const e=t.valueSpec,r=_i(t.value.type);let n,i,s,a={};const o="categorical"!==r&&void 0===t.value.property,l=!o,u="array"===Gn(t.value.stops)&&"array"===Gn(t.value.stops[0])&&"object"===Gn(t.value.stops[0][0]),c=Ai({key:t.key,value:t.value,valueSpec:t.styleSpec.function,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec,objectElementValidators:{stops:function(t){if("identity"===r)return [new Ct(t.key,t.value,'identity function may not have a "stops" property')];let e=[];const n=t.value;return e=e.concat(ki({key:t.key,value:n,valueSpec:t.valueSpec,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec,arrayElementValidator:h})),"array"===Gn(n)&&0===n.length&&e.push(new Ct(t.key,n,"array must have at least one stop")),e},default:function(t){return t.validateSpec({key:t.key,value:t.value,valueSpec:e,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec})}}});return "identity"===r&&o&&c.push(new Ct(t.key,t.value,'missing required property "property"')),"identity"===r||t.value.stops||c.push(new Ct(t.key,t.value,'missing required property "stops"')),"exponential"===r&&t.valueSpec.expression&&!qn(t.valueSpec)&&c.push(new Ct(t.key,t.value,"exponential functions not supported")),t.styleSpec.$version>=8&&(l&&!jn(t.valueSpec)?c.push(new Ct(t.key,t.value,"property functions not supported")):o&&!Nn(t.valueSpec)&&c.push(new Ct(t.key,t.value,"zoom functions not supported"))),"categorical"!==r&&!u||void 0!==t.value.property||c.push(new Ct(t.key,t.value,'"property" property is required')),c;function h(t){let r=[];const n=t.value,o=t.key;if("array"!==Gn(n))return [new Ct(o,n,`array expected, ${Gn(n)} found`)];if(2!==n.length)return [new Ct(o,n,`array length 2 expected, length ${n.length} found`)];if(u){if("object"!==Gn(n[0]))return [new Ct(o,n,`object expected, ${Gn(n[0])} found`)];if(void 0===n[0].zoom)return [new Ct(o,n,"object stop key must have zoom")];if(void 0===n[0].value)return [new Ct(o,n,"object stop key must have value")];if(s&&s>_i(n[0].zoom))return [new Ct(o,n[0].zoom,"stop zoom values must appear in ascending order")];_i(n[0].zoom)!==s&&(s=_i(n[0].zoom),i=void 0,a={}),r=r.concat(Ai({key:`${o}[0]`,value:n[0],valueSpec:{zoom:{}},validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec,objectElementValidators:{zoom:Mi,value:p}}));}else r=r.concat(p({key:`${o}[0]`,value:n[0],validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec},n));return ei(Si(n[1]))?r.concat([new Ct(`${o}[1]`,n[1],"expressions are not allowed in function stops.")]):r.concat(t.validateSpec({key:`${o}[1]`,value:n[1],valueSpec:e,validateSpec:t.validateSpec,style:t.style,styleSpec:t.styleSpec}))}function p(t,s){const o=Gn(t.value),l=_i(t.value),u=null!==t.value?t.value:s;if(n){if(o!==n)return [new Ct(t.key,u,`${o} stop domain type must match previous stop domain type ${n}`)]}else n=o;if("number"!==o&&"string"!==o&&"boolean"!==o)return [new Ct(t.key,u,"stop domain value must be a number, string, or boolean")];if("number"!==o&&"categorical"!==r){let n=`number expected, ${o} found`;return jn(e)&&void 0===r&&(n+='\nIf you intended to use a categorical function, specify `"type": "categorical"`.'),[new Ct(t.key,u,n)]}return "categorical"!==r||"number"!==o||isFinite(l)&&Math.floor(l)===l?"categorical"!==r&&"number"===o&&void 0!==i&&lnew Ct(`${t.key}${e.key}`,t.value,e.message)));const r=e.value.expression||e.value._styleExpression.expression;if("property"===t.expressionContext&&"text-font"===t.propertyKey&&!r.outputDefined())return [new Ct(t.key,t.value,`Invalid data expression for "${t.propertyKey}". Output values must be contained as literals within the expression.`)];if("property"===t.expressionContext&&"layout"===t.propertyType&&!On(r))return [new Ct(t.key,t.value,'"feature-state" data expressions are not supported with layout properties.')];if("filter"===t.expressionContext&&!On(r))return [new Ct(t.key,t.value,'"feature-state" data expressions are not supported with filters.')];if(t.expressionContext&&0===t.expressionContext.indexOf("cluster")){if(!Dn(r,["zoom","feature-state"]))return [new Ct(t.key,t.value,'"zoom" and "feature-state" expressions are not supported with cluster properties.')];if("cluster-initial"===t.expressionContext&&!Ln(r))return [new Ct(t.key,t.value,"Feature data expressions are not supported with initial expression part of cluster properties.")]}return []}function Pi(t){const e=t.key,r=t.value,n=Gn(r);return "string"!==n?[new Ct(e,r,`color expected, ${n} found`)]:Me.parse(String(r))?[]:[new Ct(e,r,`color expected, "${r}" found`)]}function Ci(t){const e=t.key,r=t.value,n=t.valueSpec,i=[];return Array.isArray(n.values)?-1===n.values.indexOf(_i(r))&&i.push(new Ct(e,r,`expected one of [${n.values.join(", ")}], ${JSON.stringify(r)} found`)):-1===Object.keys(n.values).indexOf(_i(r))&&i.push(new Ct(e,r,`expected one of [${Object.keys(n.values).join(", ")}], ${JSON.stringify(r)} found`)),i}function Ei(t){return ui(Si(t.value))?zi(Et({},t,{expressionContext:"filter",valueSpec:{value:"boolean"}})):Bi(t)}function Bi(t){const e=t.value,r=t.key;if("array"!==Gn(e))return [new Ct(r,e,`array expected, ${Gn(e)} found`)];const n=t.styleSpec;let i,s=[];if(e.length<1)return [new Ct(r,e,"filter array must have at least 1 element")];switch(s=s.concat(Ci({key:`${r}[0]`,value:e[0],valueSpec:n.filter_operator,style:t.style,styleSpec:t.styleSpec})),_i(e[0])){case "<":case "<=":case ">":case ">=":e.length>=2&&"$type"===_i(e[1])&&s.push(new Ct(r,e,`"$type" cannot be use with operator "${e[0]}"`));case "==":case "!=":3!==e.length&&s.push(new Ct(r,e,`filter array for operator "${e[0]}" must have 3 elements`));case "in":case "!in":e.length>=2&&(i=Gn(e[1]),"string"!==i&&s.push(new Ct(`${r}[1]`,e[1],`string expected, ${i} found`)));for(let a=2;a{t in r&&e.push(new Ct(n,r[t],`"${t}" is prohibited for ref layers`));})),i.layers.forEach((e=>{_i(e.id)===o&&(t=e);})),t?t.ref?e.push(new Ct(n,r.ref,"ref cannot reference another ref layer")):a=_i(t.type):e.push(new Ct(n,r.ref,`ref layer "${o}" not found`));}else if("background"!==a)if(r.source){const t=i.sources&&i.sources[r.source],s=t&&_i(t.type);t?"vector"===s&&"raster"===a?e.push(new Ct(n,r.source,`layer "${r.id}" requires a raster source`)):"raster-dem"!==s&&"hillshade"===a||"raster-dem"!==s&&"color-relief"===a?e.push(new Ct(n,r.source,`layer "${r.id}" requires a raster-dem source`)):"raster"===s&&"raster"!==a?e.push(new Ct(n,r.source,`layer "${r.id}" requires a vector source`)):"vector"!==s||r["source-layer"]?"raster-dem"===s&&"hillshade"!==a&&"color-relief"!==a?e.push(new Ct(n,r.source,"raster-dem source can only be used with layer type 'hillshade' or 'color-relief'.")):"line"!==a||!r.paint||!r.paint["line-gradient"]||"geojson"===s&&t.lineMetrics||e.push(new Ct(n,r,`layer "${r.id}" specifies a line-gradient, which requires a GeoJSON source with \`lineMetrics\` enabled.`)):e.push(new Ct(n,r,`layer "${r.id}" must specify a "source-layer"`)):e.push(new Ct(n,r.source,`source "${r.source}" not found`));}else e.push(new Ct(n,r,'missing required property "source"'));return e=e.concat(Ai({key:n,value:r,valueSpec:s.layer,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,objectElementValidators:{"*":()=>[],type:()=>t.validateSpec({key:`${n}.type`,value:r.type,valueSpec:s.layer.type,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,object:r,objectKey:"type"}),filter:Ei,layout:t=>Ai({layer:r,key:t.key,value:t.value,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,objectElementValidators:{"*":t=>Fi(Et({layerType:a},t))}}),paint:t=>Ai({layer:r,key:t.key,value:t.value,style:t.style,styleSpec:t.styleSpec,validateSpec:t.validateSpec,objectElementValidators:{"*":t=>Ti(Et({layerType:a},t))}})}})),e}function Li(t){const e=t.value,r=t.key,n=Gn(e);return "string"!==n?[new Ct(r,e,`string expected, ${n} found`)]:[]}const Oi={promoteId:function({key:t,value:e}){if("string"===Gn(e))return Li({key:t,value:e});{const r=[];for(const n in e)r.push(...Li({key:`${t}.${n}`,value:e[n]}));return r}}};function Di(t){const e=t.value,r=t.key,n=t.styleSpec,i=t.style,s=t.validateSpec;if(!e.type)return [new Ct(r,e,'"type" is required')];const a=_i(e.type);let o;switch(a){case "vector":case "raster":return o=Ai({key:r,value:e,valueSpec:n[`source_${a.replace("-","_")}`],style:t.style,styleSpec:n,objectElementValidators:Oi,validateSpec:s}),o;case "raster-dem":return o=function(t){var e;const r=null!==(e=t.sourceName)&&void 0!==e?e:"",n=t.value,i=t.styleSpec,s=i.source_raster_dem,a=t.style;let o=[];const l=Gn(n);if(void 0===n)return o;if("object"!==l)return o.push(new Ct("source_raster_dem",n,`object expected, ${l} found`)),o;const u="custom"===_i(n.encoding),c=["redFactor","greenFactor","blueFactor","baseShift"],h=t.value.encoding?`"${t.value.encoding}"`:"Default";for(const e in n)!u&&c.includes(e)?o.push(new Ct(e,n[e],`In "${r}": "${e}" is only valid when "encoding" is set to "custom". ${h} encoding found`)):s[e]?o=o.concat(t.validateSpec({key:e,value:n[e],valueSpec:s[e],validateSpec:t.validateSpec,style:a,styleSpec:i})):o.push(new Ct(e,n[e],`unknown property "${e}"`));return o}({sourceName:r,value:e,style:t.style,styleSpec:n,validateSpec:s}),o;case "geojson":if(o=Ai({key:r,value:e,valueSpec:n.source_geojson,style:i,styleSpec:n,validateSpec:s,objectElementValidators:Oi}),e.cluster)for(const t in e.clusterProperties){const[n,i]=e.clusterProperties[t],s="string"==typeof n?[n,["accumulated"],["get",t]]:n;o.push(...zi({key:`${r}.${t}.map`,value:i,expressionContext:"cluster-map"})),o.push(...zi({key:`${r}.${t}.reduce`,value:s,expressionContext:"cluster-reduce"}));}return o;case "video":return Ai({key:r,value:e,valueSpec:n.source_video,style:i,validateSpec:s,styleSpec:n});case "image":return Ai({key:r,value:e,valueSpec:n.source_image,style:i,validateSpec:s,styleSpec:n});case "canvas":return [new Ct(r,null,"Please use runtime APIs to add canvas sources, rather than including them in stylesheets.","source.canvas")];default:return Ci({key:`${r}.type`,value:e.type,valueSpec:{values:["vector","raster","raster-dem","geojson","video","image"]}})}}function Ri(t){const e=t.value,r=t.styleSpec,n=r.light,i=t.style;let s=[];const a=Gn(e);if(void 0===e)return s;if("object"!==a)return s=s.concat([new Ct("light",e,`object expected, ${a} found`)]),s;for(const a in e){const o=a.match(/^(.*)-transition$/);s=s.concat(o&&n[o[1]]&&n[o[1]].transition?t.validateSpec({key:a,value:e[a],valueSpec:r.transition,validateSpec:t.validateSpec,style:i,styleSpec:r}):n[a]?t.validateSpec({key:a,value:e[a],valueSpec:n[a],validateSpec:t.validateSpec,style:i,styleSpec:r}):[new Ct(a,e[a],`unknown property "${a}"`)]);}return s}function Ui(t){const e=t.value,r=t.styleSpec,n=r.sky,i=t.style,s=Gn(e);if(void 0===e)return [];if("object"!==s)return [new Ct("sky",e,`object expected, ${s} found`)];let a=[];for(const s in e)a=a.concat(n[s]?t.validateSpec({key:s,value:e[s],valueSpec:n[s],style:i,styleSpec:r}):[new Ct(s,e[s],`unknown property "${s}"`)]);return a}function ji(t){const e=t.value,r=t.styleSpec,n=r.terrain,i=t.style;let s=[];const a=Gn(e);if(void 0===e)return s;if("object"!==a)return s=s.concat([new Ct("terrain",e,`object expected, ${a} found`)]),s;for(const a in e)s=s.concat(n[a]?t.validateSpec({key:a,value:e[a],valueSpec:n[a],validateSpec:t.validateSpec,style:i,styleSpec:r}):[new Ct(a,e[a],`unknown property "${a}"`)]);return s}function Ni(t){let e=[];const r=t.value,n=t.key;if(Array.isArray(r)){const i=[],s=[];for(const a in r)r[a].id&&i.includes(r[a].id)&&e.push(new Ct(n,r,`all the sprites' ids must be unique, but ${r[a].id} is duplicated`)),i.push(r[a].id),r[a].url&&s.includes(r[a].url)&&e.push(new Ct(n,r,`all the sprites' URLs must be unique, but ${r[a].url} is duplicated`)),s.push(r[a].url),e=e.concat(Ai({key:`${n}[${a}]`,value:r[a],valueSpec:{id:{type:"string",required:!0},url:{type:"string",required:!0}},validateSpec:t.validateSpec}));return e}return Li({key:n,value:r})}function qi(t){return e=t.value,Boolean(e)&&e.constructor===Object?[]:[new Ct(t.key,t.value,`object expected, ${Gn(t.value)} found`)];var e;}const Gi={"*":()=>[],array:ki,boolean:function(t){const e=t.value,r=t.key,n=Gn(e);return "boolean"!==n?[new Ct(r,e,`boolean expected, ${n} found`)]:[]},number:Mi,color:Pi,constants:wi,enum:Ci,filter:Ei,function:Ii,layer:$i,object:Ai,source:Di,light:Ri,sky:Ui,terrain:ji,projection:function(t){const e=t.value,r=t.styleSpec,n=r.projection,i=t.style,s=Gn(e);if(void 0===e)return [];if("object"!==s)return [new Ct("projection",e,`object expected, ${s} found`)];let a=[];for(const s in e)a=a.concat(n[s]?t.validateSpec({key:s,value:e[s],valueSpec:n[s],style:i,styleSpec:r}):[new Ct(s,e[s],`unknown property "${s}"`)]);return a},projectionDefinition:function(t){const e=t.key;let r=t.value;r=r instanceof String?r.valueOf():r;const n=Gn(r);return "array"!==n||function(t){return Array.isArray(t)&&3===t.length&&"string"==typeof t[0]&&"string"==typeof t[1]&&"number"==typeof t[2]}(r)||function(t){return !!["interpolate","step","literal"].includes(t[0])}(r)?["array","string"].includes(n)?[]:[new Ct(e,r,`projection expected, invalid type "${n}" found`)]:[new Ct(e,r,`projection expected, invalid array ${JSON.stringify(r)} found`)]},string:Li,formatted:function(t){return 0===Li(t).length?[]:zi(t)},resolvedImage:function(t){return 0===Li(t).length?[]:zi(t)},padding:function(t){const e=t.key,r=t.value;if("array"===Gn(r)){if(r.length<1||r.length>4)return [new Ct(e,r,`padding requires 1 to 4 values; ${r.length} values found`)];const n={type:"number"};let i=[];for(let s=0;s[]}})),t.constants&&(r=r.concat(wi({key:"constants",value:t.constants}))),Ki(r)}function Hi(t){return function(e){return t({...e,validateSpec:Xi})}}function Ki(t){return [].concat(t).sort(((t,e)=>t.line-e.line))}function Ji(t){return function(...e){return Ki(t.apply(this,e))}}Yi.source=Ji(Hi(Di)),Yi.sprite=Ji(Hi(Ni)),Yi.glyphs=Ji(Hi(Zi)),Yi.light=Ji(Hi(Ri)),Yi.sky=Ji(Hi(Ui)),Yi.terrain=Ji(Hi(ji)),Yi.state=Ji(Hi(qi)),Yi.layer=Ji(Hi($i)),Yi.filter=Ji(Hi(Ei)),Yi.paintProperty=Ji(Hi(Ti)),Yi.layoutProperty=Ji(Hi(Fi));const Wi=Yi,Qi=Wi.light,ts=Wi.sky,es=Wi.paintProperty,rs=Wi.layoutProperty;function ns(t,e){let r=!1;if(e&&e.length)for(const n of e)t.fire(new mt(new Error(n.message))),r=!0;return r}class is{constructor(t,e,r){const n=this.cells=[];if(t instanceof ArrayBuffer){this.arrayBuffer=t;const i=new Int32Array(this.arrayBuffer);t=i[0],this.d=(e=i[1])+2*(r=i[2]);for(let t=0;t=u[l+0]&&n>=u[l+1])?(a[h]=!0,s.push(i[h])):a[h]=!1;}}}}_forEachCell(t,e,r,n,i,s,a,o){const l=this._convertToCellCoord(t),u=this._convertToCellCoord(e),c=this._convertToCellCoord(r),h=this._convertToCellCoord(n);for(let p=l;p<=c;p++)for(let l=u;l<=h;l++){const u=this.d*l+p;if((!o||o(this._convertFromCellCoord(p),this._convertFromCellCoord(l),this._convertFromCellCoord(p+1),this._convertFromCellCoord(l+1)))&&i.call(this,t,e,r,n,u,s,a,o))return}}_convertFromCellCoord(t){return (t-this.padding)/this.scale}_convertToCellCoord(t){return Math.max(0,Math.min(this.d-1,Math.floor(t*this.scale)+this.padding))}toArrayBuffer(){if(this.arrayBuffer)return this.arrayBuffer;const t=this.cells,e=3+this.cells.length+1+1;let r=0;for(let t=0;t=0)continue;const s=t[n];i[n]=ss[r].shallow.indexOf(n)>=0?s:cs(s,e);}t instanceof Error&&(i.message=t.message);}if(i.$name)throw new Error("$name property is reserved for worker serialization logic.");return "Object"!==r&&(i.$name=r),i}function hs(t){if(us(t))return t;if(Array.isArray(t))return t.map(hs);if("object"!=typeof t)throw new Error("can't deserialize object of type "+typeof t);const e=ls(t)||"Object";if(!ss[e])throw new Error(`can't deserialize unregistered class ${e}`);const{klass:r}=ss[e];if(!r)throw new Error(`can't deserialize unregistered class ${e}`);if(r.deserialize)return r.deserialize(t);const n=Object.create(r.prototype);for(const r of Object.keys(t)){if("$name"===r)continue;const i=t[r];n[r]=ss[e].shallow.indexOf(r)>=0?i:hs(i);}return n}class ps{constructor(){this.first=!0;}update(t,e){const r=Math.floor(t);return this.first?(this.first=!1,this.lastIntegerZoom=r,this.lastIntegerZoomTime=0,this.lastZoom=t,this.lastFloorZoom=r,!0):(this.lastFloorZoom>r?(this.lastIntegerZoom=r+1,this.lastIntegerZoomTime=e):this.lastFloorZoomt>=128&&t<=255,"Hangul Jamo":t=>t>=4352&&t<=4607,Khmer:t=>t>=6016&&t<=6143,"General Punctuation":t=>t>=8192&&t<=8303,"Letterlike Symbols":t=>t>=8448&&t<=8527,"Number Forms":t=>t>=8528&&t<=8591,"Miscellaneous Technical":t=>t>=8960&&t<=9215,"Control Pictures":t=>t>=9216&&t<=9279,"Optical Character Recognition":t=>t>=9280&&t<=9311,"Enclosed Alphanumerics":t=>t>=9312&&t<=9471,"Geometric Shapes":t=>t>=9632&&t<=9727,"Miscellaneous Symbols":t=>t>=9728&&t<=9983,"Miscellaneous Symbols and Arrows":t=>t>=11008&&t<=11263,"Ideographic Description Characters":t=>t>=12272&&t<=12287,"CJK Symbols and Punctuation":t=>t>=12288&&t<=12351,Hiragana:t=>t>=12352&&t<=12447,Katakana:t=>t>=12448&&t<=12543,Kanbun:t=>t>=12688&&t<=12703,"CJK Strokes":t=>t>=12736&&t<=12783,"Enclosed CJK Letters and Months":t=>t>=12800&&t<=13055,"CJK Compatibility":t=>t>=13056&&t<=13311,"Yijing Hexagram Symbols":t=>t>=19904&&t<=19967,"CJK Unified Ideographs":t=>t>=19968&&t<=40959,"Hangul Syllables":t=>t>=44032&&t<=55215,"Private Use Area":t=>t>=57344&&t<=63743,"Vertical Forms":t=>t>=65040&&t<=65055,"CJK Compatibility Forms":t=>t>=65072&&t<=65103,"Small Form Variants":t=>t>=65104&&t<=65135,"Halfwidth and Fullwidth Forms":t=>t>=65280&&t<=65519};function ds(t){for(const e of t)if(bs(e.charCodeAt(0)))return !0;return !1}function ys(t){for(const e of t)if(!xs(e.charCodeAt(0)))return !1;return !0}function ms(t){const e=t.map((t=>{try{return new RegExp(`\\p{sc=${t}}`,"u").source}catch(t){return null}})).filter((t=>t));return new RegExp(e.join("|"),"u")}const gs=ms(["Arab","Dupl","Mong","Ougr","Syrc"]);function xs(t){return !gs.test(String.fromCodePoint(t))}const vs=ms(["Bopo","Hani","Hira","Kana","Kits","Nshu","Tang","Yiii"]);function bs(t){return !(746!==t&&747!==t&&(t<4352||!(fs["CJK Compatibility Forms"](t)&&!(t>=65097&&t<=65103)||fs["CJK Compatibility"](t)||fs["CJK Strokes"](t)||!(!fs["CJK Symbols and Punctuation"](t)||t>=12296&&t<=12305||t>=12308&&t<=12319||12336===t)||fs["Enclosed CJK Letters and Months"](t)||fs["Ideographic Description Characters"](t)||fs.Kanbun(t)||fs.Katakana(t)&&12540!==t||!(!fs["Halfwidth and Fullwidth Forms"](t)||65288===t||65289===t||65293===t||t>=65306&&t<=65310||65339===t||65341===t||65343===t||t>=65371&&t<=65503||65507===t||t>=65512&&t<=65519)||!(!fs["Small Form Variants"](t)||t>=65112&&t<=65118||t>=65123&&t<=65126)||fs["Vertical Forms"](t)||fs["Yijing Hexagram Symbols"](t)||/\p{sc=Cans}/u.test(String.fromCodePoint(t))||/\p{sc=Hang}/u.test(String.fromCodePoint(t))||vs.test(String.fromCodePoint(t)))))}function ws(t){return !(bs(t)||function(t){return !!(fs["Latin-1 Supplement"](t)&&(167===t||169===t||174===t||177===t||188===t||189===t||190===t||215===t||247===t)||fs["General Punctuation"](t)&&(8214===t||8224===t||8225===t||8240===t||8241===t||8251===t||8252===t||8258===t||8263===t||8264===t||8265===t||8273===t)||fs["Letterlike Symbols"](t)||fs["Number Forms"](t)||fs["Miscellaneous Technical"](t)&&(t>=8960&&t<=8967||t>=8972&&t<=8991||t>=8996&&t<=9e3||9003===t||t>=9085&&t<=9114||t>=9150&&t<=9165||9167===t||t>=9169&&t<=9179||t>=9186&&t<=9215)||fs["Control Pictures"](t)&&9251!==t||fs["Optical Character Recognition"](t)||fs["Enclosed Alphanumerics"](t)||fs["Geometric Shapes"](t)||fs["Miscellaneous Symbols"](t)&&!(t>=9754&&t<=9759)||fs["Miscellaneous Symbols and Arrows"](t)&&(t>=11026&&t<=11055||t>=11088&&t<=11097||t>=11192&&t<=11243)||fs["CJK Symbols and Punctuation"](t)||fs.Katakana(t)||fs["Private Use Area"](t)||fs["CJK Compatibility Forms"](t)||fs["Small Form Variants"](t)||fs["Halfwidth and Fullwidth Forms"](t)||8734===t||8756===t||8757===t||t>=9984&&t<=10087||t>=10102&&t<=10131||65532===t||65533===t)}(t))}const _s=ms(["Adlm","Arab","Armi","Avst","Chrs","Cprt","Egyp","Elym","Gara","Hatr","Hebr","Hung","Khar","Lydi","Mand","Mani","Mend","Merc","Mero","Narb","Nbat","Nkoo","Orkh","Palm","Phli","Phlp","Phnx","Prti","Rohg","Samr","Sarb","Sogo","Syrc","Thaa","Todr","Yezi"]);function Ss(t){return _s.test(String.fromCodePoint(t))}function As(t,e){return !(!e&&Ss(t)||t>=2304&&t<=3583||t>=3840&&t<=4255||fs.Khmer(t))}function ks(t){for(const e of t)if(Ss(e.charCodeAt(0)))return !0;return !1}const Ms=new class{constructor(){this.TIMEOUT=5e3,this.applyArabicShaping=null,this.processBidirectionalText=null,this.processStyledBidirectionalText=null,this.pluginStatus="unavailable",this.pluginURL=null,this.loadScriptResolve=()=>{};}setState(t){this.pluginStatus=t.pluginStatus,this.pluginURL=t.pluginURL;}getState(){return {pluginStatus:this.pluginStatus,pluginURL:this.pluginURL}}setMethods(t){if(Ms.isParsed())throw new Error("RTL text plugin already registered.");this.applyArabicShaping=t.applyArabicShaping,this.processBidirectionalText=t.processBidirectionalText,this.processStyledBidirectionalText=t.processStyledBidirectionalText,this.loadScriptResolve();}isParsed(){return null!=this.applyArabicShaping&&null!=this.processBidirectionalText&&null!=this.processStyledBidirectionalText}getRTLTextPluginStatus(){return this.pluginStatus}syncState(t,r){return e(this,void 0,void 0,(function*(){if(this.isParsed())return this.getState();if("loading"!==t.pluginStatus)return this.setState(t),t;const e=t.pluginURL,n=new Promise((t=>{this.loadScriptResolve=t;}));r(e);const i=new Promise((t=>setTimeout((()=>t()),this.TIMEOUT)));if(yield Promise.race([n,i]),this.isParsed()){const t={pluginStatus:"loaded",pluginURL:e};return this.setState(t),t}throw this.setState({pluginStatus:"error",pluginURL:""}),new Error(`RTL Text Plugin failed to import scripts from ${e}`)}))}};class Is{constructor(t,e){this.zoom=t,e?(this.now=e.now||0,this.fadeDuration=e.fadeDuration||0,this.zoomHistory=e.zoomHistory||new ps,this.transition=e.transition||{},this.globalState=e.globalState||{}):(this.now=0,this.fadeDuration=0,this.zoomHistory=new ps,this.transition={},this.globalState={});}isSupportedScript(t){return function(t,e){for(const r of t)if(!As(r.charCodeAt(0),e))return !1;return !0}(t,"loaded"===Ms.getRTLTextPluginStatus())}crossFadingFactor(){return 0===this.fadeDuration?1:Math.min((this.now-this.zoomHistory.lastIntegerZoomTime)/this.fadeDuration,1)}getCrossfadeParameters(){const t=this.zoom,e=t-Math.floor(t),r=this.crossFadingFactor();return t>this.zoomHistory.lastIntegerZoom?{fromScale:2,toScale:1,t:e+(1-e)*r}:{fromScale:.5,toScale:1,t:1-(1-r)*e}}}class zs{constructor(t,e){this.property=t,this.value=e,this.expression=function(t,e){if(Xn(t))return new ai(t,e);if(ei(t)){const r=si(t,e);if("error"===r.result)throw new Error(r.value.map((t=>`${t.key}: ${t.message}`)).join(", "));return r.value}{let r=t;return "color"===e.type&&"string"==typeof t?r=Me.parse(t):"padding"!==e.type||"number"!=typeof t&&!Array.isArray(t)?"numberArray"!==e.type||"number"!=typeof t&&!Array.isArray(t)?"colorArray"!==e.type||"string"!=typeof t&&!Array.isArray(t)?"variableAnchorOffsetCollection"===e.type&&Array.isArray(t)?r=$e.parse(t):"projectionDefinition"===e.type&&"string"==typeof t&&(r=Oe.parse(t)):r=Ve.parse(t):r=Be.parse(t):r=Ee.parse(t),{globalStateRefs:new Set,kind:"constant",evaluate:()=>r}}}(void 0===e?t.specification.default:e,t.specification);}isDataDriven(){return "source"===this.expression.kind||"composite"===this.expression.kind}getGlobalStateRefs(){return this.expression.globalStateRefs||new Set}possiblyEvaluate(t,e,r){return this.property.possiblyEvaluate(this,t,e,r)}}class Ps{constructor(t){this.property=t,this.value=new zs(t,void 0);}transitioned(t,e){return new Es(this.property,this.value,e,O({},t.transition,this.transition),t.now)}untransitioned(){return new Es(this.property,this.value,null,{},0)}}class Cs{constructor(t){this._properties=t,this._values=Object.create(t.defaultTransitionablePropertyValues);}getValue(t){return j(this._values[t].value.value)}setValue(t,e){Object.prototype.hasOwnProperty.call(this._values,t)||(this._values[t]=new Ps(this._values[t].property)),this._values[t].value=new zs(this._values[t].property,null===e?void 0:j(e));}getTransition(t){return j(this._values[t].transition)}setTransition(t,e){Object.prototype.hasOwnProperty.call(this._values,t)||(this._values[t]=new Ps(this._values[t].property)),this._values[t].transition=j(e)||void 0;}serialize(){const t={};for(const e of Object.keys(this._values)){const r=this.getValue(e);void 0!==r&&(t[e]=r);const n=this.getTransition(e);void 0!==n&&(t[`${e}-transition`]=n);}return t}transitioned(t,e){const r=new Bs(this._properties);for(const n of Object.keys(this._values))r._values[n]=this._values[n].transitioned(t,e._values[n]);return r}untransitioned(){const t=new Bs(this._properties);for(const e of Object.keys(this._values))t._values[e]=this._values[e].untransitioned();return t}}class Es{constructor(t,e,r,n,i){this.property=t,this.value=e,this.begin=i+n.delay||0,this.end=this.begin+n.duration||0,t.specification.transition&&(n.delay||n.duration)&&(this.prior=r);}possiblyEvaluate(t,e,r){const n=t.now||0,i=this.value.possiblyEvaluate(t,e,r),s=this.prior;if(s){if(n>this.end)return this.prior=null,i;if(this.value.isDataDriven())return this.prior=null,i;if(nn.zoomHistory.lastIntegerZoom?{from:t,to:e}:{from:r,to:e}}interpolate(t){return t}}class Ds{constructor(t){this.specification=t;}possiblyEvaluate(t,e,r,n){if(void 0!==t.value){if("constant"===t.expression.kind){const i=t.expression.evaluate(e,null,{},r,n);return this._calculate(i,i,i,e)}return this._calculate(t.expression.evaluate(new Is(Math.floor(e.zoom-1),e)),t.expression.evaluate(new Is(Math.floor(e.zoom),e)),t.expression.evaluate(new Is(Math.floor(e.zoom+1),e)),e)}}_calculate(t,e,r,n){return n.zoom>n.zoomHistory.lastIntegerZoom?{from:t,to:e}:{from:r,to:e}}interpolate(t){return t}}class Rs{constructor(t){this.specification=t;}possiblyEvaluate(t,e,r,n){return !!t.expression.evaluate(e,null,{},r,n)}interpolate(){return !1}}class Us{constructor(t){this.properties=t,this.defaultPropertyValues={},this.defaultTransitionablePropertyValues={},this.defaultTransitioningPropertyValues={},this.defaultPossiblyEvaluatedValues={},this.overridableProperties=[];for(const e in t){const r=t[e];r.specification.overridable&&this.overridableProperties.push(e);const n=this.defaultPropertyValues[e]=new zs(r,void 0),i=this.defaultTransitionablePropertyValues[e]=new Ps(r);this.defaultTransitioningPropertyValues[e]=i.untransitioned(),this.defaultPossiblyEvaluatedValues[e]=n.possiblyEvaluate({});}}}as("DataDrivenProperty",Ls),as("DataConstantProperty",$s),as("CrossFadedDataDrivenProperty",Os),as("CrossFadedProperty",Ds),as("ColorRampProperty",Rs);const js="-transition";class Ns extends gt{constructor(t,e){if(super(),this.id=t.id,this.type=t.type,this._featureFilter={filter:()=>!0,needGeometry:!1,getGlobalStateRefs:()=>new Set},"custom"!==t.type&&(this.metadata=t.metadata,this.minzoom=t.minzoom,this.maxzoom=t.maxzoom,"background"!==t.type&&(this.source=t.source,this.sourceLayer=t["source-layer"],this.filter=t.filter,this._featureFilter=hi(t.filter)),e.layout&&(this._unevaluatedLayout=new Vs(e.layout)),e.paint)){this._transitionablePaint=new Cs(e.paint);for(const e in t.paint)this.setPaintProperty(e,t.paint[e],{validate:!1});for(const e in t.layout)this.setLayoutProperty(e,t.layout[e],{validate:!1});this._transitioningPaint=this._transitionablePaint.untransitioned(),this.paint=new Fs(e.paint);}}setFilter(t){this.filter=t,this._featureFilter=hi(t);}getCrossfadeParameters(){return this._crossfadeParameters}getLayoutProperty(t){return "visibility"===t?this.visibility:this._unevaluatedLayout.getValue(t)}getLayoutAffectingGlobalStateRefs(){const t=new Set;if(this._unevaluatedLayout)for(const e in this._unevaluatedLayout._values){const r=this._unevaluatedLayout._values[e];for(const e of r.getGlobalStateRefs())t.add(e);}for(const e of this._featureFilter.getGlobalStateRefs())t.add(e);return t}getPaintAffectingGlobalStateRefs(){var t;const e=new globalThis.Map;if(this._transitionablePaint)for(const r in this._transitionablePaint._values){const n=this._transitionablePaint._values[r].value;for(const i of n.getGlobalStateRefs()){const s=null!==(t=e.get(i))&&void 0!==t?t:[];s.push({name:r,value:n.value}),e.set(i,s);}}return e}setLayoutProperty(t,e,r={}){null!=e&&this._validate(rs,`layers.${this.id}.layout.${t}`,t,e,r)||("visibility"!==t?this._unevaluatedLayout.setValue(t,e):this.visibility=e);}getPaintProperty(t){return t.endsWith(js)?this._transitionablePaint.getTransition(t.slice(0,-11)):this._transitionablePaint.getValue(t)}setPaintProperty(t,e,r={}){if(null!=e&&this._validate(es,`layers.${this.id}.paint.${t}`,t,e,r))return !1;if(t.endsWith(js))return this._transitionablePaint.setTransition(t.slice(0,-11),e||void 0),!1;{const r=this._transitionablePaint._values[t],n="cross-faded-data-driven"===r.property.specification["property-type"],i=r.value.isDataDriven(),s=r.value;this._transitionablePaint.setValue(t,e),this._handleSpecialPaintPropertyUpdate(t);const a=this._transitionablePaint._values[t].value;return a.isDataDriven()||i||n||this._handleOverridablePaintPropertyUpdate(t,s,a)}}_handleSpecialPaintPropertyUpdate(t){}_handleOverridablePaintPropertyUpdate(t,e,r){return !1}isHidden(t){return !!(this.minzoom&&t=this.maxzoom)||"none"===this.visibility}updateTransitions(t){this._transitioningPaint=this._transitionablePaint.transitioned(t,this._transitioningPaint);}hasTransition(){return this._transitioningPaint.hasTransition()}recalculate(t,e){t.getCrossfadeParameters&&(this._crossfadeParameters=t.getCrossfadeParameters()),this._unevaluatedLayout&&(this.layout=this._unevaluatedLayout.possiblyEvaluate(t,void 0,e)),this.paint=this._transitioningPaint.possiblyEvaluate(t,void 0,e);}serialize(){const t={id:this.id,type:this.type,source:this.source,"source-layer":this.sourceLayer,metadata:this.metadata,minzoom:this.minzoom,maxzoom:this.maxzoom,filter:this.filter,layout:this._unevaluatedLayout&&this._unevaluatedLayout.serialize(),paint:this._transitionablePaint&&this._transitionablePaint.serialize()};return this.visibility&&(t.layout=t.layout||{},t.layout.visibility=this.visibility),U(t,((t,e)=>!(void 0===t||"layout"===e&&!Object.keys(t).length||"paint"===e&&!Object.keys(t).length)))}_validate(t,e,r,n,i={}){return (!i||!1!==i.validate)&&ns(this,t.call(Wi,{key:e,layerType:this.type,objectKey:r,value:n,styleSpec:xt,style:{glyphs:!0,sprite:!0}}))}is3D(){return !1}isTileClipped(){return !1}hasOffscreenPass(){return !1}resize(){}isStateDependent(){for(const t in this.paint._values){const e=this.paint.get(t);if(e instanceof Ts&&jn(e.property.specification)&&("source"===e.value.kind||"composite"===e.value.kind)&&e.value.isStateDependent)return !0}return !1}}const qs={Int8:Int8Array,Uint8:Uint8Array,Int16:Int16Array,Uint16:Uint16Array,Int32:Int32Array,Uint32:Uint32Array,Float32:Float32Array};class Gs{constructor(t,e){this._structArray=t,this._pos1=e*this.size,this._pos2=this._pos1/2,this._pos4=this._pos1/4,this._pos8=this._pos1/8;}}class Xs{constructor(){this.isTransferred=!1,this.capacity=-1,this.resize(0);}static serialize(t,e){return t._trim(),e&&(t.isTransferred=!0,e.push(t.arrayBuffer)),{length:t.length,arrayBuffer:t.arrayBuffer}}static deserialize(t){const e=Object.create(this.prototype);return e.arrayBuffer=t.arrayBuffer,e.length=t.length,e.capacity=t.arrayBuffer.byteLength/e.bytesPerElement,e._refreshViews(),e}_trim(){this.length!==this.capacity&&(this.capacity=this.length,this.arrayBuffer=this.arrayBuffer.slice(0,this.length*this.bytesPerElement),this._refreshViews());}clear(){this.length=0;}resize(t){this.reserve(t),this.length=t;}reserve(t){if(t>this.capacity){this.capacity=Math.max(t,Math.floor(5*this.capacity),128),this.arrayBuffer=new ArrayBuffer(this.capacity*this.bytesPerElement);const e=this.uint8;this._refreshViews(),e&&this.uint8.set(e);}}_refreshViews(){throw new Error("_refreshViews() must be implemented by each concrete StructArray layout")}}function Zs(t,e=1){let r=0,n=0;return {members:t.map((t=>{const i=qs[t.type].BYTES_PER_ELEMENT,s=r=Ys(r,Math.max(e,i)),a=t.components||1;return n=Math.max(n,i),r+=i*a,{name:t.name,type:t.type,components:a,offset:s}})),size:Ys(r,Math.max(n,e)),alignment:e}}function Ys(t,e){return Math.ceil(t/e)*e}class Hs extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e){const r=this.length;return this.resize(r+1),this.emplace(r,t,e)}emplace(t,e,r){const n=2*t;return this.int16[n+0]=e,this.int16[n+1]=r,t}}Hs.prototype.bytesPerElement=4,as("StructArrayLayout2i4",Hs);class Ks extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.int16[i+0]=e,this.int16[i+1]=r,this.int16[i+2]=n,t}}Ks.prototype.bytesPerElement=6,as("StructArrayLayout3i6",Ks);class Js extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n){const i=this.length;return this.resize(i+1),this.emplace(i,t,e,r,n)}emplace(t,e,r,n,i){const s=4*t;return this.int16[s+0]=e,this.int16[s+1]=r,this.int16[s+2]=n,this.int16[s+3]=i,t}}Js.prototype.bytesPerElement=8,as("StructArrayLayout4i8",Js);class Ws extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=6*t;return this.int16[o+0]=e,this.int16[o+1]=r,this.int16[o+2]=n,this.int16[o+3]=i,this.int16[o+4]=s,this.int16[o+5]=a,t}}Ws.prototype.bytesPerElement=12,as("StructArrayLayout2i4i12",Ws);class Qs extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=4*t,l=8*t;return this.int16[o+0]=e,this.int16[o+1]=r,this.uint8[l+4]=n,this.uint8[l+5]=i,this.uint8[l+6]=s,this.uint8[l+7]=a,t}}Qs.prototype.bytesPerElement=8,as("StructArrayLayout2i4ub8",Qs);class ta extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e){const r=this.length;return this.resize(r+1),this.emplace(r,t,e)}emplace(t,e,r){const n=2*t;return this.float32[n+0]=e,this.float32[n+1]=r,t}}ta.prototype.bytesPerElement=8,as("StructArrayLayout2f8",ta);class ea extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u){const c=this.length;return this.resize(c+1),this.emplace(c,t,e,r,n,i,s,a,o,l,u)}emplace(t,e,r,n,i,s,a,o,l,u,c){const h=10*t;return this.uint16[h+0]=e,this.uint16[h+1]=r,this.uint16[h+2]=n,this.uint16[h+3]=i,this.uint16[h+4]=s,this.uint16[h+5]=a,this.uint16[h+6]=o,this.uint16[h+7]=l,this.uint16[h+8]=u,this.uint16[h+9]=c,t}}ea.prototype.bytesPerElement=20,as("StructArrayLayout10ui20",ea);class ra extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u,c,h){const p=this.length;return this.resize(p+1),this.emplace(p,t,e,r,n,i,s,a,o,l,u,c,h)}emplace(t,e,r,n,i,s,a,o,l,u,c,h,p){const f=12*t;return this.int16[f+0]=e,this.int16[f+1]=r,this.int16[f+2]=n,this.int16[f+3]=i,this.uint16[f+4]=s,this.uint16[f+5]=a,this.uint16[f+6]=o,this.uint16[f+7]=l,this.int16[f+8]=u,this.int16[f+9]=c,this.int16[f+10]=h,this.int16[f+11]=p,t}}ra.prototype.bytesPerElement=24,as("StructArrayLayout4i4ui4i24",ra);class na extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.float32[i+0]=e,this.float32[i+1]=r,this.float32[i+2]=n,t}}na.prototype.bytesPerElement=12,as("StructArrayLayout3f12",na);class ia extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer);}emplaceBack(t){const e=this.length;return this.resize(e+1),this.emplace(e,t)}emplace(t,e){return this.uint32[1*t+0]=e,t}}ia.prototype.bytesPerElement=4,as("StructArrayLayout1ul4",ia);class sa extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l){const u=this.length;return this.resize(u+1),this.emplace(u,t,e,r,n,i,s,a,o,l)}emplace(t,e,r,n,i,s,a,o,l,u){const c=10*t,h=5*t;return this.int16[c+0]=e,this.int16[c+1]=r,this.int16[c+2]=n,this.int16[c+3]=i,this.int16[c+4]=s,this.int16[c+5]=a,this.uint32[h+3]=o,this.uint16[c+8]=l,this.uint16[c+9]=u,t}}sa.prototype.bytesPerElement=20,as("StructArrayLayout6i1ul2ui20",sa);class aa extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=6*t;return this.int16[o+0]=e,this.int16[o+1]=r,this.int16[o+2]=n,this.int16[o+3]=i,this.int16[o+4]=s,this.int16[o+5]=a,t}}aa.prototype.bytesPerElement=12,as("StructArrayLayout2i2i2i12",aa);class oa extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i){const s=this.length;return this.resize(s+1),this.emplace(s,t,e,r,n,i)}emplace(t,e,r,n,i,s){const a=4*t,o=8*t;return this.float32[a+0]=e,this.float32[a+1]=r,this.float32[a+2]=n,this.int16[o+6]=i,this.int16[o+7]=s,t}}oa.prototype.bytesPerElement=16,as("StructArrayLayout2f1f2i16",oa);class la extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s){const a=this.length;return this.resize(a+1),this.emplace(a,t,e,r,n,i,s)}emplace(t,e,r,n,i,s,a){const o=16*t,l=4*t,u=8*t;return this.uint8[o+0]=e,this.uint8[o+1]=r,this.float32[l+1]=n,this.float32[l+2]=i,this.int16[u+6]=s,this.int16[u+7]=a,t}}la.prototype.bytesPerElement=16,as("StructArrayLayout2ub2f2i16",la);class ua extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.uint16[i+0]=e,this.uint16[i+1]=r,this.uint16[i+2]=n,t}}ua.prototype.bytesPerElement=6,as("StructArrayLayout3ui6",ua);class ca extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m){const g=this.length;return this.resize(g+1),this.emplace(g,t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m)}emplace(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g){const x=24*t,v=12*t,b=48*t;return this.int16[x+0]=e,this.int16[x+1]=r,this.uint16[x+2]=n,this.uint16[x+3]=i,this.uint32[v+2]=s,this.uint32[v+3]=a,this.uint32[v+4]=o,this.uint16[x+10]=l,this.uint16[x+11]=u,this.uint16[x+12]=c,this.float32[v+7]=h,this.float32[v+8]=p,this.uint8[b+36]=f,this.uint8[b+37]=d,this.uint8[b+38]=y,this.uint32[v+10]=m,this.int16[x+22]=g,t}}ca.prototype.bytesPerElement=48,as("StructArrayLayout2i2ui3ul3ui2f3ub1ul1i48",ca);class ha extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.int16=new Int16Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,S,A,k,M,I){const z=this.length;return this.resize(z+1),this.emplace(z,t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,S,A,k,M,I)}emplace(t,e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,S,A,k,M,I,z){const P=32*t,C=16*t;return this.int16[P+0]=e,this.int16[P+1]=r,this.int16[P+2]=n,this.int16[P+3]=i,this.int16[P+4]=s,this.int16[P+5]=a,this.int16[P+6]=o,this.int16[P+7]=l,this.uint16[P+8]=u,this.uint16[P+9]=c,this.uint16[P+10]=h,this.uint16[P+11]=p,this.uint16[P+12]=f,this.uint16[P+13]=d,this.uint16[P+14]=y,this.uint16[P+15]=m,this.uint16[P+16]=g,this.uint16[P+17]=x,this.uint16[P+18]=v,this.uint16[P+19]=b,this.uint16[P+20]=w,this.uint16[P+21]=_,this.uint16[P+22]=S,this.uint32[C+12]=A,this.float32[C+13]=k,this.float32[C+14]=M,this.uint16[P+30]=I,this.uint16[P+31]=z,t}}ha.prototype.bytesPerElement=64,as("StructArrayLayout8i15ui1ul2f2ui64",ha);class pa extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t){const e=this.length;return this.resize(e+1),this.emplace(e,t)}emplace(t,e){return this.float32[1*t+0]=e,t}}pa.prototype.bytesPerElement=4,as("StructArrayLayout1f4",pa);class fa extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=3*t;return this.uint16[6*t+0]=e,this.float32[i+1]=r,this.float32[i+2]=n,t}}fa.prototype.bytesPerElement=12,as("StructArrayLayout1ui2f12",fa);class da extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint32=new Uint32Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e,r){const n=this.length;return this.resize(n+1),this.emplace(n,t,e,r)}emplace(t,e,r,n){const i=4*t;return this.uint32[2*t+0]=e,this.uint16[i+2]=r,this.uint16[i+3]=n,t}}da.prototype.bytesPerElement=8,as("StructArrayLayout1ul2ui8",da);class ya extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t,e){const r=this.length;return this.resize(r+1),this.emplace(r,t,e)}emplace(t,e,r){const n=2*t;return this.uint16[n+0]=e,this.uint16[n+1]=r,t}}ya.prototype.bytesPerElement=4,as("StructArrayLayout2ui4",ya);class ma extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.uint16=new Uint16Array(this.arrayBuffer);}emplaceBack(t){const e=this.length;return this.resize(e+1),this.emplace(e,t)}emplace(t,e){return this.uint16[1*t+0]=e,t}}ma.prototype.bytesPerElement=2,as("StructArrayLayout1ui2",ma);class ga extends Xs{_refreshViews(){this.uint8=new Uint8Array(this.arrayBuffer),this.float32=new Float32Array(this.arrayBuffer);}emplaceBack(t,e,r,n){const i=this.length;return this.resize(i+1),this.emplace(i,t,e,r,n)}emplace(t,e,r,n,i){const s=4*t;return this.float32[s+0]=e,this.float32[s+1]=r,this.float32[s+2]=n,this.float32[s+3]=i,t}}ga.prototype.bytesPerElement=16,as("StructArrayLayout4f16",ga);class xa extends Gs{get anchorPointX(){return this._structArray.int16[this._pos2+0]}get anchorPointY(){return this._structArray.int16[this._pos2+1]}get x1(){return this._structArray.int16[this._pos2+2]}get y1(){return this._structArray.int16[this._pos2+3]}get x2(){return this._structArray.int16[this._pos2+4]}get y2(){return this._structArray.int16[this._pos2+5]}get featureIndex(){return this._structArray.uint32[this._pos4+3]}get sourceLayerIndex(){return this._structArray.uint16[this._pos2+8]}get bucketIndex(){return this._structArray.uint16[this._pos2+9]}get anchorPoint(){return new r(this.anchorPointX,this.anchorPointY)}}xa.prototype.size=20;class va extends sa{get(t){return new xa(this,t)}}as("CollisionBoxArray",va);class ba extends Gs{get anchorX(){return this._structArray.int16[this._pos2+0]}get anchorY(){return this._structArray.int16[this._pos2+1]}get glyphStartIndex(){return this._structArray.uint16[this._pos2+2]}get numGlyphs(){return this._structArray.uint16[this._pos2+3]}get vertexStartIndex(){return this._structArray.uint32[this._pos4+2]}get lineStartIndex(){return this._structArray.uint32[this._pos4+3]}get lineLength(){return this._structArray.uint32[this._pos4+4]}get segment(){return this._structArray.uint16[this._pos2+10]}get lowerSize(){return this._structArray.uint16[this._pos2+11]}get upperSize(){return this._structArray.uint16[this._pos2+12]}get lineOffsetX(){return this._structArray.float32[this._pos4+7]}get lineOffsetY(){return this._structArray.float32[this._pos4+8]}get writingMode(){return this._structArray.uint8[this._pos1+36]}get placedOrientation(){return this._structArray.uint8[this._pos1+37]}set placedOrientation(t){this._structArray.uint8[this._pos1+37]=t;}get hidden(){return this._structArray.uint8[this._pos1+38]}set hidden(t){this._structArray.uint8[this._pos1+38]=t;}get crossTileID(){return this._structArray.uint32[this._pos4+10]}set crossTileID(t){this._structArray.uint32[this._pos4+10]=t;}get associatedIconIndex(){return this._structArray.int16[this._pos2+22]}}ba.prototype.size=48;class wa extends ca{get(t){return new ba(this,t)}}as("PlacedSymbolArray",wa);class _a extends Gs{get anchorX(){return this._structArray.int16[this._pos2+0]}get anchorY(){return this._structArray.int16[this._pos2+1]}get rightJustifiedTextSymbolIndex(){return this._structArray.int16[this._pos2+2]}get centerJustifiedTextSymbolIndex(){return this._structArray.int16[this._pos2+3]}get leftJustifiedTextSymbolIndex(){return this._structArray.int16[this._pos2+4]}get verticalPlacedTextSymbolIndex(){return this._structArray.int16[this._pos2+5]}get placedIconSymbolIndex(){return this._structArray.int16[this._pos2+6]}get verticalPlacedIconSymbolIndex(){return this._structArray.int16[this._pos2+7]}get key(){return this._structArray.uint16[this._pos2+8]}get textBoxStartIndex(){return this._structArray.uint16[this._pos2+9]}get textBoxEndIndex(){return this._structArray.uint16[this._pos2+10]}get verticalTextBoxStartIndex(){return this._structArray.uint16[this._pos2+11]}get verticalTextBoxEndIndex(){return this._structArray.uint16[this._pos2+12]}get iconBoxStartIndex(){return this._structArray.uint16[this._pos2+13]}get iconBoxEndIndex(){return this._structArray.uint16[this._pos2+14]}get verticalIconBoxStartIndex(){return this._structArray.uint16[this._pos2+15]}get verticalIconBoxEndIndex(){return this._structArray.uint16[this._pos2+16]}get featureIndex(){return this._structArray.uint16[this._pos2+17]}get numHorizontalGlyphVertices(){return this._structArray.uint16[this._pos2+18]}get numVerticalGlyphVertices(){return this._structArray.uint16[this._pos2+19]}get numIconVertices(){return this._structArray.uint16[this._pos2+20]}get numVerticalIconVertices(){return this._structArray.uint16[this._pos2+21]}get useRuntimeCollisionCircles(){return this._structArray.uint16[this._pos2+22]}get crossTileID(){return this._structArray.uint32[this._pos4+12]}set crossTileID(t){this._structArray.uint32[this._pos4+12]=t;}get textBoxScale(){return this._structArray.float32[this._pos4+13]}get collisionCircleDiameter(){return this._structArray.float32[this._pos4+14]}get textAnchorOffsetStartIndex(){return this._structArray.uint16[this._pos2+30]}get textAnchorOffsetEndIndex(){return this._structArray.uint16[this._pos2+31]}}_a.prototype.size=64;class Sa extends ha{get(t){return new _a(this,t)}}as("SymbolInstanceArray",Sa);class Aa extends pa{getoffsetX(t){return this.float32[1*t+0]}}as("GlyphOffsetArray",Aa);class ka extends Ks{getx(t){return this.int16[3*t+0]}gety(t){return this.int16[3*t+1]}gettileUnitDistanceFromAnchor(t){return this.int16[3*t+2]}}as("SymbolLineVertexArray",ka);class Ma extends Gs{get textAnchor(){return this._structArray.uint16[this._pos2+0]}get textOffset0(){return this._structArray.float32[this._pos4+1]}get textOffset1(){return this._structArray.float32[this._pos4+2]}}Ma.prototype.size=12;class Ia extends fa{get(t){return new Ma(this,t)}}as("TextAnchorOffsetArray",Ia);class za extends Gs{get featureIndex(){return this._structArray.uint32[this._pos4+0]}get sourceLayerIndex(){return this._structArray.uint16[this._pos2+2]}get bucketIndex(){return this._structArray.uint16[this._pos2+3]}}za.prototype.size=8;class Pa extends da{get(t){return new za(this,t)}}as("FeatureIndexArray",Pa);class Ca extends Hs{}class Ea extends Hs{}class Ba extends Hs{}class Va extends Ws{}class Ta extends Qs{}class Fa extends ta{}class $a extends ea{}class La extends ra{}class Oa extends na{}class Da extends ia{}class Ra extends aa{}class Ua extends la{}class ja extends ua{}class Na extends ya{}const qa=Zs([{name:"a_pos",components:2,type:"Int16"}],4),{members:Ga}=qa;class Xa{constructor(t=[]){this._forceNewSegmentOnNextPrepare=!1,this.segments=t;}prepareSegment(t,e,r,n){const i=this.segments[this.segments.length-1];return t>Xa.MAX_VERTEX_ARRAY_LENGTH&&q(`Max vertices per segment is ${Xa.MAX_VERTEX_ARRAY_LENGTH}: bucket requested ${t}. Consider using the \`fillLargeMeshArrays\` function if you require meshes with more than ${Xa.MAX_VERTEX_ARRAY_LENGTH} vertices.`),this._forceNewSegmentOnNextPrepare||!i||i.vertexLength+t>Xa.MAX_VERTEX_ARRAY_LENGTH||i.sortKey!==n?this.createNewSegment(e,r,n):i}createNewSegment(t,e,r){const n={vertexOffset:t.length,primitiveOffset:e.length,vertexLength:0,primitiveLength:0,vaos:{}};return void 0!==r&&(n.sortKey=r),this._forceNewSegmentOnNextPrepare=!1,this.segments.push(n),n}getOrCreateLatestSegment(t,e,r){return this.prepareSegment(0,t,e,r)}forceNewSegmentOnNextPrepare(){this._forceNewSegmentOnNextPrepare=!0;}get(){return this.segments}destroy(){for(const t of this.segments)for(const e in t.vaos)t.vaos[e].destroy();}static simpleSegment(t,e,r,n){return new Xa([{vertexOffset:t,primitiveOffset:e,vertexLength:r,primitiveLength:n,vaos:{},sortKey:0}])}}function Za(t,e){return 256*(t=$(Math.floor(t),0,255))+$(Math.floor(e),0,255)}Xa.MAX_VERTEX_ARRAY_LENGTH=Math.pow(2,16)-1,as("SegmentVector",Xa);const Ya=Zs([{name:"a_pattern_from",components:4,type:"Uint16"},{name:"a_pattern_to",components:4,type:"Uint16"},{name:"a_pixel_ratio_from",components:1,type:"Uint16"},{name:"a_pixel_ratio_to",components:1,type:"Uint16"}]);var Ha,Ka,Ja,Wa={exports:{}},Qa={exports:{}},to={exports:{}},eo=function(){if(Ja)return Wa.exports;Ja=1;var t=(Ha||(Ha=1,Qa.exports=function(t,e){var r,n,i,s,a,o,l,u;for(n=t.length-(r=3&t.length),i=e,a=3432918353,o=461845907,u=0;u>>16)*a&65535)<<16)&4294967295)<<15|l>>>17))*o+(((l>>>16)*o&65535)<<16)&4294967295)<<13|i>>>19))+((5*(i>>>16)&65535)<<16)&4294967295))+((58964+(s>>>16)&65535)<<16);switch(l=0,r){case 3:l^=(255&t.charCodeAt(u+2))<<16;case 2:l^=(255&t.charCodeAt(u+1))<<8;case 1:i^=l=(65535&(l=(l=(65535&(l^=255&t.charCodeAt(u)))*a+(((l>>>16)*a&65535)<<16)&4294967295)<<15|l>>>17))*o+(((l>>>16)*o&65535)<<16)&4294967295;}return i^=t.length,i=2246822507*(65535&(i^=i>>>16))+((2246822507*(i>>>16)&65535)<<16)&4294967295,i=3266489909*(65535&(i^=i>>>13))+((3266489909*(i>>>16)&65535)<<16)&4294967295,(i^=i>>>16)>>>0}),Qa.exports),e=(Ka||(Ka=1,to.exports=function(t,e){for(var r,n=t.length,i=e^n,s=0;n>=4;)r=1540483477*(65535&(r=255&t.charCodeAt(s)|(255&t.charCodeAt(++s))<<8|(255&t.charCodeAt(++s))<<16|(255&t.charCodeAt(++s))<<24))+((1540483477*(r>>>16)&65535)<<16),i=1540483477*(65535&i)+((1540483477*(i>>>16)&65535)<<16)^(r=1540483477*(65535&(r^=r>>>24))+((1540483477*(r>>>16)&65535)<<16)),n-=4,++s;switch(n){case 3:i^=(255&t.charCodeAt(s+2))<<16;case 2:i^=(255&t.charCodeAt(s+1))<<8;case 1:i=1540483477*(65535&(i^=255&t.charCodeAt(s)))+((1540483477*(i>>>16)&65535)<<16);}return i=1540483477*(65535&(i^=i>>>13))+((1540483477*(i>>>16)&65535)<<16),(i^=i>>>15)>>>0}),to.exports);return Wa.exports=t,Wa.exports.murmur3=t,Wa.exports.murmur2=e,Wa.exports}(),ro=n(eo);class no{constructor(){this.ids=[],this.positions=[],this.indexed=!1;}add(t,e,r,n){this.ids.push(io(t)),this.positions.push(e,r,n);}getPositions(t){if(!this.indexed)throw new Error("Trying to get index, but feature positions are not indexed");const e=io(t);let r=0,n=this.ids.length-1;for(;r>1;this.ids[t]>=e?n=t:r=t+1;}const i=[];for(;this.ids[r]===e;)i.push({index:this.positions[3*r],start:this.positions[3*r+1],end:this.positions[3*r+2]}),r++;return i}static serialize(t,e){const r=new Float64Array(t.ids),n=new Uint32Array(t.positions);return so(r,n,0,r.length-1),e&&e.push(r.buffer,n.buffer),{ids:r,positions:n}}static deserialize(t){const e=new no;return e.ids=t.ids,e.positions=t.positions,e.indexed=!0,e}}function io(t){const e=+t;return !isNaN(e)&&e<=Number.MAX_SAFE_INTEGER?e:ro(String(t))}function so(t,e,r,n){for(;r>1];let s=r-1,a=n+1;for(;;){do{s++;}while(t[s]i);if(s>=a)break;ao(t,s,a),ao(e,3*s,3*a),ao(e,3*s+1,3*a+1),ao(e,3*s+2,3*a+2);}a-r`u_${t}`)),this.type=r;}setUniform(t,e,r){t.set(r.constantOr(this.value));}getBinding(t,e,r){return "color"===this.type?new co(t,e):new lo(t,e)}}class yo{constructor(t,e){this.uniformNames=e.map((t=>`u_${t}`)),this.patternFrom=null,this.patternTo=null,this.pixelRatioFrom=1,this.pixelRatioTo=1;}setConstantPatternPositions(t,e){this.pixelRatioFrom=e.pixelRatio,this.pixelRatioTo=t.pixelRatio,this.patternFrom=e.tlbr,this.patternTo=t.tlbr;}setUniform(t,e,r,n){const i="u_pattern_to"===n?this.patternTo:"u_pattern_from"===n?this.patternFrom:"u_pixel_ratio_to"===n?this.pixelRatioTo:"u_pixel_ratio_from"===n?this.pixelRatioFrom:null;i&&t.set(i);}getBinding(t,e,r){return "u_pattern"===r.substr(0,9)?new uo(t,e):new lo(t,e)}}class mo{constructor(t,e,r,n){this.expression=t,this.type=r,this.maxValue=0,this.paintVertexAttributes=e.map((t=>({name:`a_${t}`,type:"Float32",components:"color"===r?2:1,offset:0}))),this.paintVertexArray=new n;}populatePaintArray(t,e,r){const n=this.paintVertexArray.length,i=this.expression.evaluate(new Is(0,r),e,{},r.canonical,[],r.formattedSection);this.paintVertexArray.resize(t),this._setPaintValue(n,t,i);}updatePaintArray(t,e,r,n,i){const s=this.expression.evaluate(new Is(0,i),r,n);this._setPaintValue(t,e,s);}_setPaintValue(t,e,r){if("color"===this.type){const n=po(r);for(let r=t;r`u_${t}_t`)),this.type=r,this.useIntegerZoom=n,this.zoom=i,this.maxValue=0,this.paintVertexAttributes=e.map((t=>({name:`a_${t}`,type:"Float32",components:"color"===r?4:2,offset:0}))),this.paintVertexArray=new s;}populatePaintArray(t,e,r){const n=this.expression.evaluate(new Is(this.zoom,r),e,{},r.canonical,[],r.formattedSection),i=this.expression.evaluate(new Is(this.zoom+1,r),e,{},r.canonical,[],r.formattedSection),s=this.paintVertexArray.length;this.paintVertexArray.resize(t),this._setPaintValue(s,t,n,i);}updatePaintArray(t,e,r,n,i){const s=this.expression.evaluate(new Is(this.zoom,i),r,n),a=this.expression.evaluate(new Is(this.zoom+1,i),r,n);this._setPaintValue(t,e,s,a);}_setPaintValue(t,e,r,n){if("color"===this.type){const i=po(r),s=po(n);for(let r=t;r`#define HAS_UNIFORM_${t}`)));}return t}getBinderAttributes(){const t=[];for(const e in this.binders){const r=this.binders[e];if(r instanceof mo||r instanceof go)for(let e=0;e!0){this.programConfigurations={};for(const n of t)this.programConfigurations[n.id]=new vo(n,e,r);this.needsUpload=!1,this._featureMap=new no,this._bufferOffset=0;}populatePaintArrays(t,e,r,n){for(const r in this.programConfigurations)this.programConfigurations[r].populatePaintArrays(t,e,n);void 0!==e.id&&this._featureMap.add(e.id,r,this._bufferOffset,t),this._bufferOffset=t,this.needsUpload=!0;}updatePaintArrays(t,e,r,n){for(const i of r)this.needsUpload=this.programConfigurations[i.id].updatePaintArrays(t,this._featureMap,e,i,n)||this.needsUpload;}get(t){return this.programConfigurations[t]}upload(t){if(this.needsUpload){for(const e in this.programConfigurations)this.programConfigurations[e].upload(t);this.needsUpload=!1;}}destroy(){for(const t in this.programConfigurations)this.programConfigurations[t].destroy();}}function wo(t,e){return {"text-opacity":["opacity"],"icon-opacity":["opacity"],"text-color":["fill_color"],"icon-color":["fill_color"],"text-halo-color":["halo_color"],"icon-halo-color":["halo_color"],"text-halo-blur":["halo_blur"],"icon-halo-blur":["halo_blur"],"text-halo-width":["halo_width"],"icon-halo-width":["halo_width"],"line-gap-width":["gapwidth"],"line-pattern":["pattern_to","pattern_from","pixel_ratio_to","pixel_ratio_from"],"fill-pattern":["pattern_to","pattern_from","pixel_ratio_to","pixel_ratio_from"],"fill-extrusion-pattern":["pattern_to","pattern_from","pixel_ratio_to","pixel_ratio_from"]}[t]||[t.replace(`${e}-`,"").replace(/-/g,"_")]}function _o(t,e,r){const n={color:{source:ta,composite:ga},number:{source:pa,composite:ta}},i=function(t){return {"line-pattern":{source:$a,composite:$a},"fill-pattern":{source:$a,composite:$a},"fill-extrusion-pattern":{source:$a,composite:$a}}[t]}(t);return i&&i[r]||n[e][r]}as("ConstantBinder",fo),as("CrossFadedConstantBinder",yo),as("SourceExpressionBinder",mo),as("CrossFadedCompositeBinder",xo),as("CompositeExpressionBinder",go),as("ProgramConfiguration",vo,{omit:["_buffers"]}),as("ProgramConfigurationSet",bo);const So=Math.pow(2,14)-1,Ao=-So-1;function ko(t){const e=P/t.extent,r=t.loadGeometry();for(let t=0;tr.x+1||sr.y+1)&&q("Geometry exceeds allowed extent, reduce your vector tile buffer size");}}return r}function Mo(t,e){return {type:t.type,id:t.id,properties:t.properties,geometry:e?ko(t):[]}}const Io=-32768;function zo(t,e,r,n,i){t.emplaceBack(Io+8*e+n,Io+8*r+i);}class Po{constructor(t){this.zoom=t.zoom,this.globalState=t.globalState,this.overscaling=t.overscaling,this.layers=t.layers,this.layerIds=this.layers.map((t=>t.id)),this.index=t.index,this.hasPattern=!1,this.layoutVertexArray=new Ea,this.indexArray=new ja,this.segments=new Xa,this.programConfigurations=new bo(t.layers,t.zoom),this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){const n=this.layers[0],i=[];let s=null,a=!1,o="heatmap"===n.type;if("circle"===n.type){const t=n;s=t.layout.get("circle-sort-key"),a=!s.isConstant(),o=o||"map"===t.paint.get("circle-pitch-alignment");}const l=o?e.subdivisionGranularity.circle:1;for(const{feature:e,id:n,index:o,sourceLayerIndex:l}of t){const t=this.layers[0]._featureFilter.needGeometry,u=Mo(e,t);if(!this.layers[0]._featureFilter.filter(new Is(this.zoom,{globalState:this.globalState}),u,r))continue;const c=a?s.evaluate(u,{},r):void 0,h={id:n,properties:e.properties,type:e.type,sourceLayerIndex:l,index:o,geometry:t?u.geometry:ko(e),patterns:{},sortKey:c};i.push(h);}a&&i.sort(((t,e)=>t.sortKey-e.sortKey));for(const n of i){const{geometry:i,index:s,sourceLayerIndex:a}=n,o=t[s].feature;this.addFeature(n,i,s,r,l),e.featureIndex.insert(o,i,s,a,this.index);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,{imagePositions:r,globalState:this.globalState});}isEmpty(){return 0===this.layoutVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,Ga),this.indexBuffer=t.createIndexBuffer(this.indexArray)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.programConfigurations.destroy(),this.segments.destroy());}addFeature(t,e,r,n,i=1){let s;switch(i){case 1:s=[0,7];break;case 3:s=[0,2,5,7];break;case 5:s=[0,1,3,4,6,7];break;case 7:s=[0,1,2,3,4,5,6,7];break;default:throw new Error(`Invalid circle bucket granularity: ${i}; valid values are 1, 3, 5, 7.`)}const a=s.length;for(const r of e)for(const e of r){const r=e.x,n=e.y;if(r<0||r>=P||n<0||n>=P)continue;const i=this.segments.prepareSegment(a*a,this.layoutVertexArray,this.indexArray,t.sortKey),o=i.vertexLength;for(let t=0;t1){if(To(t,e))return !0;for(let n=0;n1?r:r.sub(e)._mult(i)._add(e))}function Oo(t,e){let r,n,i,s=!1;for(let a=0;ae.y!=i.y>e.y&&e.x<(i.x-n.x)*(e.y-n.y)/(i.y-n.y)+n.x&&(s=!s);}return s}function Do(t,e){let r=!1;for(let n=0,i=t.length-1;ne.y!=a.y>e.y&&e.x<(a.x-s.x)*(e.y-s.y)/(a.y-s.y)+s.x&&(r=!r);}return r}function Ro(t,e,r){const n=r[0],i=r[2];if(t.xi.x&&e.x>i.x||t.yi.y&&e.y>i.y)return !1;const s=G(t,e,r[0]);return s!==G(t,e,r[1])||s!==G(t,e,r[2])||s!==G(t,e,r[3])}function Uo(t,e,r){const n=e.paint.get(t).value;return "constant"===n.kind?n.value:r.programConfigurations.get(e.id).getMaxValue(t)}function jo(t){return Math.sqrt(t[0]*t[0]+t[1]*t[1])}function No(t,e,n,i,s){if(!e[0]&&!e[1])return t;const a=r.convert(e)._mult(s);"viewport"===n&&a._rotate(-i);const o=[];for(let e=0;eYo(t,e,r,n)))}(l,i,a,o),p=c?u*s:u;for(const t of n)for(const e of t){const t=c?e:Yo(e,i,a,o);let r=p;const n=i.projectTileCoordinates(e.x,e.y,a,o).signedDistanceFromCamera;if("viewport"===this.paint.get("circle-pitch-scale")&&"map"===this.paint.get("circle-pitch-alignment")?r*=n/i.cameraToCenterDistance:"map"===this.paint.get("circle-pitch-scale")&&"viewport"===this.paint.get("circle-pitch-alignment")&&(r*=i.cameraToCenterDistance/n),Eo(h,t,r))return !0}return !1}}function Yo(t,e,n,i){const s=e.projectTileCoordinates(t.x,t.y,n,i).point;return new r((.5*s.x+.5)*e.width,(.5*-s.y+.5)*e.height)}class Ho extends Po{}let Ko;as("HeatmapBucket",Ho,{omit:["layers"]});var Jo={get paint(){return Ko=Ko||new Us({"heatmap-radius":new Ls(xt.paint_heatmap["heatmap-radius"]),"heatmap-weight":new Ls(xt.paint_heatmap["heatmap-weight"]),"heatmap-intensity":new $s(xt.paint_heatmap["heatmap-intensity"]),"heatmap-color":new Rs(xt.paint_heatmap["heatmap-color"]),"heatmap-opacity":new $s(xt.paint_heatmap["heatmap-opacity"])})}};function Wo(t,{width:e,height:r},n,i){if(i){if(i instanceof Uint8ClampedArray)i=new Uint8Array(i.buffer);else if(i.length!==e*r*n)throw new RangeError(`mismatched image size. expected: ${i.length} but got: ${e*r*n}`)}else i=new Uint8Array(e*r*n);return t.width=e,t.height=r,t.data=i,t}function Qo(t,{width:e,height:r},n){if(e===t.width&&r===t.height)return;const i=Wo({},{width:e,height:r},n);tl(t,i,{x:0,y:0},{x:0,y:0},{width:Math.min(t.width,e),height:Math.min(t.height,r)},n),t.width=e,t.height=r,t.data=i.data;}function tl(t,e,r,n,i,s){if(0===i.width||0===i.height)return e;if(i.width>t.width||i.height>t.height||r.x>t.width-i.width||r.y>t.height-i.height)throw new RangeError("out of range source coordinates for image copy");if(i.width>e.width||i.height>e.height||n.x>e.width-i.width||n.y>e.height-i.height)throw new RangeError("out of range destination coordinates for image copy");const a=t.data,o=e.data;if(a===o)throw new Error("srcData equals dstData, so image is already copied");for(let l=0;l{e[t.evaluationKey]=a;const o=t.expression.evaluate(e);i.setPixel(n/4/r,s/4,o);};if(t.clips)for(let e=0,i=0;ethis.max&&(this.max=r),r=this.dim+1||e<-1||e>=this.dim+1)throw new RangeError("out of range source coordinates for DEM data");return (e+1)*this.stride+(t+1)}unpack(t,e,r){return t*this.redFactor+e*this.greenFactor+r*this.blueFactor-this.baseShift}pack(t){return fl(t,this.getUnpackVector())}getPixels(){return new rl({width:this.stride,height:this.stride},new Uint8Array(this.data.buffer))}backfillBorder(t,e,r){if(this.dim!==t.dim)throw new Error("dem dimension mismatch");let n=e*this.dim,i=e*this.dim+this.dim,s=r*this.dim,a=r*this.dim+this.dim;switch(e){case -1:n=i-1;break;case 1:i=n+1;}switch(r){case -1:s=a-1;break;case 1:a=s+1;}const o=-e*this.dim,l=-r*this.dim;for(let e=s;e0)for(let i=e;i=e;i-=n)s=Ul(i/n|0,t[i],t[i+1],s);return s&&Fl(s,s.next)&&(jl(s),s=s.next),s}function bl(t,e){if(!t)return t;e||(e=t);let r,n=t;do{if(r=!1,n.steiner||!Fl(n,n.next)&&0!==Tl(n.prev,n,n.next))n=n.next;else {if(jl(n),n=e=n.prev,n===n.next)break;r=!0;}}while(r||n!==e);return e}function wl(t,e,r,n,i,s,a){if(!t)return;!a&&s&&function(t,e,r,n){let i=t;do{0===i.z&&(i.z=Pl(i.x,i.y,e,r,n)),i.prevZ=i.prev,i.nextZ=i.next,i=i.next;}while(i!==t);i.prevZ.nextZ=null,i.prevZ=null,function(t){let e,r=1;do{let n,i=t;t=null;let s=null;for(e=0;i;){e++;let a=i,o=0;for(let t=0;t0||l>0&&a;)0!==o&&(0===l||!a||i.z<=a.z)?(n=i,i=i.nextZ,o--):(n=a,a=a.nextZ,l--),s?s.nextZ=n:t=n,n.prevZ=s,s=n;i=a;}s.nextZ=null,r*=2;}while(e>1)}(i);}(t,n,i,s);let o=t;for(;t.prev!==t.next;){const l=t.prev,u=t.next;if(s?Sl(t,n,i,s):_l(t))e.push(l.i,t.i,u.i),jl(t),t=u.next,o=u.next;else if((t=u)===o){a?1===a?wl(t=Al(bl(t),e),e,r,n,i,s,2):2===a&&kl(t,e,r,n,i,s):wl(bl(t),e,r,n,i,s,1);break}}}function _l(t){const e=t.prev,r=t,n=t.next;if(Tl(e,r,n)>=0)return !1;const i=e.x,s=r.x,a=n.x,o=e.y,l=r.y,u=n.y,c=Math.min(i,s,a),h=Math.min(o,l,u),p=Math.max(i,s,a),f=Math.max(o,l,u);let d=n.next;for(;d!==e;){if(d.x>=c&&d.x<=p&&d.y>=h&&d.y<=f&&Bl(i,o,s,l,a,u,d.x,d.y)&&Tl(d.prev,d,d.next)>=0)return !1;d=d.next;}return !0}function Sl(t,e,r,n){const i=t.prev,s=t,a=t.next;if(Tl(i,s,a)>=0)return !1;const o=i.x,l=s.x,u=a.x,c=i.y,h=s.y,p=a.y,f=Math.min(o,l,u),d=Math.min(c,h,p),y=Math.max(o,l,u),m=Math.max(c,h,p),g=Pl(f,d,e,r,n),x=Pl(y,m,e,r,n);let v=t.prevZ,b=t.nextZ;for(;v&&v.z>=g&&b&&b.z<=x;){if(v.x>=f&&v.x<=y&&v.y>=d&&v.y<=m&&v!==i&&v!==a&&Bl(o,c,l,h,u,p,v.x,v.y)&&Tl(v.prev,v,v.next)>=0)return !1;if(v=v.prevZ,b.x>=f&&b.x<=y&&b.y>=d&&b.y<=m&&b!==i&&b!==a&&Bl(o,c,l,h,u,p,b.x,b.y)&&Tl(b.prev,b,b.next)>=0)return !1;b=b.nextZ;}for(;v&&v.z>=g;){if(v.x>=f&&v.x<=y&&v.y>=d&&v.y<=m&&v!==i&&v!==a&&Bl(o,c,l,h,u,p,v.x,v.y)&&Tl(v.prev,v,v.next)>=0)return !1;v=v.prevZ;}for(;b&&b.z<=x;){if(b.x>=f&&b.x<=y&&b.y>=d&&b.y<=m&&b!==i&&b!==a&&Bl(o,c,l,h,u,p,b.x,b.y)&&Tl(b.prev,b,b.next)>=0)return !1;b=b.nextZ;}return !0}function Al(t,e){let r=t;do{const n=r.prev,i=r.next.next;!Fl(n,i)&&$l(n,r,r.next,i)&&Dl(n,i)&&Dl(i,n)&&(e.push(n.i,r.i,i.i),jl(r),jl(r.next),r=t=i),r=r.next;}while(r!==t);return bl(r)}function kl(t,e,r,n,i,s){let a=t;do{let t=a.next.next;for(;t!==a.prev;){if(a.i!==t.i&&Vl(a,t)){let o=Rl(a,t);return a=bl(a,a.next),o=bl(o,o.next),wl(a,e,r,n,i,s,0),void wl(o,e,r,n,i,s,0)}t=t.next;}a=a.next;}while(a!==t)}function Ml(t,e){let r=t.x-e.x;return 0===r&&(r=t.y-e.y,0===r)&&(r=(t.next.y-t.y)/(t.next.x-t.x)-(e.next.y-e.y)/(e.next.x-e.x)),r}function Il(t,e){const r=function(t,e){let r=e;const n=t.x,i=t.y;let s,a=-1/0;if(Fl(t,r))return r;do{if(Fl(t,r.next))return r.next;if(i<=r.y&&i>=r.next.y&&r.next.y!==r.y){const t=r.x+(i-r.y)*(r.next.x-r.x)/(r.next.y-r.y);if(t<=n&&t>a&&(a=t,s=r.x=r.x&&r.x>=l&&n!==r.x&&El(is.x||r.x===s.x&&zl(s,r)))&&(s=r,c=e);}r=r.next;}while(r!==o);return s}(t,e);if(!r)return e;const n=Rl(r,t);return bl(n,n.next),bl(r,r.next)}function zl(t,e){return Tl(t.prev,t,e.prev)<0&&Tl(e.next,t,t.next)<0}function Pl(t,e,r,n,i){return (t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-r)*i|0)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=(e-n)*i|0)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function Cl(t){let e=t,r=t;do{(e.x=(t-a)*(s-o)&&(t-a)*(n-o)>=(r-a)*(e-o)&&(r-a)*(s-o)>=(i-a)*(n-o)}function Bl(t,e,r,n,i,s,a,o){return !(t===a&&e===o)&&El(t,e,r,n,i,s,a,o)}function Vl(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){let r=t;do{if(r.i!==t.i&&r.next.i!==t.i&&r.i!==e.i&&r.next.i!==e.i&&$l(r,r.next,t,e))return !0;r=r.next;}while(r!==t);return !1}(t,e)&&(Dl(t,e)&&Dl(e,t)&&function(t,e){let r=t,n=!1;const i=(t.x+e.x)/2,s=(t.y+e.y)/2;do{r.y>s!=r.next.y>s&&r.next.y!==r.y&&i<(r.next.x-r.x)*(s-r.y)/(r.next.y-r.y)+r.x&&(n=!n),r=r.next;}while(r!==t);return n}(t,e)&&(Tl(t.prev,t,e.prev)||Tl(t,e.prev,e))||Fl(t,e)&&Tl(t.prev,t,t.next)>0&&Tl(e.prev,e,e.next)>0)}function Tl(t,e,r){return (e.y-t.y)*(r.x-e.x)-(e.x-t.x)*(r.y-e.y)}function Fl(t,e){return t.x===e.x&&t.y===e.y}function $l(t,e,r,n){const i=Ol(Tl(t,e,r)),s=Ol(Tl(t,e,n)),a=Ol(Tl(r,n,t)),o=Ol(Tl(r,n,e));return i!==s&&a!==o||!(0!==i||!Ll(t,r,e))||!(0!==s||!Ll(t,n,e))||!(0!==a||!Ll(r,t,n))||!(0!==o||!Ll(r,e,n))}function Ll(t,e,r){return e.x<=Math.max(t.x,r.x)&&e.x>=Math.min(t.x,r.x)&&e.y<=Math.max(t.y,r.y)&&e.y>=Math.min(t.y,r.y)}function Ol(t){return t>0?1:t<0?-1:0}function Dl(t,e){return Tl(t.prev,t,t.next)<0?Tl(t,e,t.next)>=0&&Tl(t,t.prev,e)>=0:Tl(t,e,t.prev)<0||Tl(t,t.next,e)<0}function Rl(t,e){const r=Nl(t.i,t.x,t.y),n=Nl(e.i,e.x,e.y),i=t.next,s=e.prev;return t.next=e,e.prev=t,r.next=i,i.prev=r,n.next=r,r.prev=n,s.next=n,n.prev=s,n}function Ul(t,e,r,n){const i=Nl(t,e,r);return n?(i.next=n.next,i.prev=n,n.next.prev=i,n.next=i):(i.prev=i,i.next=i),i}function jl(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ);}function Nl(t,e,r){return {i:t,x:e,y:r,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}class ql{constructor(t,e){if(e>t)throw new Error("Min granularity must not be greater than base granularity.");this._baseZoomGranularity=t,this._minGranularity=e;}getGranularityForZoomLevel(t){return Math.max(Math.floor(this._baseZoomGranularity/(1<32767||e>32767)throw new Error("Vertex coordinates are out of signed 16 bit integer range.");const r=0|Math.round(t),n=0|Math.round(e),i=this._getKey(r,n);if(this._vertexDictionary.has(i))return this._vertexDictionary.get(i);const s=this._vertexBuffer.length/2;return this._vertexDictionary.set(i,s),this._vertexBuffer.push(r,n),s}_subdivideTrianglesScanline(t){if(this._granularity<2)return function(t,e){const r=[];for(let n=0;n0?(r.push(i),r.push(a),r.push(s)):(r.push(i),r.push(s),r.push(a));}return r}(this._vertexBuffer,t);const e=[],r=t.length;for(let n=0;n=1||v<=0)||y&&(oi)){u>=n&&u<=i&&s.push(r[(t+1)%3]);continue}!y&&x>0&&s.push(this._vertexToIndex(a+p*x,o+f*x));const b=a+p*Math.max(x,0),w=a+p*Math.min(v,1);d||this._generateIntraEdgeVertices(s,a,o,l,u,b,w),!y&&v<1&&s.push(this._vertexToIndex(a+p*v,o+f*v)),(y||u>=n&&u<=i)&&s.push(r[(t+1)%3]),!y&&(u<=n||u>=i)&&this._generateInterEdgeVertices(s,a,o,l,u,c,h,w,n,i);}return s}_generateIntraEdgeVertices(t,e,r,n,i,s,a){const o=n-e,l=i-r,u=0===l,c=u?Math.min(e,n):Math.min(s,a),h=u?Math.max(e,n):Math.max(s,a),p=Math.floor(c/this._granularityCellSize)+1,f=Math.ceil(h/this._granularityCellSize)-1;if(u?e=p;n--){const i=n*this._granularityCellSize;t.push(this._vertexToIndex(i,r+l*(i-e)/o));}}_generateInterEdgeVertices(t,e,r,n,i,s,a,o,l,u){const c=i-r,h=s-n,p=a-i,f=(l-i)/p,d=(u-i)/p,y=Math.min(f,d),m=Math.max(f,d),g=n+h*y;let x=Math.floor(Math.min(g,o)/this._granularityCellSize)+1,v=Math.ceil(Math.max(g,o)/this._granularityCellSize)-1,b=o=1||m<=0){const t=r-a,n=s+(e-s)*Math.min((l-a)/t,(u-a)/t);x=Math.floor(Math.min(n,o)/this._granularityCellSize)+1,v=Math.ceil(Math.max(n,o)/this._granularityCellSize)-1,b=o0?u:l;if(b)for(let e=x;e<=v;e++)t.push(this._vertexToIndex(e*this._granularityCellSize,_));else for(let e=v;e>=x;e--)t.push(this._vertexToIndex(e*this._granularityCellSize,_));}_generateOutline(t){const e=[];for(const r of t){const t=Kl(r,this._granularity,!0),n=this._pointArrayToIndices(t),i=[];for(let t=1;ti!=(s===Xl)?(t.push(e),t.push(r),t.push(this._vertexToIndex(n,s)),t.push(r),t.push(this._vertexToIndex(i,s)),t.push(this._vertexToIndex(n,s))):(t.push(r),t.push(e),t.push(this._vertexToIndex(n,s)),t.push(this._vertexToIndex(i,s)),t.push(r),t.push(this._vertexToIndex(n,s)));}_fillPoles(t,e,r){const n=this._vertexBuffer,i=P,s=t.length;for(let a=2;a80*r){o=t[0],l=t[1];let e=o,n=l;for(let s=r;se&&(e=r),i>n&&(n=i);}u=Math.max(e-o,n-l),u=0!==u?32767/u:0;}return wl(s,a,r,o,l,u,0),a}(r,n),e=this._convertIndices(r,t);i=this._subdivideTrianglesScanline(e);}catch(t){console.error(t);}let s=[];return e&&(s=this._generateOutline(t)),this._ensureNoPoleVertices(),this._handlePoles(i),{verticesFlattened:this._vertexBuffer,indicesTriangles:i,indicesLineList:s}}_convertIndices(t,e){const r=[];for(let n=0;n0?(Math.floor(x/o)+1)*o:(Math.ceil(x/o)-1)*o,e=y>0?(Math.floor(v/o)+1)*o:(Math.ceil(v/o)-1)*o,n=Math.abs(x-t),i=Math.abs(v-e),s=Math.abs(x-c),a=Math.abs(v-h),u=p?n/m:Number.POSITIVE_INFINITY,b=f?i/g:Number.POSITIVE_INFINITY;if((s<=n||!p)&&(a<=i||!f))break;if(u=0?a-1:s-1,i=(o+1)%s,l=t[2*e[n]],u=t[2*e[i]],c=t[2*e[a]],h=t[2*e[a]+1],p=t[2*e[o]+1];let f=!1;if(lu)f=!1;else {const r=p-h,s=-(t[2*e[o]]-c),a=h((u-c)*r+(t[2*e[i]+1]-h)*s)*a&&(f=!0);}if(f){const t=e[n],i=e[a],l=e[o];t!==i&&t!==l&&i!==l&&r.push(l,i,t),a--,a<0&&(a=s-1);}else {const t=e[i],n=e[a],l=e[o];t!==n&&t!==l&&n!==l&&r.push(l,n,t),o++,o>=s&&(o=0);}if(n===i)break}}function Wl(t,e,r,n,i,s,a,o,l){const u=i.length/2,c=a&&o&&l;if(uXa.MAX_VERTEX_ARRAY_LENGTH&&(u=t.createNewSegment(e,r),l=o.count,y=!0,m=!0,g=!0,c=0);const x=Ql(a,n,s,o,p,y,u),v=Ql(a,n,s,o,f,m,u),b=Ql(a,n,s,o,d,g,u);r.emplaceBack(c+x-l,c+v-l,c+b-l),u.primitiveLength++;}}(e,r,n,i,s,t),c&&function(t,e,r,n,i,s){const a=[];for(let t=0;tXa.MAX_VERTEX_ARRAY_LENGTH&&(u=t.createNewSegment(e,r),l=o.count,d=!0,y=!0,c=0);const m=Ql(a,n,s,o,i,d,u),g=Ql(a,n,s,o,h,y,u);r.emplaceBack(c+m-l,c+g-l),u.primitiveLength++;}}}(a,r,o,i,l,t),e.forceNewSegmentOnNextPrepare(),null==a||a.forceNewSegmentOnNextPrepare();}function Ql(t,e,r,n,i,s,a){if(s){const s=n.count;return r(e[2*i],e[2*i+1]),t[i]=n.count,n.count++,a.vertexLength++,s}return t[i]}class tu{constructor(t){this.zoom=t.zoom,this.globalState=t.globalState,this.overscaling=t.overscaling,this.layers=t.layers,this.layerIds=this.layers.map((t=>t.id)),this.index=t.index,this.hasPattern=!1,this.patternFeatures=[],this.layoutVertexArray=new Ba,this.indexArray=new ja,this.indexArray2=new Na,this.programConfigurations=new bo(t.layers,t.zoom),this.segments=new Xa,this.segments2=new Xa,this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){this.hasPattern=gl("fill",this.layers,e);const n=this.layers[0].layout.get("fill-sort-key"),i=!n.isConstant(),s=[];for(const{feature:a,id:o,index:l,sourceLayerIndex:u}of t){const t=this.layers[0]._featureFilter.needGeometry,c=Mo(a,t);if(!this.layers[0]._featureFilter.filter(new Is(this.zoom,{globalState:this.globalState}),c,r))continue;const h=i?n.evaluate(c,{},r,e.availableImages):void 0,p={id:o,properties:a.properties,type:a.type,sourceLayerIndex:u,index:l,geometry:t?c.geometry:ko(a),patterns:{},sortKey:h};s.push(p);}i&&s.sort(((t,e)=>t.sortKey-e.sortKey));for(const n of s){const{geometry:i,index:s,sourceLayerIndex:a}=n;if(this.hasPattern){const t=xl("fill",this.layers,n,this.zoom,e);this.patternFeatures.push(t);}else this.addFeature(n,i,s,r,{},e.subdivisionGranularity);e.featureIndex.insert(t[s].feature,i,s,a,this.index);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,{imagePositions:r,globalState:this.globalState});}addFeatures(t,e,r){for(const n of this.patternFeatures)this.addFeature(n,n.geometry,n.index,e,r,t.subdivisionGranularity);}isEmpty(){return 0===this.layoutVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,ml),this.indexBuffer=t.createIndexBuffer(this.indexArray),this.indexBuffer2=t.createIndexBuffer(this.indexArray2)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.indexBuffer2.destroy(),this.programConfigurations.destroy(),this.segments.destroy(),this.segments2.destroy());}addFeature(t,e,r,n,i,s){for(const t of Qr(e,500)){const e=Hl(t,n,s.fill.getGranularityForZoomLevel(n.z)),r=this.layoutVertexArray;Wl(((t,e)=>{r.emplaceBack(t,e);}),this.segments,this.layoutVertexArray,this.indexArray,e.verticesFlattened,e.indicesTriangles,this.segments2,this.indexArray2,e.indicesLineList);}this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length,t,r,{imagePositions:i,canonical:n,globalState:this.globalState});}}let eu,ru;as("FillBucket",tu,{omit:["layers","patternFeatures"]});var nu={get paint(){return ru=ru||new Us({"fill-antialias":new $s(xt.paint_fill["fill-antialias"]),"fill-opacity":new Ls(xt.paint_fill["fill-opacity"]),"fill-color":new Ls(xt.paint_fill["fill-color"]),"fill-outline-color":new Ls(xt.paint_fill["fill-outline-color"]),"fill-translate":new $s(xt.paint_fill["fill-translate"]),"fill-translate-anchor":new $s(xt.paint_fill["fill-translate-anchor"]),"fill-pattern":new Os(xt.paint_fill["fill-pattern"])})},get layout(){return eu=eu||new Us({"fill-sort-key":new Ls(xt.layout_fill["fill-sort-key"])})}};class iu extends Ns{constructor(t){super(t,nu);}recalculate(t,e){super.recalculate(t,e);const r=this.paint._values["fill-outline-color"];"constant"===r.value.kind&&void 0===r.value.value&&(this.paint._values["fill-outline-color"]=this.paint._values["fill-color"]);}createBucket(t){return new tu(t)}queryRadius(){return jo(this.paint.get("fill-translate"))}queryIntersectsFeature({queryGeometry:t,geometry:e,transform:r,pixelsToTileUnits:n}){return Bo(No(t,this.paint.get("fill-translate"),this.paint.get("fill-translate-anchor"),-r.bearingInRadians,n),e)}isTileClipped(){return !0}}const su=Zs([{name:"a_pos",components:2,type:"Int16"},{name:"a_normal_ed",components:4,type:"Int16"}],4),au=Zs([{name:"a_centroid",components:2,type:"Int16"}],4),{members:ou}=su;class lu{constructor(t,e,r,n,i){this.properties={},this.extent=r,this.type=0,this.id=void 0,this._pbf=t,this._geometry=-1,this._keys=n,this._values=i,t.readFields(uu,this,e);}loadGeometry(){const t=this._pbf;t.pos=this._geometry;const e=t.readVarint()+t.pos,n=[];let i,s=1,a=0,o=0,l=0;for(;t.pos>3;}if(a--,1===s||2===s)o+=t.readSVarint(),l+=t.readSVarint(),1===s&&(i&&n.push(i),i=[]),i&&i.push(new r(o,l));else {if(7!==s)throw new Error(`unknown command ${s}`);i&&i.push(i[0].clone());}}return i&&n.push(i),n}bbox(){const t=this._pbf;t.pos=this._geometry;const e=t.readVarint()+t.pos;let r=1,n=0,i=0,s=0,a=1/0,o=-1/0,l=1/0,u=-1/0;for(;t.pos>3;}if(n--,1===r||2===r)i+=t.readSVarint(),s+=t.readSVarint(),io&&(o=i),su&&(u=s);else if(7!==r)throw new Error(`unknown command ${r}`)}return [a,l,o,u]}toGeoJSON(t,e,r){const n=this.extent*Math.pow(2,r),i=this.extent*t,s=this.extent*e,a=this.loadGeometry();function o(t){return [360*(t.x+i)/n-180,360/Math.PI*Math.atan(Math.exp((1-2*(t.y+s)/n)*Math.PI))-90]}function l(t){return t.map(o)}let u;if(1===this.type){const t=[];for(const e of a)t.push(e[0]);const e=l(t);u=1===t.length?{type:"Point",coordinates:e[0]}:{type:"MultiPoint",coordinates:e};}else if(2===this.type){const t=a.map(l);u=1===t.length?{type:"LineString",coordinates:t[0]}:{type:"MultiLineString",coordinates:t};}else {if(3!==this.type)throw new Error("unknown feature type");{const t=function(t){const e=t.length;if(e<=1)return [t];const r=[];let n,i;for(let s=0;s=this._features.length)throw new Error("feature index out of bounds");this._pbf.pos=this._features[t];const e=this._pbf.readVarint()+this._pbf.pos;return new lu(this._pbf,e,this.extent,this._keys,this._values)}}function pu(t,e,r){15===t?e.version=r.readVarint():1===t?e.name=r.readString():5===t?e.extent=r.readVarint():2===t?e._features.push(r.pos):3===t?e._keys.push(r.readString()):4===t&&e._values.push(function(t){let e=null;const r=t.readVarint()+t.pos;for(;t.pos>3;e=1===r?t.readString():2===r?t.readFloat():3===r?t.readDouble():4===r?t.readVarint64():5===r?t.readVarint():6===r?t.readSVarint():7===r?t.readBoolean():null;}if(null==e)throw new Error("unknown feature value");return e}(r));}class fu{constructor(t,e){this.layers=t.readFields(du,{},e);}}function du(t,e,r){if(3===t){const t=new hu(r,r.readVarint()+r.pos);t.length&&(e[t.name]=t);}}const yu=Math.pow(2,13);function mu(t,e,r,n,i,s,a,o){t.emplaceBack(e,r,2*Math.floor(n*yu)+a,i*yu*2,s*yu*2,Math.round(o));}class gu{constructor(t){this.zoom=t.zoom,this.globalState=t.globalState,this.overscaling=t.overscaling,this.layers=t.layers,this.layerIds=this.layers.map((t=>t.id)),this.index=t.index,this.hasPattern=!1,this.layoutVertexArray=new Va,this.centroidVertexArray=new Ca,this.indexArray=new ja,this.programConfigurations=new bo(t.layers,t.zoom),this.segments=new Xa,this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){this.features=[],this.hasPattern=gl("fill-extrusion",this.layers,e);for(const{feature:n,id:i,index:s,sourceLayerIndex:a}of t){const t=this.layers[0]._featureFilter.needGeometry,o=Mo(n,t);if(!this.layers[0]._featureFilter.filter(new Is(this.zoom,{globalState:this.globalState}),o,r))continue;const l={id:i,sourceLayerIndex:a,index:s,geometry:t?o.geometry:ko(n),properties:n.properties,type:n.type,patterns:{}};this.hasPattern?this.features.push(xl("fill-extrusion",this.layers,l,this.zoom,e)):this.addFeature(l,l.geometry,s,r,{},e.subdivisionGranularity),e.featureIndex.insert(n,l.geometry,s,a,this.index,!0);}}addFeatures(t,e,r){for(const n of this.features){const{geometry:i}=n;this.addFeature(n,i,n.index,e,r,t.subdivisionGranularity);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,{imagePositions:r,globalState:this.globalState});}isEmpty(){return 0===this.layoutVertexArray.length&&0===this.centroidVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,ou),this.centroidVertexBuffer=t.createVertexBuffer(this.centroidVertexArray,au.members,!0),this.indexBuffer=t.createIndexBuffer(this.indexArray)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.programConfigurations.destroy(),this.segments.destroy(),this.centroidVertexBuffer.destroy());}addFeature(t,e,r,n,i,s){for(const r of Qr(e,500)){const e={x:0,y:0,sampleCount:0},i=this.layoutVertexArray.length;this.processPolygon(e,n,t,r,s);const a=this.layoutVertexArray.length-i,o=Math.floor(e.x/e.sampleCount),l=Math.floor(e.y/e.sampleCount);for(let t=0;t{mu(u,t,e,0,0,1,1,0);}),this.segments,this.layoutVertexArray,this.indexArray,l.verticesFlattened,l.indicesTriangles);}_generateSideFaces(t,e){let r=0;for(let n=1;nXa.MAX_VERTEX_ARRAY_LENGTH&&(e.segment=this.segments.prepareSegment(4,this.layoutVertexArray,this.indexArray));const a=i.sub(s)._perp()._unit(),o=s.dist(i);r+o>32768&&(r=0),mu(this.layoutVertexArray,i.x,i.y,a.x,a.y,0,0,r),mu(this.layoutVertexArray,i.x,i.y,a.x,a.y,0,1,r),r+=o,mu(this.layoutVertexArray,s.x,s.y,a.x,a.y,0,0,r),mu(this.layoutVertexArray,s.x,s.y,a.x,a.y,0,1,r);const l=e.segment.vertexLength;this.indexArray.emplaceBack(l,l+2,l+1),this.indexArray.emplaceBack(l+1,l+2,l+3),e.segment.vertexLength+=4,e.segment.primitiveLength+=2;}}}function xu(t,e){for(let r=0;rP)||t.y===e.y&&(t.y<0||t.y>P)}function bu(t){return t.every((t=>t.x<0))||t.every((t=>t.x>P))||t.every((t=>t.y<0))||t.every((t=>t.y>P))}let wu;as("FillExtrusionBucket",gu,{omit:["layers","features"]});var _u={get paint(){return wu=wu||new Us({"fill-extrusion-opacity":new $s(xt["paint_fill-extrusion"]["fill-extrusion-opacity"]),"fill-extrusion-color":new Ls(xt["paint_fill-extrusion"]["fill-extrusion-color"]),"fill-extrusion-translate":new $s(xt["paint_fill-extrusion"]["fill-extrusion-translate"]),"fill-extrusion-translate-anchor":new $s(xt["paint_fill-extrusion"]["fill-extrusion-translate-anchor"]),"fill-extrusion-pattern":new Os(xt["paint_fill-extrusion"]["fill-extrusion-pattern"]),"fill-extrusion-height":new Ls(xt["paint_fill-extrusion"]["fill-extrusion-height"]),"fill-extrusion-base":new Ls(xt["paint_fill-extrusion"]["fill-extrusion-base"]),"fill-extrusion-vertical-gradient":new $s(xt["paint_fill-extrusion"]["fill-extrusion-vertical-gradient"])})}};class Su extends Ns{constructor(t){super(t,_u);}createBucket(t){return new gu(t)}queryRadius(){return jo(this.paint.get("fill-extrusion-translate"))}is3D(){return !0}queryIntersectsFeature({queryGeometry:t,feature:e,featureState:n,geometry:i,transform:s,pixelsToTileUnits:a,pixelPosMatrix:o}){const l=No(t,this.paint.get("fill-extrusion-translate"),this.paint.get("fill-extrusion-translate-anchor"),-s.bearingInRadians,a),u=this.paint.get("fill-extrusion-height").evaluate(e,n),c=this.paint.get("fill-extrusion-base").evaluate(e,n),h=function(t,e){const n=[];for(const i of t){const t=[i.x,i.y,0,1];A(t,t,e),n.push(new r(t[0]/t[3],t[1]/t[3]));}return n}(l,o),p=function(t,e,n,i){const s=[],a=[],o=i[8]*e,l=i[9]*e,u=i[10]*e,c=i[11]*e,h=i[8]*n,p=i[9]*n,f=i[10]*n,d=i[11]*n;for(const e of t){const t=[],n=[];for(const s of e){const e=s.x,a=s.y,y=i[0]*e+i[4]*a+i[12],m=i[1]*e+i[5]*a+i[13],g=i[2]*e+i[6]*a+i[14],x=i[3]*e+i[7]*a+i[15],v=g+u,b=x+c,w=y+h,_=m+p,S=g+f,A=x+d,k=new r((y+o)/b,(m+l)/b);k.z=v/b,t.push(k);const M=new r(w/A,_/A);M.z=S/A,n.push(M);}s.push(t),a.push(n);}return [s,a]}(i,c,u,o);return function(t,e,r){let n=1/0;Bo(r,e)&&(n=ku(r,e[0]));for(let i=0;it.id)),this.index=t.index,this.hasPattern=!1,this.patternFeatures=[],this.lineClipsArray=[],this.gradients={},this.layers.forEach((t=>{this.gradients[t.id]={};})),this.layoutVertexArray=new Ta,this.layoutVertexArray2=new Fa,this.indexArray=new ja,this.programConfigurations=new bo(t.layers,t.zoom),this.segments=new Xa,this.maxLineLength=0,this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id));}populate(t,e,r){this.hasPattern=gl("line",this.layers,e);const n=this.layers[0].layout.get("line-sort-key"),i=!n.isConstant(),s=[];for(const{feature:e,id:a,index:o,sourceLayerIndex:l}of t){const t=this.layers[0]._featureFilter.needGeometry,u=Mo(e,t);if(!this.layers[0]._featureFilter.filter(new Is(this.zoom,{globalState:this.globalState}),u,r))continue;const c=i?n.evaluate(u,{},r):void 0,h={id:a,properties:e.properties,type:e.type,sourceLayerIndex:l,index:o,geometry:t?u.geometry:ko(e),patterns:{},sortKey:c};s.push(h);}i&&s.sort(((t,e)=>t.sortKey-e.sortKey));for(const n of s){const{geometry:i,index:s,sourceLayerIndex:a}=n;if(this.hasPattern){const t=xl("line",this.layers,n,this.zoom,e);this.patternFeatures.push(t);}else this.addFeature(n,i,s,r,{},e.subdivisionGranularity);e.featureIndex.insert(t[s].feature,i,s,a,this.index);}}update(t,e,r){this.stateDependentLayers.length&&this.programConfigurations.updatePaintArrays(t,e,this.stateDependentLayers,{imagePositions:r,globalState:this.globalState});}addFeatures(t,e,r){for(const n of this.patternFeatures)this.addFeature(n,n.geometry,n.index,e,r,t.subdivisionGranularity);}isEmpty(){return 0===this.layoutVertexArray.length}uploadPending(){return !this.uploaded||this.programConfigurations.needsUpload}upload(t){this.uploaded||(0!==this.layoutVertexArray2.length&&(this.layoutVertexBuffer2=t.createVertexBuffer(this.layoutVertexArray2,Pu)),this.layoutVertexBuffer=t.createVertexBuffer(this.layoutVertexArray,Iu),this.indexBuffer=t.createIndexBuffer(this.indexArray)),this.programConfigurations.upload(t),this.uploaded=!0;}destroy(){this.layoutVertexBuffer&&(this.layoutVertexBuffer.destroy(),this.indexBuffer.destroy(),this.programConfigurations.destroy(),this.segments.destroy());}lineFeatureClips(t){if(t.properties&&Object.prototype.hasOwnProperty.call(t.properties,"mapbox_clip_start")&&Object.prototype.hasOwnProperty.call(t.properties,"mapbox_clip_end"))return {start:+t.properties.mapbox_clip_start,end:+t.properties.mapbox_clip_end}}addFeature(t,e,r,n,i,s){const a=this.layers[0].layout,o=a.get("line-join").evaluate(t,{}),l=a.get("line-cap"),u=a.get("line-miter-limit"),c=a.get("line-round-limit");this.lineClips=this.lineFeatureClips(t);for(const r of e)this.addLine(r,t,o,l,u,c,n,s);this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length,t,r,{imagePositions:i,canonical:n,globalState:this.globalState});}addLine(t,e,r,n,i,s,a,o){if(this.distance=0,this.scaledDistance=0,this.totalDistance=0,t=Kl(t,a?o.line.getGranularityForZoomLevel(a.z):1),this.lineClips){this.lineClipsArray.push(this.lineClips);for(let e=0;e=2&&t[u-1].equals(t[u-2]);)u--;let c=0;for(;c0;if(w&&e>c){const t=f.dist(d);if(t>2*h){const e=f.sub(f.sub(d)._mult(h/t)._round());this.updateDistance(d,e),this.addCurrentVertex(e,m,0,0,p),d=e;}}const S=d&&y;let A=S?r:l?"butt":n;if(S&&"round"===A&&(vi&&(A="bevel"),"bevel"===A&&(v>2&&(A="flipbevel"),v100)a=g.mult(-1);else {const t=v*m.add(g).mag()/m.sub(g).mag();a._perp()._mult(t*(_?-1:1));}this.addCurrentVertex(f,a,0,0,p),this.addCurrentVertex(f,a.mult(-1),0,0,p);}else if("bevel"===A||"fakeround"===A){const t=-Math.sqrt(v*v-1),e=_?t:0,r=_?0:t;if(d&&this.addCurrentVertex(f,m,e,r,p),"fakeround"===A){const t=Math.round(180*b/Math.PI/20);for(let e=1;e2*h){const e=f.add(y.sub(f)._mult(h/t)._round());this.updateDistance(f,e),this.addCurrentVertex(e,g,0,0,p),f=e;}}}}addCurrentVertex(t,e,r,n,i,s=!1){const a=e.y*n-e.x,o=-e.y-e.x*n;this.addHalfVertex(t,e.x+e.y*r,e.y-e.x*r,s,!1,r,i),this.addHalfVertex(t,a,o,s,!0,-n,i),this.distance>Eu/2&&0===this.totalDistance&&(this.distance=0,this.updateScaledDistance(),this.addCurrentVertex(t,e,r,n,i,s));}addHalfVertex({x:t,y:e},r,n,i,s,a,o){const l=.5*(this.lineClips?this.scaledDistance*(Eu-1):this.scaledDistance);this.layoutVertexArray.emplaceBack((t<<1)+(i?1:0),(e<<1)+(s?1:0),Math.round(63*r)+128,Math.round(63*n)+128,1+(0===a?0:a<0?-1:1)|(63&l)<<2,l>>6),this.lineClips&&this.layoutVertexArray2.emplaceBack((this.scaledDistance-this.lineClips.start)/(this.lineClips.end-this.lineClips.start),this.lineClipsArray.length);const u=o.vertexLength++;this.e1>=0&&this.e2>=0&&(this.indexArray.emplaceBack(this.e1,u,this.e2),o.primitiveLength++),s?this.e2=u:this.e1=u;}updateScaledDistance(){this.scaledDistance=this.lineClips?this.lineClips.start+(this.lineClips.end-this.lineClips.start)*this.distance/this.totalDistance:this.distance;}updateDistance(t,e){this.distance+=t.dist(e),this.updateScaledDistance();}}let Vu,Tu;as("LineBucket",Bu,{omit:["layers","patternFeatures"]});var Fu={get paint(){return Tu=Tu||new Us({"line-opacity":new Ls(xt.paint_line["line-opacity"]),"line-color":new Ls(xt.paint_line["line-color"]),"line-translate":new $s(xt.paint_line["line-translate"]),"line-translate-anchor":new $s(xt.paint_line["line-translate-anchor"]),"line-width":new Ls(xt.paint_line["line-width"]),"line-gap-width":new Ls(xt.paint_line["line-gap-width"]),"line-offset":new Ls(xt.paint_line["line-offset"]),"line-blur":new Ls(xt.paint_line["line-blur"]),"line-dasharray":new Ds(xt.paint_line["line-dasharray"]),"line-pattern":new Os(xt.paint_line["line-pattern"]),"line-gradient":new Rs(xt.paint_line["line-gradient"])})},get layout(){return Vu=Vu||new Us({"line-cap":new $s(xt.layout_line["line-cap"]),"line-join":new Ls(xt.layout_line["line-join"]),"line-miter-limit":new $s(xt.layout_line["line-miter-limit"]),"line-round-limit":new $s(xt.layout_line["line-round-limit"]),"line-sort-key":new Ls(xt.layout_line["line-sort-key"])})}};class $u extends Ls{possiblyEvaluate(t,e){return e=new Is(Math.floor(e.zoom),{now:e.now,fadeDuration:e.fadeDuration,zoomHistory:e.zoomHistory,transition:e.transition}),super.possiblyEvaluate(t,e)}evaluate(t,e,r,n){return e=O({},e,{zoom:Math.floor(e.zoom)}),super.evaluate(t,e,r,n)}}let Lu;class Ou extends Ns{constructor(t){super(t,Fu),this.gradientVersion=0,Lu||(Lu=new $u(Fu.paint.properties["line-width"].specification),Lu.useIntegerZoom=!0);}_handleSpecialPaintPropertyUpdate(t){if("line-gradient"===t){const t=this.gradientExpression();this.stepInterpolant=!!function(t){return void 0!==t._styleExpression}(t)&&t._styleExpression.expression instanceof ar,this.gradientVersion=(this.gradientVersion+1)%Number.MAX_SAFE_INTEGER;}}gradientExpression(){return this._transitionablePaint._values["line-gradient"].value.expression}recalculate(t,e){super.recalculate(t,e),this.paint._values["line-floorwidth"]=Lu.possiblyEvaluate(this._transitioningPaint._values["line-width"].value,t);}createBucket(t){return new Bu(t)}queryRadius(t){const e=t,r=Du(Uo("line-width",this,e),Uo("line-gap-width",this,e)),n=Uo("line-offset",this,e);return r/2+Math.abs(n)+jo(this.paint.get("line-translate"))}queryIntersectsFeature({queryGeometry:t,feature:e,featureState:n,geometry:i,transform:s,pixelsToTileUnits:a}){const o=No(t,this.paint.get("line-translate"),this.paint.get("line-translate-anchor"),-s.bearingInRadians,a),l=a/2*Du(this.paint.get("line-width").evaluate(e,n),this.paint.get("line-gap-width").evaluate(e,n)),u=this.paint.get("line-offset").evaluate(e,n);return u&&(i=function(t,e){const n=[];for(let i=0;i=3)for(let e=0;e0?e+2*t:t}const Ru=Zs([{name:"a_pos_offset",components:4,type:"Int16"},{name:"a_data",components:4,type:"Uint16"},{name:"a_pixeloffset",components:4,type:"Int16"}],4),Uu=Zs([{name:"a_projected_pos",components:3,type:"Float32"}],4);Zs([{name:"a_fade_opacity",components:1,type:"Uint32"}],4);const ju=Zs([{name:"a_placed",components:2,type:"Uint8"},{name:"a_shift",components:2,type:"Float32"},{name:"a_box_real",components:2,type:"Int16"}]);Zs([{type:"Int16",name:"anchorPointX"},{type:"Int16",name:"anchorPointY"},{type:"Int16",name:"x1"},{type:"Int16",name:"y1"},{type:"Int16",name:"x2"},{type:"Int16",name:"y2"},{type:"Uint32",name:"featureIndex"},{type:"Uint16",name:"sourceLayerIndex"},{type:"Uint16",name:"bucketIndex"}]);const Nu=Zs([{name:"a_pos",components:2,type:"Int16"},{name:"a_anchor_pos",components:2,type:"Int16"},{name:"a_extrude",components:2,type:"Int16"}],4),qu=Zs([{name:"a_pos",components:2,type:"Float32"},{name:"a_radius",components:1,type:"Float32"},{name:"a_flags",components:2,type:"Int16"}],4);function Gu(t,e,r){return t.sections.forEach((t=>{t.text=function(t,e,r){const n=e.layout.get("text-transform").evaluate(r,{});return "uppercase"===n?t=t.toLocaleUpperCase():"lowercase"===n&&(t=t.toLocaleLowerCase()),Ms.applyArabicShaping&&(t=Ms.applyArabicShaping(t)),t}(t.text,e,r);})),t}Zs([{name:"triangle",components:3,type:"Uint16"}]),Zs([{type:"Int16",name:"anchorX"},{type:"Int16",name:"anchorY"},{type:"Uint16",name:"glyphStartIndex"},{type:"Uint16",name:"numGlyphs"},{type:"Uint32",name:"vertexStartIndex"},{type:"Uint32",name:"lineStartIndex"},{type:"Uint32",name:"lineLength"},{type:"Uint16",name:"segment"},{type:"Uint16",name:"lowerSize"},{type:"Uint16",name:"upperSize"},{type:"Float32",name:"lineOffsetX"},{type:"Float32",name:"lineOffsetY"},{type:"Uint8",name:"writingMode"},{type:"Uint8",name:"placedOrientation"},{type:"Uint8",name:"hidden"},{type:"Uint32",name:"crossTileID"},{type:"Int16",name:"associatedIconIndex"}]),Zs([{type:"Int16",name:"anchorX"},{type:"Int16",name:"anchorY"},{type:"Int16",name:"rightJustifiedTextSymbolIndex"},{type:"Int16",name:"centerJustifiedTextSymbolIndex"},{type:"Int16",name:"leftJustifiedTextSymbolIndex"},{type:"Int16",name:"verticalPlacedTextSymbolIndex"},{type:"Int16",name:"placedIconSymbolIndex"},{type:"Int16",name:"verticalPlacedIconSymbolIndex"},{type:"Uint16",name:"key"},{type:"Uint16",name:"textBoxStartIndex"},{type:"Uint16",name:"textBoxEndIndex"},{type:"Uint16",name:"verticalTextBoxStartIndex"},{type:"Uint16",name:"verticalTextBoxEndIndex"},{type:"Uint16",name:"iconBoxStartIndex"},{type:"Uint16",name:"iconBoxEndIndex"},{type:"Uint16",name:"verticalIconBoxStartIndex"},{type:"Uint16",name:"verticalIconBoxEndIndex"},{type:"Uint16",name:"featureIndex"},{type:"Uint16",name:"numHorizontalGlyphVertices"},{type:"Uint16",name:"numVerticalGlyphVertices"},{type:"Uint16",name:"numIconVertices"},{type:"Uint16",name:"numVerticalIconVertices"},{type:"Uint16",name:"useRuntimeCollisionCircles"},{type:"Uint32",name:"crossTileID"},{type:"Float32",name:"textBoxScale"},{type:"Float32",name:"collisionCircleDiameter"},{type:"Uint16",name:"textAnchorOffsetStartIndex"},{type:"Uint16",name:"textAnchorOffsetEndIndex"}]),Zs([{type:"Float32",name:"offsetX"}]),Zs([{type:"Int16",name:"x"},{type:"Int16",name:"y"},{type:"Int16",name:"tileUnitDistanceFromAnchor"}]),Zs([{type:"Uint16",name:"textAnchor"},{type:"Float32",components:2,name:"textOffset"}]);const Xu={"!":"︕","#":"#",$:"$","%":"%","&":"&","(":"︵",")":"︶","*":"*","+":"+",",":"︐","-":"︲",".":"・","/":"/",":":"︓",";":"︔","<":"︿","=":"=",">":"﹀","?":"︖","@":"@","[":"﹇","\\":"\","]":"﹈","^":"^",_:"︳","`":"`","{":"︷","|":"―","}":"︸","~":"~","¢":"¢","£":"£","¥":"¥","¦":"¦","¬":"¬","¯":" ̄","–":"︲","—":"︱","‘":"﹃","’":"﹄","“":"﹁","”":"﹂","…":"︙","‧":"・","₩":"₩","、":"︑","。":"︒","〈":"︿","〉":"﹀","《":"︽","》":"︾","「":"﹁","」":"﹂","『":"﹃","』":"﹄","【":"︻","】":"︼","〔":"︹","〕":"︺","〖":"︗","〗":"︘","!":"︕","(":"︵",")":"︶",",":"︐","-":"︲",".":"・",":":"︓",";":"︔","<":"︿",">":"﹀","?":"︖","[":"﹇","]":"﹈","_":"︳","{":"︷","|":"―","}":"︸","⦅":"︵","⦆":"︶","。":"︒","「":"﹁","」":"﹂"};var Zu=24;const Yu=4294967296,Hu=1/Yu,Ku="undefined"==typeof TextDecoder?null:new TextDecoder("utf-8");class Ju{constructor(t=new Uint8Array(16)){this.buf=ArrayBuffer.isView(t)?t:new Uint8Array(t),this.dataView=new DataView(this.buf.buffer),this.pos=0,this.type=0,this.length=this.buf.length;}readFields(t,e,r=this.length){for(;this.pos>3,i=this.pos;this.type=7&r,t(n,e,this),this.pos===i&&this.skip(r);}return e}readMessage(t,e){return this.readFields(t,e,this.readVarint()+this.pos)}readFixed32(){const t=this.dataView.getUint32(this.pos,!0);return this.pos+=4,t}readSFixed32(){const t=this.dataView.getInt32(this.pos,!0);return this.pos+=4,t}readFixed64(){const t=this.dataView.getUint32(this.pos,!0)+this.dataView.getUint32(this.pos+4,!0)*Yu;return this.pos+=8,t}readSFixed64(){const t=this.dataView.getUint32(this.pos,!0)+this.dataView.getInt32(this.pos+4,!0)*Yu;return this.pos+=8,t}readFloat(){const t=this.dataView.getFloat32(this.pos,!0);return this.pos+=4,t}readDouble(){const t=this.dataView.getFloat64(this.pos,!0);return this.pos+=8,t}readVarint(t){const e=this.buf;let r,n;return n=e[this.pos++],r=127&n,n<128?r:(n=e[this.pos++],r|=(127&n)<<7,n<128?r:(n=e[this.pos++],r|=(127&n)<<14,n<128?r:(n=e[this.pos++],r|=(127&n)<<21,n<128?r:(n=e[this.pos],r|=(15&n)<<28,function(t,e,r){const n=r.buf;let i,s;if(s=n[r.pos++],i=(112&s)>>4,s<128)return Wu(t,i,e);if(s=n[r.pos++],i|=(127&s)<<3,s<128)return Wu(t,i,e);if(s=n[r.pos++],i|=(127&s)<<10,s<128)return Wu(t,i,e);if(s=n[r.pos++],i|=(127&s)<<17,s<128)return Wu(t,i,e);if(s=n[r.pos++],i|=(127&s)<<24,s<128)return Wu(t,i,e);if(s=n[r.pos++],i|=(1&s)<<31,s<128)return Wu(t,i,e);throw new Error("Expected varint not more than 10 bytes")}(r,t,this)))))}readVarint64(){return this.readVarint(!0)}readSVarint(){const t=this.readVarint();return t%2==1?(t+1)/-2:t/2}readBoolean(){return Boolean(this.readVarint())}readString(){const t=this.readVarint()+this.pos,e=this.pos;return this.pos=t,t-e>=12&&Ku?Ku.decode(this.buf.subarray(e,t)):function(t,e,r){let n="",i=e;for(;i239?4:e>223?3:e>191?2:1;if(i+u>r)break;1===u?e<128&&(l=e):2===u?(s=t[i+1],128==(192&s)&&(l=(31&e)<<6|63&s,l<=127&&(l=null))):3===u?(s=t[i+1],a=t[i+2],128==(192&s)&&128==(192&a)&&(l=(15&e)<<12|(63&s)<<6|63&a,(l<=2047||l>=55296&&l<=57343)&&(l=null))):4===u&&(s=t[i+1],a=t[i+2],o=t[i+3],128==(192&s)&&128==(192&a)&&128==(192&o)&&(l=(15&e)<<18|(63&s)<<12|(63&a)<<6|63&o,(l<=65535||l>=1114112)&&(l=null))),null===l?(l=65533,u=1):l>65535&&(l-=65536,n+=String.fromCharCode(l>>>10&1023|55296),l=56320|1023&l),n+=String.fromCharCode(l),i+=u;}return n}(this.buf,e,t)}readBytes(){const t=this.readVarint()+this.pos,e=this.buf.subarray(this.pos,t);return this.pos=t,e}readPackedVarint(t=[],e){const r=this.readPackedEnd();for(;this.pos127;);else if(2===e)this.pos=this.readVarint()+this.pos;else if(5===e)this.pos+=4;else {if(1!==e)throw new Error(`Unimplemented type: ${e}`);this.pos+=8;}}writeTag(t,e){this.writeVarint(t<<3|e);}realloc(t){let e=this.length||16;for(;e268435455||t<0?function(t,e){let r,n;if(t>=0?(r=t%4294967296|0,n=t/4294967296|0):(r=~(-t%4294967296),n=~(-t/4294967296),4294967295^r?r=r+1|0:(r=0,n=n+1|0)),t>=0x10000000000000000||t<-0x10000000000000000)throw new Error("Given varint doesn't fit into 10 bytes");e.realloc(10),function(t,e,r){r.buf[r.pos++]=127&t|128,t>>>=7,r.buf[r.pos++]=127&t|128,t>>>=7,r.buf[r.pos++]=127&t|128,t>>>=7,r.buf[r.pos++]=127&t|128,r.buf[r.pos]=127&(t>>>=7);}(r,0,e),function(t,e){const r=(7&t)<<4;e.buf[e.pos++]|=r|((t>>>=3)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),t&&(e.buf[e.pos++]=127&t)))));}(n,e);}(t,this):(this.realloc(4),this.buf[this.pos++]=127&t|(t>127?128:0),t<=127||(this.buf[this.pos++]=127&(t>>>=7)|(t>127?128:0),t<=127||(this.buf[this.pos++]=127&(t>>>=7)|(t>127?128:0),t<=127||(this.buf[this.pos++]=t>>>7&127))));}writeSVarint(t){this.writeVarint(t<0?2*-t-1:2*t);}writeBoolean(t){this.writeVarint(+t);}writeString(t){t=String(t),this.realloc(4*t.length),this.pos++;const e=this.pos;this.pos=function(t,e,r){for(let n,i,s=0;s55295&&n<57344){if(!i){n>56319||s+1===e.length?(t[r++]=239,t[r++]=191,t[r++]=189):i=n;continue}if(n<56320){t[r++]=239,t[r++]=191,t[r++]=189,i=n;continue}n=i-55296<<10|n-56320|65536,i=null;}else i&&(t[r++]=239,t[r++]=191,t[r++]=189,i=null);n<128?t[r++]=n:(n<2048?t[r++]=n>>6|192:(n<65536?t[r++]=n>>12|224:(t[r++]=n>>18|240,t[r++]=n>>12&63|128),t[r++]=n>>6&63|128),t[r++]=63&n|128);}return r}(this.buf,t,this.pos);const r=this.pos-e;r>=128&&Qu(e,r,this),this.pos=e-1,this.writeVarint(r),this.pos+=r;}writeFloat(t){this.realloc(4),this.dataView.setFloat32(this.pos,t,!0),this.pos+=4;}writeDouble(t){this.realloc(8),this.dataView.setFloat64(this.pos,t,!0),this.pos+=8;}writeBytes(t){const e=t.length;this.writeVarint(e),this.realloc(e);for(let r=0;r=128&&Qu(r,n,this),this.pos=r-1,this.writeVarint(n),this.pos+=n;}writeMessage(t,e,r){this.writeTag(t,2),this.writeRawMessage(e,r);}writePackedVarint(t,e){e.length&&this.writeMessage(t,tc,e);}writePackedSVarint(t,e){e.length&&this.writeMessage(t,ec,e);}writePackedBoolean(t,e){e.length&&this.writeMessage(t,ic,e);}writePackedFloat(t,e){e.length&&this.writeMessage(t,rc,e);}writePackedDouble(t,e){e.length&&this.writeMessage(t,nc,e);}writePackedFixed32(t,e){e.length&&this.writeMessage(t,sc,e);}writePackedSFixed32(t,e){e.length&&this.writeMessage(t,ac,e);}writePackedFixed64(t,e){e.length&&this.writeMessage(t,oc,e);}writePackedSFixed64(t,e){e.length&&this.writeMessage(t,lc,e);}writeBytesField(t,e){this.writeTag(t,2),this.writeBytes(e);}writeFixed32Field(t,e){this.writeTag(t,5),this.writeFixed32(e);}writeSFixed32Field(t,e){this.writeTag(t,5),this.writeSFixed32(e);}writeFixed64Field(t,e){this.writeTag(t,1),this.writeFixed64(e);}writeSFixed64Field(t,e){this.writeTag(t,1),this.writeSFixed64(e);}writeVarintField(t,e){this.writeTag(t,0),this.writeVarint(e);}writeSVarintField(t,e){this.writeTag(t,0),this.writeSVarint(e);}writeStringField(t,e){this.writeTag(t,2),this.writeString(e);}writeFloatField(t,e){this.writeTag(t,5),this.writeFloat(e);}writeDoubleField(t,e){this.writeTag(t,1),this.writeDouble(e);}writeBooleanField(t,e){this.writeVarintField(t,+e);}}function Wu(t,e,r){return r?4294967296*e+(t>>>0):4294967296*(e>>>0)+(t>>>0)}function Qu(t,e,r){const n=e<=16383?1:e<=2097151?2:e<=268435455?3:Math.floor(Math.log(e)/(7*Math.LN2));r.realloc(n);for(let e=r.pos-1;e>=t;e--)r.buf[e+n]=r.buf[e];}function tc(t,e){for(let r=0;re.h-t.h));const n=[{x:0,y:0,w:Math.max(Math.ceil(Math.sqrt(e/.95)),r),h:1/0}];let i=0,s=0;for(const e of t)for(let t=n.length-1;t>=0;t--){const r=n[t];if(!(e.w>r.w||e.h>r.h)){if(e.x=r.x,e.y=r.y,s=Math.max(s,e.y+e.h),i=Math.max(i,e.x+e.w),e.w===r.w&&e.h===r.h){const e=n.pop();e&&t=0&&r>=t&&vc[this.text.charCodeAt(r)];r--)e--;this.text=this.text.substring(t,e),this.sectionIndex=this.sectionIndex.slice(t,e);}substring(t,e){const r=new gc;return r.text=this.text.substring(t,e),r.sectionIndex=this.sectionIndex.slice(t,e),r.sections=this.sections,r}toString(){return this.text}getMaxScale(){return this.sectionIndex.reduce(((t,e)=>Math.max(t,this.sections[e].scale)),0)}getMaxImageSize(t){let e=0,r=0;for(let n=0;n=63743?null:++this.imageSectionID:(this.imageSectionID=57344,this.imageSectionID)}}function xc(e,r,n,i,s,a,o,l,u,c,h,p,f,d,y){const m=gc.fromFeature(e,s);let g;p===t.ao.vertical&&m.verticalizePunctuation();const{processBidirectionalText:x,processStyledBidirectionalText:v}=Ms;if(x&&1===m.sections.length){g=[];const t=x(m.toString(),Ic(m,c,a,r,i,d));for(const e of t){const t=new gc;t.text=e,t.sections=m.sections;for(let r=0;r=0;let u=0;for(let r=0;ru){const t=Math.ceil(s/u);i*=t/a,a=t;}return {x1:n,y1:i,x2:n+s,y2:i+a}}function Lc(t,e,r,n,i,s){const a=t.image;let o;if(a.content){const t=a.content,e=a.pixelRatio||1;o=[t[0]/e,t[1]/e,a.displaySize[0]-t[2]/e,a.displaySize[1]-t[3]/e];}const l=e.left*s,u=e.right*s;let c,h,p,f;"width"===r||"both"===r?(f=i[0]+l-n[3],h=i[0]+u+n[1]):(f=i[0]+(l+u-a.displaySize[0])/2,h=f+a.displaySize[0]);const d=e.top*s,y=e.bottom*s;return "height"===r||"both"===r?(c=i[1]+d-n[0],p=i[1]+y+n[2]):(c=i[1]+(d+y-a.displaySize[1])/2,p=c+a.displaySize[1]),{image:a,top:c,right:h,bottom:p,left:f,collisionPadding:o}}const Oc=128,Dc=32640;function Rc(t,e){const{expression:r}=e;if("constant"===r.kind)return {kind:"constant",layoutSize:r.evaluate(new Is(t+1))};if("source"===r.kind)return {kind:"source"};{const{zoomStops:e,interpolationType:n}=r;let i=0;for(;it.id)),this.index=e.index,this.pixelRatio=e.pixelRatio,this.sourceLayerIndex=e.sourceLayerIndex,this.hasPattern=!1,this.hasRTLText=!1,this.sortKeyRanges=[],this.collisionCircleArray=[];const r=this.layers[0]._unevaluatedLayout._values;this.textSizeData=Rc(this.zoom,r["text-size"]),this.iconSizeData=Rc(this.zoom,r["icon-size"]);const n=this.layers[0].layout,i=n.get("symbol-sort-key"),s=n.get("symbol-z-order");this.canOverlap="never"!==Uc(n,"text-overlap","text-allow-overlap")||"never"!==Uc(n,"icon-overlap","icon-allow-overlap")||n.get("text-ignore-placement")||n.get("icon-ignore-placement"),this.sortFeaturesByKey="viewport-y"!==s&&!i.isConstant(),this.sortFeaturesByY=("viewport-y"===s||"auto"===s&&!this.sortFeaturesByKey)&&this.canOverlap,"point"===n.get("symbol-placement")&&(this.writingModes=n.get("text-writing-mode").map((e=>t.ao[e]))),this.stateDependentLayerIds=this.layers.filter((t=>t.isStateDependent())).map((t=>t.id)),this.sourceID=e.sourceID;}createArrays(){this.text=new Xc(new bo(this.layers,this.zoom,(t=>/^text/.test(t)))),this.icon=new Xc(new bo(this.layers,this.zoom,(t=>/^icon/.test(t)))),this.glyphOffsetArray=new Aa,this.lineVertexArray=new ka,this.symbolInstances=new Sa,this.textAnchorOffsets=new Ia;}calculateGlyphDependencies(t,e,r,n,i){for(let s=0;s0)&&("constant"!==a.value.kind||a.value.value.length>0),c="constant"!==l.value.kind||!!l.value.value||Object.keys(l.parameters).length>0,h=s.get("symbol-sort-key");if(this.features=[],!u&&!c)return;const p=r.iconDependencies,f=r.glyphDependencies,d=r.availableImages,y=new Is(this.zoom,{globalState:this.globalState});for(const{feature:r,id:o,index:l,sourceLayerIndex:m}of e){const e=i._featureFilter.needGeometry,g=Mo(r,e);if(!i._featureFilter.filter(y,g,n))continue;let x,v;if(e||(g.geometry=ko(r)),u){const t=i.getValueAndResolveTokens("text-field",g,n,d),e=Ce.factory(t),r=this.hasRTLText=this.hasRTLText||Gc(e);(!r||"unavailable"===Ms.getRTLTextPluginStatus()||r&&Ms.isParsed())&&(x=Gu(e,i,g));}if(c){const t=i.getValueAndResolveTokens("icon-image",g,n,d);v=t instanceof Le?t:Le.fromString(t);}if(!x&&!v)continue;const b=this.sortFeaturesByKey?h.evaluate(g,{},n):void 0;if(this.features.push({id:o,text:x,icon:v,index:l,sourceLayerIndex:m,geometry:g.geometry,properties:r.properties,type:lu.types[r.type],sortKey:b}),v&&(p[v.name]=!0),x){const e=a.evaluate(g,{},n).join(","),r="viewport"!==s.get("text-rotation-alignment")&&"point"!==s.get("symbol-placement");this.allowVerticalPlacement=this.writingModes&&this.writingModes.indexOf(t.ao.vertical)>=0;for(const t of x.sections)if(t.image)p[t.image.name]=!0;else {const n=ds(x.toString()),i=t.fontStack||e,s=f[i]=f[i]||{};this.calculateGlyphDependencies(t.text,s,r,this.allowVerticalPlacement,n);}}}"line"===s.get("symbol-placement")&&(this.features=function(t){const e={},r={},n=[];let i=0;function s(e){n.push(t[e]),i++;}function a(t,e,i){const s=r[t];return delete r[t],r[e]=s,n[s].geometry[0].pop(),n[s].geometry[0]=n[s].geometry[0].concat(i[0]),s}function o(t,r,i){const s=e[r];return delete e[r],e[t]=s,n[s].geometry[0].shift(),n[s].geometry[0]=i[0].concat(n[s].geometry[0]),s}function l(t,e,r){const n=r?e[0][e[0].length-1]:e[0][0];return `${t}:${n.x}:${n.y}`}for(let u=0;ut.geometry))}(this.features)),this.sortFeaturesByKey&&this.features.sort(((t,e)=>t.sortKey-e.sortKey));}update(t,e,r){this.stateDependentLayers.length&&(this.text.programConfigurations.updatePaintArrays(t,e,this.layers,{imagePositions:r,globalState:this.globalState}),this.icon.programConfigurations.updatePaintArrays(t,e,this.layers,{imagePositions:r,globalState:this.globalState}));}isEmpty(){return 0===this.symbolInstances.length&&!this.hasRTLText}uploadPending(){return !this.uploaded||this.text.programConfigurations.needsUpload||this.icon.programConfigurations.needsUpload}upload(t){!this.uploaded&&this.hasDebugData()&&(this.textCollisionBox.upload(t),this.iconCollisionBox.upload(t)),this.text.upload(t,this.sortFeaturesByY,!this.uploaded,this.text.programConfigurations.needsUpload),this.icon.upload(t,this.sortFeaturesByY,!this.uploaded,this.icon.programConfigurations.needsUpload),this.uploaded=!0;}destroyDebugData(){this.textCollisionBox.destroy(),this.iconCollisionBox.destroy();}destroy(){this.text.destroy(),this.icon.destroy(),this.hasDebugData()&&this.destroyDebugData();}addToLineVertexArray(t,e){const r=this.lineVertexArray.length;if(void 0!==t.segment){let r=t.dist(e[t.segment+1]),n=t.dist(e[t.segment]);const i={};for(let n=t.segment+1;n=0;r--)i[r]={x:e[r].x,y:e[r].y,tileUnitDistanceFromAnchor:n},r>0&&(n+=e[r-1].dist(e[r]));for(let t=0;t0}hasIconData(){return this.icon.segments.get().length>0}hasDebugData(){return this.textCollisionBox&&this.iconCollisionBox}hasTextCollisionBoxData(){return this.hasDebugData()&&this.textCollisionBox.segments.get().length>0}hasIconCollisionBoxData(){return this.hasDebugData()&&this.iconCollisionBox.segments.get().length>0}addIndicesForPlacedSymbol(t,e){const r=t.placedSymbolArray.get(e),n=r.vertexStartIndex+4*r.numGlyphs;for(let e=r.vertexStartIndex;en[t]-n[e]||i[e]-i[t])),s}addToSortKeyRanges(t,e){const r=this.sortKeyRanges[this.sortKeyRanges.length-1];r&&r.sortKey===e?r.symbolInstanceEnd=t+1:this.sortKeyRanges.push({sortKey:e,symbolInstanceStart:t,symbolInstanceEnd:t+1});}sortFeatures(t){if(this.sortFeaturesByY&&this.sortedAngle!==t&&!(this.text.segments.get().length>1||this.icon.segments.get().length>1)){this.symbolInstanceIndexes=this.getSortedSymbolIndexes(t),this.sortedAngle=t,this.text.indexArray.clear(),this.icon.indexArray.clear(),this.featureSortOrder=[];for(const t of this.symbolInstanceIndexes){const e=this.symbolInstances.get(t);this.featureSortOrder.push(e.featureIndex),[e.rightJustifiedTextSymbolIndex,e.centerJustifiedTextSymbolIndex,e.leftJustifiedTextSymbolIndex].forEach(((t,e,r)=>{t>=0&&r.indexOf(t)===e&&this.addIndicesForPlacedSymbol(this.text,t);})),e.verticalPlacedTextSymbolIndex>=0&&this.addIndicesForPlacedSymbol(this.text,e.verticalPlacedTextSymbolIndex),e.placedIconSymbolIndex>=0&&this.addIndicesForPlacedSymbol(this.icon,e.placedIconSymbolIndex),e.verticalPlacedIconSymbolIndex>=0&&this.addIndicesForPlacedSymbol(this.icon,e.verticalPlacedIconSymbolIndex);}this.text.indexBuffer&&this.text.indexBuffer.updateData(this.text.indexArray),this.icon.indexBuffer&&this.icon.indexBuffer.updateData(this.icon.indexArray);}}}let Hc,Kc;as("SymbolBucket",Yc,{omit:["layers","collisionBoxArray","features","compareText"]}),Yc.MAX_GLYPHS=65535,Yc.addDynamicAttributes=qc;var Jc={get paint(){return Kc=Kc||new Us({"icon-opacity":new Ls(xt.paint_symbol["icon-opacity"]),"icon-color":new Ls(xt.paint_symbol["icon-color"]),"icon-halo-color":new Ls(xt.paint_symbol["icon-halo-color"]),"icon-halo-width":new Ls(xt.paint_symbol["icon-halo-width"]),"icon-halo-blur":new Ls(xt.paint_symbol["icon-halo-blur"]),"icon-translate":new $s(xt.paint_symbol["icon-translate"]),"icon-translate-anchor":new $s(xt.paint_symbol["icon-translate-anchor"]),"text-opacity":new Ls(xt.paint_symbol["text-opacity"]),"text-color":new Ls(xt.paint_symbol["text-color"],{runtimeType:Ot,getOverride:t=>t.textColor,hasOverride:t=>!!t.textColor}),"text-halo-color":new Ls(xt.paint_symbol["text-halo-color"]),"text-halo-width":new Ls(xt.paint_symbol["text-halo-width"]),"text-halo-blur":new Ls(xt.paint_symbol["text-halo-blur"]),"text-translate":new $s(xt.paint_symbol["text-translate"]),"text-translate-anchor":new $s(xt.paint_symbol["text-translate-anchor"])})},get layout(){return Hc=Hc||new Us({"symbol-placement":new $s(xt.layout_symbol["symbol-placement"]),"symbol-spacing":new $s(xt.layout_symbol["symbol-spacing"]),"symbol-avoid-edges":new $s(xt.layout_symbol["symbol-avoid-edges"]),"symbol-sort-key":new Ls(xt.layout_symbol["symbol-sort-key"]),"symbol-z-order":new $s(xt.layout_symbol["symbol-z-order"]),"icon-allow-overlap":new $s(xt.layout_symbol["icon-allow-overlap"]),"icon-overlap":new $s(xt.layout_symbol["icon-overlap"]),"icon-ignore-placement":new $s(xt.layout_symbol["icon-ignore-placement"]),"icon-optional":new $s(xt.layout_symbol["icon-optional"]),"icon-rotation-alignment":new $s(xt.layout_symbol["icon-rotation-alignment"]),"icon-size":new Ls(xt.layout_symbol["icon-size"]),"icon-text-fit":new $s(xt.layout_symbol["icon-text-fit"]),"icon-text-fit-padding":new $s(xt.layout_symbol["icon-text-fit-padding"]),"icon-image":new Ls(xt.layout_symbol["icon-image"]),"icon-rotate":new Ls(xt.layout_symbol["icon-rotate"]),"icon-padding":new Ls(xt.layout_symbol["icon-padding"]),"icon-keep-upright":new $s(xt.layout_symbol["icon-keep-upright"]),"icon-offset":new Ls(xt.layout_symbol["icon-offset"]),"icon-anchor":new Ls(xt.layout_symbol["icon-anchor"]),"icon-pitch-alignment":new $s(xt.layout_symbol["icon-pitch-alignment"]),"text-pitch-alignment":new $s(xt.layout_symbol["text-pitch-alignment"]),"text-rotation-alignment":new $s(xt.layout_symbol["text-rotation-alignment"]),"text-field":new Ls(xt.layout_symbol["text-field"]),"text-font":new Ls(xt.layout_symbol["text-font"]),"text-size":new Ls(xt.layout_symbol["text-size"]),"text-max-width":new Ls(xt.layout_symbol["text-max-width"]),"text-line-height":new $s(xt.layout_symbol["text-line-height"]),"text-letter-spacing":new Ls(xt.layout_symbol["text-letter-spacing"]),"text-justify":new Ls(xt.layout_symbol["text-justify"]),"text-radial-offset":new Ls(xt.layout_symbol["text-radial-offset"]),"text-variable-anchor":new $s(xt.layout_symbol["text-variable-anchor"]),"text-variable-anchor-offset":new Ls(xt.layout_symbol["text-variable-anchor-offset"]),"text-anchor":new Ls(xt.layout_symbol["text-anchor"]),"text-max-angle":new $s(xt.layout_symbol["text-max-angle"]),"text-writing-mode":new $s(xt.layout_symbol["text-writing-mode"]),"text-rotate":new Ls(xt.layout_symbol["text-rotate"]),"text-padding":new $s(xt.layout_symbol["text-padding"]),"text-keep-upright":new $s(xt.layout_symbol["text-keep-upright"]),"text-transform":new Ls(xt.layout_symbol["text-transform"]),"text-offset":new Ls(xt.layout_symbol["text-offset"]),"text-allow-overlap":new $s(xt.layout_symbol["text-allow-overlap"]),"text-overlap":new $s(xt.layout_symbol["text-overlap"]),"text-ignore-placement":new $s(xt.layout_symbol["text-ignore-placement"]),"text-optional":new $s(xt.layout_symbol["text-optional"])})}};class Wc{constructor(t){if(void 0===t.property.overrides)throw new Error("overrides must be provided to instantiate FormatSectionOverride class");this.type=t.property.overrides?t.property.overrides.runtimeType:Tt,this.defaultValue=t;}evaluate(t){if(t.formattedSection){const e=this.defaultValue.property.overrides;if(e&&e.hasOverride(t.formattedSection))return e.getOverride(t.formattedSection)}return t.feature&&t.featureState?this.defaultValue.evaluate(t.feature,t.featureState):this.defaultValue.property.specification.default}eachChild(t){this.defaultValue.isConstant()||t(this.defaultValue.value._styleExpression.expression);}outputDefined(){return !1}serialize(){return null}}as("FormatSectionOverride",Wc,{omit:["defaultValue"]});class Qc extends Ns{constructor(t){super(t,Jc);}recalculate(t,e){if(super.recalculate(t,e),"auto"===this.layout.get("icon-rotation-alignment")&&(this.layout._values["icon-rotation-alignment"]="point"!==this.layout.get("symbol-placement")?"map":"viewport"),"auto"===this.layout.get("text-rotation-alignment")&&(this.layout._values["text-rotation-alignment"]="point"!==this.layout.get("symbol-placement")?"map":"viewport"),"auto"===this.layout.get("text-pitch-alignment")&&(this.layout._values["text-pitch-alignment"]="map"===this.layout.get("text-rotation-alignment")?"map":"viewport"),"auto"===this.layout.get("icon-pitch-alignment")&&(this.layout._values["icon-pitch-alignment"]=this.layout.get("icon-rotation-alignment")),"point"===this.layout.get("symbol-placement")){const t=this.layout.get("text-writing-mode");if(t){const e=[];for(const r of t)e.indexOf(r)<0&&e.push(r);this.layout._values["text-writing-mode"]=e;}else this.layout._values["text-writing-mode"]=["horizontal"];}this._setPaintOverrides();}getValueAndResolveTokens(t,e,r,n){const i=this.layout.get(t).evaluate(e,{},r,n),s=this._unevaluatedLayout._values[t];return s.isDataDriven()||ei(s.value)||!i?i:function(t,e){return e.replace(/{([^{}]+)}/g,((e,r)=>t&&r in t?String(t[r]):""))}(e.properties,i)}createBucket(t){return new Yc(t)}queryRadius(){return 0}queryIntersectsFeature(){throw new Error("Should take a different path in FeatureIndex")}_setPaintOverrides(){for(const t of Jc.paint.overridableProperties){if(!Qc.hasPaintOverride(this.layout,t))continue;const e=this.paint.get(t),r=new Wc(e),n=new ti(r,e.property.specification);let i=null;i="constant"===e.value.kind||"source"===e.value.kind?new ni("source",n):new ii("composite",n,e.value.zoomStops),this.paint._values[t]=new Ts(e.property,i,e.parameters);}}_handleOverridablePaintPropertyUpdate(t,e,r){return !(!this.layout||e.isDataDriven()||r.isDataDriven())&&Qc.hasPaintOverride(this.layout,t)}static hasPaintOverride(t,e){const r=t.get("text-field"),n=Jc.paint.properties[e];let i=!1;const s=t=>{for(const e of t)if(n.overrides&&n.overrides.hasOverride(e))return void(i=!0)};if("constant"===r.value.kind&&r.value.value instanceof Ce)s(r.value.value.sections);else if("source"===r.value.kind){const t=e=>{i||(e instanceof Ne&&Ue(e.value)===Nt?s(e.value.sections):e instanceof Ir?s(e.sections):e.eachChild(t));},e=r.value;e._styleExpression&&t(e._styleExpression.expression);}return i}}let th;var eh={get paint(){return th=th||new Us({"background-color":new $s(xt.paint_background["background-color"]),"background-pattern":new Ds(xt.paint_background["background-pattern"]),"background-opacity":new $s(xt.paint_background["background-opacity"])})}};class rh extends Ns{constructor(t){super(t,eh);}}let nh;var ih={get paint(){return nh=nh||new Us({"raster-opacity":new $s(xt.paint_raster["raster-opacity"]),"raster-hue-rotate":new $s(xt.paint_raster["raster-hue-rotate"]),"raster-brightness-min":new $s(xt.paint_raster["raster-brightness-min"]),"raster-brightness-max":new $s(xt.paint_raster["raster-brightness-max"]),"raster-saturation":new $s(xt.paint_raster["raster-saturation"]),"raster-contrast":new $s(xt.paint_raster["raster-contrast"]),"raster-resampling":new $s(xt.paint_raster["raster-resampling"]),"raster-fade-duration":new $s(xt.paint_raster["raster-fade-duration"])})}};class sh extends Ns{constructor(t){super(t,ih);}}class ah extends Ns{constructor(t){super(t,{}),this.onAdd=t=>{this.implementation.onAdd&&this.implementation.onAdd(t,t.painter.context.gl);},this.onRemove=t=>{this.implementation.onRemove&&this.implementation.onRemove(t,t.painter.context.gl);},this.implementation=t;}is3D(){return "3d"===this.implementation.renderingMode}hasOffscreenPass(){return void 0!==this.implementation.prerender}recalculate(){}updateTransitions(){}hasTransition(){return !1}serialize(){throw new Error("Custom layers cannot be serialized")}}class oh{constructor(t){this._methodToThrottle=t,this._triggered=!1,"undefined"!=typeof MessageChannel&&(this._channel=new MessageChannel,this._channel.port2.onmessage=()=>{this._triggered=!1,this._methodToThrottle();});}trigger(){this._triggered||(this._triggered=!0,this._channel?this._channel.port1.postMessage(!0):setTimeout((()=>{this._triggered=!1,this._methodToThrottle();}),0));}remove(){delete this._channel,this._methodToThrottle=()=>{};}}const lh={once:!0},uh=6371008.8;class ch{constructor(t,e){if(isNaN(t)||isNaN(e))throw new Error(`Invalid LngLat object: (${t}, ${e})`);if(this.lng=+t,this.lat=+e,this.lat>90||this.lat<-90)throw new Error("Invalid LngLat latitude value: must be between -90 and 90")}wrap(){return new ch(L(this.lng,-180,180),this.lat)}toArray(){return [this.lng,this.lat]}toString(){return `LngLat(${this.lng}, ${this.lat})`}distanceTo(t){const e=Math.PI/180,r=this.lat*e,n=t.lat*e,i=Math.sin(r)*Math.sin(n)+Math.cos(r)*Math.cos(n)*Math.cos((t.lng-this.lng)*e);return uh*Math.acos(Math.min(i,1))}static convert(t){if(t instanceof ch)return t;if(Array.isArray(t)&&(2===t.length||3===t.length))return new ch(Number(t[0]),Number(t[1]));if(!Array.isArray(t)&&"object"==typeof t&&null!==t)return new ch(Number("lng"in t?t.lng:t.lon),Number(t.lat));throw new Error("`LngLatLike` argument must be specified as a LngLat instance, an object {lng: , lat: }, an object {lon: , lat: }, or an array of [, ]")}}const hh=2*Math.PI*uh;function ph(t){return hh*Math.cos(t*Math.PI/180)}function fh(t){return (180+t)/360}function dh(t){return (180-180/Math.PI*Math.log(Math.tan(Math.PI/4+t*Math.PI/360)))/360}function yh(t,e){return t/ph(e)}function mh(t){return 360/Math.PI*Math.atan(Math.exp((180-360*t)*Math.PI/180))-90}function gh(t,e){return t*ph(mh(e))}class xh{constructor(t,e,r=0){this.x=+t,this.y=+e,this.z=+r;}static fromLngLat(t,e=0){const r=ch.convert(t);return new xh(fh(r.lng),dh(r.lat),yh(e,r.lat))}toLngLat(){return new ch(360*this.x-180,mh(this.y))}toAltitude(){return gh(this.z,this.y)}meterInMercatorCoordinateUnits(){return 1/hh*(t=mh(this.y),1/Math.cos(t*Math.PI/180));var t;}}function vh(t,e,r){var n=2*Math.PI*6378137/256/Math.pow(2,r);return [t*n-2*Math.PI*6378137/2,e*n-2*Math.PI*6378137/2]}class bh{constructor(t,e,r){if(!function(t,e,r){return !(t<0||t>25||r<0||r>=Math.pow(2,t)||e<0||e>=Math.pow(2,t))}(t,e,r))throw new Error(`x=${e}, y=${r}, z=${t} outside of bounds. 0<=x<${Math.pow(2,t)}, 0<=y<${Math.pow(2,t)} 0<=z<=25 `);this.z=t,this.x=e,this.y=r,this.key=Sh(0,t,t,e,r);}equals(t){return this.z===t.z&&this.x===t.x&&this.y===t.y}url(t,e,r){const n=(s=this.y,a=this.z,o=vh(256*(i=this.x),256*(s=Math.pow(2,a)-s-1),a),l=vh(256*(i+1),256*(s+1),a),o[0]+","+o[1]+","+l[0]+","+l[1]);var i,s,a,o,l;const u=function(t,e,r){let n,i="";for(let s=t;s>0;s--)n=1<1?"@2x":"").replace(/{quadkey}/g,u).replace(/{bbox-epsg-3857}/g,n)}isChildOf(t){const e=this.z-t.z;return e>0&&t.x===this.x>>e&&t.y===this.y>>e}getTilePoint(t){const e=Math.pow(2,this.z);return new r((t.x*e-this.x)*P,(t.y*e-this.y)*P)}toString(){return `${this.z}/${this.x}/${this.y}`}}class wh{constructor(t,e){this.wrap=t,this.canonical=e,this.key=Sh(t,e.z,e.z,e.x,e.y);}}class _h{constructor(t,e,r,n,i){if(this.terrainRttPosMatrix32f=null,t= z; overscaledZ = ${t}; z = ${r}`);this.overscaledZ=t,this.wrap=e,this.canonical=new bh(r,+n,+i),this.key=Sh(e,t,r,n,i);}clone(){return new _h(this.overscaledZ,this.wrap,this.canonical.z,this.canonical.x,this.canonical.y)}equals(t){return this.overscaledZ===t.overscaledZ&&this.wrap===t.wrap&&this.canonical.equals(t.canonical)}scaledTo(t){if(t>this.overscaledZ)throw new Error(`targetZ > this.overscaledZ; targetZ = ${t}; overscaledZ = ${this.overscaledZ}`);const e=this.canonical.z-t;return t>this.canonical.z?new _h(t,this.wrap,this.canonical.z,this.canonical.x,this.canonical.y):new _h(t,this.wrap,t,this.canonical.x>>e,this.canonical.y>>e)}calculateScaledKey(t,e){if(t>this.overscaledZ)throw new Error(`targetZ > this.overscaledZ; targetZ = ${t}; overscaledZ = ${this.overscaledZ}`);const r=this.canonical.z-t;return t>this.canonical.z?Sh(this.wrap*+e,t,this.canonical.z,this.canonical.x,this.canonical.y):Sh(this.wrap*+e,t,t,this.canonical.x>>r,this.canonical.y>>r)}isChildOf(t){if(t.wrap!==this.wrap)return !1;const e=this.canonical.z-t.canonical.z;return 0===t.overscaledZ||t.overscaledZ>e&&t.canonical.y===this.canonical.y>>e}children(t){if(this.overscaledZ>=t)return [new _h(this.overscaledZ+1,this.wrap,this.canonical.z,this.canonical.x,this.canonical.y)];const e=this.canonical.z+1,r=2*this.canonical.x,n=2*this.canonical.y;return [new _h(e,this.wrap,e,r,n),new _h(e,this.wrap,e,r+1,n),new _h(e,this.wrap,e,r,n+1),new _h(e,this.wrap,e,r+1,n+1)]}isLessThan(t){return this.wrapt.wrap)&&(this.overscaledZt.overscaledZ)&&(this.canonical.xt.canonical.x)&&this.canonical.ythis.maxX||this.minY>this.maxY)&&(this.minX=1/0,this.maxX=-1/0,this.minY=1/0,this.maxY=-1/0),this}shrinkBy(t){return this.expandBy(-t)}map(t){const e=new kh;return e.extend(t(new r(this.minX,this.minY))),e.extend(t(new r(this.maxX,this.minY))),e.extend(t(new r(this.minX,this.maxY))),e.extend(t(new r(this.maxX,this.maxY))),e}static fromPoints(t){const e=new kh;for(const r of t)e.extend(r);return e}contains(t){return t.x>=this.minX&&t.x<=this.maxX&&t.y>=this.minY&&t.y<=this.maxY}empty(){return this.minX>this.maxX}width(){return this.maxX-this.minX}height(){return this.maxY-this.minY}covers(t){return !this.empty()&&!t.empty()&&t.minX>=this.minX&&t.maxX<=this.maxX&&t.minY>=this.minY&&t.maxY<=this.maxY}intersects(t){return !this.empty()&&!t.empty()&&t.minX<=this.maxX&&t.maxX>=this.minX&&t.minY<=this.maxY&&t.maxY>=this.minY}}class Mh{constructor(t){this._stringToNumber={},this._numberToString=[];for(let e=0;e=this._numberToString.length)throw new Error(`Out of bounds. Index requested n=${t} can't be >= this._numberToString.length ${this._numberToString.length}`);return this._numberToString[t]}}class Ih{constructor(t,e,r,n,i){this.type="Feature",this._vectorTileFeature=t,t._z=e,t._x=r,t._y=n,this.properties=t.properties,this.id=i;}get geometry(){return void 0===this._geometry&&(this._geometry=this._vectorTileFeature.toGeoJSON(this._vectorTileFeature._x,this._vectorTileFeature._y,this._vectorTileFeature._z).geometry),this._geometry}set geometry(t){this._geometry=t;}toJSON(){const t={geometry:this.geometry};for(const e in this)"_geometry"!==e&&"_vectorTileFeature"!==e&&(t[e]=this[e]);return t}}class zh{constructor(t,e){this.tileID=t,this.x=t.canonical.x,this.y=t.canonical.y,this.z=t.canonical.z,this.grid=new is(P,16,0),this.grid3D=new is(P,16,0),this.featureIndexArray=new Pa,this.promoteId=e;}insert(t,e,r,n,i,s){const a=this.featureIndexArray.length;this.featureIndexArray.emplaceBack(r,n,i);const o=s?this.grid3D:this.grid;for(let t=0;t=0&&n[3]>=0&&o.insert(a,n[0],n[1],n[2],n[3]);}}loadVTLayers(){return this.vtLayers||(this.vtLayers=new fu(new Ju(this.rawTileData)).layers,this.sourceLayerCoder=new Mh(this.vtLayers?Object.keys(this.vtLayers).sort():["_geojsonTileLayer"])),this.vtLayers}query(t,e,n,i){this.loadVTLayers();const s=t.params,a=P/t.tileSize/t.scale,o=hi(s.filter),l=t.queryGeometry,u=t.queryPadding*a,c=kh.fromPoints(l),h=this.grid.query(c.minX-u,c.minY-u,c.maxX+u,c.maxY+u),p=kh.fromPoints(t.cameraQueryGeometry).expandBy(u),f=this.grid3D.query(p.minX,p.minY,p.maxX,p.maxY,((e,n,i,s)=>function(t,e,n,i,s){for(const r of t)if(e<=r.x&&n<=r.y&&i>=r.x&&s>=r.y)return !0;const a=[new r(e,n),new r(e,s),new r(i,s),new r(i,n)];if(t.length>2)for(const e of a)if(Do(t,e))return !0;for(let e=0;e(p||(p=ko(e)),r.queryIntersectsFeature({queryGeometry:l,feature:e,featureState:n,geometry:p,zoom:this.z,transform:t.transform,pixelsToTileUnits:a,pixelPosMatrix:t.pixelPosMatrix,unwrappedTileID:this.tileID.toUnwrapped(),getElevation:t.getElevation}))));}return d}loadMatchingFeature(t,e,r,n,i,s,a,o,l,u,c){const h=this.bucketLayerIDs[e];if(s&&!h.some((t=>s.has(t))))return;const p=this.sourceLayerCoder.decode(r),f=this.vtLayers[p].feature(n);if(i.needGeometry){const t=Mo(f,!0);if(!i.filter(new Is(this.tileID.overscaledZ),t,this.tileID.canonical))return}else if(!i.filter(new Is(this.tileID.overscaledZ),f))return;const d=this.getId(f,p);for(let e=0;e{const a=e instanceof Fs?e.get(s):null;return a&&a.evaluate?a.evaluate(r,n,i):a}))}function Ch(t,e){return e-t}function Eh(t,e,n,i,s){const a=[];for(let o=0;o=i&&c.x>=i||(o.x>=i?o=new r(i,o.y+(i-o.x)/(c.x-o.x)*(c.y-o.y))._round():c.x>=i&&(c=new r(i,o.y+(i-o.x)/(c.x-o.x)*(c.y-o.y))._round()),o.y>=s&&c.y>=s||(o.y>=s?o=new r(o.x+(s-o.y)/(c.y-o.y)*(c.x-o.x),s)._round():c.y>=s&&(c=new r(o.x+(s-o.y)/(c.y-o.y)*(c.x-o.x),s)._round()),u&&o.equals(u[u.length-1])||(u=[o],a.push(u)),u.push(c)))));}}return a}as("FeatureIndex",zh,{omit:["rawTileData","sourceLayerCoder"]});class Bh extends r{constructor(t,e,r,n){super(t,e),this.angle=r,void 0!==n&&(this.segment=n);}clone(){return new Bh(this.x,this.y,this.angle,this.segment)}}function Vh(t,e,r,n,i){if(void 0===e.segment||0===r)return !0;let s=e,a=e.segment+1,o=0;for(;o>-r/2;){if(a--,a<0)return !1;o-=t[a].dist(s),s=t[a];}o+=t[a].dist(t[a+1]),a++;const l=[];let u=0;for(;on;)u-=l.shift().angleDelta;if(u>i)return !1;a++,o+=e.dist(r);}return !0}function Th(t){let e=0;for(let r=0;ru){const c=(u-l)/s,h=dr.number(n.x,i.x,c),p=dr.number(n.y,i.y,c),f=new Bh(h,p,i.angleTo(n),r);return f._round(),!a||Vh(t,f,o,a,e)?f:void 0}l+=s;}}function Oh(t,e,r,n,i,s,a,o,l){const u=Fh(n,s,a),c=$h(n,i),h=c*a,p=0===t[0].x||t[0].x===l||0===t[0].y||t[0].y===l;return e-h=0&&g=0&&x=0&&p+u<=c){const r=new Bh(g,x,y,e);r._round(),n&&!Vh(t,r,s,n,i)||f.push(r);}}h+=d;}return o||f.length||a||(f=Dh(t,h/2,r,n,i,s,a,!0,l)),f}function Rh(t,e,n,i){const s=[],a=t.image,o=a.pixelRatio,l=a.paddedRect.w-2,u=a.paddedRect.h-2;let c={x1:t.left,y1:t.top,x2:t.right,y2:t.bottom};const h=a.stretchX||[[0,l]],p=a.stretchY||[[0,u]],f=(t,e)=>t+e[1]-e[0],d=h.reduce(f,0),y=p.reduce(f,0),m=l-d,g=u-y;let x=0,v=d,b=0,w=y,_=0,S=m,A=0,k=g;if(a.content&&i){const e=a.content,r=e[2]-e[0],n=e[3]-e[1];(a.textFitWidth||a.textFitHeight)&&(c=$c(t)),x=Uh(h,0,e[0]),b=Uh(p,0,e[1]),v=Uh(h,e[0],e[2]),w=Uh(p,e[1],e[3]),_=e[0]-x,A=e[1]-b,S=r-v,k=n-w;}const M=c.x1,I=c.y1,z=c.x2-M,P=c.y2-I,C=(t,i,s,l)=>{const u=Nh(t.stretch-x,v,z,M),c=qh(t.fixed-_,S,t.stretch,d),h=Nh(i.stretch-b,w,P,I),p=qh(i.fixed-A,k,i.stretch,y),f=Nh(s.stretch-x,v,z,M),m=qh(s.fixed-_,S,s.stretch,d),g=Nh(l.stretch-b,w,P,I),C=qh(l.fixed-A,k,l.stretch,y),E=new r(u,h),B=new r(f,h),V=new r(f,g),T=new r(u,g),F=new r(c/o,p/o),$=new r(m/o,C/o),L=e*Math.PI/180;if(L){const t=Math.sin(L),e=Math.cos(L),r=[e,-t,t,e];E._matMult(r),B._matMult(r),T._matMult(r),V._matMult(r);}const O=t.stretch+t.fixed,D=i.stretch+i.fixed;return {tl:E,tr:B,bl:T,br:V,tex:{x:a.paddedRect.x+1+O,y:a.paddedRect.y+1+D,w:s.stretch+s.fixed-O,h:l.stretch+l.fixed-D},writingMode:void 0,glyphOffset:[0,0],sectionIndex:0,pixelOffsetTL:F,pixelOffsetBR:$,minFontScaleX:S/o/z,minFontScaleY:k/o/P,isSDF:n}};if(i&&(a.stretchX||a.stretchY)){const t=jh(h,m,d),e=jh(p,g,y);for(let r=0;r0&&(n=Math.max(10,n),this.circleDiameter=n);}else {const u=(null===(h=a.image)||void 0===h?void 0:h.content)&&(a.image.textFitWidth||a.image.textFitHeight)?$c(a):{x1:a.left,y1:a.top,x2:a.right,y2:a.bottom};u.y1=u.y1*o-l[0],u.y2=u.y2*o+l[2],u.x1=u.x1*o-l[3],u.x2=u.x2*o+l[1];const p=a.collisionPadding;if(p&&(u.x1-=p[0]*o,u.y1-=p[1]*o,u.x2+=p[2]*o,u.y2+=p[3]*o),c){const t=new r(u.x1,u.y1),e=new r(u.x2,u.y1),n=new r(u.x1,u.y2),i=new r(u.x2,u.y2),s=c*Math.PI/180;t._rotate(s),e._rotate(s),n._rotate(s),i._rotate(s),u.x1=Math.min(t.x,e.x,n.x,i.x),u.x2=Math.max(t.x,e.x,n.x,i.x),u.y1=Math.min(t.y,e.y,n.y,i.y),u.y2=Math.max(t.y,e.y,n.y,i.y);}t.emplaceBack(e.x,e.y,u.x1,u.y1,u.x2,u.y2,n,i,s);}this.boxEndIndex=t.length;}}class Xh{constructor(t=[],e=(t,e)=>te?1:0){if(this.data=t,this.length=this.data.length,this.compare=e,this.length>0)for(let t=(this.length>>1)-1;t>=0;t--)this._down(t);}push(t){this.data.push(t),this._up(this.length++);}pop(){if(0===this.length)return;const t=this.data[0],e=this.data.pop();return --this.length>0&&(this.data[0]=e,this._down(0)),t}peek(){return this.data[0]}_up(t){const{data:e,compare:r}=this,n=e[t];for(;t>0;){const i=t-1>>1,s=e[i];if(r(n,s)>=0)break;e[t]=s,t=i;}e[t]=n;}_down(t){const{data:e,compare:r}=this,n=this.length>>1,i=e[t];for(;t=0)break;e[t]=e[n],t=n;}e[t]=i;}}function Zh(t,e=1,n=!1){const i=kh.fromPoints(t[0]),s=Math.min(i.width(),i.height());let a=s/2;const o=new Xh([],Yh),{minX:l,minY:u,maxX:c,maxY:h}=i;if(0===s)return new r(l,u);for(let e=l;ep.d||!p.d)&&(p=r,n&&console.log("found best %d after %d probes",Math.round(1e4*r.d)/1e4,f)),r.max-p.d<=e||(a=r.h/2,o.push(new Hh(r.p.x-a,r.p.y-a,a,t)),o.push(new Hh(r.p.x+a,r.p.y-a,a,t)),o.push(new Hh(r.p.x-a,r.p.y+a,a,t)),o.push(new Hh(r.p.x+a,r.p.y+a,a,t)),f+=4);}return n&&(console.log(`num probes: ${f}`),console.log(`best distance: ${p.d}`)),p.p}function Yh(t,e){return e.max-t.max}function Hh(t,e,n,i){this.p=new r(t,e),this.h=n,this.d=function(t,e){let r=!1,n=1/0;for(let i=0;it.y!=o.y>t.y&&t.x<(o.x-i.x)*(t.y-i.y)/(o.y-i.y)+i.x&&(r=!r),n=Math.min(n,Lo(t,i,o));}}return (r?1:-1)*Math.sqrt(n)}(this.p,i),this.max=this.d+this.h*Math.SQRT2;}var Kh;t.aE=void 0,(Kh=t.aE||(t.aE={}))[Kh.center=1]="center",Kh[Kh.left=2]="left",Kh[Kh.right=3]="right",Kh[Kh.top=4]="top",Kh[Kh.bottom=5]="bottom",Kh[Kh["top-left"]=6]="top-left",Kh[Kh["top-right"]=7]="top-right",Kh[Kh["bottom-left"]=8]="bottom-left",Kh[Kh["bottom-right"]=9]="bottom-right";const Jh=Number.POSITIVE_INFINITY;function Wh(t,e){return e[1]!==Jh?function(t,e,r){let n=0,i=0;switch(e=Math.abs(e),r=Math.abs(r),t){case "top-right":case "top-left":case "top":i=r-7;break;case "bottom-right":case "bottom-left":case "bottom":i=7-r;}switch(t){case "top-right":case "bottom-right":case "right":n=-e;break;case "top-left":case "bottom-left":case "left":n=e;}return [n,i]}(t,e[0],e[1]):function(t,e){let r=0,n=0;e<0&&(e=0);const i=e/Math.SQRT2;switch(t){case "top-right":case "top-left":n=i-7;break;case "bottom-right":case "bottom-left":n=7-i;break;case "bottom":n=7-e;break;case "top":n=e-7;}switch(t){case "top-right":case "bottom-right":r=-i;break;case "top-left":case "bottom-left":r=i;break;case "left":r=e;break;case "right":r=-e;}return [r,n]}(t,e[0])}function Qh(t,e,r){var n;const i=t.layout,s=null===(n=i.get("text-variable-anchor-offset"))||void 0===n?void 0:n.evaluate(e,{},r);if(s){const t=s.values,e=[];for(let r=0;rt*Zu));n.startsWith("top")?i[1]-=7:n.startsWith("bottom")&&(i[1]+=7),e[r+1]=i;}return new $e(e)}const a=i.get("text-variable-anchor");if(a){let n;n=void 0!==t._unevaluatedLayout.getValue("text-radial-offset")?[i.get("text-radial-offset").evaluate(e,{},r)*Zu,Jh]:i.get("text-offset").evaluate(e,{},r).map((t=>t*Zu));const s=[];for(const t of a)s.push(t,Wh(t,n));return new $e(s)}return null}function tp(t){switch(t){case "right":case "top-right":case "bottom-right":return "right";case "left":case "top-left":case "bottom-left":return "left"}return "center"}function ep(e,r,n,i,s,a,o,l,u,c,h,p){let f=a.textMaxSize.evaluate(r,{});void 0===f&&(f=o);const d=e.layers[0].layout,y=d.get("icon-offset").evaluate(r,{},h),m=np(n.horizontal),g=o/24,x=e.tilePixelRatio*g,v=e.tilePixelRatio*f/24,b=e.tilePixelRatio*l,w=e.tilePixelRatio*d.get("symbol-spacing"),_=d.get("text-padding")*e.tilePixelRatio,S=function(t,e,r,n=1){const i=t.get("icon-padding").evaluate(e,{},r),s=i&&i.values;return [s[0]*n,s[1]*n,s[2]*n,s[3]*n]}(d,r,h,e.tilePixelRatio),A=d.get("text-max-angle")/180*Math.PI,k="viewport"!==d.get("text-rotation-alignment")&&"point"!==d.get("symbol-placement"),M="map"===d.get("icon-rotation-alignment")&&"point"!==d.get("symbol-placement"),I=d.get("symbol-placement"),z=w/2,C=d.get("icon-text-fit");let E;i&&"none"!==C&&(e.allowVerticalPlacement&&n.vertical&&(E=Lc(i,n.vertical,C,d.get("icon-text-fit-padding"),y,g)),m&&(i=Lc(i,m,C,d.get("icon-text-fit-padding"),y,g)));const B=h?p.line.getGranularityForZoomLevel(h.z):1,V=(l,p)=>{p.x<0||p.x>=P||p.y<0||p.y>=P||function(e,r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v,b,w,_,S,A,k){const M=e.addToLineVertexArray(r,n);let I,z,P,C,E=0,B=0,V=0,T=0,F=-1,$=-1;const L={};let O=ro("");if(e.allowVerticalPlacement&&i.vertical){const t=l.layout.get("text-rotate").evaluate(w,{},A)+90;P=new Gh(u,r,c,h,p,i.vertical,f,d,y,t),o&&(C=new Gh(u,r,c,h,p,o,g,x,y,t));}if(s){const n=l.layout.get("icon-rotate").evaluate(w,{}),i="none"!==l.layout.get("icon-text-fit"),a=Rh(s,n,S,i),f=o?Rh(o,n,S,i):void 0;z=new Gh(u,r,c,h,p,s,g,x,!1,n),E=4*a.length;const d=e.iconSizeData;let y=null;"source"===d.kind?(y=[Oc*l.layout.get("icon-size").evaluate(w,{})],y[0]>Dc&&q(`${e.layerIds[0]}: Value for "icon-size" is >= 255. Reduce your "icon-size".`)):"composite"===d.kind&&(y=[Oc*_.compositeIconSizes[0].evaluate(w,{},A),Oc*_.compositeIconSizes[1].evaluate(w,{},A)],(y[0]>Dc||y[1]>Dc)&&q(`${e.layerIds[0]}: Value for "icon-size" is >= 255. Reduce your "icon-size".`)),e.addSymbols(e.icon,a,y,b,v,w,t.ao.none,r,M.lineStartIndex,M.lineLength,-1,A),F=e.icon.placedSymbolArray.length-1,f&&(B=4*f.length,e.addSymbols(e.icon,f,y,b,v,w,t.ao.vertical,r,M.lineStartIndex,M.lineLength,-1,A),$=e.icon.placedSymbolArray.length-1);}const D=Object.keys(i.horizontal);for(const n of D){const s=i.horizontal[n];if(!I){O=ro(s.text);const t=l.layout.get("text-rotate").evaluate(w,{},A);I=new Gh(u,r,c,h,p,s,f,d,y,t);}const o=1===s.positionedLines.length;if(V+=rp(e,r,s,a,l,y,w,m,M,i.vertical?t.ao.horizontal:t.ao.horizontalOnly,o?D:[n],L,F,_,A),o)break}i.vertical&&(T+=rp(e,r,i.vertical,a,l,y,w,m,M,t.ao.vertical,["vertical"],L,$,_,A));const R=I?I.boxStartIndex:e.collisionBoxArray.length,U=I?I.boxEndIndex:e.collisionBoxArray.length,j=P?P.boxStartIndex:e.collisionBoxArray.length,N=P?P.boxEndIndex:e.collisionBoxArray.length,G=z?z.boxStartIndex:e.collisionBoxArray.length,X=z?z.boxEndIndex:e.collisionBoxArray.length,Z=C?C.boxStartIndex:e.collisionBoxArray.length,Y=C?C.boxEndIndex:e.collisionBoxArray.length;let H=-1;const K=(t,e)=>t&&t.circleDiameter?Math.max(t.circleDiameter,e):e;H=K(I,H),H=K(P,H),H=K(z,H),H=K(C,H);const J=H>-1?1:0;J&&(H*=k/Zu),e.glyphOffsetArray.length>=Yc.MAX_GLYPHS&&q("Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907"),void 0!==w.sortKey&&e.addToSortKeyRanges(e.symbolInstances.length,w.sortKey);const W=Qh(l,w,A),[Q,tt]=function(e,r){const n=e.length,i=null==r?void 0:r.values;if((null==i?void 0:i.length)>0)for(let r=0;r=0?L.right:-1,L.center>=0?L.center:-1,L.left>=0?L.left:-1,L.vertical||-1,F,$,O,R,U,j,N,G,X,Z,Y,c,V,T,E,B,J,0,f,H,Q,tt);}(e,p,l,n,i,s,E,e.layers[0],e.collisionBoxArray,r.index,r.sourceLayerIndex,e.index,x,[_,_,_,_],k,u,b,S,M,y,r,a,c,h,o);};if("line"===I)for(const t of Eh(r.geometry,0,0,P,P)){const r=Kl(t,B),s=Oh(r,w,A,n.vertical||m,i,24,v,e.overscaling,P);for(const t of s)m&&ip(e,m.text,z,t)||V(r,t);}else if("line-center"===I){for(const t of r.geometry)if(t.length>1){const e=Kl(t,B),r=Lh(e,A,n.vertical||m,i,24,v);r&&V(e,r);}}else if("Polygon"===r.type)for(const t of Qr(r.geometry,0)){const e=Zh(t,16);V(Kl(t[0],B,!0),new Bh(e.x,e.y,0));}else if("LineString"===r.type)for(const t of r.geometry){const e=Kl(t,B);V(e,new Bh(e[0].x,e[0].y,0));}else if("Point"===r.type)for(const t of r.geometry)for(const e of t)V([e],new Bh(e.x,e.y,0));}function rp(t,e,n,i,s,a,o,l,u,c,h,p,f,d,y){const m=function(t,e,n,i,s,a,o,l){const u=i.layout.get("text-rotate").evaluate(a,{})*Math.PI/180,c=[];for(const t of e.positionedLines)for(const i of t.positionedGlyphs){if(!i.rect)continue;const a=i.rect||{};let h=4,p=!0,f=1,d=0;const y=(s||l)&&i.vertical,m=i.metrics.advance*i.scale/2;if(l&&e.verticalizable&&(d=t.lineOffset/2-(i.imageName?-(Zu-i.metrics.width*i.scale)/2:(i.scale-1)*Zu)),i.imageName){const t=o[i.imageName];p=t.sdf,f=t.pixelRatio,h=1/f;}const g=s?[i.x+m,i.y]:[0,0];let x=s?[0,0]:[i.x+m+n[0],i.y+n[1]-d],v=[0,0];y&&(v=x,x=[0,0]);const b=i.metrics.isDoubleResolution?2:1,w=(i.metrics.left-h)*i.scale-m+x[0],_=(-i.metrics.top-h)*i.scale+x[1],S=w+a.w/b*i.scale/f,A=_+a.h/b*i.scale/f,k=new r(w,_),M=new r(S,_),I=new r(w,A),z=new r(S,A);if(y){const t=new r(-m,m- -17),e=-Math.PI/2,n=12-m,s=new r(22-n,-(i.imageName?n:0)),a=new r(...v);k._rotateAround(e,t)._add(s)._add(a),M._rotateAround(e,t)._add(s)._add(a),I._rotateAround(e,t)._add(s)._add(a),z._rotateAround(e,t)._add(s)._add(a);}if(u){const t=Math.sin(u),e=Math.cos(u),r=[e,-t,t,e];k._matMult(r),M._matMult(r),I._matMult(r),z._matMult(r);}const P=new r(0,0),C=new r(0,0);c.push({tl:k,tr:M,bl:I,br:z,tex:a,writingMode:e.writingMode,glyphOffset:g,sectionIndex:i.sectionIndex,isSDF:p,pixelOffsetTL:P,pixelOffsetBR:C,minFontScaleX:0,minFontScaleY:0});}return c}(0,n,l,s,a,o,i,t.allowVerticalPlacement),g=t.textSizeData;let x=null;"source"===g.kind?(x=[Oc*s.layout.get("text-size").evaluate(o,{})],x[0]>Dc&&q(`${t.layerIds[0]}: Value for "text-size" is >= 255. Reduce your "text-size".`)):"composite"===g.kind&&(x=[Oc*d.compositeTextSizes[0].evaluate(o,{},y),Oc*d.compositeTextSizes[1].evaluate(o,{},y)],(x[0]>Dc||x[1]>Dc)&&q(`${t.layerIds[0]}: Value for "text-size" is >= 255. Reduce your "text-size".`)),t.addSymbols(t.text,m,x,l,a,o,c,e,u.lineStartIndex,u.lineLength,f,y);for(const e of h)p[e]=t.text.placedSymbolArray.length-1;return 4*m.length}function np(t){for(const e in t)return t[e];return null}function ip(t,e,r,n){const i=t.compareText;if(e in i){const t=i[e];for(let e=t.length-1;e>=0;e--)if(n.dist(t[e])>4;if(1!==n)throw new Error(`Got v${n} data when expected v1.`);const i=sp[15&r];if(!i)throw new Error("Unrecognized array type.");const[s]=new Uint16Array(t,2,1),[a]=new Uint32Array(t,4,1);return new ap(a,s,i,t)}constructor(t,e=64,r=Float64Array,n){if(isNaN(t)||t<0)throw new Error(`Unpexpected numItems value: ${t}.`);this.numItems=+t,this.nodeSize=Math.min(Math.max(+e,2),65535),this.ArrayType=r,this.IndexArrayType=t<65536?Uint16Array:Uint32Array;const i=sp.indexOf(this.ArrayType),s=2*t*this.ArrayType.BYTES_PER_ELEMENT,a=t*this.IndexArrayType.BYTES_PER_ELEMENT,o=(8-a%8)%8;if(i<0)throw new Error(`Unexpected typed array class: ${r}.`);n&&n instanceof ArrayBuffer?(this.data=n,this.ids=new this.IndexArrayType(this.data,8,t),this.coords=new this.ArrayType(this.data,8+a+o,2*t),this._pos=2*t,this._finished=!0):(this.data=new ArrayBuffer(8+s+a+o),this.ids=new this.IndexArrayType(this.data,8,t),this.coords=new this.ArrayType(this.data,8+a+o,2*t),this._pos=0,this._finished=!1,new Uint8Array(this.data,0,2).set([219,16+i]),new Uint16Array(this.data,2,1)[0]=e,new Uint32Array(this.data,4,1)[0]=t);}add(t,e){const r=this._pos>>1;return this.ids[r]=r,this.coords[this._pos++]=t,this.coords[this._pos++]=e,r}finish(){const t=this._pos>>1;if(t!==this.numItems)throw new Error(`Added ${t} items when expected ${this.numItems}.`);return op(this.ids,this.coords,this.nodeSize,0,this.numItems-1,0),this._finished=!0,this}range(t,e,r,n){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:i,coords:s,nodeSize:a}=this,o=[0,i.length-1,0],l=[];for(;o.length;){const u=o.pop()||0,c=o.pop()||0,h=o.pop()||0;if(c-h<=a){for(let a=h;a<=c;a++){const o=s[2*a],u=s[2*a+1];o>=t&&o<=r&&u>=e&&u<=n&&l.push(i[a]);}continue}const p=h+c>>1,f=s[2*p],d=s[2*p+1];f>=t&&f<=r&&d>=e&&d<=n&&l.push(i[p]),(0===u?t<=f:e<=d)&&(o.push(h),o.push(p-1),o.push(1-u)),(0===u?r>=f:n>=d)&&(o.push(p+1),o.push(c),o.push(1-u));}return l}within(t,e,r){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:n,coords:i,nodeSize:s}=this,a=[0,n.length-1,0],o=[],l=r*r;for(;a.length;){const u=a.pop()||0,c=a.pop()||0,h=a.pop()||0;if(c-h<=s){for(let r=h;r<=c;r++)hp(i[2*r],i[2*r+1],t,e)<=l&&o.push(n[r]);continue}const p=h+c>>1,f=i[2*p],d=i[2*p+1];hp(f,d,t,e)<=l&&o.push(n[p]),(0===u?t-r<=f:e-r<=d)&&(a.push(h),a.push(p-1),a.push(1-u)),(0===u?t+r>=f:e+r>=d)&&(a.push(p+1),a.push(c),a.push(1-u));}return o}}function op(t,e,r,n,i,s){if(i-n<=r)return;const a=n+i>>1;lp(t,e,a,n,i,s),op(t,e,r,n,a-1,1-s),op(t,e,r,a+1,i,1-s);}function lp(t,e,r,n,i,s){for(;i>n;){if(i-n>600){const a=i-n+1,o=r-n+1,l=Math.log(a),u=.5*Math.exp(2*l/3),c=.5*Math.sqrt(l*u*(a-u)/a)*(o-a/2<0?-1:1);lp(t,e,r,Math.max(n,Math.floor(r-o*u/a+c)),Math.min(i,Math.floor(r+(a-o)*u/a+c)),s);}const a=e[2*r+s];let o=n,l=i;for(up(t,e,n,r),e[2*i+s]>a&&up(t,e,n,i);oa;)l--;}e[2*n+s]===a?up(t,e,n,l):(l++,up(t,e,l,i)),l<=r&&(n=l+1),r<=l&&(i=l-1);}}function up(t,e,r,n){cp(t,r,n),cp(e,2*r,2*n),cp(e,2*r+1,2*n+1);}function cp(t,e,r){const n=t[e];t[e]=t[r],t[r]=n;}function hp(t,e,r,n){const i=t-r,s=e-n;return i*i+s*s}var pp;t.cx=void 0,(pp=t.cx||(t.cx={})).create="create",pp.load="load",pp.fullLoad="fullLoad";let fp=null,dp=[];const yp=1e3/60,mp="loadTime",gp="fullLoadTime",xp={mark(t){performance.mark(t);},frame(t){const e=t;null!=fp&&dp.push(e-fp),fp=e;},clearMetrics(){fp=null,dp=[],performance.clearMeasures(mp),performance.clearMeasures(gp);for(const e in t.cx)performance.clearMarks(t.cx[e]);},getPerformanceMetrics(){performance.measure(mp,t.cx.create,t.cx.load),performance.measure(gp,t.cx.create,t.cx.fullLoad);const e=performance.getEntriesByName(mp)[0].duration,r=performance.getEntriesByName(gp)[0].duration,n=dp.length,i=1/(dp.reduce(((t,e)=>t+e),0)/n/1e3),s=dp.filter((t=>t>yp)).reduce(((t,e)=>t+(e-yp)/yp),0);return {loadTime:e,fullLoadTime:r,fps:i,percentDroppedFrames:s/(n+s)*100,totalFrames:n}}};t.$=P,t.A=f,t.B=function([t,e,r]){return e+=90,e*=Math.PI/180,r*=Math.PI/180,{x:t*Math.cos(e)*Math.sin(r),y:t*Math.sin(e)*Math.sin(r),z:t*Math.cos(r)}},t.C=dr,t.D=$s,t.E=gt,t.F=Is,t.G=ts,t.H=function(t){if(null==Z){const e=t.navigator?t.navigator.userAgent:null;Z=!!t.safari||!(!e||!(/\b(iPad|iPhone|iPod)\b/.test(e)||e.match("Safari")&&!e.match("Chrome")));}return Z},t.I=fc,t.J=class{constructor(t,e){this.target=t,this.mapId=e,this.resolveRejects={},this.tasks={},this.taskQueue=[],this.abortControllers={},this.messageHandlers={},this.invoker=new oh((()=>this.process())),this.subscription=Q(this.target,"message",(t=>this.receive(t)),!1),this.globalScope=X(self)?t:window;}registerMessageHandler(t,e){this.messageHandlers[t]=e;}sendAsync(t,e){return new Promise(((r,n)=>{const i=Math.round(1e18*Math.random()).toString(36).substring(0,10),s=e?Q(e.signal,"abort",(()=>{null==s||s.unsubscribe(),delete this.resolveRejects[i];const e={id:i,type:"",origin:location.origin,targetMapId:t.targetMapId,sourceMapId:this.mapId};this.target.postMessage(e);}),lh):null;this.resolveRejects[i]={resolve:t=>{null==s||s.unsubscribe(),r(t);},reject:t=>{null==s||s.unsubscribe(),n(t);}};const a=[],o=Object.assign(Object.assign({},t),{id:i,sourceMapId:this.mapId,origin:location.origin,data:cs(t.data,a)});this.target.postMessage(o,{transfer:a});}))}receive(t){const e=t.data,r=e.id;if(!("file://"!==e.origin&&"file://"!==location.origin&&"resource://android"!==e.origin&&"resource://android"!==location.origin&&e.origin!==location.origin||e.targetMapId&&this.mapId!==e.targetMapId)){if(""===e.type){delete this.tasks[r];const t=this.abortControllers[r];return delete this.abortControllers[r],void(t&&t.abort())}if(X(self)||e.mustQueue)return this.tasks[r]=e,this.taskQueue.push(r),void this.invoker.trigger();this.processTask(r,e);}}process(){if(0===this.taskQueue.length)return;const t=this.taskQueue.shift(),e=this.tasks[t];delete this.tasks[t],this.taskQueue.length>0&&this.invoker.trigger(),e&&this.processTask(t,e);}processTask(t,r){return e(this,void 0,void 0,(function*(){if(""===r.type){const e=this.resolveRejects[t];if(delete this.resolveRejects[t],!e)return;return void(r.error?e.reject(hs(r.error)):e.resolve(hs(r.data)))}if(!this.messageHandlers[r.type])return void this.completeTask(t,new Error(`Could not find a registered handler for ${r.type}, map ID: ${this.mapId}, available handlers: ${Object.keys(this.messageHandlers).join(", ")}`));const e=hs(r.data),n=new AbortController;this.abortControllers[t]=n;try{const i=yield this.messageHandlers[r.type](r.sourceMapId,e,n);this.completeTask(t,null,i);}catch(e){this.completeTask(t,e);}}))}completeTask(t,e,r){const n=[];delete this.abortControllers[t];const i={id:t,type:"",sourceMapId:this.mapId,origin:location.origin,error:e?cs(e):null,data:cs(r,n)};this.target.postMessage(i,{transfer:n});}remove(){this.invoker.remove(),this.subscription.unsubscribe();}},t.K=lt,t.L=function(){var t=new f(16);return f!=Float32Array&&(t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=0,t[12]=0,t[13]=0,t[14]=0),t[0]=1,t[5]=1,t[10]=1,t[15]=1,t},t.M=function(t,e,r){var n,i,s,a,o,l,u,c,h,p,f,d,y=r[0],m=r[1],g=r[2];return e===t?(t[12]=e[0]*y+e[4]*m+e[8]*g+e[12],t[13]=e[1]*y+e[5]*m+e[9]*g+e[13],t[14]=e[2]*y+e[6]*m+e[10]*g+e[14],t[15]=e[3]*y+e[7]*m+e[11]*g+e[15]):(i=e[1],s=e[2],a=e[3],o=e[4],l=e[5],u=e[6],c=e[7],h=e[8],p=e[9],f=e[10],d=e[11],t[0]=n=e[0],t[1]=i,t[2]=s,t[3]=a,t[4]=o,t[5]=l,t[6]=u,t[7]=c,t[8]=h,t[9]=p,t[10]=f,t[11]=d,t[12]=n*y+o*m+h*g+e[12],t[13]=i*y+l*m+p*g+e[13],t[14]=s*y+u*m+f*g+e[14],t[15]=a*y+c*m+d*g+e[15]),t},t.N=function(t,e,r){var n=r[0],i=r[1],s=r[2];return t[0]=e[0]*n,t[1]=e[1]*n,t[2]=e[2]*n,t[3]=e[3]*n,t[4]=e[4]*i,t[5]=e[5]*i,t[6]=e[6]*i,t[7]=e[7]*i,t[8]=e[8]*s,t[9]=e[9]*s,t[10]=e[10]*s,t[11]=e[11]*s,t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15],t},t.O=function(t,e,r){var n=e[0],i=e[1],s=e[2],a=e[3],o=e[4],l=e[5],u=e[6],c=e[7],h=e[8],p=e[9],f=e[10],d=e[11],y=e[12],m=e[13],g=e[14],x=e[15],v=r[0],b=r[1],w=r[2],_=r[3];return t[0]=v*n+b*o+w*h+_*y,t[1]=v*i+b*l+w*p+_*m,t[2]=v*s+b*u+w*f+_*g,t[3]=v*a+b*c+w*d+_*x,t[4]=(v=r[4])*n+(b=r[5])*o+(w=r[6])*h+(_=r[7])*y,t[5]=v*i+b*l+w*p+_*m,t[6]=v*s+b*u+w*f+_*g,t[7]=v*a+b*c+w*d+_*x,t[8]=(v=r[8])*n+(b=r[9])*o+(w=r[10])*h+(_=r[11])*y,t[9]=v*i+b*l+w*p+_*m,t[10]=v*s+b*u+w*f+_*g,t[11]=v*a+b*c+w*d+_*x,t[12]=(v=r[12])*n+(b=r[13])*o+(w=r[14])*h+(_=r[15])*y,t[13]=v*i+b*l+w*p+_*m,t[14]=v*s+b*u+w*f+_*g,t[15]=v*a+b*c+w*d+_*x,t},t.P=r,t.Q=function(t,e){const r={};for(let n=0;n[t.id,t])));a.add=Array.from(r.values());}if(e.update){const t=new Map(null===(r=a.update)||void 0===r?void 0:r.map((t=>[t.id,t])));for(const r of e.update){const e=null!==(n=t.get(r.id))&&void 0!==n?n:{id:r.id};r.newGeometry&&(e.newGeometry=r.newGeometry),r.addOrUpdateProperties&&(e.addOrUpdateProperties=(null!==(i=e.addOrUpdateProperties)&&void 0!==i?i:[]).concat(r.addOrUpdateProperties)),r.removeProperties&&(e.removeProperties=(null!==(s=e.removeProperties)&&void 0!==s?s:[]).concat(r.removeProperties)),r.removeAllProperties&&(e.removeAllProperties=!0),t.set(r.id,e);}a.update=Array.from(t.values());}return a},t.a1=xh,t.a2=kh,t.a3=25,t.a4=bh,t.a5=t=>{const e=window.document.createElement("video");return e.muted=!0,new Promise((r=>{e.onloadstart=()=>{r(e);};for(const r of t){const t=window.document.createElement("source");pt(r)||(e.crossOrigin="Anonymous"),t.src=r,e.appendChild(t);}}))},t.a6=Ct,t.a7=function(){return D++},t.a8=va,t.a9=Yc,t.aA=function(t){let e=1/0,r=1/0,n=-1/0,i=-1/0;for(const s of t)e=Math.min(e,s.x),r=Math.min(r,s.y),n=Math.max(n,s.x),i=Math.max(i,s.y);return [e,r,n,i]},t.aB=Zu,t.aC=C,t.aD=function(t,e,r,n,i=!1){if(!r[0]&&!r[1])return [0,0];const s=i?"map"===n?-t.bearingInRadians:0:"viewport"===n?t.bearingInRadians:0;if(s){const t=Math.sin(s),e=Math.cos(s);r=[r[0]*e-r[1]*t,r[0]*t+r[1]*e];}return [i?r[0]:C(e,r[0],t.zoom),i?r[1]:C(e,r[1],t.zoom)]},t.aF=Uc,t.aG=tp,t.aH=zc,t.aI=ap,t.aJ=Zs,t.aK=Gl,t.aL=Ca,t.aM=Xa,t.aN=ja,t.aO=L,t.aP=et,t.aQ=gh,t.aR=b,t.aS=v,t.aT=function(t){var e=new f(3);return e[0]=t[0],e[1]=t[1],e[2]=t[2],e},t.aU=function(t,e,r){return t[0]=e[0]-r[0],t[1]=e[1]-r[1],t[2]=e[2]-r[2],t},t.aV=function(t,e){var r=e[0],n=e[1],i=e[2],s=r*r+n*n+i*i;return s>0&&(s=1/Math.sqrt(s)),t[0]=e[0]*s,t[1]=e[1]*s,t[2]=e[2]*s,t},t.aW=w,t.aX=function(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]},t.aY=function(t,e,r){return t[0]=e[0]*r[0],t[1]=e[1]*r[1],t[2]=e[2]*r[2],t[3]=e[3]*r[3],t},t.aZ=g,t.a_=function(t,e,r){const n=e[0]*r[0]+e[1]*r[1]+e[2]*r[2];return 0===n?null:(-(t[0]*r[0]+t[1]*r[1]+t[2]*r[2])-r[3])/n},t.aa=hi,t.ab=Mo,t.ac=Ih,t.ad=function(t){const e={};if(t.replace(/(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g,((t,r,n,i)=>{const s=n||i;return e[r]=!s||s.toLowerCase(),""})),e["max-age"]){const t=parseInt(e["max-age"],10);isNaN(t)?delete e["max-age"]:e["max-age"]=t;}return e},t.ae=tt,t.af=function(t){return Math.pow(2,t)},t.ag=y,t.ah=$,t.ai=85.051129,t.aj=yh,t.ak=function(t){return Math.log(t)/Math.LN2},t.al=function(t){var e=t[0],r=t[1];return e*e+r*r},t.am=function(t,e){const r=[];for(const n in t)n in e||r.push(n);return r},t.an=function(t,e){let r=0,n=0;if("constant"===t.kind)n=t.layoutSize;else if("source"!==t.kind){const{interpolationType:i,minZoom:s,maxZoom:a}=t,o=i?$(pr.interpolationFactor(i,e,s,a),0,1):0;"camera"===t.kind?n=dr.number(t.minSize,t.maxSize,o):r=o;}return {uSizeT:r,uSize:n}},t.ap=function(t,{uSize:e,uSizeT:r},{lowerSize:n,upperSize:i}){return "source"===t.kind?n/Oc:"composite"===t.kind?dr.number(n/Oc,i/Oc,r):e},t.aq=function(t,e){var r=e[0],n=e[1],i=e[2],s=e[3],a=e[4],o=e[5],l=e[6],u=e[7],c=e[8],h=e[9],p=e[10],f=e[11],d=e[12],y=e[13],m=e[14],g=e[15],x=r*o-n*a,v=r*l-i*a,b=r*u-s*a,w=n*l-i*o,_=n*u-s*o,S=i*u-s*l,A=c*y-h*d,k=c*m-p*d,M=c*g-f*d,I=h*m-p*y,z=h*g-f*y,P=p*g-f*m,C=x*P-v*z+b*I+w*M-_*k+S*A;return C?(t[0]=(o*P-l*z+u*I)*(C=1/C),t[1]=(i*z-n*P-s*I)*C,t[2]=(y*S-m*_+g*w)*C,t[3]=(p*_-h*S-f*w)*C,t[4]=(l*M-a*P-u*k)*C,t[5]=(r*P-i*M+s*k)*C,t[6]=(m*b-d*S-g*v)*C,t[7]=(c*S-p*b+f*v)*C,t[8]=(a*z-o*M+u*A)*C,t[9]=(n*M-r*z-s*A)*C,t[10]=(d*_-y*b+g*x)*C,t[11]=(h*b-c*_-f*x)*C,t[12]=(o*k-a*I-l*A)*C,t[13]=(r*I-n*k+i*A)*C,t[14]=(y*v-d*w-m*x)*C,t[15]=(c*w-h*v+p*x)*C,t):null},t.ar=I,t.as=function(t){var e=t[0],r=t[1];return Math.sqrt(e*e+r*r)},t.at=function(t){return t[0]=0,t[1]=0,t},t.au=function(t,e,r){return t[0]=e[0]*r,t[1]=e[1]*r,t},t.av=qc,t.aw=A,t.ax=function(t,e,n,i){const s=e.y-t.y,a=e.x-t.x,o=i.y-n.y,l=i.x-n.x,u=o*a-l*s;if(0===u)return null;const c=(l*(t.y-n.y)-o*(t.x-n.x))/u;return new r(t.x+c*a,t.y+c*s)},t.ay=Eh,t.az=Co,t.b=Y,t.b$=class extends oa{},t.b0=function(t,e,r){return t[0]=e[0]*r,t[1]=e[1]*r,t[2]=e[2]*r,t[3]=e[3]*r,t},t.b1=function(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]},t.b2=wh,t.b3=Sh,t.b4=function(t,e,r,n,i){var s=1/Math.tan(e/2);if(t[0]=s/r,t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=s,t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[11]=-1,t[12]=0,t[13]=0,t[15]=0,null!=i&&i!==1/0){var a=1/(n-i);t[10]=(i+n)*a,t[14]=2*i*n*a;}else t[10]=-1,t[14]=-2*n;return t},t.b5=function(t){var e=new f(16);return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4],e[5]=t[5],e[6]=t[6],e[7]=t[7],e[8]=t[8],e[9]=t[9],e[10]=t[10],e[11]=t[11],e[12]=t[12],e[13]=t[13],e[14]=t[14],e[15]=t[15],e},t.b6=function(t,e,r){var n=Math.sin(r),i=Math.cos(r),s=e[0],a=e[1],o=e[2],l=e[3],u=e[4],c=e[5],h=e[6],p=e[7];return e!==t&&(t[8]=e[8],t[9]=e[9],t[10]=e[10],t[11]=e[11],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[0]=s*i+u*n,t[1]=a*i+c*n,t[2]=o*i+h*n,t[3]=l*i+p*n,t[4]=u*i-s*n,t[5]=c*i-a*n,t[6]=h*i-o*n,t[7]=p*i-l*n,t},t.b7=function(t,e,r){var n=Math.sin(r),i=Math.cos(r),s=e[4],a=e[5],o=e[6],l=e[7],u=e[8],c=e[9],h=e[10],p=e[11];return e!==t&&(t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[4]=s*i+u*n,t[5]=a*i+c*n,t[6]=o*i+h*n,t[7]=l*i+p*n,t[8]=u*i-s*n,t[9]=c*i-a*n,t[10]=h*i-o*n,t[11]=p*i-l*n,t},t.b8=function(){const t=new Float32Array(16);return y(t),t},t.b9=function(){const t=new Float64Array(16);return y(t),t},t.bA=function(t,e){const r=E(t,360),n=E(e,360),i=n-r,s=n>r?i-360:i+360;return Math.abs(i)0?a:-a},t.bD=function(t,e){const r=E(t,2*Math.PI),n=E(e,2*Math.PI);return Math.min(Math.abs(r-n),Math.abs(r-n+2*Math.PI),Math.abs(r-n-2*Math.PI))},t.bE=function(){const t={},e=xt.$version;for(const r in xt.$root){const n=xt.$root[r];if(n.required){let i=null;i="version"===r?e:"array"===n.type?[]:{},null!=i&&(t[r]=i);}}return t},t.bF=ps,t.bG=ct,t.bH=function t(e,r){if(Array.isArray(e)){if(!Array.isArray(r)||e.length!==r.length)return !1;for(let n=0;n{"source"in t&&n[t.source]?r.push({command:"removeLayer",args:[t.id]}):s.push(t);})),r=r.concat(i),function(t,e,r){e=e||[];const n=(t=t||[]).map(zt),i=e.map(zt),s=t.reduce(Pt,{}),a=e.reduce(Pt,{}),o=n.slice(),l=Object.create(null);let u,c,h,p,f;for(let t=0,e=0;tp?(i=Math.acos(s),a=Math.sin(i),o=Math.sin((1-n)*i)/a,l=Math.sin(n*i)/a):(o=1-n,l=n),t[0]=o*u+l*d,t[1]=o*c+l*y,t[2]=o*h+l*m,t[3]=o*f+l*g,t},t.bd=function(t){const e=new Float64Array(9);var r,n,i,s,a,o,l,u,c,h,p,f,d,y,m,g,x,v;h=(i=(n=t)[0])*(l=i+i),p=(s=n[1])*l,d=(a=n[2])*l,y=a*(u=s+s),g=(o=n[3])*l,x=o*u,v=o*(c=a+a),(r=e)[0]=1-(f=s*u)-(m=a*c),r[3]=p-v,r[6]=d+x,r[1]=p+v,r[4]=1-h-m,r[7]=y-g,r[2]=d-x,r[5]=y+g,r[8]=1-h-f;const b=et(-Math.asin($(e[2],-1,1)));let w,_;return Math.hypot(e[5],e[8])<.001?(w=0,_=-et(Math.atan2(e[3],e[4]))):(w=et(0===e[5]&&0===e[8]?0:Math.atan2(e[5],e[8])),_=et(0===e[1]&&0===e[0]?0:Math.atan2(e[1],e[0]))),{roll:w,pitch:b+90,bearing:_}},t.be=function(t,e){return t.roll==e.roll&&t.pitch==e.pitch&&t.bearing==e.bearing},t.bf=Me,t.bg=lo,t.bh=Xl,t.bi=Zl,t.bj=ql,t.bk=B,t.bl=V,t.bm=Oe,t.bn=function(t,e,r,n,i){return B(n,i,$((t-e)/(r-e),0,1))},t.bo=E,t.bp=function(){return new Float64Array(3)},t.bq=function(t,e,r,n){return t[0]=e[0]+r[0]*n,t[1]=e[1]+r[1]*n,t[2]=e[2]+r[2]*n,t},t.br=M,t.bs=function(t,e,r){var n=r[0],i=r[1],s=r[2],a=r[3],o=e[0],l=e[1],u=e[2],c=i*u-s*l,h=s*o-n*u,p=n*l-i*o;return t[0]=o+a*(c+=c)+i*(p+=p)-s*(h+=h),t[1]=l+a*h+s*c-n*p,t[2]=u+a*p+n*h-i*c,t},t.bt=function(t,e,r){const n=(i=[t[0],t[1],t[2],e[0],e[1],e[2],r[0],r[1],r[2]])[0]*((c=i[8])*(a=i[4])-(o=i[5])*(u=i[7]))+i[1]*(-c*(s=i[3])+o*(l=i[6]))+i[2]*(u*s-a*l);var i,s,a,o,l,u,c;if(0===n)return null;const h=w([],[e[0],e[1],e[2]],[r[0],r[1],r[2]]),p=w([],[r[0],r[1],r[2]],[t[0],t[1],t[2]]),f=w([],[t[0],t[1],t[2]],[e[0],e[1],e[2]]),d=b([],h,-t[3]);return v(d,d,b([],p,-e[3])),v(d,d,b([],f,-r[3])),b(d,d,1/n),d},t.bu=uh,t.bv=function(){return new Float64Array(4)},t.bw=function(t,e,r,n){var i=[],s=[];return i[0]=e[0]-r[0],i[1]=e[1]-r[1],i[2]=e[2]-r[2],s[0]=i[0]*Math.cos(n)-i[1]*Math.sin(n),s[1]=i[0]*Math.sin(n)+i[1]*Math.cos(n),s[2]=i[2],t[0]=s[0]+r[0],t[1]=s[1]+r[1],t[2]=s[2]+r[2],t},t.bx=function(t,e,r,n){var i=[],s=[];return i[0]=e[0]-r[0],i[1]=e[1]-r[1],i[2]=e[2]-r[2],s[0]=i[0],s[1]=i[1]*Math.cos(n)-i[2]*Math.sin(n),s[2]=i[1]*Math.sin(n)+i[2]*Math.cos(n),t[0]=s[0]+r[0],t[1]=s[1]+r[1],t[2]=s[2]+r[2],t},t.by=function(t,e,r,n){var i=[],s=[];return i[0]=e[0]-r[0],i[1]=e[1]-r[1],i[2]=e[2]-r[2],s[0]=i[2]*Math.sin(n)+i[0]*Math.cos(n),s[1]=i[1],s[2]=i[2]*Math.cos(n)-i[0]*Math.sin(n),t[0]=s[0]+r[0],t[1]=s[1]+r[1],t[2]=s[2]+r[2],t},t.bz=function(t,e,r){var n=Math.sin(r),i=Math.cos(r),s=e[0],a=e[1],o=e[2],l=e[3],u=e[8],c=e[9],h=e[10],p=e[11];return e!==t&&(t[4]=e[4],t[5]=e[5],t[6]=e[6],t[7]=e[7],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15]),t[0]=s*i-u*n,t[1]=a*i-c*n,t[2]=o*i-h*n,t[3]=l*i-p*n,t[8]=s*n+u*i,t[9]=a*n+c*i,t[10]=o*n+h*i,t[11]=l*n+p*i,t},t.c=st,t.c0=qu,t.c1=class extends ua{},t.c2=il,t.c3=function(t){return t<=1?1:Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))},t.c4=nl,t.c5=function(t,e,r){var n=e[0],i=e[1],s=e[2],a=r[3]*n+r[7]*i+r[11]*s+r[15];return t[0]=(r[0]*n+r[4]*i+r[8]*s+r[12])/(a=a||1),t[1]=(r[1]*n+r[5]*i+r[9]*s+r[13])/a,t[2]=(r[2]*n+r[6]*i+r[10]*s+r[14])/a,t},t.c6=class extends Js{},t.c7=class extends ma{},t.c8=function(t,e){return t[0]===e[0]&&t[1]===e[1]&&t[2]===e[2]&&t[3]===e[3]&&t[4]===e[4]&&t[5]===e[5]&&t[6]===e[6]&&t[7]===e[7]&&t[8]===e[8]&&t[9]===e[9]&&t[10]===e[10]&&t[11]===e[11]&&t[12]===e[12]&&t[13]===e[13]&&t[14]===e[14]&&t[15]===e[15]},t.c9=function(t,e){var r=t[0],n=t[1],i=t[2],s=t[3],a=t[4],o=t[5],l=t[6],u=t[7],c=t[8],h=t[9],f=t[10],d=t[11],y=t[12],m=t[13],g=t[14],x=t[15],v=e[0],b=e[1],w=e[2],_=e[3],S=e[4],A=e[5],k=e[6],M=e[7],I=e[8],z=e[9],P=e[10],C=e[11],E=e[12],B=e[13],V=e[14],T=e[15];return Math.abs(r-v)<=p*Math.max(1,Math.abs(r),Math.abs(v))&&Math.abs(n-b)<=p*Math.max(1,Math.abs(n),Math.abs(b))&&Math.abs(i-w)<=p*Math.max(1,Math.abs(i),Math.abs(w))&&Math.abs(s-_)<=p*Math.max(1,Math.abs(s),Math.abs(_))&&Math.abs(a-S)<=p*Math.max(1,Math.abs(a),Math.abs(S))&&Math.abs(o-A)<=p*Math.max(1,Math.abs(o),Math.abs(A))&&Math.abs(l-k)<=p*Math.max(1,Math.abs(l),Math.abs(k))&&Math.abs(u-M)<=p*Math.max(1,Math.abs(u),Math.abs(M))&&Math.abs(c-I)<=p*Math.max(1,Math.abs(c),Math.abs(I))&&Math.abs(h-z)<=p*Math.max(1,Math.abs(h),Math.abs(z))&&Math.abs(f-P)<=p*Math.max(1,Math.abs(f),Math.abs(P))&&Math.abs(d-C)<=p*Math.max(1,Math.abs(d),Math.abs(C))&&Math.abs(y-E)<=p*Math.max(1,Math.abs(y),Math.abs(E))&&Math.abs(m-B)<=p*Math.max(1,Math.abs(m),Math.abs(B))&&Math.abs(g-V)<=p*Math.max(1,Math.abs(g),Math.abs(V))&&Math.abs(x-T)<=p*Math.max(1,Math.abs(x),Math.abs(T))},t.cA=function(t,e){at.REGISTERED_PROTOCOLS[t]=e;},t.cB=function(t){delete at.REGISTERED_PROTOCOLS[t];},t.cC=function(t,e){const r={};for(let n=0;nt*Zu));}let v=o?"center":n.get("text-justify").evaluate(i,{},e.canonical);const b="point"===n.get("symbol-placement")?n.get("text-max-width").evaluate(i,{},e.canonical)*Zu:1/0,w=()=>{e.bucket.allowVerticalPlacement&&ds(s)&&(d.vertical=xc(y,e.glyphMap,e.glyphPositions,e.imagePositions,c,b,a,m,"left",f,g,t.ao.vertical,!0,p,h));};if(!o&&x){const r=new Set;if("auto"===v)for(let t=0;t0||(null===(i=r.addOrUpdateProperties)||void 0===i?void 0:i.length)>0);if((r.newGeometry||r.removeAllProperties||o)&&(e=Object.assign({},e),t.set(r.id,e),o&&(e.properties=Object.assign({},e.properties))),r.newGeometry&&(e.geometry=r.newGeometry),r.removeAllProperties)e.properties={};else if((null===(s=r.removeProperties)||void 0===s?void 0:s.length)>0)for(const t of r.removeProperties)Object.prototype.hasOwnProperty.call(e.properties,t)&&delete e.properties[t];if((null===(a=r.addOrUpdateProperties)||void 0===a?void 0:a.length)>0)for(const{key:t,value:n}of r.addOrUpdateProperties)e.properties[t]=n;}},t.cX=Ms,t.ca=function(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t[6]=e[6],t[7]=e[7],t[8]=e[8],t[9]=e[9],t[10]=e[10],t[11]=e[11],t[12]=e[12],t[13]=e[13],t[14]=e[14],t[15]=e[15],t},t.cb=t=>"symbol"===t.type,t.cc=t=>"circle"===t.type,t.cd=t=>"heatmap"===t.type,t.ce=t=>"line"===t.type,t.cf=t=>"fill"===t.type,t.cg=t=>"fill-extrusion"===t.type,t.ch=t=>"hillshade"===t.type,t.ci=t=>"color-relief"===t.type,t.cj=t=>"raster"===t.type,t.ck=t=>"background"===t.type,t.cl=t=>"custom"===t.type,t.cm=T,t.cn=function(t,e,r){const n=z(e.x-r.x,e.y-r.y),i=z(t.x-r.x,t.y-r.y);var s,a;return et(Math.atan2(n[0]*i[1]-n[1]*i[0],(s=n)[0]*(a=i)[0]+s[1]*a[1]))},t.co=F,t.cp=function(t,e){return nt[e]&&(t instanceof MouseEvent||t instanceof WheelEvent)},t.cq=function(t,e){return rt[e]&&"touches"in t},t.cr=function(t){return rt[t]||nt[t]},t.cs=function(t,e,r){var n=e[0],i=e[1];return t[0]=r[0]*n+r[4]*i+r[12],t[1]=r[1]*n+r[5]*i+r[13],t},t.ct=function(t,e){const{x:r,y:n}=xh.fromLngLat(e);return !(t<0||t>25||n<0||n>=1||r<0||r>=1)},t.cu=function(t,e){return t[0]=e[0],t[1]=0,t[2]=0,t[3]=0,t[4]=0,t[5]=e[1],t[6]=0,t[7]=0,t[8]=0,t[9]=0,t[10]=e[2],t[11]=0,t[12]=0,t[13]=0,t[14]=0,t[15]=1,t},t.cv=class extends Ks{},t.cw=xp,t.cy=function(t){return t.message===it},t.cz=ut,t.d=pt,t.e=O,t.f=t=>e(void 0,void 0,void 0,(function*(){if(0===t.byteLength)return createImageBitmap(new ImageData(1,1));const e=new Blob([new Uint8Array(t)],{type:"image/png"});try{return createImageBitmap(e)}catch(t){throw new Error(`Could not load image because of ${t.message}. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`)}})),t.g=ot,t.h=t=>new Promise(((e,r)=>{const n=new Image;n.onload=()=>{e(n),URL.revokeObjectURL(n.src),n.onload=null,window.requestAnimationFrame((()=>{n.src=H;}));},n.onerror=()=>r(new Error("Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported."));const i=new Blob([new Uint8Array(t)],{type:"image/png"});n.src=t.byteLength?URL.createObjectURL(i):H;})),t.i=X,t.j=(t,e)=>ht(O(t,{type:"json"}),e),t.k=mt,t.l=yt,t.m=ht,t.n=(t,e)=>ht(O(t,{type:"arrayBuffer"}),e),t.o=function(t){return new Ju(t).readFields(uc,[])},t.p=pc,t.q=el,t.r=Us,t.s=Q,t.t=Cs,t.u=fs,t.v=xt,t.w=q,t.x=Qi,t.y=ns,t.z=Wi;})); + +define("worker",["./shared"],(function(e){"use strict";class t{constructor(e){this.keyCache={},e&&this.replace(e);}replace(e){this._layerConfigs={},this._layers={},this.update(e,[]);}update(t,i){for(const i of t){this._layerConfigs[i.id]=i;const t=this._layers[i.id]=e.bJ(i);t._featureFilter=e.aa(t.filter),this.keyCache[i.id]&&delete this.keyCache[i.id];}for(const e of i)delete this.keyCache[e],delete this._layerConfigs[e],delete this._layers[e];this.familiesBySource={};const o=e.cC(Object.values(this._layerConfigs),this.keyCache);for(const e of o){const t=e.map((e=>this._layers[e.id])),i=t[0];if("none"===i.visibility)continue;const o=i.source||"";let s=this.familiesBySource[o];s||(s=this.familiesBySource[o]={});const n=i.sourceLayer||"_geojsonTileLayer";let r=s[n];r||(r=s[n]=[]),r.push(t);}}}class i{constructor(t){const i={},o=[];for(const e in t){const s=t[e],n=i[e]={};for(const e in s){const t=s[+e];if(!t||0===t.bitmap.width||0===t.bitmap.height)continue;const i={x:0,y:0,w:t.bitmap.width+2,h:t.bitmap.height+2};o.push(i),n[e]={rect:i,metrics:t.metrics};}}const{w:s,h:n}=e.p(o),r=new e.q({width:s||1,height:n||1});for(const o in t){const s=t[o];for(const t in s){const n=s[+t];if(!n||0===n.bitmap.width||0===n.bitmap.height)continue;const a=i[o][t].rect;e.q.copy(n.bitmap,r,{x:0,y:0},{x:a.x+1,y:a.y+1},n.bitmap);}}this.image=r,this.positions=i;}}e.cD("GlyphAtlas",i);class o{constructor(t){this.tileID=new e.Z(t.tileID.overscaledZ,t.tileID.wrap,t.tileID.canonical.z,t.tileID.canonical.x,t.tileID.canonical.y),this.uid=t.uid,this.zoom=t.zoom,this.pixelRatio=t.pixelRatio,this.tileSize=t.tileSize,this.source=t.source,this.overscaling=this.tileID.overscaleFactor(),this.showCollisionBoxes=t.showCollisionBoxes,this.collectResourceTiming=!!t.collectResourceTiming,this.returnDependencies=!!t.returnDependencies,this.promoteId=t.promoteId,this.inFlightDependencies=[],this.globalState=t.globalState;}parse(t,o,n,r,a){return e._(this,void 0,void 0,(function*(){this.status="parsing",this.data=t,this.collisionBoxArray=new e.a8;const l=new e.cE(Object.keys(t.layers).sort()),c=new e.cF(this.tileID,this.promoteId);c.bucketLayerIDs=[];const u={},h={featureIndex:c,iconDependencies:{},patternDependencies:{},glyphDependencies:{},availableImages:n,subdivisionGranularity:a},d=o.familiesBySource[this.source];for(const i in d){const o=t.layers[i];if(!o)continue;1===o.version&&e.w(`Vector tile source "${this.source}" layer "${i}" does not use vector tile spec v2 and therefore may have some rendering errors.`);const r=l.encode(i),a=[];for(let e=0;e=i.maxzoom||"none"!==i.visibility&&(s(t,this.zoom,n,this.globalState),(u[i.id]=i.createBucket({index:c.bucketLayerIDs.length,layers:t,zoom:this.zoom,pixelRatio:this.pixelRatio,overscaling:this.overscaling,collisionBoxArray:this.collisionBoxArray,sourceLayerIndex:r,sourceID:this.source,globalState:this.globalState})).populate(a,h,this.tileID.canonical),c.bucketLayerIDs.push(t.map((e=>e.id))));}}const f=e.bN(h.glyphDependencies,(e=>Object.keys(e).map(Number)));this.inFlightDependencies.forEach((e=>null==e?void 0:e.abort())),this.inFlightDependencies=[];let g=Promise.resolve({});if(Object.keys(f).length){const e=new AbortController;this.inFlightDependencies.push(e),g=r.sendAsync({type:"GG",data:{stacks:f,source:this.source,tileID:this.tileID,type:"glyphs"}},e);}const p=Object.keys(h.iconDependencies);let m=Promise.resolve({});if(p.length){const e=new AbortController;this.inFlightDependencies.push(e),m=r.sendAsync({type:"GI",data:{icons:p,source:this.source,tileID:this.tileID,type:"icons"}},e);}const y=Object.keys(h.patternDependencies);let v=Promise.resolve({});if(y.length){const e=new AbortController;this.inFlightDependencies.push(e),v=r.sendAsync({type:"GI",data:{icons:y,source:this.source,tileID:this.tileID,type:"patterns"}},e);}const[w,x,b]=yield Promise.all([g,m,v]),S=new i(w),M=new e.cG(x,b);for(const t in u){const i=u[t];i instanceof e.a9?(s(i.layers,this.zoom,n,this.globalState),e.cH({bucket:i,glyphMap:w,glyphPositions:S.positions,imageMap:x,imagePositions:M.iconPositions,showCollisionBoxes:this.showCollisionBoxes,canonical:this.tileID.canonical,subdivisionGranularity:h.subdivisionGranularity})):i.hasPattern&&(i instanceof e.cI||i instanceof e.cJ||i instanceof e.cK)&&(s(i.layers,this.zoom,n,this.globalState),i.addFeatures(h,this.tileID.canonical,M.patternPositions));}return this.status="done",{buckets:Object.values(u).filter((e=>!e.isEmpty())),featureIndex:c,collisionBoxArray:this.collisionBoxArray,glyphAtlasImage:S.image,imageAtlas:M,glyphMap:this.returnDependencies?w:null,iconMap:this.returnDependencies?x:null,glyphPositions:this.returnDependencies?S.positions:null}}))}}function s(t,i,o,s){const n=new e.F(i,{globalState:s});for(const e of t)e.recalculate(n,o);}class n{constructor(e,t,i){this.actor=e,this.layerIndex=t,this.availableImages=i,this.fetching={},this.loading={},this.loaded={};}loadVectorTile(t,i){return e._(this,void 0,void 0,(function*(){const o=yield e.n(t.request,i);try{return {vectorTile:new e.cL(new e.cM(o.data)),rawData:o.data,cacheControl:o.cacheControl,expires:o.expires}}catch(e){const i=new Uint8Array(o.data);let s=`Unable to parse the tile at ${t.request.url}, `;throw s+=31===i[0]&&139===i[1]?"please make sure the data is not gzipped and that you have configured the relevant header in the server":`got error: ${e.message}`,new Error(s)}}))}loadTile(t){return e._(this,void 0,void 0,(function*(){const i=t.uid,s=!!(t&&t.request&&t.request.collectResourceTiming)&&new e.cN(t.request),n=new o(t);this.loading[i]=n;const r=new AbortController;n.abort=r;try{const o=yield this.loadVectorTile(t,r);if(delete this.loading[i],!o)return null;const a=o.rawData,l={};o.expires&&(l.expires=o.expires),o.cacheControl&&(l.cacheControl=o.cacheControl);const c={};if(s){const e=s.finish();e&&(c.resourceTiming=JSON.parse(JSON.stringify(e)));}n.vectorTile=o.vectorTile;const u=n.parse(o.vectorTile,this.layerIndex,this.availableImages,this.actor,t.subdivisionGranularity);this.loaded[i]=n,this.fetching[i]={rawTileData:a,cacheControl:l,resourceTiming:c};try{const t=yield u;return e.e({rawTileData:a.slice(0)},t,l,c)}finally{delete this.fetching[i];}}catch(e){throw delete this.loading[i],n.status="done",this.loaded[i]=n,e}}))}reloadTile(t){return e._(this,void 0,void 0,(function*(){const i=t.uid;if(!this.loaded||!this.loaded[i])throw new Error("Should not be trying to reload a tile that was never loaded or has been removed");const o=this.loaded[i];if(o.showCollisionBoxes=t.showCollisionBoxes,o.globalState=t.globalState,"parsing"===o.status){const s=yield o.parse(o.vectorTile,this.layerIndex,this.availableImages,this.actor,t.subdivisionGranularity);let n;if(this.fetching[i]){const{rawTileData:t,cacheControl:o,resourceTiming:r}=this.fetching[i];delete this.fetching[i],n=e.e({rawTileData:t.slice(0)},s,o,r);}else n=s;return n}if("done"===o.status&&o.vectorTile)return o.parse(o.vectorTile,this.layerIndex,this.availableImages,this.actor,t.subdivisionGranularity)}))}abortTile(t){return e._(this,void 0,void 0,(function*(){const e=this.loading,i=t.uid;e&&e[i]&&e[i].abort&&(e[i].abort.abort(),delete e[i]);}))}removeTile(t){return e._(this,void 0,void 0,(function*(){this.loaded&&this.loaded[t.uid]&&delete this.loaded[t.uid];}))}}class r{constructor(){this.loaded={};}loadTile(t){return e._(this,void 0,void 0,(function*(){const{uid:i,encoding:o,rawImageData:s,redFactor:n,greenFactor:r,blueFactor:a,baseShift:l}=t,c=s.width+2,u=s.height+2,h=e.b(s)?new e.R({width:c,height:u},yield e.cO(s,-1,-1,c,u)):s,d=new e.cP(i,h,o,n,r,a,l);return this.loaded=this.loaded||{},this.loaded[i]=d,d}))}removeTile(e){const t=this.loaded,i=e.uid;t&&t[i]&&delete t[i];}}var a,l,c=function(){if(l)return a;function e(e,i){if(0!==e.length){t(e[0],i);for(var o=1;o=Math.abs(a)?i-l+a:a-l+i,i=l;}i+o>=0!=!!t&&e.reverse();}return l=1,a=function t(i,o){var s,n=i&&i.type;if("FeatureCollection"===n)for(s=0;s>31}function v(e,t){const i=e.loadGeometry(),o=e.type;let s=0,n=0;for(const r of i){let i=1;1===o&&(i=r.length),t.writeVarint(m(1,i));const a=3===o?r.length-1:r.length;for(let e=0;ee},b=Math.fround||(S=new Float32Array(1),e=>(S[0]=+e,S[0]));var S;class M{constructor(e){this.options=Object.assign(Object.create(x),e),this.trees=new Array(this.options.maxZoom+1),this.stride=this.options.reduce?7:6,this.clusterProps=[];}load(e){const{log:t,minZoom:i,maxZoom:o}=this.options;t&&console.time("total time");const s=`prepare ${e.length} points`;t&&console.time(s),this.points=e;const n=[];for(let t=0;t=i;e--){const i=+Date.now();r=this.trees[e]=this._createTree(this._cluster(r,e)),t&&console.log("z%d: %d clusters in %dms",e,r.numItems,+Date.now()-i);}return t&&console.timeEnd("total time"),this}getClusters(e,t){let i=((e[0]+180)%360+360)%360-180;const o=Math.max(-90,Math.min(90,e[1]));let s=180===e[2]?180:((e[2]+180)%360+360)%360-180;const n=Math.max(-90,Math.min(90,e[3]));if(e[2]-e[0]>=360)i=-180,s=180;else if(i>s){const e=this.getClusters([i,o,180,n],t),r=this.getClusters([-180,o,s,n],t);return e.concat(r)}const r=this.trees[this._limitZoom(t)],a=r.range(k(i),P(n),k(s),P(o)),l=r.data,c=[];for(const e of a){const t=this.stride*e;c.push(l[t+5]>1?_(l,t,this.clusterProps):this.points[l[t+3]]);}return c}getChildren(e){const t=this._getOriginId(e),i=this._getOriginZoom(e),o="No cluster with the specified id.",s=this.trees[i];if(!s)throw new Error(o);const n=s.data;if(t*this.stride>=n.length)throw new Error(o);const r=this.options.radius/(this.options.extent*Math.pow(2,i-1)),a=s.within(n[t*this.stride],n[t*this.stride+1],r),l=[];for(const t of a){const i=t*this.stride;n[i+4]===e&&l.push(n[i+5]>1?_(n,i,this.clusterProps):this.points[n[i+3]]);}if(0===l.length)throw new Error(o);return l}getLeaves(e,t,i){const o=[];return this._appendLeaves(o,e,t=t||10,i=i||0,0),o}getTile(e,t,i){const o=this.trees[this._limitZoom(e)],s=Math.pow(2,e),{extent:n,radius:r}=this.options,a=r/n,l=(i-a)/s,c=(i+1+a)/s,u={features:[]};return this._addTileFeatures(o.range((t-a)/s,l,(t+1+a)/s,c),o.data,t,i,s,u),0===t&&this._addTileFeatures(o.range(1-a/s,l,1,c),o.data,s,i,s,u),t===s-1&&this._addTileFeatures(o.range(0,l,a/s,c),o.data,-1,i,s,u),u.features.length?u:null}getClusterExpansionZoom(e){let t=this._getOriginZoom(e)-1;for(;t<=this.options.maxZoom;){const i=this.getChildren(e);if(t++,1!==i.length)break;e=i[0].properties.cluster_id;}return t}_appendLeaves(e,t,i,o,s){const n=this.getChildren(t);for(const t of n){const n=t.properties;if(n&&n.cluster?s+n.point_count<=o?s+=n.point_count:s=this._appendLeaves(e,n.cluster_id,i,o,s):s1;let l,c,u;if(a)l=I(t,e,this.clusterProps),c=t[e],u=t[e+1];else {const i=this.points[t[e+3]];l=i.properties;const[o,s]=i.geometry.coordinates;c=k(o),u=P(s);}const h={type:1,geometry:[[Math.round(this.options.extent*(c*s-i)),Math.round(this.options.extent*(u*s-o))]],tags:l};let d;d=a||this.options.generateId?t[e+3]:this.points[t[e+3]].id,void 0!==d&&(h.id=d),n.features.push(h);}}_limitZoom(e){return Math.max(this.options.minZoom,Math.min(Math.floor(+e),this.options.maxZoom+1))}_cluster(e,t){const{radius:i,extent:o,reduce:s,minPoints:n}=this.options,r=i/(o*Math.pow(2,t)),a=e.data,l=[],c=this.stride;for(let i=0;it&&(f+=a[i+5]);}if(f>d&&f>=n){let e,n=o*d,r=u*d,g=-1;const p=(i/c<<5)+(t+1)+this.points.length;for(const o of h){const l=o*c;if(a[l+2]<=t)continue;a[l+2]=t;const u=a[l+5];n+=a[l]*u,r+=a[l+1]*u,a[l+4]=p,s&&(e||(e=this._map(a,i,!0),g=this.clusterProps.length,this.clusterProps.push(e)),s(e,this._map(a,l)));}a[i+4]=p,l.push(n/f,r/f,1/0,p,-1,f),s&&l.push(g);}else {for(let e=0;e1)for(const e of h){const i=e*c;if(!(a[i+2]<=t)){a[i+2]=t;for(let e=0;e>5}_getOriginZoom(e){return (e-this.points.length)%32}_map(e,t,i){if(e[t+5]>1){const o=this.clusterProps[e[t+6]];return i?Object.assign({},o):o}const o=this.points[e[t+3]].properties,s=this.options.map(o);return i&&s===o?Object.assign({},s):s}}function _(e,t,i){return {type:"Feature",id:e[t+3],properties:I(e,t,i),geometry:{type:"Point",coordinates:[(o=e[t],360*(o-.5)),T(e[t+1])]}};var o;}function I(e,t,i){const o=e[t+5],s=o>=1e4?`${Math.round(o/1e3)}k`:o>=1e3?Math.round(o/100)/10+"k":o,n=e[t+6],r=-1===n?{}:Object.assign({},i[n]);return Object.assign(r,{cluster:!0,cluster_id:e[t+3],point_count:o,point_count_abbreviated:s})}function k(e){return e/360+.5}function P(e){const t=Math.sin(e*Math.PI/180),i=.5-.25*Math.log((1+t)/(1-t))/Math.PI;return i<0?0:i>1?1:i}function T(e){const t=(180-360*e)*Math.PI/180;return 360*Math.atan(Math.exp(t))/Math.PI-90}function D(e,t,i,o){let s=o;const n=t+(i-t>>1);let r,a=i-t;const l=e[t],c=e[t+1],u=e[i],h=e[i+1];for(let o=t+3;os)r=o,s=t;else if(t===s){const e=Math.abs(o-n);eo&&(r-t>3&&D(e,t,r,o),e[r+2]=s,i-r>3&&D(e,r,i,o));}function C(e,t,i,o,s,n){let r=s-i,a=n-o;if(0!==r||0!==a){const l=((e-i)*r+(t-o)*a)/(r*r+a*a);l>1?(i=s,o=n):l>0&&(i+=r*l,o+=a*l);}return r=e-i,a=t-o,r*r+a*a}function L(e,t,i,o){const s={id:null==e?null:e,type:t,geometry:i,tags:o,minX:1/0,minY:1/0,maxX:-1/0,maxY:-1/0};if("Point"===t||"MultiPoint"===t||"LineString"===t)O(s,i);else if("Polygon"===t)O(s,i[0]);else if("MultiLineString"===t)for(const e of i)O(s,e);else if("MultiPolygon"===t)for(const e of i)O(s,e[0]);return s}function O(e,t){for(let i=0;i0&&(r+=o?(s*l-a*n)/2:Math.sqrt(Math.pow(a-s,2)+Math.pow(l-n,2))),s=a,n=l;}const a=t.length-3;t[2]=1,D(t,0,a,i),t[a+2]=1,t.size=Math.abs(r),t.start=0,t.end=t.size;}function A(e,t,i,o){for(let s=0;s1?1:i}function G(e,t,i,o,s,n,r,a){if(o/=t,n>=(i/=t)&&r=o)return null;const l=[];for(const t of e){const e=t.geometry;let n=t.type;const r=0===s?t.minX:t.minY,c=0===s?t.maxX:t.maxY;if(r>=i&&c=o)continue;let u=[];if("Point"===n||"MultiPoint"===n)N(e,u,i,o,s);else if("LineString"===n)R(e,u,i,o,s,!1,a.lineMetrics);else if("MultiLineString"===n)J(e,u,i,o,s,!1);else if("Polygon"===n)J(e,u,i,o,s,!0);else if("MultiPolygon"===n)for(const t of e){const e=[];J(t,e,i,o,s,!0),e.length&&u.push(e);}if(u.length){if(a.lineMetrics&&"LineString"===n){for(const e of u)l.push(L(t.id,n,e,t.tags));continue}"LineString"!==n&&"MultiLineString"!==n||(1===u.length?(n="LineString",u=u[0]):n="MultiLineString"),"Point"!==n&&"MultiPoint"!==n||(n=3===u.length?"Point":"MultiPoint"),l.push(L(t.id,n,u,t.tags));}}return l.length?l:null}function N(e,t,i,o,s){for(let n=0;n=i&&r<=o&&Y(t,e[n],e[n+1],e[n+2]);}}function R(e,t,i,o,s,n,r){let a=W(e);const l=0===s?q:X;let c,u,h=e.start;for(let d=0;di&&(u=l(a,f,g,m,y,i),r&&(a.start=h+c*u)):v>o?w=i&&(u=l(a,f,g,m,y,i),x=!0),w>o&&v<=o&&(u=l(a,f,g,m,y,o),x=!0),!n&&x&&(r&&(a.end=h+c*u),t.push(a),a=W(e)),r&&(h+=c);}let d=e.length-3;const f=e[d],g=e[d+1],p=0===s?f:g;p>=i&&p<=o&&Y(a,f,g,e[d+2]),d=a.length-3,n&&d>=3&&(a[d]!==a[0]||a[d+1]!==a[1])&&Y(a,a[0],a[1],a[2]),a.length&&t.push(a);}function W(e){const t=[];return t.size=e.size,t.start=e.start,t.end=e.end,t}function J(e,t,i,o,s,n){for(const r of e)R(r,t,i,o,s,n,!1);}function Y(e,t,i,o){e.push(t,i,o);}function q(e,t,i,o,s,n){const r=(n-t)/(o-t);return Y(e,n,i+(s-i)*r,1),r}function X(e,t,i,o,s,n){const r=(n-i)/(s-i);return Y(e,t+(o-t)*r,n,1),r}function H(e,t){const i=[];for(let o=0;o0&&t.size<(s?r:o))return void(i.numPoints+=t.length/3);const a=[];for(let e=0;er)&&(i.numSimplified++,a.push(t[e],t[e+1])),i.numPoints++;s&&function(e,t){let i=0;for(let t=0,o=e.length,s=o-2;t0===t)for(let t=0,i=e.length;t24)throw new Error("maxZoom should be in the 0-24 range");if(t.promoteId&&t.generateId)throw new Error("promoteId and generateId cannot be used together.");let o=function(e,t){const i=[];if("FeatureCollection"===e.type)for(let o=0;o1&&console.time("creation"),d=this.tiles[h]=U(e,t,i,o,l),this.tileCoords.push({z:t,x:i,y:o}),c)){c>1&&(console.log("tile z%d-%d-%d (features: %d, points: %d, simplified: %d)",t,i,o,d.numFeatures,d.numPoints,d.numSimplified),console.timeEnd("creation"));const e=`z${t}`;this.stats[e]=(this.stats[e]||0)+1,this.total++;}if(d.source=e,null==s){if(t===l.indexMaxZoom||d.numPoints<=l.indexMaxPoints)continue}else {if(t===l.maxZoom||t===s)continue;if(null!=s){const e=s-t;if(i!==n>>e||o!==r>>e)continue}}if(d.source=null,0===e.length)continue;c>1&&console.time("clipping");const f=.5*l.buffer/l.extent,g=.5-f,p=.5+f,m=1+f;let y=null,v=null,w=null,x=null,b=G(e,u,i-f,i+p,0,d.minX,d.maxX,l),S=G(e,u,i+g,i+m,0,d.minX,d.maxX,l);e=null,b&&(y=G(b,u,o-f,o+p,1,d.minY,d.maxY,l),v=G(b,u,o+g,o+m,1,d.minY,d.maxY,l),b=null),S&&(w=G(S,u,o-f,o+p,1,d.minY,d.maxY,l),x=G(S,u,o+g,o+m,1,d.minY,d.maxY,l),S=null),c>1&&console.timeEnd("clipping"),a.push(y||[],t+1,2*i,2*o),a.push(v||[],t+1,2*i,2*o+1),a.push(w||[],t+1,2*i+1,2*o),a.push(x||[],t+1,2*i+1,2*o+1);}}getTile(e,t,i){e=+e,t=+t,i=+i;const o=this.options,{extent:s,debug:n}=o;if(e<0||e>24)return null;const r=1<1&&console.log("drilling down to z%d-%d-%d",e,t,i);let l,c=e,u=t,h=i;for(;!l&&c>0;)c--,u>>=1,h>>=1,l=this.tiles[ie(c,u,h)];return l&&l.source?(n>1&&(console.log("found parent tile z%d-%d-%d",c,u,h),console.time("drilling down")),this.splitTile(l.source,c,u,h,e,t,i),n>1&&console.timeEnd("drilling down"),this.tiles[a]?B(this.tiles[a],s):null):null}}function ie(e,t,i){return 32*((1<{r.properties=e;const t={};for(const e of a)t[e]=o[e].evaluate(n,r);return t},t.reduce=(e,t)=>{r.properties=t;for(const t of a)n.accumulated=e[t],e[t]=s[t].evaluate(n,r);},t}(t)).load(i.features):function(e,t){return new te(e,t)}(i,t.geojsonVtOptions),this.loaded={};const s={data:i};if(o){const e=o.finish();e&&(s.resourceTiming={},s.resourceTiming[t.source]=JSON.parse(JSON.stringify(e)));}return s}catch(t){if(delete this._pendingRequest,e.cy(t))return {abandoned:!0};throw t}}))}getData(){return e._(this,void 0,void 0,(function*(){return this._pendingData}))}reloadTile(e){const t=this.loaded;return t&&t[e.uid]?super.reloadTile(e):this.loadTile(e)}loadAndProcessGeoJSON(t,i){return e._(this,void 0,void 0,(function*(){let o=yield this.loadGeoJSON(t,i);if(delete this._pendingRequest,"object"!=typeof o)throw new Error(`Input data given to '${t.source}' is not a valid GeoJSON object.`);if(u(o,!0),t.filter){const i=e.cT(t.filter,{type:"boolean","property-type":"data-driven",overridable:!1,transition:!1});if("error"===i.result)throw new Error(i.value.map((e=>`${e.key}: ${e.message}`)).join(", "));const s=o.features.filter((e=>i.value.evaluate({zoom:0},e)));o={type:"FeatureCollection",features:s};}return o}))}loadGeoJSON(t,i){return e._(this,void 0,void 0,(function*(){const{promoteId:o}=t;if(t.request){const s=yield e.j(t.request,i);return this._dataUpdateable=e.cV(s.data,o)?e.cU(s.data,o):void 0,s.data}if("string"==typeof t.data)try{const i=JSON.parse(t.data);return this._dataUpdateable=e.cV(i,o)?e.cU(i,o):void 0,i}catch(e){throw new Error(`Input data given to '${t.source}' is not a valid GeoJSON object.`)}if(!t.dataDiff)throw new Error(`Input data given to '${t.source}' is not a valid GeoJSON object.`);if(!this._dataUpdateable)throw new Error(`Cannot update existing geojson data in ${t.source}`);return e.cW(this._dataUpdateable,t.dataDiff,o),{type:"FeatureCollection",features:Array.from(this._dataUpdateable.values())}}))}removeSource(t){return e._(this,void 0,void 0,(function*(){this._pendingRequest&&this._pendingRequest.abort();}))}getClusterExpansionZoom(e){return this._geoJSONIndex.getClusterExpansionZoom(e.clusterId)}getClusterChildren(e){return this._geoJSONIndex.getChildren(e.clusterId)}getClusterLeaves(e){return this._geoJSONIndex.getLeaves(e.clusterId,e.limit,e.offset)}}class se{constructor(t){this.self=t,this.actor=new e.J(t),this.layerIndexes={},this.availableImages={},this.workerSources={},this.demWorkerSources={},this.externalWorkerSourceTypes={},this.self.registerWorkerSource=(e,t)=>{if(this.externalWorkerSourceTypes[e])throw new Error(`Worker source with name "${e}" already registered.`);this.externalWorkerSourceTypes[e]=t;},this.self.addProtocol=e.cA,this.self.removeProtocol=e.cB,this.self.registerRTLTextPlugin=t=>{e.cX.setMethods(t);},this.actor.registerMessageHandler("LDT",((e,t)=>this._getDEMWorkerSource(e,t.source).loadTile(t))),this.actor.registerMessageHandler("RDT",((t,i)=>e._(this,void 0,void 0,(function*(){this._getDEMWorkerSource(t,i.source).removeTile(i);})))),this.actor.registerMessageHandler("GCEZ",((t,i)=>e._(this,void 0,void 0,(function*(){return this._getWorkerSource(t,i.type,i.source).getClusterExpansionZoom(i)})))),this.actor.registerMessageHandler("GCC",((t,i)=>e._(this,void 0,void 0,(function*(){return this._getWorkerSource(t,i.type,i.source).getClusterChildren(i)})))),this.actor.registerMessageHandler("GCL",((t,i)=>e._(this,void 0,void 0,(function*(){return this._getWorkerSource(t,i.type,i.source).getClusterLeaves(i)})))),this.actor.registerMessageHandler("LD",((e,t)=>this._getWorkerSource(e,t.type,t.source).loadData(t))),this.actor.registerMessageHandler("GD",((e,t)=>this._getWorkerSource(e,t.type,t.source).getData())),this.actor.registerMessageHandler("LT",((e,t)=>this._getWorkerSource(e,t.type,t.source).loadTile(t))),this.actor.registerMessageHandler("RT",((e,t)=>this._getWorkerSource(e,t.type,t.source).reloadTile(t))),this.actor.registerMessageHandler("AT",((e,t)=>this._getWorkerSource(e,t.type,t.source).abortTile(t))),this.actor.registerMessageHandler("RMT",((e,t)=>this._getWorkerSource(e,t.type,t.source).removeTile(t))),this.actor.registerMessageHandler("RS",((t,i)=>e._(this,void 0,void 0,(function*(){if(!this.workerSources[t]||!this.workerSources[t][i.type]||!this.workerSources[t][i.type][i.source])return;const e=this.workerSources[t][i.type][i.source];delete this.workerSources[t][i.type][i.source],void 0!==e.removeSource&&e.removeSource(i);})))),this.actor.registerMessageHandler("RM",(t=>e._(this,void 0,void 0,(function*(){delete this.layerIndexes[t],delete this.availableImages[t],delete this.workerSources[t],delete this.demWorkerSources[t];})))),this.actor.registerMessageHandler("SR",((t,i)=>e._(this,void 0,void 0,(function*(){this.referrer=i;})))),this.actor.registerMessageHandler("SRPS",((e,t)=>this._syncRTLPluginState(e,t))),this.actor.registerMessageHandler("IS",((t,i)=>e._(this,void 0,void 0,(function*(){this.self.importScripts(i);})))),this.actor.registerMessageHandler("SI",((e,t)=>this._setImages(e,t))),this.actor.registerMessageHandler("UL",((t,i)=>e._(this,void 0,void 0,(function*(){this._getLayerIndex(t).update(i.layers,i.removedIds);})))),this.actor.registerMessageHandler("SL",((t,i)=>e._(this,void 0,void 0,(function*(){this._getLayerIndex(t).replace(i);}))));}_setImages(t,i){return e._(this,void 0,void 0,(function*(){this.availableImages[t]=i;for(const e in this.workerSources[t]){const o=this.workerSources[t][e];for(const e in o)o[e].availableImages=i;}}))}_syncRTLPluginState(t,i){return e._(this,void 0,void 0,(function*(){return yield e.cX.syncState(i,this.self.importScripts)}))}_getAvailableImages(e){let t=this.availableImages[e];return t||(t=[]),t}_getLayerIndex(e){let i=this.layerIndexes[e];return i||(i=this.layerIndexes[e]=new t),i}_getWorkerSource(e,t,i){if(this.workerSources[e]||(this.workerSources[e]={}),this.workerSources[e][t]||(this.workerSources[e][t]={}),!this.workerSources[e][t][i]){const o={sendAsync:(t,i)=>(t.targetMapId=e,this.actor.sendAsync(t,i))};switch(t){case "vector":this.workerSources[e][t][i]=new n(o,this._getLayerIndex(e),this._getAvailableImages(e));break;case "geojson":this.workerSources[e][t][i]=new oe(o,this._getLayerIndex(e),this._getAvailableImages(e));break;default:this.workerSources[e][t][i]=new this.externalWorkerSourceTypes[t](o,this._getLayerIndex(e),this._getAvailableImages(e));}}return this.workerSources[e][t][i]}_getDEMWorkerSource(e,t){return this.demWorkerSources[e]||(this.demWorkerSources[e]={}),this.demWorkerSources[e][t]||(this.demWorkerSources[e][t]=new r),this.demWorkerSources[e][t]}}return e.i(self)&&(self.worker=new se(self)),se})); + +define("index",["exports","./shared"],(function(e,t){"use strict";var i="5.7.0";function o(){var e=new t.A(4);return t.A!=Float32Array&&(e[1]=0,e[2]=0),e[0]=1,e[3]=1,e}let r,a;const s={now:"undefined"!=typeof performance&&performance&&performance.now?performance.now.bind(performance):Date.now.bind(Date),frame(e,i,o){const r=requestAnimationFrame((e=>{a(),i(e);})),{unsubscribe:a}=t.s(e.signal,"abort",(()=>{a(),cancelAnimationFrame(r),o(t.c());}),!1);},frameAsync(e){return new Promise(((t,i)=>{this.frame(e,t,i);}))},getImageData(e,t=0){return this.getImageCanvasContext(e).getImageData(-t,-t,e.width+2*t,e.height+2*t)},getImageCanvasContext(e){const t=window.document.createElement("canvas"),i=t.getContext("2d",{willReadFrequently:!0});if(!i)throw new Error("failed to create canvas 2d context");return t.width=e.width,t.height=e.height,i.drawImage(e,0,0,e.width,e.height),i},resolveURL:e=>(r||(r=document.createElement("a")),r.href=e,r.href),hardwareConcurrency:"undefined"!=typeof navigator&&navigator.hardwareConcurrency||4,get prefersReducedMotion(){return !!matchMedia&&(null==a&&(a=matchMedia("(prefers-reduced-motion: reduce)")),a.matches)}};class n{static testProp(e){if(!n.docStyle)return e[0];for(let t=0;t{window.removeEventListener("click",n.suppressClickInternal,!0);}),0);}static getScale(e){const t=e.getBoundingClientRect();return {x:t.width/e.offsetWidth||1,y:t.height/e.offsetHeight||1,boundingClientRect:t}}static getPoint(e,i,o){const r=i.boundingClientRect;return new t.P((o.clientX-r.left)/i.x-e.clientLeft,(o.clientY-r.top)/i.y-e.clientTop)}static mousePos(e,t){const i=n.getScale(e);return n.getPoint(e,i,t)}static touchPos(e,t){const i=[],o=n.getScale(e);for(let r=0;r{c&&_(c),c=null,d=!0;},h.onerror=()=>{u=!0,c=null;},h.src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAQAAAAfQ//73v/+BiOh/AAA="),function(e){let i,o,r,a;e.resetRequestQueue=()=>{i=[],o=0,r=0,a={};},e.addThrottleControl=e=>{const t=r++;return a[t]=e,t},e.removeThrottleControl=e=>{delete a[e],n();},e.getImage=(e,o,r=!0)=>new Promise(((a,s)=>{l.supported&&(e.headers||(e.headers={}),e.headers.accept="image/webp,*/*"),t.e(e,{type:"image"}),i.push({abortController:o,requestParameters:e,supportImageRefresh:r,state:"queued",onError:e=>{s(e);},onSuccess:e=>{a(e);}}),n();}));const s=e=>t._(this,void 0,void 0,(function*(){e.state="running";const{requestParameters:i,supportImageRefresh:r,onError:a,onSuccess:s,abortController:l}=e,h=!1===r&&!t.i(self)&&!t.g(i.url)&&(!i.headers||Object.keys(i.headers).reduce(((e,t)=>e&&"accept"===t),!0));o++;const u=h?c(i,l):t.m(i,l);try{const i=yield u;delete e.abortController,e.state="completed",i.data instanceof HTMLImageElement||t.b(i.data)?s(i):i.data&&s({data:yield(d=i.data,"function"==typeof createImageBitmap?t.f(d):t.h(d)),cacheControl:i.cacheControl,expires:i.expires});}catch(t){delete e.abortController,a(t);}finally{o--,n();}var d;})),n=()=>{const e=(()=>{for(const e of Object.keys(a))if(a[e]())return !0;return !1})()?t.a.MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME:t.a.MAX_PARALLEL_IMAGE_REQUESTS;for(let t=o;t0;t++){const e=i.shift();e.abortController.signal.aborted?t--:s(e);}},c=(e,i)=>new Promise(((o,r)=>{const a=new Image,s=e.url,n=e.credentials;n&&"include"===n?a.crossOrigin="use-credentials":(n&&"same-origin"===n||!t.d(s))&&(a.crossOrigin="anonymous"),i.signal.addEventListener("abort",(()=>{a.src="",r(t.c());})),a.fetchPriority="high",a.onload=()=>{a.onerror=a.onload=null,o({data:a});},a.onerror=()=>{a.onerror=a.onload=null,i.signal.aborted||r(new Error("Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported."));},a.src=s;}));}(p||(p={})),p.resetRequestQueue();class m{constructor(e){this._transformRequestFn=null!=e?e:null;}transformRequest(e,t){return this._transformRequestFn&&this._transformRequestFn(e,t)||{url:e}}setTransformRequest(e){this._transformRequestFn=e;}}function f(e){const t=[];if("string"==typeof e)t.push({id:"default",url:e});else if(e&&e.length>0){const i=[];for(const{id:o,url:r}of e){const e=`${o}${r}`;-1===i.indexOf(e)&&(i.push(e),t.push({id:o,url:r}));}}return t}function g(e,t,i){try{const o=new URL(e);return o.pathname+=`${t}${i}`,o.toString()}catch(t){throw new Error(`Invalid sprite URL "${e}", must be absolute. Modify style specification directly or use TransformStyleFunction to correct the issue dynamically`)}}function v(e){const{userImage:t}=e;return !!(t&&t.render&&t.render())&&(e.data.replace(new Uint8Array(t.data.buffer)),!0)}class b extends t.E{constructor(){super(),this.images={},this.updatedImages={},this.callbackDispatchedThisFrame={},this.loaded=!1,this.requestors=[],this.patterns={},this.atlasImage=new t.R({width:1,height:1}),this.dirty=!0;}isLoaded(){return this.loaded}setLoaded(e){if(this.loaded!==e&&(this.loaded=e,e)){for(const{ids:e,promiseResolve:t}of this.requestors)t(this._getImagesForIds(e));this.requestors=[];}}getImage(e){const i=this.images[e];if(i&&!i.data&&i.spriteData){const e=i.spriteData;i.data=new t.R({width:e.width,height:e.height},e.context.getImageData(e.x,e.y,e.width,e.height).data),i.spriteData=null;}return i}addImage(e,t){if(this.images[e])throw new Error(`Image id ${e} already exist, use updateImage instead`);this._validate(e,t)&&(this.images[e]=t);}_validate(e,i){let o=!0;const r=i.data||i.spriteData;return this._validateStretch(i.stretchX,r&&r.width)||(this.fire(new t.k(new Error(`Image "${e}" has invalid "stretchX" value`))),o=!1),this._validateStretch(i.stretchY,r&&r.height)||(this.fire(new t.k(new Error(`Image "${e}" has invalid "stretchY" value`))),o=!1),this._validateContent(i.content,i)||(this.fire(new t.k(new Error(`Image "${e}" has invalid "content" value`))),o=!1),o}_validateStretch(e,t){if(!e)return !0;let i=0;for(const o of e){if(o[0]{let o=!0;if(!this.isLoaded())for(const t of e)this.images[t]||(o=!1);this.isLoaded()||o?t(this._getImagesForIds(e)):this.requestors.push({ids:e,promiseResolve:t});}))}_getImagesForIds(e){const i={};for(const o of e){let e=this.getImage(o);e||(this.fire(new t.l("styleimagemissing",{id:o})),e=this.getImage(o)),e?i[o]={data:e.data.clone(),pixelRatio:e.pixelRatio,sdf:e.sdf,version:e.version,stretchX:e.stretchX,stretchY:e.stretchY,content:e.content,textFitWidth:e.textFitWidth,textFitHeight:e.textFitHeight,hasRenderCallback:Boolean(e.userImage&&e.userImage.render)}:t.w(`Image "${o}" could not be loaded. Please make sure you have added the image with map.addImage() or a "sprite" property in your style. You can provide missing images by listening for the "styleimagemissing" map event.`);}return i}getPixelSize(){const{width:e,height:t}=this.atlasImage;return {width:e,height:t}}getPattern(e){const i=this.patterns[e],o=this.getImage(e);if(!o)return null;if(i&&i.position.version===o.version)return i.position;if(i)i.position.version=o.version;else {const i={w:o.data.width+2,h:o.data.height+2,x:0,y:0},r=new t.I(i,o);this.patterns[e]={bin:i,position:r};}return this._updatePatternAtlas(),this.patterns[e].position}bind(e){const i=e.gl;this.atlasTexture?this.dirty&&(this.atlasTexture.update(this.atlasImage),this.dirty=!1):this.atlasTexture=new t.T(e,this.atlasImage,i.RGBA),this.atlasTexture.bind(i.LINEAR,i.CLAMP_TO_EDGE);}_updatePatternAtlas(){const e=[];for(const t in this.patterns)e.push(this.patterns[t].bin);const{w:i,h:o}=t.p(e),r=this.atlasImage;r.resize({width:i||1,height:o||1});for(const e in this.patterns){const{bin:i}=this.patterns[e],o=i.x+1,a=i.y+1,s=this.getImage(e).data,n=s.width,l=s.height;t.R.copy(s,r,{x:0,y:0},{x:o,y:a},{width:n,height:l}),t.R.copy(s,r,{x:0,y:l-1},{x:o,y:a-1},{width:n,height:1}),t.R.copy(s,r,{x:0,y:0},{x:o,y:a+l},{width:n,height:1}),t.R.copy(s,r,{x:n-1,y:0},{x:o-1,y:a},{width:1,height:l}),t.R.copy(s,r,{x:0,y:0},{x:o+n,y:a},{width:1,height:l});}this.dirty=!0;}beginFrame(){this.callbackDispatchedThisFrame={};}dispatchRenderCallbacks(e){for(const i of e){if(this.callbackDispatchedThisFrame[i])continue;this.callbackDispatchedThisFrame[i]=!0;const e=this.getImage(i);e||t.w(`Image with ID: "${i}" was not found`),v(e)&&this.updateImage(i,e);}}}const x=1e20;function y(e,t,i,o,r,a,s,n,l){for(let c=t;c-1);l++,a[l]=n,s[l]=c,s[l+1]=x;}for(let n=0,l=0;n65535)throw new Error("glyphs > 65535 not supported");if(t.ranges[r])return {stack:e,id:i,glyph:o};if(!this.url)throw new Error("glyphsUrl is not set");if(!t.requests[r]){const i=T.loadGlyphRange(e,r,this.url,this.requestManager);t.requests[r]=i;}const a=yield t.requests[r];for(const e in a)this._doesCharSupportLocalGlyph(+e)||(t.glyphs[+e]=a[+e]);return t.ranges[r]=!0,{stack:e,id:i,glyph:a[i]||null}}))}_doesCharSupportLocalGlyph(e){return !!this.localIdeographFontFamily&&(/\p{Ideo}|\p{sc=Hang}|\p{sc=Hira}|\p{sc=Kana}/u.test(String.fromCodePoint(e))||t.u["CJK Unified Ideographs"](e)||t.u["Hangul Syllables"](e)||t.u.Hiragana(e)||t.u.Katakana(e)||t.u["CJK Symbols and Punctuation"](e)||t.u["Halfwidth and Fullwidth Forms"](e))}_tinySDF(e,i,o){const r=this.localIdeographFontFamily;if(!r)return;if(!this._doesCharSupportLocalGlyph(o))return;let a=e.tinySDF;if(!a){let t="400";/bold/i.test(i)?t="900":/medium/i.test(i)?t="500":/light/i.test(i)&&(t="200"),a=e.tinySDF=new T.TinySDF({fontSize:48,buffer:6,radius:16,cutoff:.25,lang:this.lang,fontFamily:r,fontWeight:t});}const s=a.draw(String.fromCharCode(o));return {id:o,bitmap:new t.q({width:s.width||60,height:s.height||60},s.data),metrics:{width:s.glyphWidth/2||24,height:s.glyphHeight/2||24,left:s.glyphLeft/2+.5||0,top:s.glyphTop/2-27.5||-8,advance:s.glyphAdvance/2||24,isDoubleResolution:!0}}}}T.loadGlyphRange=function(e,i,o,r){return t._(this,void 0,void 0,(function*(){const a=256*i,s=a+255,n=r.transformRequest(o.replace("{fontstack}",e).replace("{range}",`${a}-${s}`),"Glyphs"),l=yield t.n(n,new AbortController);if(!l||!l.data)throw new Error(`Could not load glyph range. range: ${i}, ${a}-${s}`);const c={};for(const e of t.o(l.data))c[e.id]=e;return c}))},T.TinySDF=class{constructor({fontSize:e=24,buffer:t=3,radius:i=8,cutoff:o=.25,fontFamily:r="sans-serif",fontWeight:a="normal",fontStyle:s="normal",lang:n=null}={}){this.buffer=t,this.cutoff=o,this.radius=i,this.lang=n;const l=this.size=e+4*t,c=this._createCanvas(l),h=this.ctx=c.getContext("2d",{willReadFrequently:!0});h.font=`${s} ${a} ${e}px ${r}`,h.textBaseline="alphabetic",h.textAlign="left",h.fillStyle="black",this.gridOuter=new Float64Array(l*l),this.gridInner=new Float64Array(l*l),this.f=new Float64Array(l),this.z=new Float64Array(l+1),this.v=new Uint16Array(l);}_createCanvas(e){const t=document.createElement("canvas");return t.width=t.height=e,t}draw(e){const{width:t,actualBoundingBoxAscent:i,actualBoundingBoxDescent:o,actualBoundingBoxLeft:r,actualBoundingBoxRight:a}=this.ctx.measureText(e),s=Math.ceil(i),n=Math.max(0,Math.min(this.size-this.buffer,Math.ceil(a-r))),l=Math.min(this.size-this.buffer,s+Math.ceil(o)),c=n+2*this.buffer,h=l+2*this.buffer,u=Math.max(c*h,0),d=new Uint8ClampedArray(u),_={data:d,width:c,height:h,glyphWidth:n,glyphHeight:l,glyphTop:s,glyphLeft:0,glyphAdvance:t};if(0===n||0===l)return _;const{ctx:p,buffer:m,gridInner:f,gridOuter:g}=this;this.lang&&(p.lang=this.lang),p.clearRect(m,m,n,l),p.fillText(e,m,m+s);const v=p.getImageData(m,m,n,l);g.fill(x,0,u),f.fill(0,0,u);for(let e=0;e0?e*e:0,f[o]=e<0?e*e:0;}}y(g,0,0,c,h,c,this.f,this.v,this.z),y(f,m,m,n,l,c,this.f,this.v,this.z);for(let e=0;e1&&(s=e[++a]);const l=Math.abs(n-s.left),c=Math.abs(n-s.right),h=Math.min(l,c);let u;const d=t/i*(o+1);if(s.isDash){const e=o-Math.abs(d);u=Math.sqrt(h*h+e*e);}else u=o-Math.sqrt(h*h+d*d);this.data[r+n]=Math.max(0,Math.min(255,u+128));}}}addRegularDash(e){for(let t=e.length-1;t>=0;--t){const i=e[t],o=e[t+1];i.zeroLength?e.splice(t,1):o&&o.isDash===i.isDash&&(o.left=i.left,e.splice(t,1));}const t=e[0],i=e[e.length-1];t.isDash===i.isDash&&(t.left=i.left-this.width,i.right=t.right+this.width);const o=this.width*this.nextRow;let r=0,a=e[r];for(let t=0;t1&&(a=e[++r]);const i=Math.abs(t-a.left),s=Math.abs(t-a.right),n=Math.min(i,s);this.data[o+t]=Math.max(0,Math.min(255,(a.isDash?n:-n)+128));}}addDash(e,i){const o=i?7:0,r=2*o+1;if(this.nextRow+r>this.height)return t.w("LineAtlas out of space"),null;let a=0;for(let t=0;t{e.terminate();})),this.workers=null);}isPreloaded(){return !!this.active[R]}numActive(){return Object.keys(this.active).length}}const D=Math.floor(s.hardwareConcurrency/2);let A,L;function k(){return A||(A=new z),A}z.workerCount=t.H(globalThis)?Math.max(Math.min(D,3),1):1;class F{constructor(e,i){this.workerPool=e,this.actors=[],this.currentActor=0,this.id=i;const o=this.workerPool.acquire(i);for(let e=0;e{e.remove();})),this.actors=[],e&&this.workerPool.release(this.id);}registerMessageHandler(e,t){for(const i of this.actors)i.registerMessageHandler(e,t);}}function B(){return L||(L=new F(k(),t.K),L.registerMessageHandler("GR",((e,i,o)=>t.m(i,o)))),L}function O(e,i){const o=t.L();return t.M(o,o,[1,1,0]),t.N(o,o,[.5*e.width,.5*e.height,1]),e.calculatePosMatrix?t.O(o,o,e.calculatePosMatrix(i.toUnwrapped())):o}function j(e,t,i,o,r,a,s){var n;const l=function(e,t,i){if(e)for(const o of e){const e=t[o];if(e&&e.source===i&&"fill-extrusion"===e.type)return !0}else for(const e in t){const o=t[e];if(o.source===i&&"fill-extrusion"===o.type)return !0}return !1}(null!==(n=null==r?void 0:r.layers)&&void 0!==n?n:null,t,e.id),c=a.maxPitchScaleFactor(),h=e.tilesIn(o,c,l);h.sort(N);const u=[];for(const o of h)u.push({wrappedTileID:o.tileID.wrapped().key,queryResults:o.tile.queryRenderedFeatures(t,i,e._state,o.queryGeometry,o.cameraQueryGeometry,o.scale,r,a,c,O(e.transform,o.tileID),s?(e,t)=>s(o.tileID,e,t):void 0)});return function(e,t){for(const i in e)for(const o of e[i])U(o,t);return e}(function(e){const t={},i={};for(const o of e){const e=o.queryResults,r=o.wrappedTileID,a=i[r]=i[r]||{};for(const i in e){const o=e[i],r=a[i]=a[i]||{},s=t[i]=t[i]||[];for(const e of o)r[e.featureIndex]||(r[e.featureIndex]=!0,s.push(e));}}return t}(u),e)}function N(e,t){const i=e.tileID,o=t.tileID;return i.overscaledZ-o.overscaledZ||i.canonical.y-o.canonical.y||i.wrap-o.wrap||i.canonical.x-o.canonical.x}function U(e,t){const i=e.feature,o=t.getFeatureState(i.layer["source-layer"],i.id);i.source=i.layer.source,i.layer["source-layer"]&&(i.sourceLayer=i.layer["source-layer"]),i.state=o;}function Z(e,i,o){return t._(this,void 0,void 0,(function*(){let r=e;if(e.url?r=(yield t.j(i.transformRequest(e.url,"Source"),o)).data:yield s.frameAsync(o),!r)return null;const a=t.Q(t.e(r,e),["tiles","minzoom","maxzoom","attribution","bounds","scheme","tileSize","encoding"]);return "vector_layers"in r&&r.vector_layers&&(a.vectorLayerIds=r.vector_layers.map((e=>e.id))),a}))}class G{constructor(e,t){e&&(t?this.setSouthWest(e).setNorthEast(t):Array.isArray(e)&&(4===e.length?this.setSouthWest([e[0],e[1]]).setNorthEast([e[2],e[3]]):this.setSouthWest(e[0]).setNorthEast(e[1])));}setNorthEast(e){return this._ne=e instanceof t.S?new t.S(e.lng,e.lat):t.S.convert(e),this}setSouthWest(e){return this._sw=e instanceof t.S?new t.S(e.lng,e.lat):t.S.convert(e),this}extend(e){const i=this._sw,o=this._ne;let r,a;if(e instanceof t.S)r=e,a=e;else {if(!(e instanceof G))return Array.isArray(e)?4===e.length||e.every(Array.isArray)?this.extend(G.convert(e)):this.extend(t.S.convert(e)):e&&("lng"in e||"lon"in e)&&"lat"in e?this.extend(t.S.convert(e)):this;if(r=e._sw,a=e._ne,!r||!a)return this}return i||o?(i.lng=Math.min(r.lng,i.lng),i.lat=Math.min(r.lat,i.lat),o.lng=Math.max(a.lng,o.lng),o.lat=Math.max(a.lat,o.lat)):(this._sw=new t.S(r.lng,r.lat),this._ne=new t.S(a.lng,a.lat)),this}getCenter(){return new t.S((this._sw.lng+this._ne.lng)/2,(this._sw.lat+this._ne.lat)/2)}getSouthWest(){return this._sw}getNorthEast(){return this._ne}getNorthWest(){return new t.S(this.getWest(),this.getNorth())}getSouthEast(){return new t.S(this.getEast(),this.getSouth())}getWest(){return this._sw.lng}getSouth(){return this._sw.lat}getEast(){return this._ne.lng}getNorth(){return this._ne.lat}toArray(){return [this._sw.toArray(),this._ne.toArray()]}toString(){return `LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`}isEmpty(){return !(this._sw&&this._ne)}contains(e){const{lng:i,lat:o}=t.S.convert(e);let r=this._sw.lng<=i&&i<=this._ne.lng;return this._sw.lng>this._ne.lng&&(r=this._sw.lng>=i&&i>=this._ne.lng),this._sw.lat<=o&&o<=this._ne.lat&&r}static convert(e){return e instanceof G?e:e?new G(e):e}static fromLngLat(e,i=0){const o=360*i/40075017,r=o/Math.cos(Math.PI/180*e.lat);return new G(new t.S(e.lng-r,e.lat-o),new t.S(e.lng+r,e.lat+o))}adjustAntiMeridian(){const e=new t.S(this._sw.lng,this._sw.lat),i=new t.S(this._ne.lng,this._ne.lat);return new G(e,e.lng>i.lng?new t.S(i.lng+360,i.lat):i)}}class V{constructor(e,t,i){this.bounds=G.convert(this.validateBounds(e)),this.minzoom=t||0,this.maxzoom=i||24;}validateBounds(e){return Array.isArray(e)&&4===e.length?[Math.max(-180,e[0]),Math.max(-90,e[1]),Math.min(180,e[2]),Math.min(90,e[3])]:[-180,-90,180,90]}contains(e){const i=Math.pow(2,e.z),o=Math.floor(t.V(this.bounds.getWest())*i),r=Math.floor(t.U(this.bounds.getNorth())*i),a=Math.ceil(t.V(this.bounds.getEast())*i),s=Math.ceil(t.U(this.bounds.getSouth())*i);return e.x>=o&&e.x=r&&e.y{this._options.tiles=e;})),this}setUrl(e){return this.setSourceProperty((()=>{this.url=e,this._options.url=e;})),this}onRemove(){this._tileJSONRequest&&(this._tileJSONRequest.abort(),this._tileJSONRequest=null);}serialize(){return t.e({},this._options)}loadTile(e){return t._(this,void 0,void 0,(function*(){const t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),i={request:this.map._requestManager.transformRequest(t,"Tile"),uid:e.uid,tileID:e.tileID,zoom:e.tileID.overscaledZ,tileSize:this.tileSize*e.tileID.overscaleFactor(),type:this.type,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId,subdivisionGranularity:this.map.style.projection.subdivisionGranularity,globalState:this.map.getGlobalState()};i.request.collectResourceTiming=this._collectResourceTiming;let o="RT";if(e.actor&&"expired"!==e.state){if("loading"===e.state)return new Promise(((t,i)=>{e.reloadPromise={resolve:t,reject:i};}))}else e.actor=this.dispatcher.getActor(),o="LT";e.abortController=new AbortController;try{const t=yield e.actor.sendAsync({type:o,data:i},e.abortController);if(delete e.abortController,e.aborted)return;this._afterTileLoadWorkerResponse(e,t);}catch(t){if(delete e.abortController,e.aborted)return;if(t&&404!==t.status)throw t;this._afterTileLoadWorkerResponse(e,null);}}))}_afterTileLoadWorkerResponse(e,t){if(t&&t.resourceTiming&&(e.resourceTiming=t.resourceTiming),t&&this.map._refreshExpiredTiles&&e.setExpiryData(t),e.loadVectorData(t,this.map.painter),e.reloadPromise){const t=e.reloadPromise;e.reloadPromise=null,this.loadTile(e).then(t.resolve).catch(t.reject);}}abortTile(e){return t._(this,void 0,void 0,(function*(){e.abortController&&(e.abortController.abort(),delete e.abortController),e.actor&&(yield e.actor.sendAsync({type:"AT",data:{uid:e.uid,type:this.type,source:this.id}}));}))}unloadTile(e){return t._(this,void 0,void 0,(function*(){e.unloadVectorData(),e.actor&&(yield e.actor.sendAsync({type:"RMT",data:{uid:e.uid,type:this.type,source:this.id}}));}))}hasTransition(){return !1}}class q extends t.E{constructor(e,i,o,r){super(),this.id=e,this.dispatcher=o,this.setEventedParent(r),this.type="raster",this.minzoom=0,this.maxzoom=22,this.roundZoom=!0,this.scheme="xyz",this.tileSize=512,this._loaded=!1,this._options=t.e({type:"raster"},i),t.e(this,t.Q(i,["url","scheme","tileSize"]));}load(){return t._(this,arguments,void 0,(function*(e=!1){this._loaded=!1,this.fire(new t.l("dataloading",{dataType:"source"})),this._tileJSONRequest=new AbortController;try{const i=yield Z(this._options,this.map._requestManager,this._tileJSONRequest);this._tileJSONRequest=null,this._loaded=!0,i&&(t.e(this,i),i.bounds&&(this.tileBounds=new V(i.bounds,this.minzoom,this.maxzoom)),this.fire(new t.l("data",{dataType:"source",sourceDataType:"metadata"})),this.fire(new t.l("data",{dataType:"source",sourceDataType:"content",sourceDataChanged:e})));}catch(e){this._tileJSONRequest=null,this.fire(new t.k(e));}}))}loaded(){return this._loaded}onAdd(e){this.map=e,this.load();}onRemove(){this._tileJSONRequest&&(this._tileJSONRequest.abort(),this._tileJSONRequest=null);}setSourceProperty(e){this._tileJSONRequest&&(this._tileJSONRequest.abort(),this._tileJSONRequest=null),e(),this.load(!0);}setTiles(e){return this.setSourceProperty((()=>{this._options.tiles=e;})),this}setUrl(e){return this.setSourceProperty((()=>{this.url=e,this._options.url=e;})),this}serialize(){return t.e({},this._options)}hasTile(e){return !this.tileBounds||this.tileBounds.contains(e.canonical)}loadTile(e){return t._(this,void 0,void 0,(function*(){const i=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme);e.abortController=new AbortController;try{const o=yield p.getImage(this.map._requestManager.transformRequest(i,"Tile"),e.abortController,this.map._refreshExpiredTiles);if(delete e.abortController,e.aborted)return void(e.state="unloaded");if(o&&o.data){this.map._refreshExpiredTiles&&(o.cacheControl||o.expires)&&e.setExpiryData({cacheControl:o.cacheControl,expires:o.expires});const i=this.map.painter.context,r=i.gl,a=o.data;e.texture=this.map.painter.getTileTexture(a.width),e.texture?e.texture.update(a,{useMipmap:!0}):(e.texture=new t.T(i,a,r.RGBA,{useMipmap:!0}),e.texture.bind(r.LINEAR,r.CLAMP_TO_EDGE,r.LINEAR_MIPMAP_NEAREST)),e.state="loaded";}}catch(t){if(delete e.abortController,e.aborted)e.state="unloaded";else if(t)throw e.state="errored",t}}))}abortTile(e){return t._(this,void 0,void 0,(function*(){e.abortController&&(e.abortController.abort(),delete e.abortController);}))}unloadTile(e){return t._(this,void 0,void 0,(function*(){e.texture&&this.map.painter.saveTileTexture(e.texture);}))}hasTransition(){return !1}}class W extends q{constructor(e,i,o,r){super(e,i,o,r),this.type="raster-dem",this.maxzoom=22,this._options=t.e({type:"raster-dem"},i),this.encoding=i.encoding||"mapbox",this.redFactor=i.redFactor,this.greenFactor=i.greenFactor,this.blueFactor=i.blueFactor,this.baseShift=i.baseShift;}loadTile(e){return t._(this,void 0,void 0,(function*(){const i=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),o=this.map._requestManager.transformRequest(i,"Tile");e.neighboringTiles=this._getNeighboringTiles(e.tileID),e.abortController=new AbortController;try{const i=yield p.getImage(o,e.abortController,this.map._refreshExpiredTiles);if(delete e.abortController,e.aborted)return void(e.state="unloaded");if(i&&i.data){const o=i.data;this.map._refreshExpiredTiles&&(i.cacheControl||i.expires)&&e.setExpiryData({cacheControl:i.cacheControl,expires:i.expires});const r=t.b(o)&&t.W()?o:yield this.readImageNow(o),a={type:this.type,uid:e.uid,source:this.id,rawImageData:r,encoding:this.encoding,redFactor:this.redFactor,greenFactor:this.greenFactor,blueFactor:this.blueFactor,baseShift:this.baseShift};if(!e.actor||"expired"===e.state){e.actor=this.dispatcher.getActor();const t=yield e.actor.sendAsync({type:"LDT",data:a});e.dem=t,e.needsHillshadePrepare=!0,e.needsTerrainPrepare=!0,e.state="loaded";}}}catch(t){if(delete e.abortController,e.aborted)e.state="unloaded";else if(t)throw e.state="errored",t}}))}readImageNow(e){return t._(this,void 0,void 0,(function*(){if("undefined"!=typeof VideoFrame&&t.X()){const i=e.width+2,o=e.height+2;try{return new t.R({width:i,height:o},yield t.Y(e,-1,-1,i,o))}catch(e){}}return s.getImageData(e,1)}))}_getNeighboringTiles(e){const i=e.canonical,o=Math.pow(2,i.z),r=(i.x-1+o)%o,a=0===i.x?e.wrap-1:e.wrap,s=(i.x+1+o)%o,n=i.x+1===o?e.wrap+1:e.wrap,l={};return l[new t.Z(e.overscaledZ,a,i.z,r,i.y).key]={backfilled:!1},l[new t.Z(e.overscaledZ,n,i.z,s,i.y).key]={backfilled:!1},i.y>0&&(l[new t.Z(e.overscaledZ,a,i.z,r,i.y-1).key]={backfilled:!1},l[new t.Z(e.overscaledZ,e.wrap,i.z,i.x,i.y-1).key]={backfilled:!1},l[new t.Z(e.overscaledZ,n,i.z,s,i.y-1).key]={backfilled:!1}),i.y+1e.coordinates)).flat(1/0):e.coordinates.flat(1/0)}getBounds(){return t._(this,void 0,void 0,(function*(){const e=new G,t=yield this.getData();let i;switch(t.type){case "FeatureCollection":i=t.features.map((e=>this.getCoordinatesFromGeometry(e.geometry))).flat(1/0);break;case "Feature":i=this.getCoordinatesFromGeometry(t.geometry);break;default:i=this.getCoordinatesFromGeometry(t);}if(0==i.length)return e;for(let t=0;t0&&t.e(r,{resourceTiming:i}),this.fire(new t.l("data",Object.assign(Object.assign({},r),{sourceDataType:"metadata"}))),this.fire(new t.l("data",Object.assign(Object.assign({},r),{sourceDataType:"content"})));}catch(e){if(this._isUpdatingWorker=!1,this._removed)return void this.fire(new t.l("dataabort",{dataType:"source"}));this.fire(new t.k(e));}finally{(this._pendingWorkerUpdate.data||this._pendingWorkerUpdate.diff)&&this._updateWorkerData();}}))}loaded(){return !this._isUpdatingWorker&&void 0===this._pendingWorkerUpdate.data&&void 0===this._pendingWorkerUpdate.diff}loadTile(e){return t._(this,void 0,void 0,(function*(){const t=e.actor?"RT":"LT";e.actor=this.actor;const i={type:this.type,uid:e.uid,tileID:e.tileID,zoom:e.tileID.overscaledZ,maxZoom:this.maxzoom,tileSize:this.tileSize,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId,subdivisionGranularity:this.map.style.projection.subdivisionGranularity,globalState:this.map.getGlobalState()};e.abortController=new AbortController;const o=yield this.actor.sendAsync({type:t,data:i},e.abortController);delete e.abortController,e.unloadVectorData(),e.aborted||e.loadVectorData(o,this.map.painter,"RT"===t);}))}abortTile(e){return t._(this,void 0,void 0,(function*(){e.abortController&&(e.abortController.abort(),delete e.abortController),e.aborted=!0;}))}unloadTile(e){return t._(this,void 0,void 0,(function*(){e.unloadVectorData(),yield this.actor.sendAsync({type:"RMT",data:{uid:e.uid,type:this.type,source:this.id}});}))}onRemove(){this._removed=!0,this.actor.sendAsync({type:"RS",data:{type:this.type,source:this.id}});}serialize(){return t.e({},this._options,{type:this.type,data:this._data})}hasTransition(){return !1}}class X extends t.E{constructor(e,t,i,o){super(),this.flippedWindingOrder=!1,this.id=e,this.dispatcher=i,this.coordinates=t.coordinates,this.type="image",this.minzoom=0,this.maxzoom=22,this.tileSize=512,this.tiles={},this._loaded=!1,this.setEventedParent(o),this.options=t;}load(e){return t._(this,void 0,void 0,(function*(){this._loaded=!1,this.fire(new t.l("dataloading",{dataType:"source"})),this.url=this.options.url,this._request=new AbortController;try{const t=yield p.getImage(this.map._requestManager.transformRequest(this.url,"Image"),this._request);this._request=null,this._loaded=!0,t&&t.data&&(this.image=t.data,e&&(this.coordinates=e),this._finishLoading());}catch(e){this._request=null,this._loaded=!0,this.fire(new t.k(e));}}))}loaded(){return this._loaded}updateImage(e){return e.url?(this._request&&(this._request.abort(),this._request=null),this.options.url=e.url,this.load(e.coordinates).finally((()=>{this.texture=null;})),this):this}_finishLoading(){this.map&&(this.setCoordinates(this.coordinates),this.fire(new t.l("data",{dataType:"source",sourceDataType:"metadata"})));}onAdd(e){this.map=e,this.load();}onRemove(){this._request&&(this._request.abort(),this._request=null);}setCoordinates(e){this.coordinates=e;const i=e.map(t.a1.fromLngLat);var o;return this.tileID=function(e){const i=t.a2.fromPoints(e),o=i.width(),r=i.height(),a=Math.max(o,r),s=Math.max(0,Math.floor(-Math.log(a)/Math.LN2)),n=Math.pow(2,s);return new t.a4(s,Math.floor((i.minX+i.maxX)/2*n),Math.floor((i.minY+i.maxY)/2*n))}(i),this.terrainTileRanges=this._getOverlappingTileRanges(i),this.minzoom=this.maxzoom=this.tileID.z,this.tileCoords=i.map((e=>this.tileID.getTilePoint(e)._round())),this.flippedWindingOrder=((o=this.tileCoords)[1].x-o[0].x)*(o[2].y-o[0].y)-(o[1].y-o[0].y)*(o[2].x-o[0].x)<0,this.fire(new t.l("data",{dataType:"source",sourceDataType:"content"})),this}prepare(){if(0===Object.keys(this.tiles).length||!this.image)return;const e=this.map.painter.context,i=e.gl;this.texture||(this.texture=new t.T(e,this.image,i.RGBA),this.texture.bind(i.LINEAR,i.CLAMP_TO_EDGE));let o=!1;for(const e in this.tiles){const t=this.tiles[e];"loaded"!==t.state&&(t.state="loaded",t.texture=this.texture,o=!0);}o&&this.fire(new t.l("data",{dataType:"source",sourceDataType:"idle",sourceId:this.id}));}loadTile(e){return t._(this,void 0,void 0,(function*(){this.tileID&&this.tileID.equals(e.tileID.canonical)?(this.tiles[String(e.tileID.wrap)]=e,e.buckets={}):e.state="errored";}))}serialize(){return {type:"image",url:this.options.url,coordinates:this.coordinates}}hasTransition(){return !1}_getOverlappingTileRanges(e){const{minX:i,minY:o,maxX:r,maxY:a}=t.a2.fromPoints(e),s={};for(let e=0;e<=t.a3;e++){const t=Math.pow(2,e),n=Math.floor(i*t),l=Math.floor(o*t),c=Math.floor(r*t),h=Math.floor(a*t);s[e]={minTileX:n,minTileY:l,maxTileX:c,maxTileY:h};}return s}}class K extends X{constructor(e,t,i,o){super(e,t,i,o),this.roundZoom=!0,this.type="video",this.options=t;}load(){return t._(this,void 0,void 0,(function*(){this._loaded=!1;const e=this.options;this.urls=[];for(const t of e.urls)this.urls.push(this.map._requestManager.transformRequest(t,"Source").url);try{const e=yield t.a5(this.urls);if(this._loaded=!0,!e)return;this.video=e,this.video.loop=!0,this.video.addEventListener("playing",(()=>{this.map.triggerRepaint();})),this.map&&this.video.play(),this._finishLoading();}catch(e){this.fire(new t.k(e));}}))}pause(){this.video&&this.video.pause();}play(){this.video&&this.video.play();}seek(e){if(this.video){const i=this.video.seekable;ei.end(0)?this.fire(new t.k(new t.a6(`sources.${this.id}`,null,`Playback for this video can be set only between the ${i.start(0)} and ${i.end(0)}-second mark.`))):this.video.currentTime=e;}}getVideo(){return this.video}onAdd(e){this.map||(this.map=e,this.load(),this.video&&(this.video.play(),this.setCoordinates(this.coordinates)));}prepare(){if(0===Object.keys(this.tiles).length||this.video.readyState<2)return;const e=this.map.painter.context,i=e.gl;this.texture?this.video.paused||(this.texture.bind(i.LINEAR,i.CLAMP_TO_EDGE),i.texSubImage2D(i.TEXTURE_2D,0,0,0,i.RGBA,i.UNSIGNED_BYTE,this.video)):(this.texture=new t.T(e,this.video,i.RGBA),this.texture.bind(i.LINEAR,i.CLAMP_TO_EDGE));let o=!1;for(const e in this.tiles){const t=this.tiles[e];"loaded"!==t.state&&(t.state="loaded",t.texture=this.texture,o=!0);}o&&this.fire(new t.l("data",{dataType:"source",sourceDataType:"idle",sourceId:this.id}));}serialize(){return {type:"video",urls:this.urls,coordinates:this.coordinates}}hasTransition(){return this.video&&!this.video.paused}}class Y extends X{constructor(e,i,o,r){super(e,i,o,r),i.coordinates?Array.isArray(i.coordinates)&&4===i.coordinates.length&&!i.coordinates.some((e=>!Array.isArray(e)||2!==e.length||e.some((e=>"number"!=typeof e))))||this.fire(new t.k(new t.a6(`sources.${e}`,null,'"coordinates" property must be an array of 4 longitude/latitude array pairs'))):this.fire(new t.k(new t.a6(`sources.${e}`,null,'missing required property "coordinates"'))),i.animate&&"boolean"!=typeof i.animate&&this.fire(new t.k(new t.a6(`sources.${e}`,null,'optional "animate" property must be a boolean value'))),i.canvas?"string"==typeof i.canvas||i.canvas instanceof HTMLCanvasElement||this.fire(new t.k(new t.a6(`sources.${e}`,null,'"canvas" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance'))):this.fire(new t.k(new t.a6(`sources.${e}`,null,'missing required property "canvas"'))),this.options=i,this.animate=void 0===i.animate||i.animate;}load(){return t._(this,void 0,void 0,(function*(){this._loaded=!0,this.canvas||(this.canvas=this.options.canvas instanceof HTMLCanvasElement?this.options.canvas:document.getElementById(this.options.canvas)),this.width=this.canvas.width,this.height=this.canvas.height,this._hasInvalidDimensions()?this.fire(new t.k(new Error("Canvas dimensions cannot be less than or equal to zero."))):(this.play=function(){this._playing=!0,this.map.triggerRepaint();},this.pause=function(){this._playing&&(this.prepare(),this._playing=!1);},this._finishLoading());}))}getCanvas(){return this.canvas}onAdd(e){this.map=e,this.load(),this.canvas&&this.animate&&this.play();}onRemove(){this.pause();}prepare(){let e=!1;if(this.canvas.width!==this.width&&(this.width=this.canvas.width,e=!0),this.canvas.height!==this.height&&(this.height=this.canvas.height,e=!0),this._hasInvalidDimensions())return;if(0===Object.keys(this.tiles).length)return;const i=this.map.painter.context,o=i.gl;this.texture?(e||this._playing)&&this.texture.update(this.canvas,{premultiply:!0}):this.texture=new t.T(i,this.canvas,o.RGBA,{premultiply:!0});let r=!1;for(const e in this.tiles){const t=this.tiles[e];"loaded"!==t.state&&(t.state="loaded",t.texture=this.texture,r=!0);}r&&this.fire(new t.l("data",{dataType:"source",sourceDataType:"idle",sourceId:this.id}));}serialize(){return {type:"canvas",coordinates:this.coordinates}}hasTransition(){return this._playing}_hasInvalidDimensions(){for(const e of [this.canvas.width,this.canvas.height])if(isNaN(e)||e<=0)return !0;return !1}}const Q={},J=e=>{switch(e){case "geojson":return H;case "image":return X;case "raster":return q;case "raster-dem":return W;case "vector":return $;case "video":return K;case "canvas":return Y}return Q[e]},ee="RTLPluginLoaded";class te extends t.E{constructor(){super(...arguments),this.status="unavailable",this.url=null,this.dispatcher=B();}_syncState(e){return this.status=e,this.dispatcher.broadcast("SRPS",{pluginStatus:e,pluginURL:this.url}).catch((e=>{throw this.status="error",e}))}getRTLTextPluginStatus(){return this.status}clearRTLTextPlugin(){this.status="unavailable",this.url=null;}setRTLTextPlugin(e){return t._(this,arguments,void 0,(function*(e,t=!1){if(this.url)throw new Error("setRTLTextPlugin cannot be called multiple times.");if(this.url=s.resolveURL(e),!this.url)throw new Error(`requested url ${e} is invalid`);if("unavailable"===this.status){if(!t)return this._requestImport();this.status="deferred",this._syncState(this.status);}else if("requested"===this.status)return this._requestImport()}))}_requestImport(){return t._(this,void 0,void 0,(function*(){yield this._syncState("loading"),this.status="loaded",this.fire(new t.l(ee));}))}lazyLoad(){"unavailable"===this.status?this.status="requested":"deferred"===this.status&&this._requestImport();}}let ie=null;function oe(){return ie||(ie=new te),ie}class re{constructor(e,i){this.timeAdded=0,this.fadeEndTime=0,this.tileID=e,this.uid=t.a7(),this.uses=0,this.tileSize=i,this.buckets={},this.expirationTime=null,this.queryPadding=0,this.hasSymbolBuckets=!1,this.hasRTLText=!1,this.dependencies={},this.rtt=[],this.rttCoords={},this.expiredRequestCount=0,this.state="loading";}registerFadeDuration(e){const t=e+this.timeAdded;tt.getLayer(e))).filter(Boolean);if(0!==e.length){o.layers=e,o.stateDependentLayerIds&&(o.stateDependentLayers=o.stateDependentLayerIds.map((t=>e.filter((e=>e.id===t))[0])));for(const t of e)i[t.id]=o;}}return i}(e.buckets,null==i?void 0:i.style),this.hasSymbolBuckets=!1;for(const e in this.buckets){const i=this.buckets[e];if(i instanceof t.a9){if(this.hasSymbolBuckets=!0,!o)break;i.justReloaded=!0;}}if(this.hasRTLText=!1,this.hasSymbolBuckets)for(const e in this.buckets){const i=this.buckets[e];if(i instanceof t.a9&&i.hasRTLText){this.hasRTLText=!0,oe().lazyLoad();break}}this.queryPadding=0;for(const e in this.buckets){const t=this.buckets[e];this.queryPadding=Math.max(this.queryPadding,i.style.getLayer(e).queryRadius(t));}e.imageAtlas&&(this.imageAtlas=e.imageAtlas),e.glyphAtlasImage&&(this.glyphAtlasImage=e.glyphAtlasImage);}else this.collisionBoxArray=new t.a8;}unloadVectorData(){for(const e in this.buckets)this.buckets[e].destroy();this.buckets={},this.imageAtlasTexture&&this.imageAtlasTexture.destroy(),this.imageAtlas&&(this.imageAtlas=null),this.glyphAtlasTexture&&this.glyphAtlasTexture.destroy(),this.latestFeatureIndex=null,this.state="unloaded";}getBucket(e){return this.buckets[e.id]}upload(e){for(const t in this.buckets){const i=this.buckets[t];i.uploadPending()&&i.upload(e);}const i=e.gl;this.imageAtlas&&!this.imageAtlas.uploaded&&(this.imageAtlasTexture=new t.T(e,this.imageAtlas.image,i.RGBA),this.imageAtlas.uploaded=!0),this.glyphAtlasImage&&(this.glyphAtlasTexture=new t.T(e,this.glyphAtlasImage,i.ALPHA),this.glyphAtlasImage=null);}prepare(e){this.imageAtlas&&this.imageAtlas.patchUpdatedImages(e,this.imageAtlasTexture);}queryRenderedFeatures(e,t,i,o,r,a,s,n,l,c,h){return this.latestFeatureIndex&&this.latestFeatureIndex.rawTileData?this.latestFeatureIndex.query({queryGeometry:o,cameraQueryGeometry:r,scale:a,tileSize:this.tileSize,pixelPosMatrix:c,transform:n,params:s,queryPadding:this.queryPadding*l,getElevation:h},e,t,i):{}}querySourceFeatures(e,i){const o=this.latestFeatureIndex;if(!o||!o.rawTileData)return;const r=o.loadVTLayers(),a=i&&i.sourceLayer?i.sourceLayer:"",s=r._geojsonTileLayer||r[a];if(!s)return;const n=t.aa(i&&i.filter),{z:l,x:c,y:h}=this.tileID.canonical,u={z:l,x:c,y:h};for(let i=0;ie)t=!1;else if(i)if(this.expirationTime{this.remove(e,r);}),i)),this.data[o].push(r),this.order.push(o),this.order.length>this.max){const e=this._getAndRemoveByKey(this.order[0]);e&&this.onRemove(e);}return this}has(e){return e.wrapped().key in this.data}getAndRemove(e){return this.has(e)?this._getAndRemoveByKey(e.wrapped().key):null}_getAndRemoveByKey(e){const t=this.data[e].shift();return t.timeout&&clearTimeout(t.timeout),0===this.data[e].length&&delete this.data[e],this.order.splice(this.order.indexOf(e),1),t.value}getByKey(e){const t=this.data[e];return t?t[0].value:null}get(e){return this.has(e)?this.data[e.wrapped().key][0].value:null}remove(e,t){if(!this.has(e))return this;const i=e.wrapped().key,o=void 0===t?0:this.data[i].indexOf(t),r=this.data[i][o];return this.data[i].splice(o,1),r.timeout&&clearTimeout(r.timeout),0===this.data[i].length&&delete this.data[i],this.onRemove(r.value),this.order.splice(this.order.indexOf(i),1),this}setMaxSize(e){for(this.max=e;this.order.length>this.max;){const e=this._getAndRemoveByKey(this.order[0]);e&&this.onRemove(e);}return this}filter(e){const t=[];for(const i in this.data)for(const o of this.data[i])e(o.value)||t.push(o);for(const e of t)this.remove(e.value.tileID,e);}}class se{constructor(){this.state={},this.stateChanges={},this.deletedStates={};}updateState(e,i,o){const r=String(i);if(this.stateChanges[e]=this.stateChanges[e]||{},this.stateChanges[e][r]=this.stateChanges[e][r]||{},t.e(this.stateChanges[e][r],o),null===this.deletedStates[e]){this.deletedStates[e]={};for(const t in this.state[e])t!==r&&(this.deletedStates[e][t]=null);}else if(this.deletedStates[e]&&null===this.deletedStates[e][r]){this.deletedStates[e][r]={};for(const t in this.state[e][r])o[t]||(this.deletedStates[e][r][t]=null);}else for(const t in o)this.deletedStates[e]&&this.deletedStates[e][r]&&null===this.deletedStates[e][r][t]&&delete this.deletedStates[e][r][t];}removeFeatureState(e,t,i){if(null===this.deletedStates[e])return;const o=String(t);if(this.deletedStates[e]=this.deletedStates[e]||{},i&&void 0!==t)null!==this.deletedStates[e][o]&&(this.deletedStates[e][o]=this.deletedStates[e][o]||{},this.deletedStates[e][o][i]=null);else if(void 0!==t)if(this.stateChanges[e]&&this.stateChanges[e][o])for(i in this.deletedStates[e][o]={},this.stateChanges[e][o])this.deletedStates[e][o][i]=null;else this.deletedStates[e][o]=null;else this.deletedStates[e]=null;}getState(e,i){const o=String(i),r=t.e({},(this.state[e]||{})[o],(this.stateChanges[e]||{})[o]);if(null===this.deletedStates[e])return {};if(this.deletedStates[e]){const t=this.deletedStates[e][i];if(null===t)return {};for(const e in t)delete r[e];}return r}initializeTileState(e,t){e.setFeatureState(this.state,t);}coalesceChanges(e,i){const o={};for(const e in this.stateChanges){this.state[e]=this.state[e]||{};const i={};for(const o in this.stateChanges[e])this.state[e][o]||(this.state[e][o]={}),t.e(this.state[e][o],this.stateChanges[e][o]),i[o]=this.state[e][o];o[e]=i;}for(const e in this.deletedStates){this.state[e]=this.state[e]||{};const i={};if(null===this.deletedStates[e])for(const t in this.state[e])i[t]={},this.state[e][t]={};else for(const t in this.deletedStates[e]){if(null===this.deletedStates[e][t])this.state[e][t]={};else for(const i of Object.keys(this.deletedStates[e][t]))delete this.state[e][t][i];i[t]=this.state[e][t];}o[e]=o[e]||{},t.e(o[e],i);}if(this.stateChanges={},this.deletedStates={},0!==Object.keys(o).length)for(const t in e)e[t].setFeatureState(o,i);}}const ne=89.25;function le(e,i){const o=t.ah(i.lat,-t.ai,t.ai);return new t.P(t.V(i.lng)*e,t.U(o)*e)}function ce(e,i){return new t.a1(i.x/e,i.y/e).toLngLat()}function he(e){return e.cameraToCenterDistance*Math.min(.85*Math.tan(t.ae(90-e.pitch)),Math.tan(t.ae(ne-e.pitch)))}function ue(e,i){const o=e.canonical,r=i/t.af(o.z),a=o.x+Math.pow(2,o.z)*e.wrap,s=t.ag(new Float64Array(16));return t.M(s,s,[a*r,o.y*r,0]),t.N(s,s,[r/t.$,r/t.$,1]),s}function de(e,i,o,r,a){const s=t.a1.fromLngLat(e,i),n=a*t.aj(1,e.lat),l=n*Math.cos(t.ae(o)),c=Math.sqrt(n*n-l*l),h=c*Math.sin(t.ae(-r)),u=c*Math.cos(t.ae(-r));return new t.a1(s.x+h,s.y+u,s.z+l)}function _e(e,t,i){const o=t.intersectsFrustum(e);if(!i||0===o)return o;const r=t.intersectsPlane(i);return 0===r?0:2===o&&2===r?2:1}function pe(e,t,i){let o=0;const r=(i-t)/10;for(let a=0;a<10;a++)o+=r*Math.pow(Math.cos(t+(a+.5)/10*(i-t)),e);return o}function me(e,i){return function(o,r,a,s,n){const l=2*((e-1)/t.ak(Math.cos(t.ae(ne-n))/Math.cos(t.ae(ne)))-1),c=Math.acos(a/s),h=2*pe(l-1,0,t.ae(n/2)),u=Math.min(t.ae(ne),c+t.ae(n/2)),d=pe(l-1,Math.min(u,c-t.ae(n/2)),u),_=Math.atan(r/a),p=Math.hypot(r,a);let m=o;return m+=t.ak(s/p/Math.max(.5,Math.cos(t.ae(n/2)))),m+=l*t.ak(Math.cos(_))/2,m-=t.ak(Math.max(1,d/h/i))/2,m}}const fe=me(9.314,3);function ge(e,i){const o=(i.roundZoom?Math.round:Math.floor)(e.zoom+t.ak(e.tileSize/i.tileSize));return Math.max(0,o)}function ve(e,i){const o=e.getCameraFrustum(),r=e.getClippingPlane(),a=e.screenPointToMercatorCoordinate(e.getCameraPoint()),s=t.a1.fromLngLat(e.center,e.elevation);a.z=s.z+Math.cos(e.pitchInRadians)*e.cameraToCenterDistance/e.worldSize;const n=e.getCoveringTilesDetailsProvider(),l=n.allowVariableZoom(e,i),c=ge(e,i),h=i.minzoom||0,u=void 0!==i.maxzoom?i.maxzoom:e.maxZoom,d=Math.min(Math.max(0,c),u),_=Math.pow(2,d),p=[_*a.x,_*a.y,0],m=[_*s.x,_*s.y,0],f=Math.hypot(s.x-a.x,s.y-a.y),g=Math.abs(s.z-a.z),v=Math.hypot(f,g),b=e=>({zoom:0,x:0,y:0,wrap:e,fullyVisible:!1}),x=[],y=[];if(e.renderWorldCopies&&n.allowWorldCopies())for(let e=1;e<=3;e++)x.push(b(-e)),x.push(b(e));for(x.push(b(0));x.length>0;){const _=x.pop(),f=_.x,b=_.y;let w=_.fullyVisible;const T={x:f,y:b,z:_.zoom},P=n.getTileBoundingVolume(T,_.wrap,e.elevation,i);if(!w){const e=_e(o,P,r);if(0===e)continue;w=2===e;}const C=n.distanceToTile2d(a.x,a.y,T,P);let I=c;l&&(I=(i.calculateTileZoom||fe)(e.zoom+t.ak(e.tileSize/i.tileSize),C,g,v,e.fov)),I=(i.roundZoom?Math.round:Math.floor)(I),I=Math.max(0,I);const M=Math.min(I,u);if(_.wrap=n.getWrap(s,T,_.wrap),_.zoom>=M){if(_.zoom>1),wrap:_.wrap,fullyVisible:w});}return y.sort(((e,t)=>e.distanceSq-t.distanceSq)).map((e=>e.tileID))}const be=t.a2.fromPoints([new t.P(0,0),new t.P(t.$,t.$)]);class xe extends t.E{constructor(e,t,i){super(),this.id=e,this.dispatcher=i,this.on("data",(e=>this._dataHandler(e))),this.on("dataloading",(()=>{this._sourceErrored=!1;})),this.on("error",(()=>{this._sourceErrored=this._source.loaded();})),this._source=((e,t,i,o)=>{const r=new(J(t.type))(e,t,i,o);if(r.id!==e)throw new Error(`Expected Source id to be ${e} instead of ${r.id}`);return r})(e,t,i,this),this._tiles={},this._cache=new ae(0,(e=>this._unloadTile(e))),this._timers={},this._cacheTimers={},this._maxTileCacheSize=null,this._maxTileCacheZoomLevels=null,this._loadedParentTiles={},this._coveredTiles={},this._state=new se,this._didEmitContent=!1,this._updated=!1;}onAdd(e){this.map=e,this._maxTileCacheSize=e?e._maxTileCacheSize:null,this._maxTileCacheZoomLevels=e?e._maxTileCacheZoomLevels:null,this._source&&this._source.onAdd&&this._source.onAdd(e);}onRemove(e){this.clearTiles(),this._source&&this._source.onRemove&&this._source.onRemove(e);}loaded(){if(this._sourceErrored)return !0;if(!this._sourceLoaded)return !1;if(!this._source.loaded())return !1;if(!(void 0===this.used&&void 0===this.usedForTerrain||this.used||this.usedForTerrain))return !0;if(!this._updated)return !1;for(const e in this._tiles){const t=this._tiles[e];if("loaded"!==t.state&&"errored"!==t.state)return !1}return !0}getSource(){return this._source}pause(){this._paused=!0;}resume(){if(!this._paused)return;const e=this._shouldReloadOnResume;this._paused=!1,this._shouldReloadOnResume=!1,e&&this.reload(),this.transform&&this.update(this.transform,this.terrain);}_loadTile(e,i,o){return t._(this,void 0,void 0,(function*(){try{yield this._source.loadTile(e),this._tileLoaded(e,i,o);}catch(i){e.state="errored",404!==i.status?this._source.fire(new t.k(i,{tile:e})):this.update(this.transform,this.terrain);}}))}_unloadTile(e){this._source.unloadTile&&this._source.unloadTile(e);}_abortTile(e){this._source.abortTile&&this._source.abortTile(e),this._source.fire(new t.l("dataabort",{tile:e,coord:e.tileID,dataType:"source"}));}serialize(){return this._source.serialize()}prepare(e){this._source.prepare&&this._source.prepare(),this._state.coalesceChanges(this._tiles,this.map?this.map.painter:null);for(const t in this._tiles){const i=this._tiles[t];i.upload(e),i.prepare(this.map.style.imageManager);}}getIds(){return Object.values(this._tiles).map((e=>e.tileID)).sort(ye).map((e=>e.key))}getRenderableIds(e){const i=[];for(const t in this._tiles)this._isIdRenderable(t,e)&&i.push(this._tiles[t]);return e?i.sort(((e,i)=>{const o=e.tileID,r=i.tileID,a=new t.P(o.canonical.x,o.canonical.y)._rotate(-this.transform.bearingInRadians),s=new t.P(r.canonical.x,r.canonical.y)._rotate(-this.transform.bearingInRadians);return o.overscaledZ-r.overscaledZ||s.y-a.y||s.x-a.x})).map((e=>e.tileID.key)):i.map((e=>e.tileID)).sort(ye).map((e=>e.key))}hasRenderableParent(e){const t=this.findLoadedParent(e,0);return !!t&&this._isIdRenderable(t.tileID.key)}_isIdRenderable(e,t){return this._tiles[e]&&this._tiles[e].hasData()&&!this._coveredTiles[e]&&(t||!this._tiles[e].holdingForFade())}reload(e){if(this._paused)this._shouldReloadOnResume=!0;else {this._cache.reset();for(const t in this._tiles)e?this._reloadTile(t,"expired"):"errored"!==this._tiles[t].state&&this._reloadTile(t,"reloading");}}_reloadTile(e,i){return t._(this,void 0,void 0,(function*(){const t=this._tiles[e];t&&("loading"!==t.state&&(t.state=i),yield this._loadTile(t,e,i));}))}_tileLoaded(e,i,o){e.timeAdded=s.now(),"expired"===o&&(e.refreshedUponExpiration=!0),this._setTileReloadTimer(i,e),"raster-dem"===this.getSource().type&&e.dem&&this._backfillDEM(e),this._state.initializeTileState(e,this.map?this.map.painter:null),e.aborted||this._source.fire(new t.l("data",{dataType:"source",tile:e,coord:e.tileID}));}_backfillDEM(e){const t=this.getRenderableIds();for(let o=0;o1||(Math.abs(i)>1&&(1===Math.abs(i+r)?i+=r:1===Math.abs(i-r)&&(i-=r)),t.dem&&e.dem&&(e.dem.backfillBorder(t.dem,i,o),e.neighboringTiles&&e.neighboringTiles[a]&&(e.neighboringTiles[a].backfilled=!0)));}}getTile(e){return this.getTileByID(e.key)}getTileByID(e){return this._tiles[e]}_retainLoadedChildren(e,t,i,o){for(const r in this._tiles){let a=this._tiles[r];if(o[r]||!a.hasData()||a.tileID.overscaledZ<=t||a.tileID.overscaledZ>i)continue;let s=a.tileID;for(;a&&a.tileID.overscaledZ>t+1;){const e=a.tileID.scaledTo(a.tileID.overscaledZ-1);a=this._tiles[e.key],a&&a.hasData()&&(s=e);}let n=s;for(;n.overscaledZ>t;)if(n=n.scaledTo(n.overscaledZ-1),e[n.key]||e[n.canonical.key]){o[s.key]=s;break}}}findLoadedParent(e,t){if(e.key in this._loadedParentTiles){const i=this._loadedParentTiles[e.key];return i&&i.tileID.overscaledZ>=t?i:null}for(let i=e.overscaledZ-1;i>=t;i--){const t=e.scaledTo(i),o=this._getLoadedTile(t);if(o)return o}}findLoadedSibling(e){return this._getLoadedTile(e)}_getLoadedTile(e){const t=this._tiles[e.key];return t&&t.hasData()?t:this._cache.getByKey(e.wrapped().key)}updateCacheSize(e){const i=Math.ceil(e.width/this._source.tileSize)+1,o=Math.ceil(e.height/this._source.tileSize)+1,r=Math.floor(i*o*(null===this._maxTileCacheZoomLevels?t.a.MAX_TILE_CACHE_ZOOM_LEVELS:this._maxTileCacheZoomLevels)),a="number"==typeof this._maxTileCacheSize?Math.min(this._maxTileCacheSize,r):r;this._cache.setMaxSize(a);}handleWrapJump(e){const t=Math.round((e-(void 0===this._prevLng?e:this._prevLng))/360);if(this._prevLng=e,t){const e={};for(const i in this._tiles){const o=this._tiles[i];o.tileID=o.tileID.unwrapTo(o.tileID.wrap+t),e[o.tileID.key]=o;}this._tiles=e;for(const e in this._timers)clearTimeout(this._timers[e]),delete this._timers[e];for(const e in this._tiles)this._setTileReloadTimer(e,this._tiles[e]);}}_updateCoveredAndRetainedTiles(e,t,i,o,r,a){const n={},l={},c=Object.keys(e),h=s.now();for(const i of c){const o=e[i],r=this._tiles[i];if(!r||0!==r.fadeEndTime&&r.fadeEndTime<=h)continue;const a=this.findLoadedParent(o,t),s=this.findLoadedSibling(o),c=a||s||null;c&&(this._addTile(c.tileID),n[c.tileID.key]=c.tileID),l[i]=o;}this._retainLoadedChildren(l,o,i,e);for(const t in n)e[t]||(this._coveredTiles[t]=!0,e[t]=n[t]);if(a){const t={},i={};for(const e of r)this._tiles[e.key].hasData()?t[e.key]=e:i[e.key]=e;for(const o in i){const r=i[o].children(this._source.maxzoom);this._tiles[r[0].key]&&this._tiles[r[1].key]&&this._tiles[r[2].key]&&this._tiles[r[3].key]&&(t[r[0].key]=e[r[0].key]=r[0],t[r[1].key]=e[r[1].key]=r[1],t[r[2].key]=e[r[2].key]=r[2],t[r[3].key]=e[r[3].key]=r[3],delete i[o]);}for(const o in i){const r=i[o],a=this.findLoadedParent(r,this._source.minzoom),s=this.findLoadedSibling(r),n=a||s||null;if(n){t[n.tileID.key]=e[n.tileID.key]=n.tileID;for(const e in t)t[e].isChildOf(n.tileID)&&delete t[e];}}for(const e in this._tiles)t[e]||(this._coveredTiles[e]=!0);}}update(e,i){if(!this._sourceLoaded||this._paused)return;let o;this.transform=e,this.terrain=i,this.updateCacheSize(e),this.handleWrapJump(this.transform.center.lng),this._coveredTiles={},this.used||this.usedForTerrain?this._source.tileID?o=e.getVisibleUnwrappedCoordinates(this._source.tileID).map((e=>new t.Z(e.canonical.z,e.wrap,e.canonical.z,e.canonical.x,e.canonical.y))):(o=ve(e,{tileSize:this.usedForTerrain?this.tileSize:this._source.tileSize,minzoom:this._source.minzoom,maxzoom:this._source.maxzoom,roundZoom:!this.usedForTerrain&&this._source.roundZoom,reparseOverscaled:this._source.reparseOverscaled,terrain:i,calculateTileZoom:this._source.calculateTileZoom}),this._source.hasTile&&(o=o.filter((e=>this._source.hasTile(e))))):o=[];const r=ge(e,this._source),a=Math.max(r-xe.maxOverzooming,this._source.minzoom),s=Math.max(r+xe.maxUnderzooming,this._source.minzoom);if(this.usedForTerrain){const e={};for(const t of o)if(t.canonical.z>this._source.minzoom){const i=t.scaledTo(t.canonical.z-1);e[i.key]=i;const o=t.scaledTo(Math.max(this._source.minzoom,Math.min(t.canonical.z,5)));e[o.key]=o;}o=o.concat(Object.values(e));}const n=0===o.length&&!this._updated&&this._didEmitContent;this._updated=!0,n&&this.fire(new t.l("data",{sourceDataType:"idle",dataType:"source",sourceId:this.id}));const l=this._updateRetainedTiles(o,r);we(this._source.type)&&this._updateCoveredAndRetainedTiles(l,a,s,r,o,i);for(const e in l)this._tiles[e].clearFadeHold();const c=t.am(this._tiles,l);for(const e of c){const t=this._tiles[e];t.hasSymbolBuckets&&!t.holdingForFade()?t.setHoldDuration(this.map._fadeDuration):t.hasSymbolBuckets&&!t.symbolFadeFinished()||this._removeTile(e);}this._updateLoadedParentTileCache(),this._updateLoadedSiblingTileCache();}releaseSymbolFadeTiles(){for(const e in this._tiles)this._tiles[e].holdingForFade()&&this._removeTile(e);}_updateRetainedTiles(e,t){var i;const o={},r={},a=Math.max(t-xe.maxOverzooming,this._source.minzoom),s=Math.max(t+xe.maxUnderzooming,this._source.minzoom),n={};for(const i of e){const e=this._addTile(i);o[i.key]=i,e.hasData()||tthis._source.maxzoom){const e=s.children(this._source.maxzoom)[0],t=this.getTile(e);if(t&&t.hasData()){o[e.key]=e;continue}}else {const e=s.children(this._source.maxzoom);if(o[e[0].key]&&o[e[1].key]&&o[e[2].key]&&o[e[3].key])continue}let n=e.wasRequested();for(let t=s.overscaledZ-1;t>=a;--t){const a=s.scaledTo(t);if(r[a.key])break;if(r[a.key]=!0,e=this.getTile(a),!e&&n&&(e=this._addTile(a)),e){const t=e.hasData();if((t||!(null===(i=this.map)||void 0===i?void 0:i.cancelPendingTileRequestsWhileZooming)||n)&&(o[a.key]=a),n=e.wasRequested(),t)break}}}return o}_updateLoadedParentTileCache(){this._loadedParentTiles={};for(const e in this._tiles){const t=[];let i,o=this._tiles[e].tileID;for(;o.overscaledZ>0;){if(o.key in this._loadedParentTiles){i=this._loadedParentTiles[o.key];break}t.push(o.key);const e=o.scaledTo(o.overscaledZ-1);if(i=this._getLoadedTile(e),i)break;o=e;}for(const e of t)this._loadedParentTiles[e]=i;}}_updateLoadedSiblingTileCache(){this._loadedSiblingTiles={};for(const e in this._tiles){const t=this._tiles[e].tileID,i=this._getLoadedTile(t);this._loadedSiblingTiles[t.key]=i;}}_addTile(e){let i=this._tiles[e.key];if(i)return i;i=this._cache.getAndRemove(e),i&&(this._setTileReloadTimer(e.key,i),i.tileID=e,this._state.initializeTileState(i,this.map?this.map.painter:null),this._cacheTimers[e.key]&&(clearTimeout(this._cacheTimers[e.key]),delete this._cacheTimers[e.key],this._setTileReloadTimer(e.key,i)));const o=i;return i||(i=new re(e,this._source.tileSize*e.overscaleFactor()),this._loadTile(i,e.key,i.state)),i.uses++,this._tiles[e.key]=i,o||this._source.fire(new t.l("dataloading",{tile:i,coord:i.tileID,dataType:"source"})),i}_setTileReloadTimer(e,t){e in this._timers&&(clearTimeout(this._timers[e]),delete this._timers[e]);const i=t.getExpiryTimeout();i&&(this._timers[e]=setTimeout((()=>{this._reloadTile(e,"expired"),delete this._timers[e];}),i));}refreshTiles(e){for(const t in this._tiles)(this._isIdRenderable(t)||"errored"==this._tiles[t].state)&&e.some((e=>e.equals(this._tiles[t].tileID.canonical)))&&this._reloadTile(t,"expired");}_removeTile(e){const t=this._tiles[e];t&&(t.uses--,delete this._tiles[e],this._timers[e]&&(clearTimeout(this._timers[e]),delete this._timers[e]),t.uses>0||(t.hasData()&&"reloading"!==t.state?this._cache.add(t.tileID,t,t.getExpiryTimeout()):(t.aborted=!0,this._abortTile(t),this._unloadTile(t))));}_dataHandler(e){const t=e.sourceDataType;"source"===e.dataType&&"metadata"===t&&(this._sourceLoaded=!0),this._sourceLoaded&&!this._paused&&"source"===e.dataType&&"content"===t&&(this.reload(e.sourceDataChanged),this.transform&&this.update(this.transform,this.terrain),this._didEmitContent=!0);}clearTiles(){this._shouldReloadOnResume=!1,this._paused=!1;for(const e in this._tiles)this._removeTile(e);this._cache.reset();}tilesIn(e,i,o){const r=[],a=this.transform;if(!a)return r;const s=a.getCoveringTilesDetailsProvider().allowWorldCopies(),n=o?a.getCameraQueryGeometry(e):e,l=e=>a.screenPointToMercatorCoordinate(e,this.terrain),c=this.transformBbox(e,l,!s),h=this.transformBbox(n,l,!s),u=this.getIds(),d=t.a2.fromPoints(h);for(let e=0;ee.getTilePoint(new t.a1(i.x,i.y))));if(i.expandBy(_),i.intersects(be)){const t=c.map((t=>e.getTilePoint(t))),i=h.map((t=>e.getTilePoint(t)));r.push({tile:o,tileID:s?e:e.unwrapTo(0),queryGeometry:t,cameraQueryGeometry:i,scale:l});}}}return r}transformBbox(e,i,o){let r=e.map(i);if(o){const o=t.a2.fromPoints(e);o.shrinkBy(.001*Math.min(o.width(),o.height()));const a=o.map(i);t.a2.fromPoints(r).covers(a)||(r=r.map((e=>e.x>.5?new t.a1(e.x-1,e.y,e.z):e)));}return r}getVisibleCoordinates(e){const t=this.getRenderableIds(e).map((e=>this._tiles[e].tileID));return this.transform&&this.transform.populateCache(t),t}hasTransition(){if(this._source.hasTransition())return !0;if(we(this._source.type)){const e=s.now();for(const t in this._tiles)if(this._tiles[t].fadeEndTime>=e)return !0}return !1}setFeatureState(e,t,i){this._state.updateState(e=e||"_geojsonTileLayer",t,i);}removeFeatureState(e,t,i){this._state.removeFeatureState(e=e||"_geojsonTileLayer",t,i);}getFeatureState(e,t){return this._state.getState(e=e||"_geojsonTileLayer",t)}setDependencies(e,t,i){const o=this._tiles[e];o&&o.setDependencies(t,i);}reloadTilesForDependencies(e,t){for(const i in this._tiles)this._tiles[i].hasDependency(e,t)&&this._reloadTile(i,"reloading");this._cache.filter((i=>!i.hasDependency(e,t)));}}function ye(e,t){const i=Math.abs(2*e.wrap)-+(e.wrap<0),o=Math.abs(2*t.wrap)-+(t.wrap<0);return e.overscaledZ-t.overscaledZ||o-i||t.canonical.y-e.canonical.y||t.canonical.x-e.canonical.x}function we(e){return "raster"===e||"image"===e||"video"===e}xe.maxOverzooming=10,xe.maxUnderzooming=3;class Te{constructor(e,t){this.reset(e,t);}reset(e,t){this.points=e||[],this._distances=[0];for(let e=1;e0?(r-s)/n:0;return this.points[a].mult(1-l).add(this.points[i].mult(l))}}function Pe(e,t){let i=!0;return "always"===e||"never"!==e&&"never"!==t||(i=!1),i}class Ce{constructor(e,t,i){const o=this.boxCells=[],r=this.circleCells=[];this.xCellCount=Math.ceil(e/i),this.yCellCount=Math.ceil(t/i);for(let e=0;ethis.width||o<0||t>this.height)return [];const n=[];if(e<=0&&t<=0&&this.width<=i&&this.height<=o){if(r)return [{key:null,x1:e,y1:t,x2:i,y2:o}];for(let e=0;e0}hitTestCircle(e,t,i,o,r){const a=e-i,s=e+i,n=t-i,l=t+i;if(s<0||a>this.width||l<0||n>this.height)return !1;const c=[];return this._forEachCell(a,n,s,l,this._queryCellCircle,c,{hitTest:!0,overlapMode:o,circle:{x:e,y:t,radius:i},seenUids:{box:{},circle:{}}},r),c.length>0}_queryCell(e,t,i,o,r,a,s,n){const{seenUids:l,hitTest:c,overlapMode:h}=s,u=this.boxCells[r];if(null!==u){const r=this.bboxes;for(const s of u)if(!l.box[s]){l.box[s]=!0;const u=4*s,d=this.boxKeys[s];if(e<=r[u+2]&&t<=r[u+3]&&i>=r[u+0]&&o>=r[u+1]&&(!n||n(d))&&(!c||!Pe(h,d.overlapMode))&&(a.push({key:d,x1:r[u],y1:r[u+1],x2:r[u+2],y2:r[u+3]}),c))return !0}}const d=this.circleCells[r];if(null!==d){const r=this.circles;for(const s of d)if(!l.circle[s]){l.circle[s]=!0;const u=3*s,d=this.circleKeys[s];if(this._circleAndRectCollide(r[u],r[u+1],r[u+2],e,t,i,o)&&(!n||n(d))&&(!c||!Pe(h,d.overlapMode))){const e=r[u],t=r[u+1],i=r[u+2];if(a.push({key:d,x1:e-i,y1:t-i,x2:e+i,y2:t+i}),c)return !0}}}return !1}_queryCellCircle(e,t,i,o,r,a,s,n){const{circle:l,seenUids:c,overlapMode:h}=s,u=this.boxCells[r];if(null!==u){const e=this.bboxes;for(const t of u)if(!c.box[t]){c.box[t]=!0;const i=4*t,o=this.boxKeys[t];if(this._circleAndRectCollide(l.x,l.y,l.radius,e[i+0],e[i+1],e[i+2],e[i+3])&&(!n||n(o))&&!Pe(h,o.overlapMode))return a.push(!0),!0}}const d=this.circleCells[r];if(null!==d){const e=this.circles;for(const t of d)if(!c.circle[t]){c.circle[t]=!0;const i=3*t,o=this.circleKeys[t];if(this._circlesCollide(e[i],e[i+1],e[i+2],l.x,l.y,l.radius)&&(!n||n(o))&&!Pe(h,o.overlapMode))return a.push(!0),!0}}}_forEachCell(e,t,i,o,r,a,s,n){const l=this._convertToXCellCoord(e),c=this._convertToYCellCoord(t),h=this._convertToXCellCoord(i),u=this._convertToYCellCoord(o);for(let d=l;d<=h;d++)for(let l=c;l<=u;l++)if(r.call(this,e,t,i,o,this.xCellCount*l+d,a,s,n))return}_convertToXCellCoord(e){return Math.max(0,Math.min(this.xCellCount-1,Math.floor(e*this.xScale)))}_convertToYCellCoord(e){return Math.max(0,Math.min(this.yCellCount-1,Math.floor(e*this.yScale)))}_circlesCollide(e,t,i,o,r,a){const s=o-e,n=r-t,l=i+a;return l*l>s*s+n*n}_circleAndRectCollide(e,t,i,o,r,a,s){const n=(a-o)/2,l=Math.abs(e-(o+n));if(l>n+i)return !1;const c=(s-r)/2,h=Math.abs(t-(r+c));if(h>c+i)return !1;if(l<=n||h<=c)return !0;const u=l-n,d=h-c;return u*u+d*d<=i*i}}function Ie(e,i,r){const a=t.L();if(!e){const{vecSouth:e,vecEast:t}=Se(i),r=o();r[0]=t[0],r[1]=t[1],r[2]=e[0],r[3]=e[1],s=r,(d=(l=(n=r)[0])*(u=n[3])-(h=n[2])*(c=n[1]))&&(s[0]=u*(d=1/d),s[1]=-c*d,s[2]=-h*d,s[3]=l*d),a[0]=r[0],a[1]=r[1],a[4]=r[2],a[5]=r[3];}var s,n,l,c,h,u,d;return t.N(a,a,[1/r,1/r,1]),a}function Me(e,i,o,r){if(e){const e=t.L();if(!i){const{vecSouth:t,vecEast:i}=Se(o);e[0]=i[0],e[1]=i[1],e[4]=t[0],e[5]=t[1];}return t.N(e,e,[r,r,1]),e}return o.pixelsToClipSpaceMatrix}function Se(e){const i=Math.cos(e.rollInRadians),o=Math.sin(e.rollInRadians),r=Math.cos(e.pitchInRadians),a=Math.cos(e.bearingInRadians),s=Math.sin(e.bearingInRadians),n=t.ar();n[0]=-a*r*o-s*i,n[1]=-s*r*o+a*i;const l=t.as(n);l<1e-9?t.at(n):t.au(n,n,1/l);const c=t.ar();c[0]=a*r*i-s*o,c[1]=s*r*i+a*o;const h=t.as(c);return h<1e-9?t.at(c):t.au(c,c,1/h),{vecEast:c,vecSouth:n}}function Ee(e,i,o,r){let a;r?(a=[e,i,r(e,i),1],t.aw(a,a,o)):(a=[e,i,0,1],qe(a,a,o));const s=a[3];return {point:new t.P(a[0]/s,a[1]/s),signedDistanceFromCamera:s,isOccluded:!1}}function Re(e,t){return .5+e/t*.5}function ze(e,t){return e.x>=-t[0]&&e.x<=t[0]&&e.y>=-t[1]&&e.y<=t[1]}function De(e,i,o,r,a,s,n,l,c,h,u,d,_){const p=o?e.textSizeData:e.iconSizeData,m=t.an(p,i.transform.zoom),f=[256/i.width*2+1,256/i.height*2+1],g=o?e.text.dynamicLayoutVertexArray:e.icon.dynamicLayoutVertexArray;g.clear();const v=e.lineVertexArray,b=o?e.text.placedSymbolArray:e.icon.placedSymbolArray,x=i.transform.width/i.transform.height;let y=!1;for(let o=0;oMath.abs(o.x-i.x)*r?{useVertical:!0}:(e===t.ao.vertical?i.yo.x)?{needsFlipping:!0}:null}function ke(e){const{projectionContext:i,pitchedLabelPlaneMatrixInverse:o,symbol:r,fontSize:a,flip:s,keepUpright:n,glyphOffsetArray:l,dynamicLayoutVertexArray:c,aspectRatio:h,rotateToLine:u}=e,d=a/24,_=r.lineOffsetX*d,p=r.lineOffsetY*d;let m;if(r.numGlyphs>1){const e=r.glyphStartIndex+r.numGlyphs,t=r.lineStartIndex,a=r.lineStartIndex+r.lineLength,c=Ae(d,l,_,p,s,r,u,i);if(!c)return {notEnoughRoom:!0};const f=je(c.first.point.x,c.first.point.y,i,o),g=je(c.last.point.x,c.last.point.y,i,o);if(n&&!s){const e=Le(r.writingMode,f,g,h);if(e)return e}m=[c.first];for(let o=r.glyphStartIndex+1;o0?n.point:Fe(i.tileAnchorPoint,s,e,1,i),c=je(e.x,e.y,i,o),u=je(l.x,l.y,i,o),d=Le(r.writingMode,c,u,h);if(d)return d}const e=Ge(d*l.getoffsetX(r.glyphStartIndex),_,p,s,r.segment,r.lineStartIndex,r.lineStartIndex+r.lineLength,i,u);if(!e||i.projectionCache.anyProjectionOccluded)return {notEnoughRoom:!0};m=[e];}for(const e of m)t.av(c,e.point,e.angle);return {}}function Fe(e,t,i,o,r){const a=e.add(e.sub(t)._unit()),s=Oe(a.x,a.y,r).point,n=i.sub(s);return i.add(n._mult(o/n.mag()))}function Be(e,i,o){const r=i.projectionCache;if(r.projections[e])return r.projections[e];const a=new t.P(i.lineVertexArray.getx(e),i.lineVertexArray.gety(e)),s=Oe(a.x,a.y,i);if(s.signedDistanceFromCamera>0)return r.projections[e]=s.point,r.anyProjectionOccluded=r.anyProjectionOccluded||s.isOccluded,s.point;const n=e-o.direction;return Fe(0===o.distanceFromAnchor?i.tileAnchorPoint:new t.P(i.lineVertexArray.getx(n),i.lineVertexArray.gety(n)),a,o.previousVertex,o.absOffsetX-o.distanceFromAnchor+1,i)}function Oe(e,t,i){const o=e+i.translation[0],r=t+i.translation[1];let a;return i.pitchWithMap?(a=Ee(o,r,i.pitchedLabelPlaneMatrix,i.getElevation),a.isOccluded=!1):(a=i.transform.projectTileCoordinates(o,r,i.unwrappedTileID,i.getElevation),a.point.x=(.5*a.point.x+.5)*i.width,a.point.y=(.5*-a.point.y+.5)*i.height),a}function je(e,i,o,r){if(o.pitchWithMap){const a=[e,i,0,1];return t.aw(a,a,r),o.transform.projectTileCoordinates(a[0]/a[3],a[1]/a[3],o.unwrappedTileID,o.getElevation).point}return {x:e/o.width*2-1,y:1-i/o.height*2}}function Ne(e,t,i){return i.transform.projectTileCoordinates(e,t,i.unwrappedTileID,i.getElevation)}function Ue(e,t,i){return e._unit()._perp()._mult(t*i)}function Ze(e,i,o,r,a,s,n,l,c){if(l.projectionCache.offsets[e])return l.projectionCache.offsets[e];const h=o.add(i);if(e+c.direction=a)return l.projectionCache.offsets[e]=h,h;const u=Be(e+c.direction,l,c),d=Ue(u.sub(o),n,c.direction),_=o.add(d),p=u.add(d);return l.projectionCache.offsets[e]=t.ax(s,h,_,p)||h,l.projectionCache.offsets[e]}function Ge(e,t,i,o,r,a,s,n,l){const c=o?e-t:e+t;let h=c>0?1:-1,u=0;o&&(h*=-1,u=Math.PI),h<0&&(u+=Math.PI);let d,_=h>0?a+r:a+r+1;n.projectionCache.cachedAnchorPoint?d=n.projectionCache.cachedAnchorPoint:(d=Oe(n.tileAnchorPoint.x,n.tileAnchorPoint.y,n).point,n.projectionCache.cachedAnchorPoint=d);let p,m,f=d,g=d,v=0,b=0;const x=Math.abs(c),y=[];let w;for(;v+b<=x;){if(_+=h,_=s)return null;v+=b,g=f,m=p;const e={absOffsetX:x,direction:h,distanceFromAnchor:v,previousVertex:g};if(f=Be(_,n,e),0===i)y.push(g),w=f.sub(g);else {let t;const o=f.sub(g);t=0===o.mag()?Ue(Be(_+h,n,e).sub(f),i,h):Ue(o,i,h),m||(m=g.add(t)),p=Ze(_,t,f,a,s,m,i,n,e),y.push(m),w=p.sub(m);}b=w.mag();}const T=w._mult((x-v)/b)._add(m||g),P=u+Math.atan2(f.y-g.y,f.x-g.x);return y.push(T),{point:T,angle:l?P:0,path:y}}const Ve=new Float32Array([-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0]);function $e(e,t){for(let i=0;i=1;e--)_.push(s.path[e]);for(let e=1;ee.signedDistanceFromCamera<=0))?[]:e.map((e=>e.point));}let f=[];if(_.length>0){const e=_[0].clone(),i=_[0].clone();for(let t=1;t<_.length;t++)e.x=Math.min(e.x,_[t].x),e.y=Math.min(e.y,_[t].y),i.x=Math.max(i.x,_[t].x),i.y=Math.max(i.y,_[t].y);f=e.x>=o.x&&i.x<=r.x&&e.y>=o.y&&i.y<=r.y?[_]:i.xr.x||i.yr.y?[]:t.ay([_],o.x,o.y,r.x,r.y);}for(const t of f){a.reset(t,.25*i);let o=0;o=a.length<=.5*i?1:Math.ceil(a.paddedLength/p)+1;for(let t=0;t{const t=Ee(e.x,e.y,o,i.getElevation),r=i.transform.projectTileCoordinates(t.point.x,t.point.y,i.unwrappedTileID,i.getElevation);return r.point.x=(.5*r.point.x+.5)*i.width,r.point.y=(.5*-r.point.y+.5)*i.height,r}))}(e,i);return function(e){let t=0,i=0,o=0,r=0;for(let a=0;ai&&(i=r,t=o));return e.slice(t,t+i)}(o)}queryRenderedSymbols(e){if(0===e.length||0===this.grid.keysLength()&&0===this.ignoredGrid.keysLength())return {};const i=[],o=new t.a2;for(const r of e){const e=new t.P(r.x+We,r.y+We);o.extend(e),i.push(e);}const{minX:r,minY:a,maxX:s,maxY:n}=o,l=this.grid.query(r,a,s,n).concat(this.ignoredGrid.query(r,a,s,n)),c={},h={};for(const e of l){const o=e.key;if(void 0===c[o.bucketInstanceId]&&(c[o.bucketInstanceId]={}),c[o.bucketInstanceId][o.featureIndex])continue;const r=[new t.P(e.x1,e.y1),new t.P(e.x2,e.y1),new t.P(e.x2,e.y2),new t.P(e.x1,e.y2)];t.az(i,r)&&(c[o.bucketInstanceId][o.featureIndex]=!0,void 0===h[o.bucketInstanceId]&&(h[o.bucketInstanceId]=[]),h[o.bucketInstanceId].push(o.featureIndex));}return h}insertCollisionBox(e,t,i,o,r,a){(i?this.ignoredGrid:this.grid).insert({bucketInstanceId:o,featureIndex:r,collisionGroupID:a,overlapMode:t},e[0],e[1],e[2],e[3]);}insertCollisionCircles(e,t,i,o,r,a){const s=i?this.ignoredGrid:this.grid,n={bucketInstanceId:o,featureIndex:r,collisionGroupID:a,overlapMode:t};for(let t=0;t=this.screenRightBoundary||othis.screenBottomBoundary}isInsideGrid(e,t,i,o){return i>=0&&e=0&&tthis.projectAndGetPerspectiveRatio(e.x,e.y,r,c,u)));E=e.some((e=>!e.isOccluded)),S=e.map((e=>new t.P(e.x,e.y)));}else E=!0;return {box:t.aA(S),allPointsOccluded:!E}}}class Xe{constructor(e,t,i,o){this.opacity=e?Math.max(0,Math.min(1,e.opacity+(e.placed?t:-t))):o&&i?1:0,this.placed=i;}isHidden(){return 0===this.opacity&&!this.placed}}class Ke{constructor(e,t,i,o,r){this.text=new Xe(e?e.text:null,t,i,r),this.icon=new Xe(e?e.icon:null,t,o,r);}isHidden(){return this.text.isHidden()&&this.icon.isHidden()}}class Ye{constructor(e,t,i){this.text=e,this.icon=t,this.skipFade=i;}}class Qe{constructor(e,t,i,o,r){this.bucketInstanceId=e,this.featureIndex=t,this.sourceLayerIndex=i,this.bucketIndex=o,this.tileID=r;}}class Je{constructor(e){this.crossSourceCollisions=e,this.maxGroupID=0,this.collisionGroups={};}get(e){if(this.crossSourceCollisions)return {ID:0,predicate:null};if(!this.collisionGroups[e]){const t=++this.maxGroupID;this.collisionGroups[e]={ID:t,predicate:e=>e.collisionGroupID===t};}return this.collisionGroups[e]}}function et(e,i,o,r,a){const{horizontalAlign:s,verticalAlign:n}=t.aH(e);return new t.P(-(s-.5)*i+r[0]*a,-(n-.5)*o+r[1]*a)}class tt{constructor(e,t,i,o,r){this.transform=e.clone(),this.terrain=t,this.collisionIndex=new He(this.transform),this.placements={},this.opacities={},this.variableOffsets={},this.stale=!1,this.commitTime=0,this.fadeDuration=i,this.retainedQueryData={},this.collisionGroups=new Je(o),this.collisionCircleArrays={},this.collisionBoxArrays=new Map,this.prevPlacement=r,r&&(r.prevPlacement=void 0),this.placedOrientations={};}_getTerrainElevationFunc(e){const t=this.terrain;return t?(i,o)=>t.getElevation(e,i,o):null}getBucketParts(e,i,o,r){const a=o.getBucket(i),s=o.latestFeatureIndex;if(!a||!s||i.id!==a.layerIds[0])return;const n=o.collisionBoxArray,l=a.layers[0].layout,c=a.layers[0].paint,h=Math.pow(2,this.transform.zoom-o.tileID.overscaledZ),u=o.tileSize/t.$,d=o.tileID.toUnwrapped(),_="map"===l.get("text-rotation-alignment"),p=t.aC(o,1,this.transform.zoom),m=t.aD(this.collisionIndex.transform,o,c.get("text-translate"),c.get("text-translate-anchor")),f=t.aD(this.collisionIndex.transform,o,c.get("icon-translate"),c.get("icon-translate-anchor")),g=Ie(_,this.transform,p);this.retainedQueryData[a.bucketInstanceId]=new Qe(a.bucketInstanceId,s,a.sourceLayerIndex,a.index,o.tileID);const v={bucket:a,layout:l,translationText:m,translationIcon:f,unwrappedTileID:d,pitchedLabelPlaneMatrix:g,scale:h,textPixelRatio:u,holdingForFade:o.holdingForFade(),collisionBoxArray:n,partiallyEvaluatedTextSize:t.an(a.textSizeData,this.transform.zoom),collisionGroup:this.collisionGroups.get(a.sourceID)};if(r)for(const t of a.sortKeyRanges){const{sortKey:i,symbolInstanceStart:o,symbolInstanceEnd:r}=t;e.push({sortKey:i,symbolInstanceStart:o,symbolInstanceEnd:r,parameters:v});}else e.push({symbolInstanceStart:0,symbolInstanceEnd:a.symbolInstances.length,parameters:v});}attemptAnchorPlacement(e,i,o,r,a,s,n,l,c,h,u,d,_,p,m,f,g,v,b,x){const y=t.aE[e.textAnchor],w=[e.textOffset0,e.textOffset1],T=et(y,o,r,w,a),P=this.collisionIndex.placeCollisionBox(i,d,l,c,h,n,s,f,u.predicate,b,T,x);if((!v||this.collisionIndex.placeCollisionBox(v,d,l,c,h,n,s,g,u.predicate,b,T,x).placeable)&&P.placeable){let e;if(this.prevPlacement&&this.prevPlacement.variableOffsets[_.crossTileID]&&this.prevPlacement.placements[_.crossTileID]&&this.prevPlacement.placements[_.crossTileID].text&&(e=this.prevPlacement.variableOffsets[_.crossTileID].anchor),0===_.crossTileID)throw new Error("symbolInstance.crossTileID can't be 0");return this.variableOffsets[_.crossTileID]={textOffset:w,width:o,height:r,anchor:y,textBoxScale:a,prevAnchor:e},this.markUsedJustification(p,y,_,m),p.allowVerticalPlacement&&(this.markUsedOrientation(p,m,_),this.placedOrientations[_.crossTileID]=m),{shift:T,placedGlyphBoxes:P}}}placeLayerBucketPart(e,i,o){const{bucket:r,layout:a,translationText:s,translationIcon:n,unwrappedTileID:l,pitchedLabelPlaneMatrix:c,textPixelRatio:h,holdingForFade:u,collisionBoxArray:d,partiallyEvaluatedTextSize:_,collisionGroup:p}=e.parameters,m=a.get("text-optional"),f=a.get("icon-optional"),g=t.aF(a,"text-overlap","text-allow-overlap"),v="always"===g,b=t.aF(a,"icon-overlap","icon-allow-overlap"),x="always"===b,y="map"===a.get("text-rotation-alignment"),w="map"===a.get("text-pitch-alignment"),T="none"!==a.get("icon-text-fit"),P="viewport-y"===a.get("symbol-z-order"),C=v&&(x||!r.hasIconData()||f),I=x&&(v||!r.hasTextData()||m);!r.collisionArrays&&d&&r.deserializeCollisionBoxes(d);const M=this.retainedQueryData[r.bucketInstanceId].tileID,S=this._getTerrainElevationFunc(M),E=this.transform.getFastPathSimpleProjectionMatrix(M),R=(e,d,x)=>{var P,R;if(i[e.crossTileID])return;if(u)return void(this.placements[e.crossTileID]=new Ye(!1,!1,!1));let z=!1,D=!1,A=!0,L=null,k={box:null,placeable:!1,offscreen:null,occluded:!1},F={placeable:!1},B=null,O=null,j=null,N=0,U=0,Z=0;d.textFeatureIndex?N=d.textFeatureIndex:e.useRuntimeCollisionCircles&&(N=e.featureIndex),d.verticalTextFeatureIndex&&(U=d.verticalTextFeatureIndex);const G=d.textBox;if(G){const i=i=>{let o=t.ao.horizontal;if(r.allowVerticalPlacement&&!i&&this.prevPlacement){const t=this.prevPlacement.placedOrientations[e.crossTileID];t&&(this.placedOrientations[e.crossTileID]=t,o=t,this.markUsedOrientation(r,o,e));}return o},a=(i,o)=>{if(r.allowVerticalPlacement&&e.numVerticalGlyphVertices>0&&d.verticalTextBox){for(const e of r.writingModes)if(e===t.ao.vertical?(k=o(),F=k):k=i(),k&&k.placeable)break}else k=i();},c=e.textAnchorOffsetStartIndex,u=e.textAnchorOffsetEndIndex;if(u===c){const o=(t,i)=>{const o=this.collisionIndex.placeCollisionBox(t,g,h,M,l,w,y,s,p.predicate,S,void 0,E);return o&&o.placeable&&(this.markUsedOrientation(r,i,e),this.placedOrientations[e.crossTileID]=i),o};a((()=>o(G,t.ao.horizontal)),(()=>{const i=d.verticalTextBox;return r.allowVerticalPlacement&&e.numVerticalGlyphVertices>0&&i?o(i,t.ao.vertical):{box:null,offscreen:null}})),i(k&&k.placeable);}else {let _=t.aE[null===(R=null===(P=this.prevPlacement)||void 0===P?void 0:P.variableOffsets[e.crossTileID])||void 0===R?void 0:R.anchor];const m=(t,i,a)=>{const d=t.x2-t.x1,m=t.y2-t.y1,f=e.textBoxScale,v=T&&"never"===b?i:null;let x=null,P="never"===g?1:2,C="never";_&&P++;for(let i=0;im(G,d.iconBox,t.ao.horizontal)),(()=>{const i=d.verticalTextBox;return r.allowVerticalPlacement&&(!k||!k.placeable)&&e.numVerticalGlyphVertices>0&&i?m(i,d.verticalIconBox,t.ao.vertical):{box:null,occluded:!0,offscreen:null}})),k&&(z=k.placeable,A=k.offscreen);const f=i(k&&k.placeable);if(!z&&this.prevPlacement){const t=this.prevPlacement.variableOffsets[e.crossTileID];t&&(this.variableOffsets[e.crossTileID]=t,this.markUsedJustification(r,t.anchor,e,f));}}}if(B=k,z=B&&B.placeable,A=B&&B.offscreen,e.useRuntimeCollisionCircles){const i=r.text.placedSymbolArray.get(e.centerJustifiedTextSymbolIndex),n=t.ap(r.textSizeData,_,i),h=a.get("text-padding");O=this.collisionIndex.placeCollisionCircles(g,i,r.lineVertexArray,r.glyphOffsetArray,n,l,c,o,w,p.predicate,e.collisionCircleDiameter,h,s,S),O.circles.length&&O.collisionDetected&&!o&&t.w("Collisions detected, but collision boxes are not shown"),z=v||O.circles.length>0&&!O.collisionDetected,A=A&&O.offscreen;}if(d.iconFeatureIndex&&(Z=d.iconFeatureIndex),d.iconBox){const e=e=>this.collisionIndex.placeCollisionBox(e,b,h,M,l,w,y,n,p.predicate,S,T&&L?L:void 0,E);F&&F.placeable&&d.verticalIconBox?(j=e(d.verticalIconBox),D=j.placeable):(j=e(d.iconBox),D=j.placeable),A=A&&j.offscreen;}const V=m||0===e.numHorizontalGlyphVertices&&0===e.numVerticalGlyphVertices,$=f||0===e.numIconVertices;V||$?$?V||(D=D&&z):z=D&&z:D=z=D&&z;const q=D&&j.placeable;if(z&&B.placeable&&this.collisionIndex.insertCollisionBox(B.box,g,a.get("text-ignore-placement"),r.bucketInstanceId,F&&F.placeable&&U?U:N,p.ID),q&&this.collisionIndex.insertCollisionBox(j.box,b,a.get("icon-ignore-placement"),r.bucketInstanceId,Z,p.ID),O&&z&&this.collisionIndex.insertCollisionCircles(O.circles,g,a.get("text-ignore-placement"),r.bucketInstanceId,N,p.ID),o&&this.storeCollisionData(r.bucketInstanceId,x,d,B,j,O),0===e.crossTileID)throw new Error("symbolInstance.crossTileID can't be 0");if(0===r.bucketInstanceId)throw new Error("bucket.bucketInstanceId can't be 0");this.placements[e.crossTileID]=new Ye((z||C)&&!(null==B?void 0:B.occluded),(D||I)&&!(null==j?void 0:j.occluded),A||r.justReloaded),i[e.crossTileID]=!0;};if(P){if(0!==e.symbolInstanceStart)throw new Error("bucket.bucketInstanceId should be 0");const t=r.getSortedSymbolIndexes(-this.transform.bearingInRadians);for(let e=t.length-1;e>=0;--e){const i=t[e];R(r.symbolInstances.get(i),r.collisionArrays[i],i);}}else for(let t=e.symbolInstanceStart;t=0&&(e.text.placedSymbolArray.get(t).crossTileID=a>=0&&t!==a?0:o.crossTileID);}markUsedOrientation(e,i,o){const r=i===t.ao.horizontal||i===t.ao.horizontalOnly?i:0,a=i===t.ao.vertical?i:0,s=[o.leftJustifiedTextSymbolIndex,o.centerJustifiedTextSymbolIndex,o.rightJustifiedTextSymbolIndex];for(const t of s)e.text.placedSymbolArray.get(t).placedOrientation=r;o.verticalPlacedTextSymbolIndex&&(e.text.placedSymbolArray.get(o.verticalPlacedTextSymbolIndex).placedOrientation=a);}commit(e){this.commitTime=e,this.zoomAtLastRecencyCheck=this.transform.zoom;const t=this.prevPlacement;let i=!1;this.prevZoomAdjustment=t?t.zoomAdjustment(this.transform.zoom):0;const o=t?t.symbolFadeChange(e):1,r=t?t.opacities:{},a=t?t.variableOffsets:{},s=t?t.placedOrientations:{};for(const e in this.placements){const t=this.placements[e],a=r[e];a?(this.opacities[e]=new Ke(a,o,t.text,t.icon),i=i||t.text!==a.text.placed||t.icon!==a.icon.placed):(this.opacities[e]=new Ke(null,o,t.text,t.icon,t.skipFade),i=i||t.text||t.icon);}for(const e in r){const t=r[e];if(!this.opacities[e]){const r=new Ke(t,o,!1,!1);r.isHidden()||(this.opacities[e]=r,i=i||t.text.placed||t.icon.placed);}}for(const e in a)this.variableOffsets[e]||!this.opacities[e]||this.opacities[e].isHidden()||(this.variableOffsets[e]=a[e]);for(const e in s)this.placedOrientations[e]||!this.opacities[e]||this.opacities[e].isHidden()||(this.placedOrientations[e]=s[e]);if(t&&void 0===t.lastPlacementChangeTime)throw new Error("Last placement time for previous placement is not defined");i?this.lastPlacementChangeTime=e:"number"!=typeof this.lastPlacementChangeTime&&(this.lastPlacementChangeTime=t?t.lastPlacementChangeTime:e);}updateLayerOpacities(e,t){const i={};for(const o of t){const t=o.getBucket(e);t&&o.latestFeatureIndex&&e.id===t.layerIds[0]&&this.updateBucketOpacities(t,o.tileID,i,o.collisionBoxArray);}}updateBucketOpacities(e,i,o,r){e.hasTextData()&&(e.text.opacityVertexArray.clear(),e.text.hasVisibleVertices=!1),e.hasIconData()&&(e.icon.opacityVertexArray.clear(),e.icon.hasVisibleVertices=!1),e.hasIconCollisionBoxData()&&e.iconCollisionBox.collisionVertexArray.clear(),e.hasTextCollisionBoxData()&&e.textCollisionBox.collisionVertexArray.clear();const a=e.layers[0],s=a.layout,n=new Ke(null,0,!1,!1,!0),l=s.get("text-allow-overlap"),c=s.get("icon-allow-overlap"),h=a._unevaluatedLayout.hasValue("text-variable-anchor")||a._unevaluatedLayout.hasValue("text-variable-anchor-offset"),u="map"===s.get("text-rotation-alignment"),d="map"===s.get("text-pitch-alignment"),_="none"!==s.get("icon-text-fit"),p=new Ke(null,0,l&&(c||!e.hasIconData()||s.get("icon-optional")),c&&(l||!e.hasTextData()||s.get("text-optional")),!0);!e.collisionArrays&&r&&(e.hasIconCollisionBoxData()||e.hasTextCollisionBoxData())&&e.deserializeCollisionBoxes(r);const m=(e,t,i)=>{for(let o=0;o0,v=this.placedOrientations[r.crossTileID],b=v===t.ao.vertical,x=v===t.ao.horizontal||v===t.ao.horizontalOnly;if(a>0||s>0){const t=ht(c.text);m(e.text,a,b?ut:t),m(e.text,s,x?ut:t);const i=c.text.isHidden();[r.rightJustifiedTextSymbolIndex,r.centerJustifiedTextSymbolIndex,r.leftJustifiedTextSymbolIndex].forEach((t=>{t>=0&&(e.text.placedSymbolArray.get(t).hidden=i||b?1:0);})),r.verticalPlacedTextSymbolIndex>=0&&(e.text.placedSymbolArray.get(r.verticalPlacedTextSymbolIndex).hidden=i||x?1:0);const o=this.variableOffsets[r.crossTileID];o&&this.markUsedJustification(e,o.anchor,r,v);const n=this.placedOrientations[r.crossTileID];n&&(this.markUsedJustification(e,"left",r,n),this.markUsedOrientation(e,n,r));}if(g){const t=ht(c.icon),i=!(_&&r.verticalPlacedIconSymbolIndex&&b);r.placedIconSymbolIndex>=0&&(m(e.icon,r.numIconVertices,i?t:ut),e.icon.placedSymbolArray.get(r.placedIconSymbolIndex).hidden=c.icon.isHidden()),r.verticalPlacedIconSymbolIndex>=0&&(m(e.icon,r.numVerticalIconVertices,i?ut:t),e.icon.placedSymbolArray.get(r.verticalPlacedIconSymbolIndex).hidden=c.icon.isHidden());}const y=f&&f.has(i)?f.get(i):{text:null,icon:null};if(e.hasIconCollisionBoxData()||e.hasTextCollisionBoxData()){const o=e.collisionArrays[i];if(o){let i=new t.P(0,0);if(o.textBox||o.verticalTextBox){let t=!0;if(h){const e=this.variableOffsets[l];e?(i=et(e.anchor,e.width,e.height,e.textOffset,e.textBoxScale),u&&i._rotate(d?-this.transform.bearingInRadians:this.transform.bearingInRadians)):t=!1;}if(o.textBox||o.verticalTextBox){let r;o.textBox&&(r=b),o.verticalTextBox&&(r=x),it(e.textCollisionBox.collisionVertexArray,c.text.placed,!t||r,y.text,i.x,i.y);}}if(o.iconBox||o.verticalIconBox){const t=Boolean(!x&&o.verticalIconBox);let r;o.iconBox&&(r=t),o.verticalIconBox&&(r=!t),it(e.iconCollisionBox.collisionVertexArray,c.icon.placed,r,y.icon,_?i.x:0,_?i.y:0);}}}}if(e.sortFeatures(-this.transform.bearingInRadians),this.retainedQueryData[e.bucketInstanceId]&&(this.retainedQueryData[e.bucketInstanceId].featureSortOrder=e.featureSortOrder),e.hasTextData()&&e.text.opacityVertexBuffer&&e.text.opacityVertexBuffer.updateData(e.text.opacityVertexArray),e.hasIconData()&&e.icon.opacityVertexBuffer&&e.icon.opacityVertexBuffer.updateData(e.icon.opacityVertexArray),e.hasIconCollisionBoxData()&&e.iconCollisionBox.collisionVertexBuffer&&e.iconCollisionBox.collisionVertexBuffer.updateData(e.iconCollisionBox.collisionVertexArray),e.hasTextCollisionBoxData()&&e.textCollisionBox.collisionVertexBuffer&&e.textCollisionBox.collisionVertexBuffer.updateData(e.textCollisionBox.collisionVertexArray),e.text.opacityVertexArray.length!==e.text.layoutVertexArray.length/4)throw new Error(`bucket.text.opacityVertexArray.length (= ${e.text.opacityVertexArray.length}) !== bucket.text.layoutVertexArray.length (= ${e.text.layoutVertexArray.length}) / 4`);if(e.icon.opacityVertexArray.length!==e.icon.layoutVertexArray.length/4)throw new Error(`bucket.icon.opacityVertexArray.length (= ${e.icon.opacityVertexArray.length}) !== bucket.icon.layoutVertexArray.length (= ${e.icon.layoutVertexArray.length}) / 4`);e.bucketInstanceId in this.collisionCircleArrays&&(e.collisionCircleArray=this.collisionCircleArrays[e.bucketInstanceId],delete this.collisionCircleArrays[e.bucketInstanceId]);}symbolFadeChange(e){return 0===this.fadeDuration?1:(e-this.commitTime)/this.fadeDuration+this.prevZoomAdjustment}zoomAdjustment(e){return Math.max(0,(this.transform.zoom-e)/1.5)}hasTransitions(e){return this.stale||e-this.lastPlacementChangeTimee}setStale(){this.stale=!0;}}function it(e,t,i,o,r,a){o&&0!==o.length||(o=[0,0,0,0]);const s=o[0]-We,n=o[1]-We,l=o[2]-We,c=o[3]-We;e.emplaceBack(t?1:0,i?1:0,r||0,a||0,s,n),e.emplaceBack(t?1:0,i?1:0,r||0,a||0,l,n),e.emplaceBack(t?1:0,i?1:0,r||0,a||0,l,c),e.emplaceBack(t?1:0,i?1:0,r||0,a||0,s,c);}const ot=Math.pow(2,25),rt=Math.pow(2,24),at=Math.pow(2,17),st=Math.pow(2,16),nt=Math.pow(2,9),lt=Math.pow(2,8),ct=Math.pow(2,1);function ht(e){if(0===e.opacity&&!e.placed)return 0;if(1===e.opacity&&e.placed)return 4294967295;const t=e.placed?1:0,i=Math.floor(127*e.opacity);return i*ot+t*rt+i*at+t*st+i*nt+t*lt+i*ct+t}const ut=0;class dt{constructor(e){this._sortAcrossTiles="viewport-y"!==e.layout.get("symbol-z-order")&&!e.layout.get("symbol-sort-key").isConstant(),this._currentTileIndex=0,this._currentPartIndex=0,this._seenCrossTileIDs={},this._bucketParts=[];}continuePlacement(e,t,i,o,r){const a=this._bucketParts;for(;this._currentTileIndexe.sortKey-t.sortKey)));this._currentPartIndex!this._forceFullPlacement&&s.now()-o>2;for(;this._currentPlacementIndex>=0;){const o=t[e[this._currentPlacementIndex]],a=this.placement.collisionIndex.transform.zoom;if("symbol"===o.type&&(!o.minzoom||o.minzoom<=a)&&(!o.maxzoom||o.maxzoom>a)){if(this._inProgressLayer||(this._inProgressLayer=new dt(o)),this._inProgressLayer.continuePlacement(i[o.source],this.placement,this._showCollisionBoxes,o,r))return;delete this._inProgressLayer;}this._currentPlacementIndex--;}this._done=!0;}commit(e){return this.placement.commit(e),this.placement}}const pt=512/t.$/2;class mt{constructor(e,i,o){this.tileID=e,this.bucketInstanceId=o,this._symbolsByKey={};const r=new Map;for(let e=0;e({x:Math.floor(e.anchorX*pt),y:Math.floor(e.anchorY*pt)}))),crossTileIDs:i.map((e=>e.crossTileID))};if(o.positions.length>128){const e=new t.aI(o.positions.length,16,Uint16Array);for(const{x:t,y:i}of o.positions)e.add(t,i);e.finish(),delete o.positions,o.index=e;}this._symbolsByKey[e]=o;}}getScaledCoordinates(e,i){const{x:o,y:r,z:a}=this.tileID.canonical,{x:s,y:n,z:l}=i.canonical,c=pt/Math.pow(2,l-a),h=(n*t.$+e.anchorY)*c,u=r*t.$*pt;return {x:Math.floor((s*t.$+e.anchorX)*c-o*t.$*pt),y:Math.floor(h-u)}}findMatches(e,t,i){const o=this.tileID.canonical.ze))}}class ft{constructor(){this.maxCrossTileID=0;}generate(){return ++this.maxCrossTileID}}class gt{constructor(){this.indexes={},this.usedCrossTileIDs={},this.lng=0;}handleWrapJump(e){const t=Math.round((e-this.lng)/360);if(0!==t)for(const e in this.indexes){const i=this.indexes[e],o={};for(const e in i){const r=i[e];r.tileID=r.tileID.unwrapTo(r.tileID.wrap+t),o[r.tileID.key]=r;}this.indexes[e]=o;}this.lng=e;}addBucket(e,t,i){if(this.indexes[e.overscaledZ]&&this.indexes[e.overscaledZ][e.key]){if(this.indexes[e.overscaledZ][e.key].bucketInstanceId===t.bucketInstanceId)return !1;this.removeBucketCrossTileIDs(e.overscaledZ,this.indexes[e.overscaledZ][e.key]);}for(let e=0;ee.overscaledZ)for(const i in r){const a=r[i];a.tileID.isChildOf(e)&&a.findMatches(t.symbolInstances,e,o);}else {const a=r[e.scaledTo(Number(i)).key];a&&a.findMatches(t.symbolInstances,e,o);}}for(let e=0;e{t[e]=!0;}));for(const e in this.layerIndexes)t[e]||delete this.layerIndexes[e];}}var bt="void main() {fragColor=vec4(1.0);}";const xt={prelude:yt("#ifdef GL_ES\nprecision mediump float;\n#else\n#if !defined(lowp)\n#define lowp\n#endif\n#if !defined(mediump)\n#define mediump\n#endif\n#if !defined(highp)\n#define highp\n#endif\n#endif\nout highp vec4 fragColor;","#ifdef GL_ES\nprecision highp float;\n#else\n#if !defined(lowp)\n#define lowp\n#endif\n#if !defined(mediump)\n#define mediump\n#endif\n#if !defined(highp)\n#define highp\n#endif\n#endif\nvec2 unpack_float(const float packedValue) {int packedIntValue=int(packedValue);int v0=packedIntValue/256;return vec2(v0,packedIntValue-v0*256);}vec2 unpack_opacity(const float packedOpacity) {int intOpacity=int(packedOpacity)/2;return vec2(float(intOpacity)/127.0,mod(packedOpacity,2.0));}vec4 decode_color(const vec2 encodedColor) {return vec4(unpack_float(encodedColor[0])/255.0,unpack_float(encodedColor[1])/255.0\n);}float unpack_mix_vec2(const vec2 packedValue,const float t) {return mix(packedValue[0],packedValue[1],t);}vec4 unpack_mix_color(const vec4 packedColors,const float t) {vec4 minColor=decode_color(vec2(packedColors[0],packedColors[1]));vec4 maxColor=decode_color(vec2(packedColors[2],packedColors[3]));return mix(minColor,maxColor,t);}vec2 get_pattern_pos(const vec2 pixel_coord_upper,const vec2 pixel_coord_lower,const vec2 pattern_size,const float tile_units_to_pixels,const vec2 pos) {vec2 offset=mod(mod(mod(pixel_coord_upper,pattern_size)*256.0,pattern_size)*256.0+pixel_coord_lower,pattern_size);return (tile_units_to_pixels*pos+offset)/pattern_size;}mat3 rotationMatrixFromAxisAngle(vec3 u,float angle) {float c=cos(angle);float s=sin(angle);float c2=1.0-c;return mat3(u.x*u.x*c2+ c,u.x*u.y*c2-u.z*s,u.x*u.z*c2+u.y*s,u.y*u.x*c2+u.z*s,u.y*u.y*c2+ c,u.y*u.z*c2-u.x*s,u.z*u.x*c2-u.y*s,u.z*u.y*c2+u.x*s,u.z*u.z*c2+ c\n);}\n#ifdef TERRAIN3D\nuniform sampler2D u_terrain;uniform float u_terrain_dim;uniform mat4 u_terrain_matrix;uniform vec4 u_terrain_unpack;uniform float u_terrain_exaggeration;uniform highp sampler2D u_depth;\n#endif\nconst highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitShifts=vec4(1.)/bitSh;highp float unpack(highp vec4 color) {return dot(color,bitShifts);}highp float depthOpacity(vec3 frag) {\n#ifdef TERRAIN3D\nhighp float d=unpack(texture(u_depth,frag.xy*0.5+0.5))+0.0001-frag.z;return 1.0-max(0.0,min(1.0,-d*500.0));\n#else\nreturn 1.0;\n#endif\n}float calculate_visibility(vec4 pos) {\n#ifdef TERRAIN3D\nvec3 frag=pos.xyz/pos.w;highp float d=depthOpacity(frag);if (d > 0.95) return 1.0;return (d+depthOpacity(frag+vec3(0.0,0.01,0.0)))/2.0;\n#else\nreturn 1.0;\n#endif\n}float ele(vec2 pos) {\n#ifdef TERRAIN3D\nvec4 rgb=(texture(u_terrain,pos)*255.0)*u_terrain_unpack;return rgb.r+rgb.g+rgb.b-u_terrain_unpack.a;\n#else\nreturn 0.0;\n#endif\n}float get_elevation(vec2 pos) {\n#ifdef TERRAIN3D\n#ifdef GLOBE\nif ((pos.y <-32767.5) || (pos.y > 32766.5)) {return 0.0;}\n#endif\nvec2 coord=(u_terrain_matrix*vec4(pos,0.0,1.0)).xy*u_terrain_dim+1.0;vec2 f=fract(coord);vec2 c=(floor(coord)+0.5)/(u_terrain_dim+2.0);float d=1.0/(u_terrain_dim+2.0);float tl=ele(c);float tr=ele(c+vec2(d,0.0));float bl=ele(c+vec2(0.0,d));float br=ele(c+vec2(d,d));float elevation=mix(mix(tl,tr,f.x),mix(bl,br,f.x),f.y);return elevation*u_terrain_exaggeration;\n#else\nreturn 0.0;\n#endif\n}const float PI=3.141592653589793;uniform mat4 u_projection_matrix;"),projectionMercator:yt("","float projectLineThickness(float tileY) {return 1.0;}float projectCircleRadius(float tileY) {return 1.0;}vec4 projectTile(vec2 p) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);return result;}vec4 projectTile(vec2 p,vec2 rawPos) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);if (rawPos.y <-32767.5 || rawPos.y > 32766.5) {result.z=-10000000.0;}return result;}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_projection_matrix*vec4(posInTile,elevation,1.0);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {return projectTileWithElevation(posInTile,elevation);}"),projectionGlobe:yt("","#define GLOBE_RADIUS 6371008.8\nuniform highp vec4 u_projection_tile_mercator_coords;uniform highp vec4 u_projection_clipping_plane;uniform highp float u_projection_transition;uniform mat4 u_projection_fallback_matrix;vec3 globeRotateVector(vec3 vec,vec2 angles) {vec3 axisRight=vec3(vec.z,0.0,-vec.x);vec3 axisUp=cross(axisRight,vec);axisRight=normalize(axisRight);axisUp=normalize(axisUp);vec2 t=tan(angles);return normalize(vec+axisRight*t.x+axisUp*t.y);}mat3 globeGetRotationMatrix(vec3 spherePos) {vec3 axisRight=vec3(spherePos.z,0.0,-spherePos.x);vec3 axisDown=cross(axisRight,spherePos);axisRight=normalize(axisRight);axisDown=normalize(axisDown);return mat3(axisRight,axisDown,spherePos\n);}float circumferenceRatioAtTileY(float tileY) {float mercator_pos_y=u_projection_tile_mercator_coords.y+u_projection_tile_mercator_coords.w*tileY;float spherical_y=2.0*atan(exp(PI-(mercator_pos_y*PI*2.0)))-PI*0.5;return cos(spherical_y);}float projectLineThickness(float tileY) {float thickness=1.0/circumferenceRatioAtTileY(tileY); \nif (u_projection_transition < 0.999) {return mix(1.0,thickness,u_projection_transition);} else {return thickness;}}vec3 projectToSphere(vec2 translatedPos,vec2 rawPos) {vec2 mercator_pos=u_projection_tile_mercator_coords.xy+u_projection_tile_mercator_coords.zw*translatedPos;vec2 spherical;spherical.x=mercator_pos.x*PI*2.0+PI;spherical.y=2.0*atan(exp(PI-(mercator_pos.y*PI*2.0)))-PI*0.5;float len=cos(spherical.y);vec3 pos=vec3(sin(spherical.x)*len,sin(spherical.y),cos(spherical.x)*len\n);if (rawPos.y <-32767.5) {pos=vec3(0.0,1.0,0.0);}if (rawPos.y > 32766.5) {pos=vec3(0.0,-1.0,0.0);}return pos;}vec3 projectToSphere(vec2 posInTile) {return projectToSphere(posInTile,vec2(0.0,0.0));}float globeComputeClippingZ(vec3 spherePos) {return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));}vec4 interpolateProjection(vec2 posInTile,vec3 spherePos,float elevation) {vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);globePosition.z=globeComputeClippingZ(elevatedPos)*globePosition.w;if (u_projection_transition > 0.999) {return globePosition;}vec4 flatPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);const float z_globeness_threshold=0.2;vec4 result=globePosition;result.z=mix(0.0,globePosition.z,clamp((u_projection_transition-z_globeness_threshold)/(1.0-z_globeness_threshold),0.0,1.0));result.xyw=mix(flatPosition.xyw,globePosition.xyw,u_projection_transition);if ((posInTile.y <-32767.5) || (posInTile.y > 32766.5)) {result=globePosition;const float poles_hidden_anim_percentage=0.02;result.z=mix(globePosition.z,100.0,pow(max((1.0-u_projection_transition)/poles_hidden_anim_percentage,0.0),8.0));}return result;}vec4 interpolateProjectionFor3D(vec2 posInTile,vec3 spherePos,float elevation) {vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);if (u_projection_transition > 0.999) {return globePosition;}vec4 fallbackPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);return mix(fallbackPosition,globePosition,u_projection_transition);}vec4 projectTile(vec2 posInTile) {return interpolateProjection(posInTile,projectToSphere(posInTile),0.0);}vec4 projectTile(vec2 posInTile,vec2 rawPos) {return interpolateProjection(posInTile,projectToSphere(posInTile,rawPos),0.0);}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return interpolateProjection(posInTile,projectToSphere(posInTile),elevation);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {vec3 spherePos=projectToSphere(posInTile,posInTile);return interpolateProjectionFor3D(posInTile,spherePos,elevation);}"),background:yt("uniform vec4 u_color;uniform float u_opacity;void main() {fragColor=u_color*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}"),backgroundPattern:yt("uniform vec2 u_pattern_tl_a;uniform vec2 u_pattern_br_a;uniform vec2 u_pattern_tl_b;uniform vec2 u_pattern_br_b;uniform vec2 u_texsize;uniform float u_mix;uniform float u_opacity;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;void main() {vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(u_pattern_tl_a/u_texsize,u_pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(u_pattern_tl_b/u_texsize,u_pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);fragColor=mix(color1,color2,u_mix)*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_pattern_size_a;uniform vec2 u_pattern_size_b;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_scale_a;uniform float u_scale_b;uniform float u_tile_units_to_pixels;in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;void main() {gl_Position=projectTile(a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_a*u_pattern_size_a,u_tile_units_to_pixels,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_b*u_pattern_size_b,u_tile_units_to_pixels,a_pos);}"),circle:yt("in vec3 v_data;in float v_visibility;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define mediump float radius\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define highp vec4 stroke_color\n#pragma mapbox: define mediump float stroke_width\n#pragma mapbox: define lowp float stroke_opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize mediump float radius\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize highp vec4 stroke_color\n#pragma mapbox: initialize mediump float stroke_width\n#pragma mapbox: initialize lowp float stroke_opacity\nvec2 extrude=v_data.xy;float extrude_length=length(extrude);float antialiased_blur=v_data.z;float opacity_t=smoothstep(0.0,antialiased_blur,extrude_length-1.0);float color_t=stroke_width < 0.01 ? 0.0 : smoothstep(antialiased_blur,0.0,extrude_length-radius/(radius+stroke_width));fragColor=v_visibility*opacity_t*mix(color*opacity,stroke_color*stroke_opacity,color_t);const float epsilon=0.5/255.0;if (fragColor.r < epsilon && fragColor.g < epsilon && fragColor.b < epsilon && fragColor.a < epsilon) {discard;}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform bool u_scale_with_map;uniform bool u_pitch_with_map;uniform vec2 u_extrude_scale;uniform highp float u_globe_extrude_scale;uniform lowp float u_device_pixel_ratio;uniform highp float u_camera_to_center_distance;uniform vec2 u_translate;in vec2 a_pos;out vec3 v_data;out float v_visibility;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define mediump float radius\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define highp vec4 stroke_color\n#pragma mapbox: define mediump float stroke_width\n#pragma mapbox: define lowp float stroke_opacity\nvoid main(void) {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize mediump float radius\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize highp vec4 stroke_color\n#pragma mapbox: initialize mediump float stroke_width\n#pragma mapbox: initialize lowp float stroke_opacity\nvec2 pos_raw=a_pos+32768.0;vec2 extrude=vec2(mod(pos_raw,8.0)/7.0*2.0-1.0);vec2 circle_center=floor(pos_raw/8.0)+u_translate;float ele=get_elevation(circle_center);v_visibility=calculate_visibility(projectTileWithElevation(circle_center,ele));if (u_pitch_with_map) {\n#ifdef GLOBE\nvec3 center_vector=projectToSphere(circle_center);\n#endif\nfloat angle_scale=u_globe_extrude_scale;vec2 corner_position=circle_center;if (u_scale_with_map) {angle_scale*=(radius+stroke_width);corner_position+=extrude*u_extrude_scale*(radius+stroke_width);} else {\n#ifdef GLOBE\nvec4 projected_center=interpolateProjection(circle_center,center_vector,ele);\n#else\nvec4 projected_center=projectTileWithElevation(circle_center,ele);\n#endif\ncorner_position+=extrude*u_extrude_scale*(radius+stroke_width)*(projected_center.w/u_camera_to_center_distance);angle_scale*=(radius+stroke_width)*(projected_center.w/u_camera_to_center_distance);}\n#ifdef GLOBE\nvec2 angles=extrude*angle_scale;vec3 corner_vector=globeRotateVector(center_vector,angles);gl_Position=interpolateProjection(corner_position,corner_vector,ele);\n#else\ngl_Position=projectTileWithElevation(corner_position,ele);\n#endif\n} else {gl_Position=projectTileWithElevation(circle_center,ele);if (gl_Position.z/gl_Position.w > 1.0) {gl_Position.xy=vec2(10000.0);}if (u_scale_with_map) {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*u_camera_to_center_distance;} else {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*gl_Position.w;}}float antialiasblur=-max(1.0/u_device_pixel_ratio/(radius+stroke_width),blur);v_data=vec3(extrude.x,extrude.y,antialiasblur);}"),clippingMask:yt(bt,"in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}"),heatmap:yt("uniform highp float u_intensity;in vec2 v_extrude;\n#pragma mapbox: define highp float weight\n#define GAUSS_COEF 0.3989422804014327\nvoid main() {\n#pragma mapbox: initialize highp float weight\nfloat d=-0.5*3.0*3.0*dot(v_extrude,v_extrude);float val=weight*u_intensity*GAUSS_COEF*exp(d);fragColor=vec4(val,1.0,1.0,1.0);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform float u_extrude_scale;uniform float u_opacity;uniform float u_intensity;uniform highp float u_globe_extrude_scale;in vec2 a_pos;out vec2 v_extrude;\n#pragma mapbox: define highp float weight\n#pragma mapbox: define mediump float radius\nconst highp float ZERO=1.0/255.0/16.0;\n#define GAUSS_COEF 0.3989422804014327\nvoid main(void) {\n#pragma mapbox: initialize highp float weight\n#pragma mapbox: initialize mediump float radius\nvec2 pos_raw=a_pos+32768.0;vec2 unscaled_extrude=vec2(mod(pos_raw,8.0)/7.0*2.0-1.0);float S=sqrt(-2.0*log(ZERO/weight/u_intensity/GAUSS_COEF))/3.0;v_extrude=S*unscaled_extrude;vec2 extrude=v_extrude*radius*u_extrude_scale;vec2 circle_center=floor(pos_raw/8.0);\n#ifdef GLOBE\nvec2 angles=v_extrude*radius*u_globe_extrude_scale;vec3 center_vector=projectToSphere(circle_center);vec3 corner_vector=globeRotateVector(center_vector,angles);gl_Position=interpolateProjection(circle_center+extrude,corner_vector,0.0);\n#else\ngl_Position=projectTileFor3D(circle_center+extrude,get_elevation(circle_center));\n#endif\n}"),heatmapTexture:yt("uniform sampler2D u_image;uniform sampler2D u_color_ramp;uniform float u_opacity;in vec2 v_pos;void main() {float t=texture(u_image,v_pos).r;vec4 color=texture(u_color_ramp,vec2(t,0.5));fragColor=color*u_opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(0.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_world;in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos*u_world,0,1);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}"),collisionBox:yt("in float v_placed;in float v_notUsed;void main() {float alpha=0.5;fragColor=vec4(1.0,0.0,0.0,1.0)*alpha;if (v_placed > 0.5) {fragColor=vec4(0.0,0.0,1.0,0.5)*alpha;}if (v_notUsed > 0.5) {fragColor*=.1;}}","in vec2 a_anchor_pos;in vec2 a_placed;in vec2 a_box_real;uniform vec2 u_pixel_extrude_scale;out float v_placed;out float v_notUsed;void main() {gl_Position=projectTileWithElevation(a_anchor_pos,get_elevation(a_anchor_pos));gl_Position.xy=((a_box_real+0.5)*u_pixel_extrude_scale*2.0-1.0)*vec2(1.0,-1.0)*gl_Position.w;if (gl_Position.z/gl_Position.w < 1.1) {gl_Position.z=0.5;}v_placed=a_placed.x;v_notUsed=a_placed.y;}"),collisionCircle:yt("in float v_radius;in vec2 v_extrude;in float v_collision;void main() {float alpha=0.5;float stroke_radius=0.9;float distance_to_center=length(v_extrude);float distance_to_edge=abs(distance_to_center-v_radius);float opacity_t=smoothstep(-stroke_radius,0.0,-distance_to_edge);vec4 color=mix(vec4(0.0,0.0,1.0,0.5),vec4(1.0,0.0,0.0,1.0),v_collision);fragColor=color*alpha*opacity_t;}","in vec2 a_pos;in float a_radius;in vec2 a_flags;uniform vec2 u_viewport_size;out float v_radius;out vec2 v_extrude;out float v_collision;void main() {float radius=a_radius;float collision=a_flags.x;float vertexIdx=a_flags.y;vec2 quadVertexOffset=vec2(mix(-1.0,1.0,float(vertexIdx >=2.0)),mix(-1.0,1.0,float(vertexIdx >=1.0 && vertexIdx <=2.0)));vec2 quadVertexExtent=quadVertexOffset*radius;float padding_factor=1.2;v_radius=radius;v_extrude=quadVertexExtent*padding_factor;v_collision=collision;gl_Position=vec4((a_pos/u_viewport_size*2.0-1.0)*vec2(1.0,-1.0),0.0,1.0)+vec4(quadVertexExtent*padding_factor/u_viewport_size*2.0,0.0,0.0);}"),colorRelief:yt("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform sampler2D u_image;uniform vec4 u_unpack;uniform sampler2D u_elevation_stops;uniform sampler2D u_color_stops;uniform int u_color_ramp_size;uniform float u_opacity;in vec2 v_pos;float getElevation(vec2 coord) {vec4 data=texture(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack);}float getElevationStop(int stop) {float x=(float(stop)+0.5)/float(u_color_ramp_size);vec4 data=texture(u_elevation_stops,vec2(x,0))*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {float el=getElevation(v_pos);int r=(u_color_ramp_size-1);int l=0;float el_l=getElevationStop(l);float el_r=getElevationStop(r);while(r-l > 1){int m=(r+l)/2;float el_m=getElevationStop(m);if(el < el_m){r=m;el_r=el_m;}else\n{l=m;el_l=el_m;}}float x=(float(l)+(el-el_l)/(el_r-el_l)+0.5)/float(u_color_ramp_size);fragColor=u_opacity*texture(u_color_stops,vec2(x,0));\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_dimension;in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_pos/8192.0)*scale+epsilon;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}"),debug:yt("uniform highp vec4 u_color;uniform sampler2D u_overlay;in vec2 v_uv;void main() {vec4 overlay_color=texture(u_overlay,v_uv);fragColor=mix(u_color,overlay_color,overlay_color.a);}","in vec2 a_pos;out vec2 v_uv;uniform float u_overlay_scale;void main() {v_uv=a_pos/8192.0;gl_Position=projectTileWithElevation(a_pos*u_overlay_scale,get_elevation(a_pos));}"),depth:yt(bt,"in vec2 a_pos;void main() {\n#ifdef GLOBE\ngl_Position=projectTileFor3D(a_pos,0.0);\n#else\ngl_Position=u_projection_matrix*vec4(a_pos,0.0,1.0);\n#endif\n}"),fill:yt("#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float opacity\nfragColor=color*opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_fill_translate;in vec2 a_pos;\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float opacity\ngl_Position=projectTile(a_pos+u_fill_translate,a_pos);}"),fillOutline:yt("in vec2 v_pos;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define highp vec4 outline_color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 outline_color\n#pragma mapbox: initialize lowp float opacity\nfloat dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);fragColor=outline_color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_world;uniform vec2 u_fill_translate;in vec2 a_pos;out vec2 v_pos;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define highp vec4 outline_color\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 outline_color\n#pragma mapbox: initialize lowp float opacity\ngl_Position=projectTile(a_pos+u_fill_translate,a_pos);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n}"),fillOutlinePattern:yt("uniform vec2 u_texsize;uniform sampler2D u_image;uniform float u_fade;in vec2 v_pos_a;in vec2 v_pos_b;in vec2 v_pos;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);float dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);fragColor=mix(color1,color2,u_fade)*alpha*opacity;\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_world;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;uniform vec2 u_fill_translate;in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;out vec2 v_pos;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,a_pos);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n}"),fillPattern:yt("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);fragColor=mix(color1,color2,u_fade)*opacity;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;uniform vec2 u_fill_translate;in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileZoomRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileZoomRatio,a_pos);}"),fillExtrusion:yt("in vec4 v_color;void main() {fragColor=v_color;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;in vec2 a_pos;in vec4 a_normal_ed;\n#ifdef TERRAIN3D\nin vec2 a_centroid;\n#endif\nout vec4 v_color;\n#pragma mapbox: define highp float base\n#pragma mapbox: define highp float height\n#pragma mapbox: define highp vec4 color\nvoid main() {\n#pragma mapbox: initialize highp float base\n#pragma mapbox: initialize highp float height\n#pragma mapbox: initialize highp vec4 color\nvec3 normal=a_normal_ed.xyz;\n#ifdef TERRAIN3D\nfloat height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0);\n#else\nfloat height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0;\n#endif\nbase=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=mod(normal.x,2.0);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate;\n#ifdef GLOBE\nvec3 spherePos=projectToSphere(posInTile,a_pos);gl_Position=interpolateProjectionFor3D(posInTile,spherePos,elevation);\n#else\ngl_Position=u_projection_matrix*vec4(posInTile,elevation,1.0);\n#endif\nfloat colorvalue=color.r*0.2126+color.g*0.7152+color.b*0.0722;v_color=vec4(0.0,0.0,0.0,1.0);vec4 ambientlight=vec4(0.03,0.03,0.03,1.0);color+=ambientlight;vec3 normalForLighting=normal/16384.0;float directional=clamp(dot(normalForLighting,u_lightpos),0.0,1.0);\n#ifdef GLOBE\nmat3 rotMatrix=globeGetRotationMatrix(spherePos);normalForLighting=rotMatrix*normalForLighting;directional=mix(directional,clamp(dot(normalForLighting,u_lightpos_globe),0.0,1.0),u_projection_transition);\n#endif\ndirectional=mix((1.0-u_lightintensity),max((1.0-colorvalue+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_color.r+=clamp(color.r*directional*u_lightcolor.r,mix(0.0,0.3,1.0-u_lightcolor.r),1.0);v_color.g+=clamp(color.g*directional*u_lightcolor.g,mix(0.0,0.3,1.0-u_lightcolor.g),1.0);v_color.b+=clamp(color.b*directional*u_lightcolor.b,mix(0.0,0.3,1.0-u_lightcolor.b),1.0);v_color*=u_opacity;}"),fillExtrusionPattern:yt("uniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;in vec4 v_lighting;\n#pragma mapbox: define lowp float base\n#pragma mapbox: define lowp float height\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float base\n#pragma mapbox: initialize lowp float height\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);vec4 mixedColor=mix(color1,color2,u_fade);fragColor=mixedColor*v_lighting;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_height_factor;uniform vec3 u_scale;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;in vec2 a_pos;in vec4 a_normal_ed;\n#ifdef TERRAIN3D\nin vec2 a_centroid;\n#endif\n#ifdef GLOBE\nout vec3 v_sphere_pos;\n#endif\nout vec2 v_pos_a;out vec2 v_pos_b;out vec4 v_lighting;\n#pragma mapbox: define lowp float base\n#pragma mapbox: define lowp float height\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float base\n#pragma mapbox: initialize lowp float height\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec3 normal=a_normal_ed.xyz;float edgedistance=a_normal_ed.w;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;\n#ifdef TERRAIN3D\nfloat height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0);\n#else\nfloat height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0;\n#endif\nbase=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=mod(normal.x,2.0);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate;\n#ifdef GLOBE\nvec3 spherePos=projectToSphere(posInTile,a_pos);vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);v_sphere_pos=elevatedPos;gl_Position=interpolateProjectionFor3D(posInTile,spherePos,elevation);\n#else\ngl_Position=u_projection_matrix*vec4(posInTile,elevation,1.0);\n#endif\nvec2 pos=normal.x==1.0 && normal.y==0.0 && normal.z==16384.0\n? a_pos\n: vec2(edgedistance,elevation*u_height_factor);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,pos);v_lighting=vec4(0.0,0.0,0.0,1.0);float directional=clamp(dot(normal/16383.0,u_lightpos),0.0,1.0);directional=mix((1.0-u_lightintensity),max((0.5+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_lighting.rgb+=clamp(directional*u_lightcolor,mix(vec3(0.0),vec3(0.3),1.0-u_lightcolor),vec3(1.0));v_lighting*=u_opacity;}"),hillshadePrepare:yt("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_dimension;uniform float u_zoom;uniform vec4 u_unpack;float getElevation(vec2 coord,float bias) {vec4 data=texture(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {vec2 epsilon=1.0/u_dimension;float tileSize=u_dimension.x-2.0;float a=getElevation(v_pos+vec2(-epsilon.x,-epsilon.y),0.0);float b=getElevation(v_pos+vec2(0,-epsilon.y),0.0);float c=getElevation(v_pos+vec2(epsilon.x,-epsilon.y),0.0);float d=getElevation(v_pos+vec2(-epsilon.x,0),0.0);float e=getElevation(v_pos,0.0);float f=getElevation(v_pos+vec2(epsilon.x,0),0.0);float g=getElevation(v_pos+vec2(-epsilon.x,epsilon.y),0.0);float h=getElevation(v_pos+vec2(0,epsilon.y),0.0);float i=getElevation(v_pos+vec2(epsilon.x,epsilon.y),0.0);float exaggerationFactor=u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3;float exaggeration=u_zoom < 15.0 ? (u_zoom-15.0)*exaggerationFactor : 0.0;vec2 deriv=vec2((c+f+f+i)-(a+d+d+g),(g+h+h+i)-(a+b+b+c))*tileSize/pow(2.0,exaggeration+(28.2562-u_zoom));fragColor=clamp(vec4(deriv.x/8.0+0.5,deriv.y/8.0+0.5,1.0,1.0),0.0,1.0);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;uniform vec2 u_dimension;in vec2 a_pos;in vec2 a_texture_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_texture_pos/8192.0)*scale+epsilon;}"),hillshade:yt("uniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_latrange;uniform float u_exaggeration;uniform vec4 u_accent;uniform int u_method;uniform float u_altitudes[NUM_ILLUMINATION_SOURCES];uniform float u_azimuths[NUM_ILLUMINATION_SOURCES];uniform vec4 u_shadows[NUM_ILLUMINATION_SOURCES];uniform vec4 u_highlights[NUM_ILLUMINATION_SOURCES];\n#define PI 3.141592653589793\n#define STANDARD 0\n#define COMBINED 1\n#define IGOR 2\n#define MULTIDIRECTIONAL 3\n#define BASIC 4\nfloat get_aspect(vec2 deriv){return deriv.x !=0.0 ? atan(deriv.y,-deriv.x) : PI/2.0*(deriv.y > 0.0 ? 1.0 :-1.0);}void igor_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float aspect=get_aspect(deriv);float azimuth=u_azimuths[0]+PI;float slope_stength=atan(length(deriv))*2.0/PI;float aspect_strength=1.0-abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);float shadow_strength=slope_stength*aspect_strength;float highlight_strength=slope_stength*(1.0-aspect_strength);fragColor=u_shadows[0]*shadow_strength+u_highlights[0]*highlight_strength;}void standard_hillshade(vec2 deriv){float azimuth=u_azimuths[0]+PI;float slope=atan(0.625*length(deriv));float aspect=get_aspect(deriv);float intensity=u_exaggeration;float base=1.875-intensity*1.75;float maxValue=0.5*PI;float scaledSlope=intensity !=0.5 ? ((pow(base,slope)-1.0)/(pow(base,maxValue)-1.0))*maxValue : slope;float accent=cos(scaledSlope);vec4 accent_color=(1.0-accent)*u_accent*clamp(intensity*2.0,0.0,1.0);float shade=abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);vec4 shade_color=mix(u_shadows[0],u_highlights[0],shade)*sin(scaledSlope)*clamp(intensity*2.0,0.0,1.0);fragColor=accent_color*(1.0-shade_color.a)+shade_color;}void basic_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float azimuth=u_azimuths[0]+PI;float cos_az=cos(azimuth);float sin_az=sin(azimuth);float cos_alt=cos(u_altitudes[0]);float sin_alt=sin(u_altitudes[0]);float cang=(sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv));float shade=clamp(cang,0.0,1.0);if(shade > 0.5){fragColor=u_highlights[0]*(2.0*shade-1.0);}else\n{fragColor=u_shadows[0]*(1.0-2.0*shade);}}void multidirectional_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;fragColor=vec4(0,0,0,0);for(int i=0; i < NUM_ILLUMINATION_SOURCES; i++){float cos_alt=cos(u_altitudes[i]);float sin_alt=sin(u_altitudes[i]);float cos_az=-cos(u_azimuths[i]);float sin_az=-sin(u_azimuths[i]);float cang=(sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv));float shade=clamp(cang,0.0,1.0);if(shade > 0.5){fragColor+=u_highlights[i]*(2.0*shade-1.0)/float(NUM_ILLUMINATION_SOURCES);}else\n{fragColor+=u_shadows[i]*(1.0-2.0*shade)/float(NUM_ILLUMINATION_SOURCES);}}}void combined_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float azimuth=u_azimuths[0]+PI;float cos_az=cos(azimuth);float sin_az=sin(azimuth);float cos_alt=cos(u_altitudes[0]);float sin_alt=sin(u_altitudes[0]);float cang=acos((sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv)));cang=clamp(cang,0.0,PI/2.0);float shade=cang*atan(length(deriv))*4.0/PI/PI;float highlight=(PI/2.0-cang)*atan(length(deriv))*4.0/PI/PI;fragColor=u_shadows[0]*shade+u_highlights[0]*highlight;}void main() {vec4 pixel=texture(u_image,v_pos);float scaleFactor=cos(radians((u_latrange[0]-u_latrange[1])*(1.0-v_pos.y)+u_latrange[1]));vec2 deriv=((pixel.rg*8.0)-4.0)/scaleFactor;if (u_method==BASIC) {basic_hillshade(deriv);} else if (u_method==COMBINED) {combined_hillshade(deriv);} else if (u_method==IGOR) {igor_hillshade(deriv);} else if (u_method==MULTIDIRECTIONAL) {multidirectional_hillshade(deriv);} else if (u_method==STANDARD) {standard_hillshade(deriv);} else {standard_hillshade(deriv);}\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform mat4 u_matrix;in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);v_pos=a_pos/8192.0;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}"),line:yt("uniform lowp float u_device_pixel_ratio;in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\nin vec2 a_pos_normal;in vec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform vec2 u_units_to_pixels;uniform lowp float u_device_pixel_ratio;out vec2 v_normal;out vec2 v_width2;out float v_gamma_scale;out highp float v_linesofar;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;v_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*2.0;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_width2=vec2(outset,inset);}"),lineGradient:yt("uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale;in highp vec2 v_uv;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\nin vec2 a_pos_normal;in vec4 a_data;in float a_uv_x;in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_image_height;out vec2 v_normal;out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\nvoid main() {\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;highp float texel_height=1.0/u_image_height;highp float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_width2=vec2(outset,inset);}"),linePattern:yt("#ifdef GL_ES\nprecision highp float;\n#endif\nuniform lowp float u_device_pixel_ratio;uniform vec2 u_texsize;uniform float u_fade;uniform mediump vec3 u_scale;uniform sampler2D u_image;in vec2 v_normal;in vec2 v_width2;in float v_linesofar;in float v_gamma_scale;in float v_width;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\nvec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;vec2 pattern_size_a=vec2(display_size_a.x*fromScale/tileZoomRatio,display_size_a.y);vec2 pattern_size_b=vec2(display_size_b.x*toScale/tileZoomRatio,display_size_b.y);float aspect_a=display_size_a.y/v_width;float aspect_b=display_size_b.y/v_width;float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float x_a=mod(v_linesofar/pattern_size_a.x*aspect_a,1.0);float x_b=mod(v_linesofar/pattern_size_b.x*aspect_b,1.0);float y=0.5*v_normal.y+0.5;vec2 texel_size=1.0/u_texsize;vec2 pos_a=mix(pattern_tl_a*texel_size-texel_size,pattern_br_a*texel_size+texel_size,vec2(x_a,y));vec2 pos_b=mix(pattern_tl_b*texel_size-texel_size,pattern_br_b*texel_size+texel_size,vec2(x_b,y));vec4 color=mix(texture(u_image,pos_a),texture(u_image,pos_b),u_fade);fragColor=color*alpha*opacity;\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nin vec2 a_pos_normal;in vec4 a_data;uniform vec2 u_translation;uniform vec2 u_units_to_pixels;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;out vec2 v_normal;out vec2 v_width2;out float v_linesofar;out float v_gamma_scale;out float v_width;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\n#pragma mapbox: define lowp vec4 pattern_from\n#pragma mapbox: define lowp vec4 pattern_to\n#pragma mapbox: define lowp float pixel_ratio_from\n#pragma mapbox: define lowp float pixel_ratio_to\nvoid main() {\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\n#pragma mapbox: initialize mediump vec4 pattern_from\n#pragma mapbox: initialize mediump vec4 pattern_to\n#pragma mapbox: initialize lowp float pixel_ratio_from\n#pragma mapbox: initialize lowp float pixel_ratio_to\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_linesofar=a_linesofar;v_width2=vec2(outset,inset);v_width=floorwidth;}"),lineSDF:yt("uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;uniform float u_sdfgamma;uniform float u_mix;in vec2 v_normal;in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;\n#ifdef GLOBE\nin float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\nfloat dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture(u_image,v_tex_a).a;float sdfdist_b=texture(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);alpha*=smoothstep(0.5-u_sdfgamma/floorwidth,0.5+u_sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*opacity);\n#ifdef GLOBE\nif (v_depth > 1.0) {discard;}\n#endif\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","\n#define scale 0.015873016\n#define LINE_DISTANCE_SCALE 2.0\nin vec2 a_pos_normal;in vec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_patternscale_a;uniform float u_tex_y_a;uniform vec2 u_patternscale_b;uniform float u_tex_y_b;uniform vec2 u_units_to_pixels;out vec2 v_normal;out vec2 v_width2;out vec2 v_tex_a;out vec2 v_tex_b;out float v_gamma_scale;\n#ifdef GLOBE\nout float v_depth;\n#endif\n#pragma mapbox: define highp vec4 color\n#pragma mapbox: define lowp float blur\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define mediump float gapwidth\n#pragma mapbox: define lowp float offset\n#pragma mapbox: define mediump float width\n#pragma mapbox: define lowp float floorwidth\nvoid main() {\n#pragma mapbox: initialize highp vec4 color\n#pragma mapbox: initialize lowp float blur\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize mediump float gapwidth\n#pragma mapbox: initialize lowp float offset\n#pragma mapbox: initialize mediump float width\n#pragma mapbox: initialize lowp float floorwidth\nfloat ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude;\n#ifdef GLOBE\nv_depth=gl_Position.z/gl_Position.w;\n#endif\n#ifdef TERRAIN3D\nv_gamma_scale=1.0;\n#else\nfloat extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective;\n#endif\nv_tex_a=vec2(a_linesofar*u_patternscale_a.x/floorwidth,normal.y*u_patternscale_a.y+u_tex_y_a);v_tex_b=vec2(a_linesofar*u_patternscale_b.x/floorwidth,normal.y*u_patternscale_b.y+u_tex_y_b);v_width2=vec2(outset,inset);}"),raster:yt("uniform float u_fade_t;uniform float u_opacity;uniform sampler2D u_image0;uniform sampler2D u_image1;in vec2 v_pos0;in vec2 v_pos1;uniform float u_brightness_low;uniform float u_brightness_high;uniform float u_saturation_factor;uniform float u_contrast_factor;uniform vec3 u_spin_weights;void main() {vec4 color0=texture(u_image0,v_pos0);vec4 color1=texture(u_image1,v_pos1);if (color0.a > 0.0) {color0.rgb=color0.rgb/color0.a;}if (color1.a > 0.0) {color1.rgb=color1.rgb/color1.a;}vec4 color=mix(color0,color1,u_fade_t);color.a*=u_opacity;vec3 rgb=color.rgb;rgb=vec3(dot(rgb,u_spin_weights.xyz),dot(rgb,u_spin_weights.zxy),dot(rgb,u_spin_weights.yzx));float average=(color.r+color.g+color.b)/3.0;rgb+=(average-rgb)*u_saturation_factor;rgb=(rgb-0.5)*u_contrast_factor+0.5;vec3 u_high_vec=vec3(u_brightness_low,u_brightness_low,u_brightness_low);vec3 u_low_vec=vec3(u_brightness_high,u_brightness_high,u_brightness_high);fragColor=vec4(mix(u_high_vec,u_low_vec,rgb)*color.a,color.a);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","uniform vec2 u_tl_parent;uniform float u_scale_parent;uniform float u_buffer_scale;uniform vec4 u_coords_top;uniform vec4 u_coords_bottom;in vec2 a_pos;out vec2 v_pos0;out vec2 v_pos1;void main() {vec2 fractionalPos=a_pos/8192.0;vec2 position=mix(mix(u_coords_top.xy,u_coords_top.zw,fractionalPos.x),mix(u_coords_bottom.xy,u_coords_bottom.zw,fractionalPos.x),fractionalPos.y);gl_Position=projectTile(position,position);v_pos0=((fractionalPos-0.5)/u_buffer_scale)+0.5;\n#ifdef GLOBE\nif (a_pos.y <-32767.5) {v_pos0.y=0.0;}if (a_pos.y > 32766.5) {v_pos0.y=1.0;}\n#endif\nv_pos1=(v_pos0*u_scale_parent)+u_tl_parent;}"),symbolIcon:yt("uniform sampler2D u_texture;in vec2 v_tex;in float v_fade_opacity;\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\nlowp float alpha=opacity*v_fade_opacity;fragColor=texture(u_texture,v_tex)*alpha;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","in vec4 a_pos_offset;in vec4 a_data;in vec4 a_pixeloffset;in vec3 a_projected_pos;in float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform highp float u_camera_to_center_distance;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform float u_fade_change;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform vec2 u_texsize;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;out vec2 v_tex;out float v_fade_opacity;\n#pragma mapbox: define lowp float opacity\nvoid main() {\n#pragma mapbox: initialize lowp float opacity\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);vec2 a_pxoffset=a_pixeloffset.xy;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);size*=perspective_ratio;float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;\n#ifdef GLOBE\nif(u_pitch_with_map) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);}\n#endif\nvec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset/16.0)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}gl_Position=finalPos;v_tex=a_tex/u_texsize;vec2 fade_opacity=unpack_opacity(a_fade_opacity);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float visibility=calculate_visibility(projectedPoint);v_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));}"),symbolSDF:yt("#define SDF_PX 8.0\nuniform bool u_is_halo;uniform sampler2D u_texture;uniform highp float u_gamma_scale;uniform lowp float u_device_pixel_ratio;uniform bool u_is_text;in vec2 v_data0;in vec3 v_data1;\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nfloat EDGE_GAMMA=0.105/u_device_pixel_ratio;vec2 tex=v_data0.xy;float gamma_scale=v_data1.x;float size=v_data1.y;float fade_opacity=v_data1[2];float fontScale=u_is_text ? size/24.0 : size;lowp vec4 color=fill_color;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float inner_edge=(256.0-64.0)/256.0;if (u_is_halo) {color=halo_color;gamma=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);inner_edge=inner_edge+gamma*gamma_scale;}lowp float dist=texture(u_texture,tex).a;highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(inner_edge-gamma_scaled,inner_edge+gamma_scaled,dist);if (u_is_halo) {lowp float halo_edge=(6.0-halo_width/fontScale)/SDF_PX;alpha=min(smoothstep(halo_edge-gamma_scaled,halo_edge+gamma_scaled,dist),1.0-alpha);}fragColor=color*(alpha*opacity*fade_opacity);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","in vec4 a_pos_offset;in vec4 a_data;in vec4 a_pixeloffset;in vec3 a_projected_pos;in float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_translation;uniform float u_pitched_scale;out vec2 v_data0;out vec3 v_data1;\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);vec2 a_pxoffset=a_pixeloffset.xy;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);size*=perspective_ratio;float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;\n#ifdef GLOBE\nif(u_pitch_with_map) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);}\n#endif\nvec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale+a_pxoffset)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));v_data0=a_tex/u_texsize;v_data1=vec3(gamma_scale,size,interpolated_fade_opacity);}"),symbolTextAndIcon:yt("#define SDF_PX 8.0\n#define SDF 1.0\n#define ICON 0.0\nuniform bool u_is_halo;uniform sampler2D u_texture;uniform sampler2D u_texture_icon;uniform highp float u_gamma_scale;uniform lowp float u_device_pixel_ratio;in vec4 v_data0;in vec4 v_data1;\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nfloat fade_opacity=v_data1[2];if (v_data1.w==ICON) {vec2 tex_icon=v_data0.zw;lowp float alpha=opacity*fade_opacity;fragColor=texture(u_texture_icon,tex_icon)*alpha;\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\nreturn;}vec2 tex=v_data0.xy;float EDGE_GAMMA=0.105/u_device_pixel_ratio;float gamma_scale=v_data1.x;float size=v_data1.y;float fontScale=size/24.0;lowp vec4 color=fill_color;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float buff=(256.0-64.0)/256.0;if (u_is_halo) {color=halo_color;gamma=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);buff=(6.0-halo_width/fontScale)/SDF_PX;}lowp float dist=texture(u_texture,tex).a;highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(buff-gamma_scaled,buff+gamma_scaled,dist);fragColor=color*(alpha*opacity*fade_opacity);\n#ifdef OVERDRAW_INSPECTOR\nfragColor=vec4(1.0);\n#endif\n}","in vec4 a_pos_offset;in vec4 a_data;in vec3 a_projected_pos;in float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_texsize_icon;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;out vec4 v_data0;out vec4 v_data1;\n#pragma mapbox: define highp vec4 fill_color\n#pragma mapbox: define highp vec4 halo_color\n#pragma mapbox: define lowp float opacity\n#pragma mapbox: define lowp float halo_width\n#pragma mapbox: define lowp float halo_blur\nvoid main() {\n#pragma mapbox: initialize highp vec4 fill_color\n#pragma mapbox: initialize highp vec4 halo_color\n#pragma mapbox: initialize lowp float opacity\n#pragma mapbox: initialize lowp float halo_width\n#pragma mapbox: initialize lowp float halo_blur\nvec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);float is_sdf=a_size[0]-2.0*a_size_min;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ?\ncamera_to_anchor_distance/u_camera_to_center_distance :\nu_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);size*=perspective_ratio;float fontScale=size/24.0;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0;\n#ifdef GLOBE\nif(u_pitch_with_map && !u_is_along_line) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);}\n#endif\nvec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));v_data0.xy=a_tex/u_texsize;v_data0.zw=a_tex/u_texsize_icon;v_data1=vec4(gamma_scale,size,interpolated_fade_opacity,is_sdf);}"),terrain:yt("uniform sampler2D u_texture;uniform vec4 u_fog_color;uniform vec4 u_horizon_color;uniform float u_fog_ground_blend;uniform float u_fog_ground_blend_opacity;uniform float u_horizon_fog_blend;uniform bool u_is_globe_mode;in vec2 v_texture_pos;in float v_fog_depth;const float gamma=2.2;vec4 gammaToLinear(vec4 color) {return pow(color,vec4(gamma));}vec4 linearToGamma(vec4 color) {return pow(color,vec4(1.0/gamma));}void main() {vec4 surface_color=texture(u_texture,vec2(v_texture_pos.x,1.0-v_texture_pos.y));if (!u_is_globe_mode && v_fog_depth > u_fog_ground_blend) {vec4 surface_color_linear=gammaToLinear(surface_color);float blend_color=smoothstep(0.0,1.0,max((v_fog_depth-u_horizon_fog_blend)/(1.0-u_horizon_fog_blend),0.0));vec4 fog_horizon_color_linear=mix(gammaToLinear(u_fog_color),gammaToLinear(u_horizon_color),blend_color);float factor_fog=max(v_fog_depth-u_fog_ground_blend,0.0)/(1.0-u_fog_ground_blend);fragColor=linearToGamma(mix(surface_color_linear,fog_horizon_color_linear,pow(factor_fog,2.0)*u_fog_ground_blend_opacity));} else {fragColor=surface_color;}}","in vec3 a_pos3d;uniform mat4 u_fog_matrix;uniform float u_ele_delta;out vec2 v_texture_pos;out float v_fog_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=projectTileFor3D(a_pos3d.xy,get_elevation(a_pos3d.xy)-ele_delta);vec4 pos=u_fog_matrix*vec4(a_pos3d.xy,ele,1.0);v_fog_depth=pos.z/pos.w*0.5+0.5;}"),terrainDepth:yt("in float v_depth;const highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitMsk=vec4(0.,vec3(1./256.0));highp vec4 pack(highp float value) {highp vec4 comp=fract(value*bitSh);comp-=comp.xxyz*bitMsk;return comp;}void main() {fragColor=pack(v_depth);}","in vec3 a_pos3d;uniform float u_ele_delta;out float v_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);v_depth=gl_Position.z/gl_Position.w;}"),terrainCoords:yt("precision mediump float;uniform sampler2D u_texture;uniform float u_terrain_coords_id;in vec2 v_texture_pos;void main() {vec4 rgba=texture(u_texture,v_texture_pos);fragColor=vec4(rgba.r,rgba.g,rgba.b,u_terrain_coords_id);}","in vec3 a_pos3d;uniform float u_ele_delta;out vec2 v_texture_pos;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);}"),projectionErrorMeasurement:yt("in vec4 v_output_error_encoded;void main() {fragColor=v_output_error_encoded;}","in vec2 a_pos;uniform highp float u_input;uniform highp float u_output_expected;out vec4 v_output_error_encoded;void main() {float real_output=2.0*atan(exp(PI-(u_input*PI*2.0)))-PI*0.5;float error=real_output-u_output_expected;float abs_error=abs(error)*128.0;v_output_error_encoded.x=min(floor(abs_error*256.0),255.0)/255.0;abs_error-=v_output_error_encoded.x;v_output_error_encoded.y=min(floor(abs_error*65536.0),255.0)/255.0;abs_error-=v_output_error_encoded.x/255.0;v_output_error_encoded.z=min(floor(abs_error*16777216.0),255.0)/255.0;v_output_error_encoded.w=error >=0.0 ? 1.0 : 0.0;gl_Position=vec4(a_pos,0.0,1.0);}"),atmosphere:yt("in vec3 view_direction;uniform vec3 u_sun_pos;uniform vec3 u_globe_position;uniform float u_globe_radius;uniform float u_atmosphere_blend;/**Shader use from https:*Made some change to adapt to MapLibre Globe geometry*/const float PI=3.141592653589793;const int iSteps=5;const int jSteps=3;/*radius of the planet*/const float EARTH_RADIUS=6371e3;/*radius of the atmosphere*/const float ATMOS_RADIUS=6471e3;vec2 rsi(vec3 r0,vec3 rd,float sr) {float a=dot(rd,rd);float b=2.0*dot(rd,r0);float c=dot(r0,r0)-(sr*sr);float d=(b*b)-4.0*a*c;if (d < 0.0) return vec2(1e5,-1e5);return vec2((-b-sqrt(d))/(2.0*a),(-b+sqrt(d))/(2.0*a));}vec4 atmosphere(vec3 r,vec3 r0,vec3 pSun,float iSun,float rPlanet,float rAtmos,vec3 kRlh,float kMie,float shRlh,float shMie,float g) {pSun=normalize(pSun);r=normalize(r);vec2 p=rsi(r0,r,rAtmos);if (p.x > p.y) {return vec4(0.0,0.0,0.0,1.0);}if (p.x < 0.0) {p.x=0.0;}vec3 pos=r0+r*p.x;vec2 p2=rsi(r0,r,rPlanet);if (p2.x <=p2.y && p2.x > 0.0) {p.y=min(p.y,p2.x);}float iStepSize=(p.y-p.x)/float(iSteps);float iTime=p.x+iStepSize*0.5;vec3 totalRlh=vec3(0,0,0);vec3 totalMie=vec3(0,0,0);float iOdRlh=0.0;float iOdMie=0.0;float mu=dot(r,pSun);float mumu=mu*mu;float gg=g*g;float pRlh=3.0/(16.0*PI)*(1.0+mumu);float pMie=3.0/(8.0*PI)*((1.0-gg)*(mumu+1.0))/(pow(1.0+gg-2.0*mu*g,1.5)*(2.0+gg));for (int i=0; i < iSteps; i++) {vec3 iPos=r0+r*iTime;float iHeight=length(iPos)-rPlanet;float odStepRlh=exp(-iHeight/shRlh)*iStepSize;float odStepMie=exp(-iHeight/shMie)*iStepSize;iOdRlh+=odStepRlh;iOdMie+=odStepMie;float jStepSize=rsi(iPos,pSun,rAtmos).y/float(jSteps);float jTime=jStepSize*0.5;float jOdRlh=0.0;float jOdMie=0.0;for (int j=0; j < jSteps; j++) {vec3 jPos=iPos+pSun*jTime;float jHeight=length(jPos)-rPlanet;jOdRlh+=exp(-jHeight/shRlh)*jStepSize;jOdMie+=exp(-jHeight/shMie)*jStepSize;jTime+=jStepSize;}vec3 attn=exp(-(kMie*(iOdMie+jOdMie)+kRlh*(iOdRlh+jOdRlh)));totalRlh+=odStepRlh*attn;totalMie+=odStepMie*attn;iTime+=iStepSize;}float opacity=exp(-(length(kRlh)*length(totalRlh)+kMie*length(totalMie)));vec3 color=iSun*(pRlh*kRlh*totalRlh+pMie*kMie*totalMie);return vec4(color,opacity);}void main() {vec3 scale_camera_pos=-u_globe_position*EARTH_RADIUS/u_globe_radius;vec4 color=atmosphere(normalize(view_direction),scale_camera_pos,u_sun_pos,22.0,EARTH_RADIUS,ATMOS_RADIUS,vec3(5.5e-6,13.0e-6,22.4e-6),21e-6,8e3,1.2e3,0.758\n);color.rgb=1.0-exp(-1.0*color.rgb);color=pow(color,vec4(1.0/2.2));fragColor=vec4(color.rgb,1.0-color.a)*u_atmosphere_blend;}","in vec2 a_pos;uniform mat4 u_inv_proj_matrix;out vec3 view_direction;void main() {view_direction=(u_inv_proj_matrix*vec4(a_pos,0.0,1.0)).xyz;gl_Position=vec4(a_pos,0.0,1.0);}"),sky:yt("uniform vec4 u_sky_color;uniform vec4 u_horizon_color;uniform vec2 u_horizon;uniform vec2 u_horizon_normal;uniform float u_sky_horizon_blend;uniform float u_sky_blend;void main() {float x=gl_FragCoord.x;float y=gl_FragCoord.y;float blend=(y-u_horizon.y)*u_horizon_normal.y+(x-u_horizon.x)*u_horizon_normal.x;if (blend > 0.0) {if (blend < u_sky_horizon_blend) {fragColor=mix(u_sky_color,u_horizon_color,pow(1.0-blend/u_sky_horizon_blend,2.0));} else {fragColor=u_sky_color;}}fragColor=mix(fragColor,vec4(vec3(0.0),0.0),u_sky_blend);}","in vec2 a_pos;void main() {gl_Position=vec4(a_pos,1.0,1.0);}")};function yt(e,t){const i=/#pragma mapbox: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g,o=t.match(/in ([\w]+) ([\w]+)/g),r=e.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),a=t.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),s=a?a.concat(r):r,n={};return {fragmentSource:e=e.replace(i,((e,t,i,o,r)=>(n[r]=!0,"define"===t?`\n#ifndef HAS_UNIFORM_u_${r}\nin ${i} ${o} ${r};\n#else\nuniform ${i} ${o} u_${r};\n#endif\n`:`\n#ifdef HAS_UNIFORM_u_${r}\n ${i} ${o} ${r} = u_${r};\n#endif\n`))),vertexSource:t=t.replace(i,((e,t,i,o,r)=>{const a="float"===o?"vec2":"vec4",s=r.match(/color/)?"color":a;return n[r]?"define"===t?`\n#ifndef HAS_UNIFORM_u_${r}\nuniform lowp float u_${r}_t;\nin ${i} ${a} a_${r};\nout ${i} ${o} ${r};\n#else\nuniform ${i} ${o} u_${r};\n#endif\n`:"vec4"===s?`\n#ifndef HAS_UNIFORM_u_${r}\n ${r} = a_${r};\n#else\n ${i} ${o} ${r} = u_${r};\n#endif\n`:`\n#ifndef HAS_UNIFORM_u_${r}\n ${r} = unpack_mix_${s}(a_${r}, u_${r}_t);\n#else\n ${i} ${o} ${r} = u_${r};\n#endif\n`:"define"===t?`\n#ifndef HAS_UNIFORM_u_${r}\nuniform lowp float u_${r}_t;\nin ${i} ${a} a_${r};\n#else\nuniform ${i} ${o} u_${r};\n#endif\n`:"vec4"===s?`\n#ifndef HAS_UNIFORM_u_${r}\n ${i} ${o} ${r} = a_${r};\n#else\n ${i} ${o} ${r} = u_${r};\n#endif\n`:`\n#ifndef HAS_UNIFORM_u_${r}\n ${i} ${o} ${r} = unpack_mix_${s}(a_${r}, u_${r}_t);\n#else\n ${i} ${o} ${r} = u_${r};\n#endif\n`})),staticAttributes:o,staticUniforms:s}}class wt{constructor(e,t,i){this.vertexBuffer=e,this.indexBuffer=t,this.segments=i;}destroy(){this.vertexBuffer.destroy(),this.indexBuffer.destroy(),this.segments.destroy(),this.vertexBuffer=null,this.indexBuffer=null,this.segments=null;}}var Tt=t.aJ([{name:"a_pos",type:"Int16",components:2}]);const Pt="#define PROJECTION_MERCATOR",Ct="mercator";class It{constructor(){this._cachedMesh=null;}get name(){return "mercator"}get useSubdivision(){return !1}get shaderVariantName(){return Ct}get shaderDefine(){return Pt}get shaderPreludeCode(){return xt.projectionMercator}get vertexShaderPreludeCode(){return xt.projectionMercator.vertexSource}get subdivisionGranularity(){return t.aK.noSubdivision}get useGlobeControls(){return !1}get transitionState(){return 0}get latitudeErrorCorrectionRadians(){return 0}destroy(){}updateGPUdependent(e){}getMeshFromTileID(e,i,o,r,a){if(this._cachedMesh)return this._cachedMesh;const s=new t.aL;s.emplaceBack(0,0),s.emplaceBack(t.$,0),s.emplaceBack(0,t.$),s.emplaceBack(t.$,t.$);const n=e.createVertexBuffer(s,Tt.members),l=t.aM.simpleSegment(0,0,4,2),c=new t.aN;c.emplaceBack(1,0,2),c.emplaceBack(1,2,3);const h=e.createIndexBuffer(c);return this._cachedMesh=new wt(n,h,l),this._cachedMesh}recalculate(){}hasTransition(){return !1}setErrorQueryLatitudeDegrees(e){}}class Mt{constructor(e=0,t=0,i=0,o=0){if(isNaN(e)||e<0||isNaN(t)||t<0||isNaN(i)||i<0||isNaN(o)||o<0)throw new Error("Invalid value for edge-insets, top, bottom, left and right must all be numbers");this.top=e,this.bottom=t,this.left=i,this.right=o;}interpolate(e,i,o){return null!=i.top&&null!=e.top&&(this.top=t.C.number(e.top,i.top,o)),null!=i.bottom&&null!=e.bottom&&(this.bottom=t.C.number(e.bottom,i.bottom,o)),null!=i.left&&null!=e.left&&(this.left=t.C.number(e.left,i.left,o)),null!=i.right&&null!=e.right&&(this.right=t.C.number(e.right,i.right,o)),this}getCenter(e,i){const o=t.ah((this.left+e-this.right)/2,0,e),r=t.ah((this.top+i-this.bottom)/2,0,i);return new t.P(o,r)}equals(e){return this.top===e.top&&this.bottom===e.bottom&&this.left===e.left&&this.right===e.right}clone(){return new Mt(this.top,this.bottom,this.left,this.right)}toJSON(){return {top:this.top,bottom:this.bottom,left:this.left,right:this.right}}}function St(e,t){if(!e.renderWorldCopies||e.lngRange)return;const i=t.lng-e.center.lng;t.lng+=i>180?-360:i<-180?360:0;}function Et(e){return Math.max(0,Math.floor(e))}class Rt{constructor(e,i,o,r,a,s){this._callbacks=e,this._tileSize=512,this._renderWorldCopies=void 0===s||!!s,this._minZoom=i||0,this._maxZoom=o||22,this._minPitch=null==r?0:r,this._maxPitch=null==a?60:a,this.setMaxBounds(),this._width=0,this._height=0,this._center=new t.S(0,0),this._elevation=0,this._zoom=0,this._tileZoom=Et(this._zoom),this._scale=t.af(this._zoom),this._bearingInRadians=0,this._fovInRadians=.6435011087932844,this._pitchInRadians=0,this._rollInRadians=0,this._unmodified=!0,this._edgeInsets=new Mt,this._minElevationForCurrentTile=0,this._autoCalculateNearFarZ=!0;}apply(e,i,o){this._latRange=e.latRange,this._lngRange=e.lngRange,this._width=e.width,this._height=e.height,this._center=e.center,this._elevation=e.elevation,this._minElevationForCurrentTile=e.minElevationForCurrentTile,this._zoom=e.zoom,this._tileZoom=Et(this._zoom),this._scale=t.af(this._zoom),this._bearingInRadians=e.bearingInRadians,this._fovInRadians=e.fovInRadians,this._pitchInRadians=e.pitchInRadians,this._rollInRadians=e.rollInRadians,this._unmodified=e.unmodified,this._edgeInsets=new Mt(e.padding.top,e.padding.bottom,e.padding.left,e.padding.right),this._minZoom=e.minZoom,this._maxZoom=e.maxZoom,this._minPitch=e.minPitch,this._maxPitch=e.maxPitch,this._renderWorldCopies=e.renderWorldCopies,this._cameraToCenterDistance=e.cameraToCenterDistance,this._nearZ=e.nearZ,this._farZ=e.farZ,this._autoCalculateNearFarZ=!o&&e.autoCalculateNearFarZ,i&&this._constrain(),this._calcMatrices();}get pixelsToClipSpaceMatrix(){return this._pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._clipSpaceToPixelsMatrix}get minElevationForCurrentTile(){return this._minElevationForCurrentTile}setMinElevationForCurrentTile(e){this._minElevationForCurrentTile=e;}get tileSize(){return this._tileSize}get tileZoom(){return this._tileZoom}get scale(){return this._scale}get width(){return this._width}get height(){return this._height}get bearingInRadians(){return this._bearingInRadians}get lngRange(){return this._lngRange}get latRange(){return this._latRange}get pixelsToGLUnits(){return this._pixelsToGLUnits}get minZoom(){return this._minZoom}setMinZoom(e){this._minZoom!==e&&(this._minZoom=e,this.setZoom(this.getConstrained(this._center,this.zoom).zoom));}get maxZoom(){return this._maxZoom}setMaxZoom(e){this._maxZoom!==e&&(this._maxZoom=e,this.setZoom(this.getConstrained(this._center,this.zoom).zoom));}get minPitch(){return this._minPitch}setMinPitch(e){this._minPitch!==e&&(this._minPitch=e,this.setPitch(Math.max(this.pitch,e)));}get maxPitch(){return this._maxPitch}setMaxPitch(e){this._maxPitch!==e&&(this._maxPitch=e,this.setPitch(Math.min(this.pitch,e)));}get renderWorldCopies(){return this._renderWorldCopies}setRenderWorldCopies(e){void 0===e?e=!0:null===e&&(e=!1),this._renderWorldCopies=e;}get worldSize(){return this._tileSize*this._scale}get centerOffset(){return this.centerPoint._sub(this.size._div(2))}get size(){return new t.P(this._width,this._height)}get bearing(){return this._bearingInRadians/Math.PI*180}setBearing(e){const i=t.aO(e,-180,180)*Math.PI/180;var r,a,s,n,l,c,h,u,d;this._bearingInRadians!==i&&(this._unmodified=!1,this._bearingInRadians=i,this._calcMatrices(),this._rotationMatrix=o(),r=this._rotationMatrix,s=-this._bearingInRadians,n=(a=this._rotationMatrix)[0],l=a[1],c=a[2],h=a[3],u=Math.sin(s),d=Math.cos(s),r[0]=n*d+c*u,r[1]=l*d+h*u,r[2]=n*-u+c*d,r[3]=l*-u+h*d);}get rotationMatrix(){return this._rotationMatrix}get pitchInRadians(){return this._pitchInRadians}get pitch(){return this._pitchInRadians/Math.PI*180}setPitch(e){const i=t.ah(e,this.minPitch,this.maxPitch)/180*Math.PI;this._pitchInRadians!==i&&(this._unmodified=!1,this._pitchInRadians=i,this._calcMatrices());}get rollInRadians(){return this._rollInRadians}get roll(){return this._rollInRadians/Math.PI*180}setRoll(e){const t=e/180*Math.PI;this._rollInRadians!==t&&(this._unmodified=!1,this._rollInRadians=t,this._calcMatrices());}get fovInRadians(){return this._fovInRadians}get fov(){return t.aP(this._fovInRadians)}setFov(e){e=t.ah(e,.1,150),this.fov!==e&&(this._unmodified=!1,this._fovInRadians=t.ae(e),this._calcMatrices());}get zoom(){return this._zoom}setZoom(e){const i=this.getConstrained(this._center,e).zoom;this._zoom!==i&&(this._unmodified=!1,this._zoom=i,this._tileZoom=Math.max(0,Math.floor(i)),this._scale=t.af(i),this._constrain(),this._calcMatrices());}get center(){return this._center}setCenter(e){e.lat===this._center.lat&&e.lng===this._center.lng||(this._unmodified=!1,this._center=e,this._constrain(),this._calcMatrices());}get elevation(){return this._elevation}setElevation(e){e!==this._elevation&&(this._elevation=e,this._constrain(),this._calcMatrices());}get padding(){return this._edgeInsets.toJSON()}setPadding(e){this._edgeInsets.equals(e)||(this._unmodified=!1,this._edgeInsets.interpolate(this._edgeInsets,e,1),this._calcMatrices());}get centerPoint(){return this._edgeInsets.getCenter(this._width,this._height)}get pixelsPerMeter(){return this._pixelPerMeter}get unmodified(){return this._unmodified}get cameraToCenterDistance(){return this._cameraToCenterDistance}get nearZ(){return this._nearZ}get farZ(){return this._farZ}get autoCalculateNearFarZ(){return this._autoCalculateNearFarZ}overrideNearFarZ(e,t){this._autoCalculateNearFarZ=!1,this._nearZ=e,this._farZ=t,this._calcMatrices();}clearNearFarZOverride(){this._autoCalculateNearFarZ=!0,this._calcMatrices();}isPaddingEqual(e){return this._edgeInsets.equals(e)}interpolatePadding(e,t,i){this._unmodified=!1,this._edgeInsets.interpolate(e,t,i),this._constrain(),this._calcMatrices();}resize(e,t,i=!0){this._width=e,this._height=t,i&&this._constrain(),this._calcMatrices();}getMaxBounds(){return this._latRange&&2===this._latRange.length&&this._lngRange&&2===this._lngRange.length?new G([this._lngRange[0],this._latRange[0]],[this._lngRange[1],this._latRange[1]]):null}setMaxBounds(e){e?(this._lngRange=[e.getWest(),e.getEast()],this._latRange=[e.getSouth(),e.getNorth()],this._constrain()):(this._lngRange=null,this._latRange=[-t.ai,t.ai]);}getConstrained(e,t){return this._callbacks.getConstrained(e,t)}getCameraQueryGeometry(e,i){if(1===i.length)return [i[0],e];{const{minX:o,minY:r,maxX:a,maxY:s}=t.a2.fromPoints(i).extend(e);return [new t.P(o,r),new t.P(a,r),new t.P(a,s),new t.P(o,s),new t.P(o,r)]}}_constrain(){if(!this.center||!this._width||!this._height||this._constraining)return;this._constraining=!0;const e=this._unmodified,{center:t,zoom:i}=this.getConstrained(this.center,this.zoom);this.setCenter(t),this.setZoom(i),this._unmodified=e,this._constraining=!1;}_calcMatrices(){if(this._width&&this._height){this._pixelsToGLUnits=[2/this._width,-2/this._height];let e=t.ag(new Float64Array(16));t.N(e,e,[this._width/2,-this._height/2,1]),t.M(e,e,[1,-1,0]),this._clipSpaceToPixelsMatrix=e,e=t.ag(new Float64Array(16)),t.N(e,e,[1,-1,1]),t.M(e,e,[-1,-1,0]),t.N(e,e,[2/this._width,2/this._height,1]),this._pixelsToClipSpaceMatrix=e,this._cameraToCenterDistance=.5/Math.tan(this.fovInRadians/2)*this._height;}this._callbacks.calcMatrices();}calculateCenterFromCameraLngLatAlt(e,i,o,r){const a=void 0!==o?o:this.bearing,s=r=void 0!==r?r:this.pitch,n=t.a1.fromLngLat(e,i),l=-Math.cos(t.ae(s)),c=Math.sin(t.ae(s)),h=c*Math.sin(t.ae(a)),u=-c*Math.cos(t.ae(a));let d=this.elevation;const _=i-d;let p;l*_>=0||Math.abs(l)<.1?(p=1e4,d=i+p*l):p=-_/l;let m,f,g=t.aQ(1,n.y),v=0;do{if(v+=1,v>10)break;f=p/g,m=new t.a1(n.x+h*f,n.y+u*f),g=1/m.meterInMercatorCoordinateUnits();}while(Math.abs(p-f*g)>1e-12);return {center:m.toLngLat(),elevation:d,zoom:t.ak(this.height/2/Math.tan(this.fovInRadians/2)/f/this.tileSize)}}recalculateZoomAndCenter(e){if(this.elevation-e==0)return;const i=t.aj(1,this.center.lat)*this.worldSize,o=this.cameraToCenterDistance/i,r=t.a1.fromLngLat(this.center,this.elevation),a=de(this.center,this.elevation,this.pitch,this.bearing,o);this._elevation=e;const s=this.calculateCenterFromCameraLngLatAlt(a.toLngLat(),t.aQ(a.z,r.y),this.bearing,this.pitch);this._elevation=s.elevation,this._center=s.center,this.setZoom(s.zoom);}getCameraPoint(){const e=Math.tan(this.pitchInRadians)*(this.cameraToCenterDistance||1);return this.centerPoint.add(new t.P(e*Math.sin(this.rollInRadians),e*Math.cos(this.rollInRadians)))}getCameraAltitude(){return Math.cos(this.pitchInRadians)*this._cameraToCenterDistance/this._pixelPerMeter+this.elevation}getCameraLngLat(){const e=t.aj(1,this.center.lat)*this.worldSize;return de(this.center,this.elevation,this.pitch,this.bearing,this.cameraToCenterDistance/e).toLngLat()}getMercatorTileCoordinates(e){if(!e)return [0,0,1,1];const i=e.canonical.z>=0?1<this.max[0]||e.aabb.min[1]>this.max[1]||e.aabb.min[2]>this.max[2]||e.aabb.max[0]0?(t+=e[o]*this.min[o],i+=e[o]*this.max[o]):(i+=e[o]*this.min[o],t+=e[o]*this.max[o]);return t>=0?2:i<0?0:1}}class Dt{distanceToTile2d(e,t,i,o){const r=o.distanceX([e,t]),a=o.distanceY([e,t]);return Math.hypot(r,a)}getWrap(e,t,i){return i}getTileBoundingVolume(e,i,o,r){var a,s;let n=0,l=0;if(null==r?void 0:r.terrain){const c=new t.Z(e.z,i,e.z,e.x,e.y),h=r.terrain.getMinMaxElevation(c);n=null!==(a=h.minElevation)&&void 0!==a?a:Math.min(0,o),l=null!==(s=h.maxElevation)&&void 0!==s?s:Math.max(0,o);}const c=1<r}allowWorldCopies(){return !0}prepareNextFrame(){}}class At{constructor(e,t,i){this.points=e,this.planes=t,this.aabb=i;}static fromInvProjectionMatrix(e,i=1,o=0,r,a){const s=a?[[6,5,4],[0,1,2],[0,3,7],[2,1,5],[3,2,6],[0,4,5]]:[[0,1,2],[6,5,4],[0,3,7],[2,1,5],[3,2,6],[0,4,5]],n=Math.pow(2,o),l=[[-1,1,-1,1],[1,1,-1,1],[1,-1,-1,1],[-1,-1,-1,1],[-1,1,1,1],[1,1,1,1],[1,-1,1,1],[-1,-1,1,1]].map((o=>function(e,i,o,r){const a=t.aw([],e,i),s=1/a[3]/o*r;return t.aY(a,a,[s,s,1/a[3],s])}(o,e,i,n)));r&&function(e,i,o,r){const a=r?4:0,s=r?0:4;let n=0;const l=[],c=[];for(let i=0;i<4;i++){const o=t.aU([],e[i+s],e[i+a]),r=t.aZ(o);t.aR(o,o,1/r),l.push(r),c.push(o);}for(let i=0;i<4;i++){const r=t.a_(e[i+a],c[i],o);n=null!==r&&r>=0?Math.max(n,r):Math.max(n,l[i]);}const h=function(e,i){const o=t.aU([],e[i[0]],e[i[1]]),r=t.aU([],e[i[2]],e[i[1]]),a=[0,0,0,0];return t.aV(a,t.aW([],o,r)),a[3]=-t.aX(a,e[i[0]]),a}(e,i),u=function(e,i){const o=t.a$(e),r=t.b0([],e,1/o),a=t.aU([],i,t.aR([],r,t.aX(i,r))),s=t.a$(a);if(s>0){const e=Math.sqrt(1-r[3]*r[3]),o=t.aR([],r,-r[3]),n=t.aS([],o,t.aR([],a,e/s));return t.b1(i,n)}return null}(o,h);if(null!==u){const e=u/t.aX(c[0],h);n=Math.min(n,e);}for(let t=0;t<4;t++){const i=Math.min(n,l[t]);e[t+s]=[e[t+a][0]+c[t][0]*i,e[t+a][1]+c[t][1]*i,e[t+a][2]+c[t][2]*i,1];}}(l,s[0],r,a);const c=s.map((e=>{const i=t.aU([],l[e[0]],l[e[1]]),o=t.aU([],l[e[2]],l[e[1]]),r=t.aV([],t.aW([],i,o)),a=-t.aX(r,l[e[1]]);return r.concat(a)})),h=[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY],u=[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY];for(const e of l)for(let t=0;t<3;t++)h[t]=Math.min(h[t],e[t]),u[t]=Math.max(u[t],e[t]);return new At(l,c,new zt(h,u))}}class Lt{get pixelsToClipSpaceMatrix(){return this._helper.pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._helper.clipSpaceToPixelsMatrix}get pixelsToGLUnits(){return this._helper.pixelsToGLUnits}get centerOffset(){return this._helper.centerOffset}get size(){return this._helper.size}get rotationMatrix(){return this._helper.rotationMatrix}get centerPoint(){return this._helper.centerPoint}get pixelsPerMeter(){return this._helper.pixelsPerMeter}setMinZoom(e){this._helper.setMinZoom(e);}setMaxZoom(e){this._helper.setMaxZoom(e);}setMinPitch(e){this._helper.setMinPitch(e);}setMaxPitch(e){this._helper.setMaxPitch(e);}setRenderWorldCopies(e){this._helper.setRenderWorldCopies(e);}setBearing(e){this._helper.setBearing(e);}setPitch(e){this._helper.setPitch(e);}setRoll(e){this._helper.setRoll(e);}setFov(e){this._helper.setFov(e);}setZoom(e){this._helper.setZoom(e);}setCenter(e){this._helper.setCenter(e);}setElevation(e){this._helper.setElevation(e);}setMinElevationForCurrentTile(e){this._helper.setMinElevationForCurrentTile(e);}setPadding(e){this._helper.setPadding(e);}interpolatePadding(e,t,i){return this._helper.interpolatePadding(e,t,i)}isPaddingEqual(e){return this._helper.isPaddingEqual(e)}resize(e,t,i=!0){this._helper.resize(e,t,i);}getMaxBounds(){return this._helper.getMaxBounds()}setMaxBounds(e){this._helper.setMaxBounds(e);}overrideNearFarZ(e,t){this._helper.overrideNearFarZ(e,t);}clearNearFarZOverride(){this._helper.clearNearFarZOverride();}getCameraQueryGeometry(e){return this._helper.getCameraQueryGeometry(this.getCameraPoint(),e)}get tileSize(){return this._helper.tileSize}get tileZoom(){return this._helper.tileZoom}get scale(){return this._helper.scale}get worldSize(){return this._helper.worldSize}get width(){return this._helper.width}get height(){return this._helper.height}get lngRange(){return this._helper.lngRange}get latRange(){return this._helper.latRange}get minZoom(){return this._helper.minZoom}get maxZoom(){return this._helper.maxZoom}get zoom(){return this._helper.zoom}get center(){return this._helper.center}get minPitch(){return this._helper.minPitch}get maxPitch(){return this._helper.maxPitch}get pitch(){return this._helper.pitch}get pitchInRadians(){return this._helper.pitchInRadians}get roll(){return this._helper.roll}get rollInRadians(){return this._helper.rollInRadians}get bearing(){return this._helper.bearing}get bearingInRadians(){return this._helper.bearingInRadians}get fov(){return this._helper.fov}get fovInRadians(){return this._helper.fovInRadians}get elevation(){return this._helper.elevation}get minElevationForCurrentTile(){return this._helper.minElevationForCurrentTile}get padding(){return this._helper.padding}get unmodified(){return this._helper.unmodified}get renderWorldCopies(){return this._helper.renderWorldCopies}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}get nearZ(){return this._helper.nearZ}get farZ(){return this._helper.farZ}get autoCalculateNearFarZ(){return this._helper.autoCalculateNearFarZ}setTransitionState(e,t){}constructor(e,t,i,o,r){this._posMatrixCache=new Map,this._alignedPosMatrixCache=new Map,this._fogMatrixCacheF32=new Map,this._helper=new Rt({calcMatrices:()=>{this._calcMatrices();},getConstrained:(e,t)=>this.getConstrained(e,t)},e,t,i,o,r),this._coveringTilesDetailsProvider=new Dt;}clone(){const e=new Lt;return e.apply(this),e}apply(e,t,i){this._helper.apply(e,t,i);}get cameraPosition(){return this._cameraPosition}get projectionMatrix(){return this._projectionMatrix}get modelViewProjectionMatrix(){return this._viewProjMatrix}get inverseProjectionMatrix(){return this._invProjMatrix}get mercatorMatrix(){return this._mercatorMatrix}getVisibleUnwrappedCoordinates(e){const i=[new t.b2(0,e)];if(this._helper._renderWorldCopies){const o=this.screenPointToMercatorCoordinate(new t.P(0,0)),r=this.screenPointToMercatorCoordinate(new t.P(this._helper._width,0)),a=this.screenPointToMercatorCoordinate(new t.P(this._helper._width,this._helper._height)),s=this.screenPointToMercatorCoordinate(new t.P(0,this._helper._height)),n=Math.floor(Math.min(o.x,r.x,a.x,s.x)),l=Math.floor(Math.max(o.x,r.x,a.x,s.x)),c=1;for(let o=n-c;o<=l+c;o++)0!==o&&i.push(new t.b2(o,e));}return i}getCameraFrustum(){return At.fromInvProjectionMatrix(this._invViewProjMatrix,this.worldSize)}getClippingPlane(){return null}getCoveringTilesDetailsProvider(){return this._coveringTilesDetailsProvider}recalculateZoomAndCenter(e){const t=this.screenPointToLocation(this.centerPoint,e),i=e?e.getElevationForLngLatZoom(t,this._helper._tileZoom):0;this._helper.recalculateZoomAndCenter(i);}setLocationAtPoint(e,i){const o=t.aj(this.elevation,this.center.lat),r=this.screenPointToMercatorCoordinateAtZ(i,o),a=this.screenPointToMercatorCoordinateAtZ(this.centerPoint,o),s=t.a1.fromLngLat(e),n=new t.a1(s.x-(r.x-a.x),s.y-(r.y-a.y));this.setCenter(null==n?void 0:n.toLngLat()),this._helper._renderWorldCopies&&this.setCenter(this.center.wrap());}locationToScreenPoint(e,i){return i?this.coordinatePoint(t.a1.fromLngLat(e),i.getElevationForLngLatZoom(e,this._helper._tileZoom),this._pixelMatrix3D):this.coordinatePoint(t.a1.fromLngLat(e))}screenPointToLocation(e,t){var i;return null===(i=this.screenPointToMercatorCoordinate(e,t))||void 0===i?void 0:i.toLngLat()}screenPointToMercatorCoordinate(e,t){if(t){const i=t.pointCoordinate(e);if(null!=i)return i}return this.screenPointToMercatorCoordinateAtZ(e)}screenPointToMercatorCoordinateAtZ(e,i){const o=i||0,r=[e.x,e.y,0,1],a=[e.x,e.y,1,1];t.aw(r,r,this._pixelMatrixInverse),t.aw(a,a,this._pixelMatrixInverse);const s=r[3],n=a[3],l=r[1]/s,c=a[1]/n,h=r[2]/s,u=a[2]/n,d=h===u?0:(o-h)/(u-h);return new t.a1(t.C.number(r[0]/s,a[0]/n,d)/this.worldSize,t.C.number(l,c,d)/this.worldSize,o)}coordinatePoint(e,i=0,o=this._pixelMatrix){const r=[e.x*this.worldSize,e.y*this.worldSize,i,1];return t.aw(r,r,o),new t.P(r[0]/r[3],r[1]/r[3])}getBounds(){const e=Math.max(0,this._helper._height/2-he(this));return (new G).extend(this.screenPointToLocation(new t.P(0,e))).extend(this.screenPointToLocation(new t.P(this._helper._width,e))).extend(this.screenPointToLocation(new t.P(this._helper._width,this._helper._height))).extend(this.screenPointToLocation(new t.P(0,this._helper._height)))}isPointOnMapSurface(e,t){return t?null!=t.pointCoordinate(e):e.y>this.height/2-he(this)}calculatePosMatrix(e,i=!1,o){var r;const a=null!==(r=e.key)&&void 0!==r?r:t.b3(e.wrap,e.canonical.z,e.canonical.z,e.canonical.x,e.canonical.y),s=i?this._alignedPosMatrixCache:this._posMatrixCache;if(s.has(a)){const e=s.get(a);return o?e.f32:e.f64}const n=ue(e,this.worldSize);t.O(n,i?this._alignedProjMatrix:this._viewProjMatrix,n);const l={f64:n,f32:new Float32Array(n)};return s.set(a,l),o?l.f32:l.f64}calculateFogMatrix(e){const i=e.key,o=this._fogMatrixCacheF32;if(o.has(i))return o.get(i);const r=ue(e,this.worldSize);return t.O(r,this._fogMatrix,r),o.set(i,new Float32Array(r)),o.get(i)}getConstrained(e,i){i=t.ah(+i,this.minZoom,this.maxZoom);const o={center:new t.S(e.lng,e.lat),zoom:i};let r=this._helper._lngRange;if(!this._helper._renderWorldCopies&&null===r){const e=180-1e-10;r=[-e,e];}const a=this.tileSize*t.af(o.zoom);let s=0,n=a,l=0,c=a,h=0,u=0;const{x:d,y:_}=this.size;if(this._helper._latRange){const e=this._helper._latRange;s=t.U(e[1])*a,n=t.U(e[0])*a,n-s<_&&(h=_/(n-s));}r&&(l=t.aO(t.V(r[0])*a,0,a),c=t.aO(t.V(r[1])*a,0,a),cn&&(g=n-e);}if(r){const e=(l+c)/2;let i=p;this._helper._renderWorldCopies&&(i=t.aO(p,e-a/2,e+a/2));const o=d/2;i-oc&&(f=c-o);}if(void 0!==f||void 0!==g){const e=new t.P(null!=f?f:p,null!=g?g:m);o.center=ce(a,e).wrap();}return o}calculateCenterFromCameraLngLatAlt(e,t,i,o){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,i,o)}_calculateNearFarZIfNeeded(e,i,o){if(!this._helper.autoCalculateNearFarZ)return;const r=Math.min(this.elevation,this.minElevationForCurrentTile,this.getCameraAltitude()-100),a=e-r*this._helper._pixelPerMeter/Math.cos(i),s=r<0?a:e,n=Math.PI/2+this.pitchInRadians,l=t.ae(this.fov)*(Math.abs(Math.cos(t.ae(this.roll)))*this.height+Math.abs(Math.sin(t.ae(this.roll)))*this.width)/this.height*(.5+o.y/this.height),c=Math.sin(l)*s/Math.sin(t.ah(Math.PI-n-l,.01,Math.PI-.01)),h=he(this),u=Math.atan(h/this._helper.cameraToCenterDistance),d=t.ae(.75),_=u>d?2*u*(.5+o.y/(2*h)):d,p=Math.sin(_)*s/Math.sin(t.ah(Math.PI-n-_,.01,Math.PI-.01)),m=Math.min(c,p);this._helper._farZ=1.01*(Math.cos(Math.PI/2-i)*m+s),this._helper._nearZ=this._helper._height/50;}_calcMatrices(){if(!this._helper._height)return;const e=this.centerOffset,i=le(this.worldSize,this.center),o=i.x,r=i.y;this._helper._pixelPerMeter=t.aj(1,this.center.lat)*this.worldSize;const a=t.ae(Math.min(this.pitch,ne)),s=Math.max(this._helper.cameraToCenterDistance/2,this._helper.cameraToCenterDistance+this._helper._elevation*this._helper._pixelPerMeter/Math.cos(a));let n;this._calculateNearFarZIfNeeded(s,a,e),n=new Float64Array(16),t.b4(n,this.fovInRadians,this._helper._width/this._helper._height,this._helper._nearZ,this._helper._farZ),this._invProjMatrix=new Float64Array(16),t.aq(this._invProjMatrix,n),n[8]=2*-e.x/this._helper._width,n[9]=2*e.y/this._helper._height,this._projectionMatrix=t.b5(n),t.N(n,n,[1,-1,1]),t.M(n,n,[0,0,-this._helper.cameraToCenterDistance]),t.b6(n,n,-this.rollInRadians),t.b7(n,n,this.pitchInRadians),t.b6(n,n,-this.bearingInRadians),t.M(n,n,[-o,-r,0]),this._mercatorMatrix=t.N([],n,[this.worldSize,this.worldSize,this.worldSize]),t.N(n,n,[1,1,this._helper._pixelPerMeter]),this._pixelMatrix=t.O(new Float64Array(16),this.clipSpaceToPixelsMatrix,n),t.M(n,n,[0,0,-this.elevation]),this._viewProjMatrix=n,this._invViewProjMatrix=t.aq([],n);const l=[0,0,-1,1];t.aw(l,l,this._invViewProjMatrix),this._cameraPosition=[l[0]/l[3],l[1]/l[3],l[2]/l[3]],this._fogMatrix=new Float64Array(16),t.b4(this._fogMatrix,this.fovInRadians,this.width/this.height,s,this._helper._farZ),this._fogMatrix[8]=2*-e.x/this.width,this._fogMatrix[9]=2*e.y/this.height,t.N(this._fogMatrix,this._fogMatrix,[1,-1,1]),t.M(this._fogMatrix,this._fogMatrix,[0,0,-this.cameraToCenterDistance]),t.b6(this._fogMatrix,this._fogMatrix,-this.rollInRadians),t.b7(this._fogMatrix,this._fogMatrix,this.pitchInRadians),t.b6(this._fogMatrix,this._fogMatrix,-this.bearingInRadians),t.M(this._fogMatrix,this._fogMatrix,[-o,-r,0]),t.N(this._fogMatrix,this._fogMatrix,[1,1,this._helper._pixelPerMeter]),t.M(this._fogMatrix,this._fogMatrix,[0,0,-this.elevation]),this._pixelMatrix3D=t.O(new Float64Array(16),this.clipSpaceToPixelsMatrix,n);const c=this._helper._width%2/2,h=this._helper._height%2/2,u=Math.cos(this.bearingInRadians),d=Math.sin(-this.bearingInRadians),_=o-Math.round(o)+u*c+d*h,p=r-Math.round(r)+u*h+d*c,m=new Float64Array(n);if(t.M(m,m,[_>.5?_-1:_,p>.5?p-1:p,0]),this._alignedProjMatrix=m,n=t.aq(new Float64Array(16),this._pixelMatrix),!n)throw new Error("failed to invert matrix");this._pixelMatrixInverse=n,this._clearMatrixCaches();}_clearMatrixCaches(){this._posMatrixCache.clear(),this._alignedPosMatrixCache.clear(),this._fogMatrixCacheF32.clear();}maxPitchScaleFactor(){if(!this._pixelMatrixInverse)return 1;const e=this.screenPointToMercatorCoordinate(new t.P(0,0)),i=[e.x*this.worldSize,e.y*this.worldSize,0,1];return t.aw(i,i,this._pixelMatrix)[3]/this._helper.cameraToCenterDistance}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){const e=t.aj(1,this.center.lat)*this.worldSize;return de(this.center,this.elevation,this.pitch,this.bearing,this._helper.cameraToCenterDistance/e).toLngLat()}lngLatToCameraDepth(e,i){const o=t.a1.fromLngLat(e),r=[o.x*this.worldSize,o.y*this.worldSize,i,1];return t.aw(r,r,this._viewProjMatrix),r[2]/r[3]}getProjectionData(e){const{overscaledTileID:i,aligned:o,applyTerrainMatrix:r}=e,a=this._helper.getMercatorTileCoordinates(i),s=i?this.calculatePosMatrix(i,o,!0):null;let n;return n=i&&i.terrainRttPosMatrix32f&&r?i.terrainRttPosMatrix32f:s||t.b8(),{mainMatrix:n,tileMercatorCoords:a,clippingPlane:[0,0,0,0],projectionTransition:0,fallbackMatrix:n}}isLocationOccluded(e){return !1}getPixelScale(){return 1}getCircleRadiusCorrection(){return 1}getPitchedTextCorrection(e,t,i){return 1}transformLightDirection(e){return t.aT(e)}getRayDirectionFromPixel(e){throw new Error("Not implemented.")}projectTileCoordinates(e,i,o,r){const a=this.calculatePosMatrix(o);let s;r?(s=[e,i,r(e,i),1],t.aw(s,s,a)):(s=[e,i,0,1],qe(s,s,a));const n=s[3];return {point:new t.P(s[0]/n,s[1]/n),signedDistanceFromCamera:n,isOccluded:!1}}populateCache(e){for(const t of e)this.calculatePosMatrix(t);}getMatrixForModel(e,i){const o=t.a1.fromLngLat(e,i),r=o.meterInMercatorCoordinateUnits(),a=t.b9();return t.M(a,a,[o.x,o.y,o.z]),t.b6(a,a,Math.PI),t.b7(a,a,Math.PI/2),t.N(a,a,[-r,r,r]),a}getProjectionDataForCustomLayer(e=!0){const i=new t.Z(0,0,0,0,0),o=this.getProjectionData({overscaledTileID:i,applyGlobeMatrix:e}),r=ue(i,this.worldSize);t.O(r,this._viewProjMatrix,r),o.tileMercatorCoords=[0,0,1,1];const a=[t.$,t.$,this.worldSize/this._helper.pixelsPerMeter],s=t.ba();return t.N(s,r,a),o.fallbackMatrix=s,o.mainMatrix=s,o}getFastPathSimpleProjectionMatrix(e){return this.calculatePosMatrix(e)}}function kt(){t.w("Map cannot fit within canvas with the given bounds, padding, and/or offset.");}function Ft(e){if(e.useSlerp)if(e.k<1){const i=t.bb(e.startEulerAngles.roll,e.startEulerAngles.pitch,e.startEulerAngles.bearing),o=t.bb(e.endEulerAngles.roll,e.endEulerAngles.pitch,e.endEulerAngles.bearing),r=new Float64Array(4);t.bc(r,i,o,e.k);const a=t.bd(r);e.tr.setRoll(a.roll),e.tr.setPitch(a.pitch),e.tr.setBearing(a.bearing);}else e.tr.setRoll(e.endEulerAngles.roll),e.tr.setPitch(e.endEulerAngles.pitch),e.tr.setBearing(e.endEulerAngles.bearing);else e.tr.setRoll(t.C.number(e.startEulerAngles.roll,e.endEulerAngles.roll,e.k)),e.tr.setPitch(t.C.number(e.startEulerAngles.pitch,e.endEulerAngles.pitch,e.k)),e.tr.setBearing(t.C.number(e.startEulerAngles.bearing,e.endEulerAngles.bearing,e.k));}function Bt(e,i,o,r,a){const s=a.padding,n=le(a.worldSize,o.getNorthWest()),l=le(a.worldSize,o.getNorthEast()),c=le(a.worldSize,o.getSouthEast()),h=le(a.worldSize,o.getSouthWest()),u=t.ae(-r),d=n.rotate(u),_=l.rotate(u),p=c.rotate(u),m=h.rotate(u),f=new t.P(Math.max(d.x,_.x,m.x,p.x),Math.max(d.y,_.y,m.y,p.y)),g=new t.P(Math.min(d.x,_.x,m.x,p.x),Math.min(d.y,_.y,m.y,p.y)),v=f.sub(g),b=(a.width-(s.left+s.right+i.left+i.right))/v.x,x=(a.height-(s.top+s.bottom+i.top+i.bottom))/v.y;if(x<0||b<0)return void kt();const y=Math.min(t.ak(a.scale*Math.min(b,x)),e.maxZoom),w=t.P.convert(e.offset),T=new t.P((i.left-i.right)/2,(i.top-i.bottom)/2).rotate(t.ae(r)),P=w.add(T).mult(a.scale/t.af(y));return {center:ce(a.worldSize,n.add(c).div(2).sub(P)),zoom:y,bearing:r}}class Ot{get useGlobeControls(){return !1}handlePanInertia(e,t){return {easingOffset:e,easingCenter:t.center}}handleMapControlsRollPitchBearingZoom(e,t){e.bearingDelta&&t.setBearing(t.bearing+e.bearingDelta),e.pitchDelta&&t.setPitch(t.pitch+e.pitchDelta),e.rollDelta&&t.setRoll(t.roll+e.rollDelta),e.zoomDelta&&t.setZoom(t.zoom+e.zoomDelta);}handleMapControlsPan(e,t,i){e.around.distSqr(t.centerPoint)<.01||t.setLocationAtPoint(i,e.around);}cameraForBoxAndBearing(e,t,i,o,r){return Bt(e,t,i,o,r)}handleJumpToCenterZoom(e,i){e.zoom!==(void 0!==i.zoom?+i.zoom:e.zoom)&&e.setZoom(+i.zoom),void 0!==i.center&&e.setCenter(t.S.convert(i.center));}handleEaseTo(e,i){const o=e.zoom,r=e.padding,a={roll:e.roll,pitch:e.pitch,bearing:e.bearing},s={roll:void 0===i.roll?e.roll:i.roll,pitch:void 0===i.pitch?e.pitch:i.pitch,bearing:void 0===i.bearing?e.bearing:i.bearing},n=void 0!==i.zoom,l=!e.isPaddingEqual(i.padding);let c=!1;const h=n?+i.zoom:e.zoom;let u=e.centerPoint.add(i.offsetAsPoint);const d=e.screenPointToLocation(u),{center:_,zoom:p}=e.getConstrained(t.S.convert(i.center||d),null!=h?h:o);St(e,_);const m=le(e.worldSize,d),f=le(e.worldSize,_).sub(m),g=t.af(p-o);return c=p!==o,{easeFunc:n=>{if(c&&e.setZoom(t.C.number(o,p,n)),t.be(a,s)||Ft({startEulerAngles:a,endEulerAngles:s,tr:e,k:n,useSlerp:a.roll!=s.roll}),l&&(e.interpolatePadding(r,i.padding,n),u=e.centerPoint.add(i.offsetAsPoint)),i.around)e.setLocationAtPoint(i.around,i.aroundPoint);else {const i=t.af(e.zoom-o),r=p>o?Math.min(2,g):Math.max(.5,g),a=Math.pow(r,1-n),s=ce(e.worldSize,m.add(f.mult(n*a)).mult(i));e.setLocationAtPoint(e.renderWorldCopies?s.wrap():s,u);}},isZooming:c,elevationCenter:_}}handleFlyTo(e,i){const o=void 0!==i.zoom,r=e.zoom,a=e.getConstrained(t.S.convert(i.center||i.locationAtOffset),o?+i.zoom:r),s=a.center,n=a.zoom;St(e,s);const l=le(e.worldSize,i.locationAtOffset),c=le(e.worldSize,s).sub(l),h=c.mag(),u=t.af(n-r);let d;if(void 0!==i.minZoom){const o=Math.min(+i.minZoom,r,n),a=e.getConstrained(s,o).zoom;d=t.af(a-r);}return {easeFunc:(i,o,a,h)=>{e.setZoom(1===i?n:r+t.ak(o));const u=1===i?s:ce(e.worldSize,l.add(c.mult(a)).mult(o));e.setLocationAtPoint(e.renderWorldCopies?u.wrap():u,h);},scaleOfZoom:u,targetCenter:s,scaleOfMinZoom:d,pixelPathLength:h}}}class jt{constructor(e,t,i){this.blendFunction=e,this.blendColor=t,this.mask=i;}}jt.Replace=[1,0],jt.disabled=new jt(jt.Replace,t.bf.transparent,[!1,!1,!1,!1]),jt.unblended=new jt(jt.Replace,t.bf.transparent,[!0,!0,!0,!0]),jt.alphaBlended=new jt([1,771],t.bf.transparent,[!0,!0,!0,!0]);const Nt=2305;class Ut{constructor(e,t,i){this.enable=e,this.mode=t,this.frontFace=i;}}Ut.disabled=new Ut(!1,1029,Nt),Ut.backCCW=new Ut(!0,1029,Nt),Ut.frontCCW=new Ut(!0,1028,Nt);class Zt{constructor(e,t,i){this.func=e,this.mask=t,this.range=i;}}Zt.ReadOnly=!1,Zt.ReadWrite=!0,Zt.disabled=new Zt(519,Zt.ReadOnly,[0,1]);const Gt=7680;class Vt{constructor(e,t,i,o,r,a){this.test=e,this.ref=t,this.mask=i,this.fail=o,this.depthFail=r,this.pass=a;}}Vt.disabled=new Vt({func:519,mask:0},0,0,Gt,Gt,Gt);const $t=new WeakMap;function qt(e){var t;if($t.has(e))return $t.get(e);{const i=null===(t=e.getParameter(e.VERSION))||void 0===t?void 0:t.startsWith("WebGL 2.0");return $t.set(e,i),i}}class Wt{get awaitingQuery(){return !!this._readbackQueue}constructor(e){this._readbackWaitFrames=4,this._measureWaitFrames=6,this._texWidth=1,this._texHeight=1,this._measuredError=0,this._updateCount=0,this._lastReadbackFrame=-1e3,this._readbackQueue=null,this._cachedRenderContext=e;const i=e.context,o=i.gl;this._texFormat=o.RGBA,this._texType=o.UNSIGNED_BYTE;const r=new t.aL;r.emplaceBack(-1,-1),r.emplaceBack(2,-1),r.emplaceBack(-1,2);const a=new t.aN;a.emplaceBack(0,1,2),this._fullscreenTriangle=new wt(i.createVertexBuffer(r,Tt.members),i.createIndexBuffer(a),t.aM.simpleSegment(0,0,r.length,a.length)),this._resultBuffer=new Uint8Array(4),i.activeTexture.set(o.TEXTURE1);const s=o.createTexture();o.bindTexture(o.TEXTURE_2D,s),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MIN_FILTER,o.NEAREST),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MAG_FILTER,o.NEAREST),o.texImage2D(o.TEXTURE_2D,0,this._texFormat,this._texWidth,this._texHeight,0,this._texFormat,this._texType,null),this._fbo=i.createFramebuffer(this._texWidth,this._texHeight,!1,!1),this._fbo.colorAttachment.set(s),qt(o)&&(this._pbo=o.createBuffer(),o.bindBuffer(o.PIXEL_PACK_BUFFER,this._pbo),o.bufferData(o.PIXEL_PACK_BUFFER,4,o.STREAM_READ),o.bindBuffer(o.PIXEL_PACK_BUFFER,null));}destroy(){const e=this._cachedRenderContext.context.gl;this._fullscreenTriangle.destroy(),this._fbo.destroy(),e.deleteBuffer(this._pbo),this._fullscreenTriangle=null,this._fbo=null,this._pbo=null,this._resultBuffer=null;}updateErrorLoop(e,t){const i=this._updateCount;return this._readbackQueue?i>=this._readbackQueue.frameNumberIssued+this._readbackWaitFrames&&this._tryReadback():i>=this._lastReadbackFrame+this._measureWaitFrames&&this._renderErrorTexture(e,t),this._updateCount++,this._measuredError}_bindFramebuffer(){const e=this._cachedRenderContext.context,t=e.gl;e.activeTexture.set(t.TEXTURE1),t.bindTexture(t.TEXTURE_2D,this._fbo.colorAttachment.get()),e.bindFramebuffer.set(this._fbo.framebuffer);}_renderErrorTexture(e,i){const o=this._cachedRenderContext.context,r=o.gl;if(this._bindFramebuffer(),o.viewport.set([0,0,this._texWidth,this._texHeight]),o.clear({color:t.bf.transparent}),this._cachedRenderContext.useProgram("projectionErrorMeasurement").draw(o,r.TRIANGLES,Zt.disabled,Vt.disabled,jt.unblended,Ut.disabled,((e,t)=>({u_input:e,u_output_expected:t}))(e,i),null,null,"$clipping",this._fullscreenTriangle.vertexBuffer,this._fullscreenTriangle.indexBuffer,this._fullscreenTriangle.segments),this._pbo&&qt(r)){r.bindBuffer(r.PIXEL_PACK_BUFFER,this._pbo),r.readBuffer(r.COLOR_ATTACHMENT0),r.readPixels(0,0,this._texWidth,this._texHeight,this._texFormat,this._texType,0),r.bindBuffer(r.PIXEL_PACK_BUFFER,null);const e=r.fenceSync(r.SYNC_GPU_COMMANDS_COMPLETE,0);r.flush(),this._readbackQueue={frameNumberIssued:this._updateCount,sync:e};}else this._readbackQueue={frameNumberIssued:this._updateCount,sync:null};}_tryReadback(){const e=this._cachedRenderContext.context.gl;if(this._pbo&&this._readbackQueue&&qt(e)){const i=e.clientWaitSync(this._readbackQueue.sync,0,0);if(i===e.WAIT_FAILED)return t.w("WebGL2 clientWaitSync failed."),this._readbackQueue=null,void(this._lastReadbackFrame=this._updateCount);if(i===e.TIMEOUT_EXPIRED)return;e.bindBuffer(e.PIXEL_PACK_BUFFER,this._pbo),e.getBufferSubData(e.PIXEL_PACK_BUFFER,0,this._resultBuffer,0,4),e.bindBuffer(e.PIXEL_PACK_BUFFER,null);}else this._bindFramebuffer(),e.readPixels(0,0,this._texWidth,this._texHeight,this._texFormat,this._texType,this._resultBuffer);this._readbackQueue=null,this._measuredError=Wt._parseRGBA8float(this._resultBuffer),this._lastReadbackFrame=this._updateCount;}static _parseRGBA8float(e){let t=0;return t+=e[0]/256,t+=e[1]/65536,t+=e[2]/16777216,e[3]<127&&(t=-t),t/128}}const Ht=t.$/128;function Xt(e,i){const o=void 0!==e.granularity?Math.max(e.granularity,1):1,r=o+(e.generateBorders?2:0),a=o+(e.extendToNorthPole||e.generateBorders?1:0)+(e.extendToSouthPole||e.generateBorders?1:0),s=r+1,n=a+1,l=e.generateBorders?-1:0,c=e.generateBorders||e.extendToNorthPole?-1:0,h=o+(e.generateBorders?1:0),u=o+(e.generateBorders||e.extendToSouthPole?1:0),d=s*n,_=r*a*6,p=s*n>65536;if(p&&"16bit"===i)throw new Error("Granularity is too large and meshes would not fit inside 16 bit vertex indices.");const m=p||"32bit"===i,f=new Int16Array(2*d);let g=0;for(let i=c;i<=u;i++)for(let r=l;r<=h;r++){let a=r/o*t.$;-1===r&&(a=-Ht),r===o+1&&(a=t.$+Ht);let s=i/o*t.$;-1===i&&(s=e.extendToNorthPole?t.bh:-Ht),i===o+1&&(s=e.extendToSouthPole?t.bi:t.$+Ht),f[g++]=a,f[g++]=s;}const v=m?new Uint32Array(_):new Uint16Array(_);let b=0;for(let e=0;e0}get latitudeErrorCorrectionRadians(){return this._verticalPerspectiveProjection.latitudeErrorCorrectionRadians}get currentProjection(){return this.useGlobeRendering?this._verticalPerspectiveProjection:this._mercatorProjection}get name(){return "globe"}get useSubdivision(){return this.currentProjection.useSubdivision}get shaderVariantName(){return this.currentProjection.shaderVariantName}get shaderDefine(){return this.currentProjection.shaderDefine}get shaderPreludeCode(){return this.currentProjection.shaderPreludeCode}get vertexShaderPreludeCode(){return this.currentProjection.vertexShaderPreludeCode}get subdivisionGranularity(){return this.currentProjection.subdivisionGranularity}get useGlobeControls(){return this.transitionState>0}destroy(){this._mercatorProjection.destroy(),this._verticalPerspectiveProjection.destroy();}updateGPUdependent(e){this._mercatorProjection.updateGPUdependent(e),this._verticalPerspectiveProjection.updateGPUdependent(e);}getMeshFromTileID(e,t,i,o,r){return this.currentProjection.getMeshFromTileID(e,t,i,o,r)}setProjection(e){this._transitionable.setValue("type",(null==e?void 0:e.type)||"mercator");}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning);}hasTransition(){return this._transitioning.hasTransition()||this.currentProjection.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e);}setErrorQueryLatitudeDegrees(e){this._verticalPerspectiveProjection.setErrorQueryLatitudeDegrees(e),this._mercatorProjection.setErrorQueryLatitudeDegrees(e);}}function ei(e){const t=oi(e.worldSize,e.center.lat);return 2*Math.PI*t}function ti(e,i,o,r,a){const s=1/(1<1e-6){const r=e[0]/o,a=Math.acos(e[2]/o),s=(r>0?a:-a)/Math.PI*180;return new t.S(t.aO(s,-180,180),i)}return new t.S(0,i)}function ai(e){return Math.cos(e*Math.PI/180)}function si(e,i){const o=ai(e),r=ai(i);return t.ak(r/o)}function ni(e,i){const o=e.rotate(i.bearingInRadians),r=i.zoom+si(i.center.lat,0),a=t.bk(1/ai(i.center.lat),1/ai(Math.min(Math.abs(i.center.lat),60)),t.bn(r,7,3,0,1)),s=360/ei({worldSize:i.worldSize,center:{lat:i.center.lat}});return new t.S(i.center.lng-o.x*s*a,t.ah(i.center.lat+o.y*s,-t.ai,t.ai))}function li(e){const t=.5*e,i=Math.sin(t),o=Math.cos(t);return Math.log(i+o)-Math.log(o-i)}function ci(e,i,o,r){const a=e.lat+o*r;if(Math.abs(o)>1){const s=(Math.sign(e.lat+o)!==Math.sign(e.lat)?-Math.abs(e.lat):Math.abs(e.lat))*Math.PI/180,n=Math.abs(e.lat+o)*Math.PI/180,l=li(s+r*(n-s)),c=li(s),h=li(n);return new t.S(e.lng+i*((l-c)/(h-c)),a)}return new t.S(e.lng+i*r,a)}class hi{constructor(e){this._cachePrevious=new Map,this._cache=new Map,this._hadAnyChanges=!1,this._boundingVolumeFactory=e;}swapBuffers(){if(!this._hadAnyChanges)return;const e=this._cachePrevious;this._cachePrevious=this._cache,this._cache=e,this._cache.clear(),this._hadAnyChanges=!1;}getTileBoundingVolume(e,t,i,o){const r=`${e.z}_${e.x}_${e.y}_${(null==o?void 0:o.terrain)?"t":""}`,a=this._cache.get(r);if(a)return a;const s=this._cachePrevious.get(r);if(s)return this._cache.set(r,s),s;const n=this._boundingVolumeFactory(e,t,i,o);return this._cache.set(r,n),this._hadAnyChanges=!0,n}}class ui{constructor(e,t,i,o){this.min=i,this.max=o,this.points=e,this.planes=t;}static fromAabb(e,t){const i=[];for(let o=0;o<8;o++)i.push([1&~o?e[0]:t[0],1==(o>>1&1)?t[1]:e[1],1==(o>>2&1)?t[2]:e[2]]);return new ui(i,[[-1,0,0,t[0]],[1,0,0,-e[0]],[0,-1,0,t[1]],[0,1,0,-e[1]],[0,0,-1,t[2]],[0,0,1,-e[2]]],e,t)}static fromCenterSizeAngles(e,i,o){const r=t.br([],o[0],o[1],o[2]),a=t.bs([],[i[0],0,0],r),s=t.bs([],[0,i[1],0],r),n=t.bs([],[0,0,i[2]],r),l=[...e],c=[...e];for(let t=0;t<8;t++)for(let i=0;i<3;i++){const o=e[i]+a[i]*(1&~t?-1:1)+s[i]*(1==(t>>1&1)?1:-1)+n[i]*(1==(t>>2&1)?1:-1);l[i]=Math.min(l[i],o),c[i]=Math.max(c[i],o);}const h=[];for(let i=0;i<8;i++){const o=[...e];t.aS(o,o,t.aR([],a,1&~i?-1:1)),t.aS(o,o,t.aR([],s,1==(i>>1&1)?1:-1)),t.aS(o,o,t.aR([],n,1==(i>>2&1)?1:-1)),h.push(o);}return new ui(h,[[...a,-t.aX(a,h[0])],[...s,-t.aX(s,h[0])],[...n,-t.aX(n,h[0])],[-a[0],-a[1],-a[2],-t.aX(a,h[7])],[-s[0],-s[1],-s[2],-t.aX(s,h[7])],[-n[0],-n[1],-n[2],-t.aX(n,h[7])]],l,c)}intersectsFrustum(e){let t=!0;const i=this.points.length,o=this.planes.length,r=e.planes.length,a=e.points.length;for(let o=0;o=0&&a++;}if(0===a)return 0;a=0&&o++;}if(0===o)return 0}return 1}intersectsPlane(e){const t=this.points.length;let i=0;for(let o=0;o=0&&i++;}return i===t?2:0===i?0:1}}function di(e,t,i){const o=e-t;return o<0?-o:Math.max(0,o-i)}function _i(e,t,i,o,r){const a=e-i;let s;return s=a<0?Math.min(-a,1+a-r):a>1?Math.min(Math.max(a-r,0),1-a):0,Math.max(s,di(t,o,r))}class pi{constructor(){this._boundingVolumeCache=new hi(this._computeTileBoundingVolume);}prepareNextFrame(){this._boundingVolumeCache.swapBuffers();}distanceToTile2d(e,t,i,o){const r=1<4}allowWorldCopies(){return !1}getTileBoundingVolume(e,t,i,o){return this._boundingVolumeCache.getTileBoundingVolume(e,t,i,o)}_computeTileBoundingVolume(e,i,o,r){var a,s;let n=0,l=0;if(null==r?void 0:r.terrain){const c=new t.Z(e.z,i,e.z,e.x,e.y),h=r.terrain.getMinMaxElevation(c);n=null!==(a=h.minElevation)&&void 0!==a?a:Math.min(0,o),l=null!==(s=h.maxElevation)&&void 0!==s?s:Math.max(0,o);}if(n/=t.bu,l/=t.bu,n+=1,l+=1,e.z<=0)return ui.fromAabb([-l,-l,-l],[l,l,l]);if(1===e.z)return ui.fromAabb([0===e.x?-l:0,0===e.y?0:-l,-l],[0===e.x?0:l,0===e.y?l:0,l]);{const i=[ti(0,0,e.x,e.y,e.z),ti(t.$,0,e.x,e.y,e.z),ti(t.$,t.$,e.x,e.y,e.z),ti(0,t.$,e.x,e.y,e.z)],o=[];for(const e of i)o.push(t.aR([],e,l));if(l!==n)for(const e of i)o.push(t.aR([],e,n));0===e.y&&o.push([0,1,0]),e.y===(1<=(1<{this._calcMatrices();},getConstrained:(e,t)=>this.getConstrained(e,t)}),this._coveringTilesDetailsProvider=new pi;}clone(){const e=new fi;return e.apply(this),e}apply(e,t){this._globeLatitudeErrorCorrectionRadians=t||0,this._helper.apply(e);}get projectionMatrix(){return this._projectionMatrix}get modelViewProjectionMatrix(){return this._globeViewProjMatrixNoCorrection}get inverseProjectionMatrix(){return this._globeProjMatrixInverted}get cameraPosition(){const e=t.bp();return e[0]=this._cameraPosition[0],e[1]=this._cameraPosition[1],e[2]=this._cameraPosition[2],e}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}getProjectionData(e){const{overscaledTileID:t,applyGlobeMatrix:i}=e,o=this._helper.getMercatorTileCoordinates(t);return {mainMatrix:this._globeViewProjMatrix32f,tileMercatorCoords:o,clippingPlane:this._cachedClippingPlane,projectionTransition:i?1:0,fallbackMatrix:this._globeViewProjMatrix32f}}_computeClippingPlane(e){const i=this.pitchInRadians,o=this.cameraToCenterDistance/e,r=Math.sin(i)*o,a=Math.cos(i)*o+1,s=1/Math.sqrt(r*r+a*a)*1;let n=-r,l=a;const c=Math.sqrt(n*n+l*l);n/=c,l/=c;const h=[0,n,l];t.bw(h,h,[0,0,0],-this.bearingInRadians),t.bx(h,h,[0,0,0],-1*this.center.lat*Math.PI/180),t.by(h,h,[0,0,0],this.center.lng*Math.PI/180);const u=1/t.aZ(h);return t.aR(h,h,u),[...h,-s*u]}isLocationOccluded(e){return !this.isSurfacePointVisible(ii(e))}transformLightDirection(e){const i=this._helper._center.lng*Math.PI/180,o=this._helper._center.lat*Math.PI/180,r=Math.cos(o),a=[Math.sin(i)*r,Math.sin(o),Math.cos(i)*r],s=[a[2],0,-a[0]],n=[0,0,0];t.aW(n,s,a),t.aV(s,s),t.aV(n,n);const l=[0,0,0];return t.aV(l,[s[0]*e[0]+n[0]*e[1]+a[0]*e[2],s[1]*e[0]+n[1]*e[1]+a[1]*e[2],s[2]*e[0]+n[2]*e[1]+a[2]*e[2]]),l}getPixelScale(){return 1/Math.cos(this._helper._center.lat*Math.PI/180)}getCircleRadiusCorrection(){return Math.cos(this._helper._center.lat*Math.PI/180)}getPitchedTextCorrection(e,i,o){const r=function(e,i,o){const r=1/(1<a&&(a=i),on&&(n=o);}const h=[c.lng+s,c.lat+l,c.lng+a,c.lat+n];return this.isSurfacePointOnScreen([0,1,0])&&(h[3]=90,h[0]=-180,h[2]=180),this.isSurfacePointOnScreen([0,-1,0])&&(h[1]=-90,h[0]=-180,h[2]=180),new G(h)}getConstrained(e,i){const o=t.ah(e.lat,-t.ai,t.ai),r=t.ah(+i,this.minZoom+si(0,o),this.maxZoom);return {center:new t.S(e.lng,o),zoom:r}}calculateCenterFromCameraLngLatAlt(e,t,i,o){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,i,o)}setLocationAtPoint(e,i){const o=ii(this.unprojectScreenPoint(i)),r=ii(e),a=t.bp();t.bB(a);const s=t.bp();t.by(s,o,a,-this.center.lng*Math.PI/180),t.bx(s,s,a,this.center.lat*Math.PI/180);const n=r[0]*r[0]+r[2]*r[2],l=s[0]*s[0];if(n=-g&&p<=g,b=f>=-g&&f<=g;let x,y;if(v&&b){const e=this.center.lng*Math.PI/180,i=this.center.lat*Math.PI/180;t.bD(u,e)+t.bD(p,i)=0}isSurfacePointOnScreen(e){if(!this.isSurfacePointVisible(e))return !1;const i=t.bv();return t.aw(i,[...e,1],this._globeViewProjMatrixNoCorrection),i[0]/=i[3],i[1]/=i[3],i[2]/=i[3],i[0]>-1&&i[0]<1&&i[1]>-1&&i[1]<1&&i[2]>-1&&i[2]<1}rayPlanetIntersection(e,i){const o=t.aX(e,i),r=t.bp(),a=t.bp();t.aR(a,i,o),t.aU(r,e,a);const s=1-t.aX(r,r);if(s<0)return null;const n=t.aX(e,e)-1,l=-o+(o<0?1:-1)*Math.sqrt(s),c=n/l,h=l;return {tMin:Math.min(c,h),tMax:Math.max(c,h)}}unprojectScreenPoint(e){const i=this._cameraPosition,o=this.getRayDirectionFromPixel(e),r=this.rayPlanetIntersection(i,o);if(r){const e=t.bp();t.aS(e,i,[o[0]*r.tMin,o[1]*r.tMin,o[2]*r.tMin]);const a=t.bp();return t.aV(a,e),ri(a)}const a=this._cachedClippingPlane,s=a[0]*o[0]+a[1]*o[1]+a[2]*o[2],n=-t.b1(a,i)/s,l=t.bp();if(n>0)t.aS(l,i,[o[0]*n,o[1]*n,o[2]*n]);else {const e=t.bp();t.aS(e,i,[2*o[0],2*o[1],2*o[2]]);const r=t.b1(this._cachedClippingPlane,e);t.aU(l,e,[this._cachedClippingPlane[0]*r,this._cachedClippingPlane[1]*r,this._cachedClippingPlane[2]*r]);}const c=function(e){const i=t.bp();return i[0]=e[0]*-e[3],i[1]=e[1]*-e[3],i[2]=e[2]*-e[3],{center:i,radius:Math.sqrt(1-e[3]*e[3])}}(a);return ri(function(e,i,o){const r=t.bp();t.aU(r,o,e);const a=t.bp();return t.bq(a,e,r,i/t.a$(r)),a}(c.center,c.radius,l))}getMatrixForModel(e,i){const o=t.S.convert(e),r=1/t.bu,a=t.b9();return t.bz(a,a,o.lng/180*Math.PI),t.b7(a,a,-o.lat/180*Math.PI),t.M(a,a,[0,0,1+i/t.bu]),t.b7(a,a,.5*Math.PI),t.N(a,a,[r,r,r]),a}getProjectionDataForCustomLayer(e=!0){const i=this.getProjectionData({overscaledTileID:new t.Z(0,0,0,0,0),applyGlobeMatrix:e});return i.tileMercatorCoords=[0,0,1,1],i}getFastPathSimpleProjectionMatrix(e){}}class gi{get pixelsToClipSpaceMatrix(){return this._helper.pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._helper.clipSpaceToPixelsMatrix}get pixelsToGLUnits(){return this._helper.pixelsToGLUnits}get centerOffset(){return this._helper.centerOffset}get size(){return this._helper.size}get rotationMatrix(){return this._helper.rotationMatrix}get centerPoint(){return this._helper.centerPoint}get pixelsPerMeter(){return this._helper.pixelsPerMeter}setMinZoom(e){this._helper.setMinZoom(e);}setMaxZoom(e){this._helper.setMaxZoom(e);}setMinPitch(e){this._helper.setMinPitch(e);}setMaxPitch(e){this._helper.setMaxPitch(e);}setRenderWorldCopies(e){this._helper.setRenderWorldCopies(e);}setBearing(e){this._helper.setBearing(e);}setPitch(e){this._helper.setPitch(e);}setRoll(e){this._helper.setRoll(e);}setFov(e){this._helper.setFov(e);}setZoom(e){this._helper.setZoom(e);}setCenter(e){this._helper.setCenter(e);}setElevation(e){this._helper.setElevation(e);}setMinElevationForCurrentTile(e){this._helper.setMinElevationForCurrentTile(e);}setPadding(e){this._helper.setPadding(e);}interpolatePadding(e,t,i){return this._helper.interpolatePadding(e,t,i)}isPaddingEqual(e){return this._helper.isPaddingEqual(e)}resize(e,t,i=!0){this._helper.resize(e,t,i);}getMaxBounds(){return this._helper.getMaxBounds()}setMaxBounds(e){this._helper.setMaxBounds(e);}overrideNearFarZ(e,t){this._helper.overrideNearFarZ(e,t);}clearNearFarZOverride(){this._helper.clearNearFarZOverride();}getCameraQueryGeometry(e){return this._helper.getCameraQueryGeometry(this.getCameraPoint(),e)}get tileSize(){return this._helper.tileSize}get tileZoom(){return this._helper.tileZoom}get scale(){return this._helper.scale}get worldSize(){return this._helper.worldSize}get width(){return this._helper.width}get height(){return this._helper.height}get lngRange(){return this._helper.lngRange}get latRange(){return this._helper.latRange}get minZoom(){return this._helper.minZoom}get maxZoom(){return this._helper.maxZoom}get zoom(){return this._helper.zoom}get center(){return this._helper.center}get minPitch(){return this._helper.minPitch}get maxPitch(){return this._helper.maxPitch}get pitch(){return this._helper.pitch}get pitchInRadians(){return this._helper.pitchInRadians}get roll(){return this._helper.roll}get rollInRadians(){return this._helper.rollInRadians}get bearing(){return this._helper.bearing}get bearingInRadians(){return this._helper.bearingInRadians}get fov(){return this._helper.fov}get fovInRadians(){return this._helper.fovInRadians}get elevation(){return this._helper.elevation}get minElevationForCurrentTile(){return this._helper.minElevationForCurrentTile}get padding(){return this._helper.padding}get unmodified(){return this._helper.unmodified}get renderWorldCopies(){return this._helper.renderWorldCopies}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}get nearZ(){return this._helper.nearZ}get farZ(){return this._helper.farZ}get autoCalculateNearFarZ(){return this._helper.autoCalculateNearFarZ}get isGlobeRendering(){return this._globeness>0}setTransitionState(e,t){this._globeness=e,this._globeLatitudeErrorCorrectionRadians=t,this._calcMatrices(),this._verticalPerspectiveTransform.getCoveringTilesDetailsProvider().prepareNextFrame(),this._mercatorTransform.getCoveringTilesDetailsProvider().prepareNextFrame();}get currentTransform(){return this.isGlobeRendering?this._verticalPerspectiveTransform:this._mercatorTransform}constructor(){this._globeLatitudeErrorCorrectionRadians=0,this._globeness=1,this._helper=new Rt({calcMatrices:()=>{this._calcMatrices();},getConstrained:(e,t)=>this.getConstrained(e,t)}),this._globeness=1,this._mercatorTransform=new Lt,this._verticalPerspectiveTransform=new fi;}clone(){const e=new gi;return e._globeness=this._globeness,e._globeLatitudeErrorCorrectionRadians=this._globeLatitudeErrorCorrectionRadians,e.apply(this),e}apply(e){this._helper.apply(e),this._mercatorTransform.apply(this),this._verticalPerspectiveTransform.apply(this,this._globeLatitudeErrorCorrectionRadians);}get projectionMatrix(){return this.currentTransform.projectionMatrix}get modelViewProjectionMatrix(){return this.currentTransform.modelViewProjectionMatrix}get inverseProjectionMatrix(){return this.currentTransform.inverseProjectionMatrix}get cameraPosition(){return this.currentTransform.cameraPosition}getProjectionData(e){const t=this._mercatorTransform.getProjectionData(e),i=this._verticalPerspectiveTransform.getProjectionData(e);return {mainMatrix:this.isGlobeRendering?i.mainMatrix:t.mainMatrix,clippingPlane:i.clippingPlane,tileMercatorCoords:i.tileMercatorCoords,projectionTransition:e.applyGlobeMatrix?this._globeness:0,fallbackMatrix:t.fallbackMatrix}}isLocationOccluded(e){return this.currentTransform.isLocationOccluded(e)}transformLightDirection(e){return this.currentTransform.transformLightDirection(e)}getPixelScale(){return t.bk(this._mercatorTransform.getPixelScale(),this._verticalPerspectiveTransform.getPixelScale(),this._globeness)}getCircleRadiusCorrection(){return t.bk(this._mercatorTransform.getCircleRadiusCorrection(),this._verticalPerspectiveTransform.getCircleRadiusCorrection(),this._globeness)}getPitchedTextCorrection(e,i,o){const r=this._mercatorTransform.getPitchedTextCorrection(e,i,o),a=this._verticalPerspectiveTransform.getPitchedTextCorrection(e,i,o);return t.bk(r,a,this._globeness)}projectTileCoordinates(e,t,i,o){return this.currentTransform.projectTileCoordinates(e,t,i,o)}_calcMatrices(){this._helper._width&&this._helper._height&&(this._verticalPerspectiveTransform.apply(this,this._globeLatitudeErrorCorrectionRadians),this._helper._nearZ=this._verticalPerspectiveTransform.nearZ,this._helper._farZ=this._verticalPerspectiveTransform.farZ,this._mercatorTransform.apply(this,!0,this.isGlobeRendering),this._helper._nearZ=this._mercatorTransform.nearZ,this._helper._farZ=this._mercatorTransform.farZ);}calculateFogMatrix(e){return this.currentTransform.calculateFogMatrix(e)}getVisibleUnwrappedCoordinates(e){return this.currentTransform.getVisibleUnwrappedCoordinates(e)}getCameraFrustum(){return this.currentTransform.getCameraFrustum()}getClippingPlane(){return this.currentTransform.getClippingPlane()}getCoveringTilesDetailsProvider(){return this.currentTransform.getCoveringTilesDetailsProvider()}recalculateZoomAndCenter(e){this._mercatorTransform.recalculateZoomAndCenter(e),this._verticalPerspectiveTransform.recalculateZoomAndCenter(e);}maxPitchScaleFactor(){return this._mercatorTransform.maxPitchScaleFactor()}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){return this._helper.getCameraLngLat()}lngLatToCameraDepth(e,t){return this.currentTransform.lngLatToCameraDepth(e,t)}populateCache(e){this._mercatorTransform.populateCache(e),this._verticalPerspectiveTransform.populateCache(e);}getBounds(){return this.currentTransform.getBounds()}getConstrained(e,t){return this.currentTransform.getConstrained(e,t)}calculateCenterFromCameraLngLatAlt(e,t,i,o){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,i,o)}setLocationAtPoint(e,t){if(!this.isGlobeRendering)return this._mercatorTransform.setLocationAtPoint(e,t),void this.apply(this._mercatorTransform);this._verticalPerspectiveTransform.setLocationAtPoint(e,t),this.apply(this._verticalPerspectiveTransform);}locationToScreenPoint(e,t){return this.currentTransform.locationToScreenPoint(e,t)}screenPointToMercatorCoordinate(e,t){return this.currentTransform.screenPointToMercatorCoordinate(e,t)}screenPointToLocation(e,t){return this.currentTransform.screenPointToLocation(e,t)}isPointOnMapSurface(e,t){return this.currentTransform.isPointOnMapSurface(e,t)}getRayDirectionFromPixel(e){return this._verticalPerspectiveTransform.getRayDirectionFromPixel(e)}getMatrixForModel(e,t){return this.currentTransform.getMatrixForModel(e,t)}getProjectionDataForCustomLayer(e=!0){const t=this._mercatorTransform.getProjectionDataForCustomLayer(e);if(!this.isGlobeRendering)return t;const i=this._verticalPerspectiveTransform.getProjectionDataForCustomLayer(e);return i.fallbackMatrix=t.mainMatrix,i}getFastPathSimpleProjectionMatrix(e){return this.currentTransform.getFastPathSimpleProjectionMatrix(e)}}class vi{get useGlobeControls(){return !0}handlePanInertia(e,i){const o=ni(e,i);return Math.abs(o.lng-i.center.lng)>180&&(o.lng=i.center.lng+179.5*Math.sign(o.lng-i.center.lng)),{easingCenter:o,easingOffset:new t.P(0,0)}}handleMapControlsRollPitchBearingZoom(e,i){const o=e.around,r=i.screenPointToLocation(o);e.bearingDelta&&i.setBearing(i.bearing+e.bearingDelta),e.pitchDelta&&i.setPitch(i.pitch+e.pitchDelta),e.rollDelta&&i.setRoll(i.roll+e.rollDelta);const a=i.zoom;e.zoomDelta&&i.setZoom(i.zoom+e.zoomDelta);const s=i.zoom-a;if(0===s)return;const n=t.bA(i.center.lng,r.lng),l=n/(Math.abs(n/180)+1),c=t.bA(i.center.lat,r.lat),h=i.getRayDirectionFromPixel(o),u=i.cameraPosition,d=-1*t.aX(u,h),_=t.bp();t.aS(_,u,[h[0]*d,h[1]*d,h[2]*d]);const p=t.aZ(_)-1,m=Math.exp(.5*-Math.max(p-.3,0)),f=oi(i.worldSize,i.center.lat)/Math.min(i.width,i.height),g=t.bn(f,.9,.5,1,.25),v=(1-t.af(-s))*Math.min(m,g),b=i.center.lat,x=i.zoom,y=new t.S(i.center.lng+l*v,t.ah(i.center.lat+c*v,-t.ai,t.ai));i.setLocationAtPoint(r,o);const w=i.center,T=t.bn(Math.abs(n),45,85,0,1),P=t.bn(f,.75,.35,0,1),C=Math.pow(Math.max(T,P),.25),I=t.bA(w.lng,y.lng),M=t.bA(w.lat,y.lat);i.setCenter(new t.S(w.lng+I*C,w.lat+M*C).wrap()),i.setZoom(x+si(b,i.center.lat));}handleMapControlsPan(e,t,i){if(!e.panDelta)return;const o=t.center.lat,r=t.zoom;t.setCenter(ni(e.panDelta,t).wrap()),t.setZoom(r+si(o,t.center.lat));}cameraForBoxAndBearing(e,i,o,r,a){const s=Bt(e,i,o,r,a),n=i.left/a.width*2-1,l=(a.width-i.right)/a.width*2-1,c=i.top/a.height*-2+1,h=(a.height-i.bottom)/a.height*-2+1,u=t.bA(o.getWest(),o.getEast())<0,d=u?o.getEast():o.getWest(),_=u?o.getWest():o.getEast(),p=Math.max(o.getNorth(),o.getSouth()),m=Math.min(o.getNorth(),o.getSouth()),f=d+.5*t.bA(d,_),g=p+.5*t.bA(p,m),v=a.clone();v.setCenter(s.center),v.setBearing(s.bearing),v.setPitch(0),v.setRoll(0),v.setZoom(s.zoom);const b=v.modelViewProjectionMatrix,x=[ii(o.getNorthWest()),ii(o.getNorthEast()),ii(o.getSouthWest()),ii(o.getSouthEast()),ii(new t.S(_,g)),ii(new t.S(d,g)),ii(new t.S(f,p)),ii(new t.S(f,m))],y=ii(s.center);let w=Number.POSITIVE_INFINITY;for(const e of x)n<0&&(w=vi.getLesserNonNegativeNonNull(w,vi.solveVectorScale(e,y,b,"x",n))),l>0&&(w=vi.getLesserNonNegativeNonNull(w,vi.solveVectorScale(e,y,b,"x",l))),c>0&&(w=vi.getLesserNonNegativeNonNull(w,vi.solveVectorScale(e,y,b,"y",c))),h<0&&(w=vi.getLesserNonNegativeNonNull(w,vi.solveVectorScale(e,y,b,"y",h)));if(Number.isFinite(w)&&0!==w)return s.zoom=v.zoom+t.ak(w),s;kt();}handleJumpToCenterZoom(e,i){const o=e.center.lat,r=e.getConstrained(i.center?t.S.convert(i.center):e.center,e.zoom).center;e.setCenter(r.wrap());const a=void 0!==i.zoom?+i.zoom:e.zoom+si(o,r.lat);e.zoom!==a&&e.setZoom(a);}handleEaseTo(e,i){const o=e.zoom,r=e.center,a=e.padding,s={roll:e.roll,pitch:e.pitch,bearing:e.bearing},n={roll:void 0===i.roll?e.roll:i.roll,pitch:void 0===i.pitch?e.pitch:i.pitch,bearing:void 0===i.bearing?e.bearing:i.bearing},l=void 0!==i.zoom,c=!e.isPaddingEqual(i.padding);let h=!1;const u=i.center?t.S.convert(i.center):r,d=e.getConstrained(u,o).center;St(e,d);const _=e.clone();_.setCenter(d),_.setZoom(l?+i.zoom:o+si(r.lat,u.lat)),_.setBearing(i.bearing);const p=new t.P(t.ah(e.centerPoint.x+i.offsetAsPoint.x,0,e.width),t.ah(e.centerPoint.y+i.offsetAsPoint.y,0,e.height));_.setLocationAtPoint(d,p);const m=(i.offset&&i.offsetAsPoint.mag())>0?_.center:d,f=l?+i.zoom:o+si(r.lat,m.lat),g=o+si(r.lat,0),v=f+si(m.lat,0),b=t.bA(r.lng,m.lng),x=t.bA(r.lat,m.lat),y=t.af(v-g);return h=f!==o,{easeFunc:o=>{if(t.be(s,n)||Ft({startEulerAngles:s,endEulerAngles:n,tr:e,k:o,useSlerp:s.roll!=n.roll}),c&&e.interpolatePadding(a,i.padding,o),i.around)t.w("Easing around a point is not supported under globe projection."),e.setLocationAtPoint(i.around,i.aroundPoint);else {const t=v>g?Math.min(2,y):Math.max(.5,y),i=Math.pow(t,1-o),a=ci(r,b,x,o*i);e.setCenter(a.wrap());}if(h){const i=t.C.number(g,v,o)+si(0,e.center.lat);e.setZoom(i);}},isZooming:h,elevationCenter:m}}handleFlyTo(e,i){const o=void 0!==i.zoom,r=e.center,a=e.zoom,s=e.padding,n=!e.isPaddingEqual(i.padding),l=e.getConstrained(t.S.convert(i.center||i.locationAtOffset),a).center,c=o?+i.zoom:e.zoom+si(e.center.lat,l.lat),h=e.clone();h.setCenter(l),h.setZoom(c),h.setBearing(i.bearing);const u=new t.P(t.ah(e.centerPoint.x+i.offsetAsPoint.x,0,e.width),t.ah(e.centerPoint.y+i.offsetAsPoint.y,0,e.height));h.setLocationAtPoint(l,u);const d=h.center;St(e,d);const _=function(e,i,o){const r=ii(i),a=ii(o),s=t.aX(r,a),n=Math.acos(s),l=ei(e);return n/(2*Math.PI)*l}(e,r,d),p=a+si(r.lat,0),m=c+si(d.lat,0),f=t.af(m-p);let g;if("number"==typeof i.minZoom){const o=+i.minZoom+si(d.lat,0),r=Math.min(o,p,m)+si(0,d.lat),a=e.getConstrained(d,r).zoom+si(d.lat,0);g=t.af(a-p);}const v=t.bA(r.lng,d.lng),b=t.bA(r.lat,d.lat);return {easeFunc:(o,a,l,h)=>{const u=ci(r,v,b,l);n&&e.interpolatePadding(s,i.padding,o);const _=1===o?d:u;e.setCenter(_.wrap());const m=p+t.ak(a);e.setZoom(1===o?c:m+si(0,_.lat));},scaleOfZoom:f,targetCenter:d,scaleOfMinZoom:g,pixelPathLength:_}}static solveVectorScale(e,t,i,o,r){const a="x"===o?[i[0],i[4],i[8],i[12]]:[i[1],i[5],i[9],i[13]],s=[i[3],i[7],i[11],i[15]],n=e[0]*a[0]+e[1]*a[1]+e[2]*a[2],l=e[0]*s[0]+e[1]*s[1]+e[2]*s[2],c=t[0]*a[0]+t[1]*a[1]+t[2]*a[2],h=t[0]*s[0]+t[1]*s[1]+t[2]*s[2];return c+r*l===n+r*h||s[3]*(n-c)+a[3]*(h-l)+n*h==c*l?null:(c+a[3]-r*h-r*s[3])/(c-n-r*h+r*l)}static getLesserNonNegativeNonNull(e,t){return null!==t&&t>=0&&tt.y(e,i&&i.filter((e=>"source.canvas"!==e.identifier))),yi=t.bE();class wi extends t.E{constructor(e,i={}){var o,r;super(),this._rtlPluginLoaded=()=>{for(const e in this.sourceCaches){const t=this.sourceCaches[e].getSource().type;"vector"!==t&&"geojson"!==t||this.sourceCaches[e].reload();}},this.map=e,this.dispatcher=new F(k(),e._getMapId()),this.dispatcher.registerMessageHandler("GG",((e,t)=>this.getGlyphs(e,t))),this.dispatcher.registerMessageHandler("GI",((e,t)=>this.getImages(e,t))),this.imageManager=new b,this.imageManager.setEventedParent(this);const a=(null===(o=e._container)||void 0===o?void 0:o.lang)||"undefined"!=typeof document&&(null===(r=document.documentElement)||void 0===r?void 0:r.lang)||void 0;this.glyphManager=new T(e._requestManager,i.localIdeographFontFamily,a),this.lineAtlas=new E(256,512),this.crossTileSymbolIndex=new vt,this._spritesImagesIds={},this._layers={},this._order=[],this.sourceCaches={},this.zoomHistory=new t.bF,this._loaded=!1,this._availableImages=[],this._globalState={},this._resetUpdates(),this.dispatcher.broadcast("SR",t.bG()),oe().on(ee,this._rtlPluginLoaded),this.on("data",(e=>{if("source"!==e.dataType||"metadata"!==e.sourceDataType)return;const t=this.sourceCaches[e.sourceId];if(!t)return;const i=t.getSource();if(i&&i.vectorLayerIds)for(const e in this._layers){const t=this._layers[e];t.source===i.id&&this._validateLayer(t);}}));}setGlobalStateProperty(e,i){var o,r,a;this._checkLoaded();const s=null===i?null!==(a=null===(r=null===(o=this.stylesheet.state)||void 0===o?void 0:o[e])||void 0===r?void 0:r.default)&&void 0!==a?a:null:i;if(t.bH(s,this._globalState[e]))return this;this._globalState[e]=s,this._applyGlobalStateChanges([e]);}getGlobalState(){return this._globalState}setGlobalState(e){this._checkLoaded();const i=[];for(const o in e)!t.bH(this._globalState[o],e[o].default)&&(i.push(o),this._globalState[o]=e[o].default);this._applyGlobalStateChanges(i);}_applyGlobalStateChanges(e){if(0===e.length)return;const t=new Set;for(const i in this._layers){const o=this._layers[i],r=o.getLayoutAffectingGlobalStateRefs(),a=o.getPaintAffectingGlobalStateRefs();for(const i of e)if(r.has(i)&&t.add(o.source),a.has(i))for(const{name:e,value:t}of a.get(i))this._updatePaintProperty(o,e,t);}for(const e in this.sourceCaches)t.has(e)&&(this._reloadSource(e),this._changed=!0);}loadURL(e,i={},o){this.fire(new t.l("dataloading",{dataType:"style"})),i.validate="boolean"!=typeof i.validate||i.validate;const r=this.map._requestManager.transformRequest(e,"Style");this._loadStyleRequest=new AbortController;const a=this._loadStyleRequest;t.j(r,this._loadStyleRequest).then((e=>{this._loadStyleRequest=null,this._load(e.data,i,o);})).catch((e=>{this._loadStyleRequest=null,e&&!a.signal.aborted&&this.fire(new t.k(e));}));}loadJSON(e,i={},o){this.fire(new t.l("dataloading",{dataType:"style"})),this._frameRequest=new AbortController,s.frameAsync(this._frameRequest).then((()=>{this._frameRequest=null,i.validate=!1!==i.validate,this._load(e,i,o);})).catch((()=>{}));}loadEmpty(){this.fire(new t.l("dataloading",{dataType:"style"})),this._load(yi,{validate:!1});}_load(e,i,o){var r,a,s;let n=i.transformStyle?i.transformStyle(o,e):e;if(!i.validate||!xi(this,t.z(n))){n=Object.assign({},n),this._loaded=!0,this.stylesheet=n;for(const e in n.sources)this.addSource(e,n.sources[e],{validate:!1});n.sprite?this._loadSprite(n.sprite):this.imageManager.setLoaded(!0),this.glyphManager.setURL(n.glyphs),this._createLayers(),this.light=new I(this.stylesheet.light),this._setProjectionInternal((null===(r=this.stylesheet.projection)||void 0===r?void 0:r.type)||"mercator"),this.sky=new S(this.stylesheet.sky),this.map.setTerrain(null!==(a=this.stylesheet.terrain)&&void 0!==a?a:null),this.setGlobalState(null!==(s=this.stylesheet.state)&&void 0!==s?s:null),this.fire(new t.l("data",{dataType:"style"})),this.fire(new t.l("style.load"));}}_createLayers(){const e=t.bI(this.stylesheet.layers);this.dispatcher.broadcast("SL",e),this._order=e.map((e=>e.id)),this._layers={},this._serializedLayers=null;for(const i of e){const e=t.bJ(i);e.setEventedParent(this,{layer:{id:i.id}}),this._layers[i.id]=e;}}_loadSprite(e,i=!1,o=void 0){let r;this.imageManager.setLoaded(!1),this._spriteRequest=new AbortController,function(e,i,o,r){return t._(this,void 0,void 0,(function*(){const a=f(e),n=o>1?"@2x":"",l={},c={};for(const{id:e,url:o}of a){const a=i.transformRequest(g(o,n,".json"),"SpriteJSON");l[e]=t.j(a,r);const s=i.transformRequest(g(o,n,".png"),"SpriteImage");c[e]=p.getImage(s,r);}return yield Promise.all([...Object.values(l),...Object.values(c)]),function(e,i){return t._(this,void 0,void 0,(function*(){const t={};for(const o in e){t[o]={};const r=s.getImageCanvasContext((yield i[o]).data),a=(yield e[o]).data;for(const e in a){const{width:i,height:s,x:n,y:l,sdf:c,pixelRatio:h,stretchX:u,stretchY:d,content:_,textFitWidth:p,textFitHeight:m}=a[e];t[o][e]={data:null,pixelRatio:h,sdf:c,stretchX:u,stretchY:d,content:_,textFitWidth:p,textFitHeight:m,spriteData:{width:i,height:s,x:n,y:l,context:r}};}}return t}))}(l,c)}))}(e,this.map._requestManager,this.map.getPixelRatio(),this._spriteRequest).then((e=>{if(this._spriteRequest=null,e)for(const t in e){this._spritesImagesIds[t]=[];const o=this._spritesImagesIds[t]?this._spritesImagesIds[t].filter((t=>!(t in e))):[];for(const e of o)this.imageManager.removeImage(e),this._changedImages[e]=!0;for(const o in e[t]){const r="default"===t?o:`${t}:${o}`;this._spritesImagesIds[t].push(r),r in this.imageManager.images?this.imageManager.updateImage(r,e[t][o],!1):this.imageManager.addImage(r,e[t][o]),i&&(this._changedImages[r]=!0);}}})).catch((e=>{this._spriteRequest=null,r=e,this.fire(new t.k(r));})).finally((()=>{this.imageManager.setLoaded(!0),this._availableImages=this.imageManager.listImages(),i&&(this._changed=!0),this.dispatcher.broadcast("SI",this._availableImages),this.fire(new t.l("data",{dataType:"style"})),o&&o(r);}));}_unloadSprite(){for(const e of Object.values(this._spritesImagesIds).flat())this.imageManager.removeImage(e),this._changedImages[e]=!0;this._spritesImagesIds={},this._availableImages=this.imageManager.listImages(),this._changed=!0,this.dispatcher.broadcast("SI",this._availableImages),this.fire(new t.l("data",{dataType:"style"}));}_validateLayer(e){const i=this.sourceCaches[e.source];if(!i)return;const o=e.sourceLayer;if(!o)return;const r=i.getSource();("geojson"===r.type||r.vectorLayerIds&&-1===r.vectorLayerIds.indexOf(o))&&this.fire(new t.k(new Error(`Source layer "${o}" does not exist on source "${r.id}" as specified by style layer "${e.id}".`)));}loaded(){if(!this._loaded)return !1;if(Object.keys(this._updatedSources).length)return !1;for(const e in this.sourceCaches)if(!this.sourceCaches[e].loaded())return !1;return !!this.imageManager.isLoaded()}_serializeByIds(e,i=!1){const o=this._serializedAllLayers();if(!e||0===e.length)return Object.values(i?t.bK(o):o);const r=[];for(const a of e)if(o[a]){const e=i?t.bK(o[a]):o[a];r.push(e);}return r}_serializedAllLayers(){let e=this._serializedLayers;if(e)return e;e=this._serializedLayers={};const t=Object.keys(this._layers);for(const i of t){const t=this._layers[i];"custom"!==t.type&&(e[i]=t.serialize());}return e}hasTransitions(){var e,t,i;if(null===(e=this.light)||void 0===e?void 0:e.hasTransition())return !0;if(null===(t=this.sky)||void 0===t?void 0:t.hasTransition())return !0;if(null===(i=this.projection)||void 0===i?void 0:i.hasTransition())return !0;for(const e in this.sourceCaches)if(this.sourceCaches[e].hasTransition())return !0;for(const e in this._layers)if(this._layers[e].hasTransition())return !0;return !1}_checkLoaded(){if(!this._loaded)throw new Error("Style is not done loading.")}update(e){if(!this._loaded)return;const i=this._changed;if(i){const t=Object.keys(this._updatedLayers),i=Object.keys(this._removedLayers);(t.length||i.length)&&this._updateWorkerLayers(t,i);for(const e in this._updatedSources){const t=this._updatedSources[e];if("reload"===t)this._reloadSource(e);else {if("clear"!==t)throw new Error(`Invalid action ${t}`);this._clearSource(e);}}this._updateTilesForChangedImages(),this._updateTilesForChangedGlyphs();for(const t in this._updatedPaintProps)this._layers[t].updateTransitions(e);this.light.updateTransitions(e),this.sky.updateTransitions(e),this._resetUpdates();}const o={};for(const e in this.sourceCaches){const t=this.sourceCaches[e];o[e]=t.used,t.used=!1;}for(const t of this._order){const i=this._layers[t];i.recalculate(e,this._availableImages),!i.isHidden(e.zoom)&&i.source&&(this.sourceCaches[i.source].used=!0);}for(const e in o){const i=this.sourceCaches[e];!!o[e]!=!!i.used&&i.fire(new t.l("data",{sourceDataType:"visibility",dataType:"source",sourceId:e}));}this.light.recalculate(e),this.sky.recalculate(e),this.projection.recalculate(e),this.z=e.zoom,i&&this.fire(new t.l("data",{dataType:"style"}));}_updateTilesForChangedImages(){const e=Object.keys(this._changedImages);if(e.length){for(const t in this.sourceCaches)this.sourceCaches[t].reloadTilesForDependencies(["icons","patterns"],e);this._changedImages={};}}_updateTilesForChangedGlyphs(){if(this._glyphsDidChange){for(const e in this.sourceCaches)this.sourceCaches[e].reloadTilesForDependencies(["glyphs"],[""]);this._glyphsDidChange=!1;}}_updateWorkerLayers(e,t){this.dispatcher.broadcast("UL",{layers:this._serializeByIds(e,!1),removedIds:t});}_resetUpdates(){this._changed=!1,this._updatedLayers={},this._removedLayers={},this._updatedSources={},this._updatedPaintProps={},this._changedImages={},this._glyphsDidChange=!1;}setState(e,i={}){var o;this._checkLoaded();const r=this.serialize();if(e=i.transformStyle?i.transformStyle(r,e):e,(null===(o=i.validate)||void 0===o||o)&&xi(this,t.z(e)))return !1;(e=t.bK(e)).layers=t.bI(e.layers);const a=t.bL(r,e),s=this._getOperationsToPerform(a);if(s.unimplemented.length>0)throw new Error(`Unimplemented: ${s.unimplemented.join(", ")}.`);if(0===s.operations.length)return !1;for(const e of s.operations)e();return this.stylesheet=e,this._serializedLayers=null,!0}_getOperationsToPerform(e){const t=[],i=[];for(const o of e)switch(o.command){case "setCenter":case "setZoom":case "setBearing":case "setPitch":case "setRoll":continue;case "addLayer":t.push((()=>this.addLayer.apply(this,o.args)));break;case "removeLayer":t.push((()=>this.removeLayer.apply(this,o.args)));break;case "setPaintProperty":t.push((()=>this.setPaintProperty.apply(this,o.args)));break;case "setLayoutProperty":t.push((()=>this.setLayoutProperty.apply(this,o.args)));break;case "setFilter":t.push((()=>this.setFilter.apply(this,o.args)));break;case "addSource":t.push((()=>this.addSource.apply(this,o.args)));break;case "removeSource":t.push((()=>this.removeSource.apply(this,o.args)));break;case "setLayerZoomRange":t.push((()=>this.setLayerZoomRange.apply(this,o.args)));break;case "setLight":t.push((()=>this.setLight.apply(this,o.args)));break;case "setGeoJSONSourceData":t.push((()=>this.setGeoJSONSourceData.apply(this,o.args)));break;case "setGlyphs":t.push((()=>this.setGlyphs.apply(this,o.args)));break;case "setSprite":t.push((()=>this.setSprite.apply(this,o.args)));break;case "setTerrain":t.push((()=>this.map.setTerrain.apply(this,o.args)));break;case "setSky":t.push((()=>this.setSky.apply(this,o.args)));break;case "setProjection":this.setProjection.apply(this,o.args);break;case "setGlobalState":t.push((()=>this.setGlobalState.apply(this,o.args)));break;case "setTransition":t.push((()=>{}));break;default:i.push(o.command);}return {operations:t,unimplemented:i}}addImage(e,i){if(this.getImage(e))return this.fire(new t.k(new Error(`An image named "${e}" already exists.`)));this.imageManager.addImage(e,i),this._afterImageUpdated(e);}updateImage(e,t){this.imageManager.updateImage(e,t);}getImage(e){return this.imageManager.getImage(e)}removeImage(e){if(!this.getImage(e))return this.fire(new t.k(new Error(`An image named "${e}" does not exist.`)));this.imageManager.removeImage(e),this._afterImageUpdated(e);}_afterImageUpdated(e){this._availableImages=this.imageManager.listImages(),this._changedImages[e]=!0,this._changed=!0,this.dispatcher.broadcast("SI",this._availableImages),this.fire(new t.l("data",{dataType:"style"}));}listImages(){return this._checkLoaded(),this.imageManager.listImages()}addSource(e,i,o={}){if(this._checkLoaded(),void 0!==this.sourceCaches[e])throw new Error(`Source "${e}" already exists.`);if(!i.type)throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(i).join(", ")}.`);if(["vector","raster","geojson","video","image"].indexOf(i.type)>=0&&this._validate(t.z.source,`sources.${e}`,i,null,o))return;this.map&&this.map._collectResourceTiming&&(i.collectResourceTiming=!0);const r=this.sourceCaches[e]=new xe(e,i,this.dispatcher);r.style=this,r.setEventedParent(this,(()=>({isSourceLoaded:r.loaded(),source:r.serialize(),sourceId:e}))),r.onAdd(this.map),this._changed=!0;}removeSource(e){if(this._checkLoaded(),void 0===this.sourceCaches[e])throw new Error("There is no source with this ID");for(const i in this._layers)if(this._layers[i].source===e)return this.fire(new t.k(new Error(`Source "${e}" cannot be removed while layer "${i}" is using it.`)));const i=this.sourceCaches[e];delete this.sourceCaches[e],delete this._updatedSources[e],i.fire(new t.l("data",{sourceDataType:"metadata",dataType:"source",sourceId:e})),i.setEventedParent(null),i.onRemove(this.map),this._changed=!0;}setGeoJSONSourceData(e,t){if(this._checkLoaded(),void 0===this.sourceCaches[e])throw new Error(`There is no source with this ID=${e}`);const i=this.sourceCaches[e].getSource();if("geojson"!==i.type)throw new Error(`geojsonSource.type is ${i.type}, which is !== 'geojson`);i.setData(t),this._changed=!0;}getSource(e){return this.sourceCaches[e]&&this.sourceCaches[e].getSource()}addLayer(e,i,o={}){this._checkLoaded();const r=e.id;if(this.getLayer(r))return void this.fire(new t.k(new Error(`Layer "${r}" already exists on this map.`)));let a;if("custom"===e.type){if(xi(this,t.bM(e)))return;a=t.bJ(e);}else {if("source"in e&&"object"==typeof e.source&&(this.addSource(r,e.source),e=t.bK(e),e=t.e(e,{source:r})),this._validate(t.z.layer,`layers.${r}`,e,{arrayIndex:-1},o))return;a=t.bJ(e),this._validateLayer(a),a.setEventedParent(this,{layer:{id:r}});}const s=i?this._order.indexOf(i):this._order.length;if(i&&-1===s)this.fire(new t.k(new Error(`Cannot add layer "${r}" before non-existing layer "${i}".`)));else {if(this._order.splice(s,0,r),this._layerOrderChanged=!0,this._layers[r]=a,this._removedLayers[r]&&a.source&&"custom"!==a.type){const e=this._removedLayers[r];delete this._removedLayers[r],e.type!==a.type?this._updatedSources[a.source]="clear":(this._updatedSources[a.source]="reload",this.sourceCaches[a.source].pause());}this._updateLayer(a),a.onAdd&&a.onAdd(this.map);}}moveLayer(e,i){if(this._checkLoaded(),this._changed=!0,!this._layers[e])return void this.fire(new t.k(new Error(`The layer '${e}' does not exist in the map's style and cannot be moved.`)));if(e===i)return;const o=this._order.indexOf(e);this._order.splice(o,1);const r=i?this._order.indexOf(i):this._order.length;i&&-1===r?this.fire(new t.k(new Error(`Cannot move layer "${e}" before non-existing layer "${i}".`))):(this._order.splice(r,0,e),this._layerOrderChanged=!0);}removeLayer(e){this._checkLoaded();const i=this._layers[e];if(!i)return void this.fire(new t.k(new Error(`Cannot remove non-existing layer "${e}".`)));i.setEventedParent(null);const o=this._order.indexOf(e);this._order.splice(o,1),this._layerOrderChanged=!0,this._changed=!0,this._removedLayers[e]=i,delete this._layers[e],this._serializedLayers&&delete this._serializedLayers[e],delete this._updatedLayers[e],delete this._updatedPaintProps[e],i.onRemove&&i.onRemove(this.map);}getLayer(e){return this._layers[e]}getLayersOrder(){return [...this._order]}hasLayer(e){return e in this._layers}setLayerZoomRange(e,i,o){this._checkLoaded();const r=this.getLayer(e);r?r.minzoom===i&&r.maxzoom===o||(null!=i&&(r.minzoom=i),null!=o&&(r.maxzoom=o),this._updateLayer(r)):this.fire(new t.k(new Error(`Cannot set the zoom range of non-existing layer "${e}".`)));}setFilter(e,i,o={}){this._checkLoaded();const r=this.getLayer(e);if(r){if(!t.bH(r.filter,i))return null==i?(r.setFilter(void 0),void this._updateLayer(r)):void(this._validate(t.z.filter,`layers.${r.id}.filter`,i,null,o)||(r.setFilter(t.bK(i)),this._updateLayer(r)))}else this.fire(new t.k(new Error(`Cannot filter non-existing layer "${e}".`)));}getFilter(e){return t.bK(this.getLayer(e).filter)}setLayoutProperty(e,i,o,r={}){this._checkLoaded();const a=this.getLayer(e);a?t.bH(a.getLayoutProperty(i),o)||(a.setLayoutProperty(i,o,r),this._updateLayer(a)):this.fire(new t.k(new Error(`Cannot style non-existing layer "${e}".`)));}getLayoutProperty(e,i){const o=this.getLayer(e);if(o)return o.getLayoutProperty(i);this.fire(new t.k(new Error(`Cannot get style of non-existing layer "${e}".`)));}setPaintProperty(e,i,o,r={}){this._checkLoaded();const a=this.getLayer(e);a?t.bH(a.getPaintProperty(i),o)||this._updatePaintProperty(a,i,o,r):this.fire(new t.k(new Error(`Cannot style non-existing layer "${e}".`)));}_updatePaintProperty(e,t,i,o={}){e.setPaintProperty(t,i,o)&&this._updateLayer(e),this._changed=!0,this._updatedPaintProps[e.id]=!0,this._serializedLayers=null;}getPaintProperty(e,t){return this.getLayer(e).getPaintProperty(t)}setFeatureState(e,i){this._checkLoaded();const o=e.source,r=e.sourceLayer,a=this.sourceCaches[o];if(void 0===a)return void this.fire(new t.k(new Error(`The source '${o}' does not exist in the map's style.`)));const s=a.getSource().type;"geojson"===s&&r?this.fire(new t.k(new Error("GeoJSON sources cannot have a sourceLayer parameter."))):"vector"!==s||r?(void 0===e.id&&this.fire(new t.k(new Error("The feature id parameter must be provided."))),a.setFeatureState(r,e.id,i)):this.fire(new t.k(new Error("The sourceLayer parameter must be provided for vector source types.")));}removeFeatureState(e,i){this._checkLoaded();const o=e.source,r=this.sourceCaches[o];if(void 0===r)return void this.fire(new t.k(new Error(`The source '${o}' does not exist in the map's style.`)));const a=r.getSource().type,s="vector"===a?e.sourceLayer:void 0;"vector"!==a||s?i&&"string"!=typeof e.id&&"number"!=typeof e.id?this.fire(new t.k(new Error("A feature id is required to remove its specific state property."))):r.removeFeatureState(s,e.id,i):this.fire(new t.k(new Error("The sourceLayer parameter must be provided for vector source types.")));}getFeatureState(e){this._checkLoaded();const i=e.source,o=e.sourceLayer,r=this.sourceCaches[i];if(void 0!==r)return "vector"!==r.getSource().type||o?(void 0===e.id&&this.fire(new t.k(new Error("The feature id parameter must be provided."))),r.getFeatureState(o,e.id)):void this.fire(new t.k(new Error("The sourceLayer parameter must be provided for vector source types.")));this.fire(new t.k(new Error(`The source '${i}' does not exist in the map's style.`)));}getTransition(){return t.e({duration:300,delay:0},this.stylesheet&&this.stylesheet.transition)}serialize(){if(!this._loaded)return;const e=t.bN(this.sourceCaches,(e=>e.serialize())),i=this._serializeByIds(this._order,!0),o=this.map.getTerrain()||void 0,r=this.stylesheet;return t.bO({version:r.version,name:r.name,metadata:r.metadata,light:r.light,sky:r.sky,center:r.center,zoom:r.zoom,bearing:r.bearing,pitch:r.pitch,sprite:r.sprite,glyphs:r.glyphs,transition:r.transition,projection:r.projection,sources:e,layers:i,terrain:o},(e=>void 0!==e))}_updateLayer(e){this._updatedLayers[e.id]=!0,e.source&&!this._updatedSources[e.source]&&"raster"!==this.sourceCaches[e.source].getSource().type&&(this._updatedSources[e.source]="reload",this.sourceCaches[e.source].pause()),this._serializedLayers=null,this._changed=!0;}_flattenAndSortRenderedFeatures(e){const t=e=>"fill-extrusion"===this._layers[e].type,i={},o=[];for(let r=this._order.length-1;r>=0;r--){const a=this._order[r];if(t(a)){i[a]=r;for(const t of e){const e=t[a];if(e)for(const t of e)o.push(t);}}}o.sort(((e,t)=>t.intersectionZ-e.intersectionZ));const r=[];for(let a=this._order.length-1;a>=0;a--){const s=this._order[a];if(t(s))for(let e=o.length-1;e>=0;e--){const t=o[e].feature;if(i[t.layer.id]this.map.terrain.getElevation(e,t,i):void 0));return this.placement&&a.push(function(e,t,i,o,r,a,s){const n={},l=a.queryRenderedSymbols(o),c=[];for(const e of Object.keys(l).map(Number))c.push(s[e]);c.sort(N);for(const i of c){const o=i.featureIndex.lookupSymbolFeatures(l[i.bucketInstanceId],t,i.bucketIndex,i.sourceLayerIndex,r.filter,r.layers,r.availableImages,e);for(const e in o){const t=n[e]=n[e]||[],r=o[e];r.sort(((e,t)=>{const o=i.featureSortOrder;if(o){const i=o.indexOf(e.featureIndex);return o.indexOf(t.featureIndex)-i}return t.featureIndex-e.featureIndex}));for(const e of r)t.push(e);}}return function(e,t,i){for(const o in e)for(const r of e[o])U(r,i[t[o].source]);return e}(n,e,i)}(this._layers,s,this.sourceCaches,e,l,this.placement.collisionIndex,this.placement.retainedQueryData)),this._flattenAndSortRenderedFeatures(a)}querySourceFeatures(e,i){i&&i.filter&&this._validate(t.z.filter,"querySourceFeatures.filter",i.filter,null,i);const o=this.sourceCaches[e];return o?function(e,t){const i=e.getRenderableIds().map((t=>e.getTileByID(t))),o=[],r={};for(let e=0;ee.getTileByID(t))).sort(((e,t)=>t.tileID.overscaledZ-e.tileID.overscaledZ||(e.tileID.isLessThan(t.tileID)?-1:1)));}const o=this.crossTileSymbolIndex.addLayer(i,l[i.source],e.center.lng);a=a||o;}if(this.crossTileSymbolIndex.pruneUnusedLayers(this._order),((r=r||this._layerOrderChanged||0===i)||!this.pauseablePlacement||this.pauseablePlacement.isDone()&&!this.placement.stillRecent(s.now(),e.zoom))&&(this.pauseablePlacement=new _t(e,this.map.terrain,this._order,r,t,i,o,this.placement),this._layerOrderChanged=!1),this.pauseablePlacement.isDone()?this.placement.setStale():(this.pauseablePlacement.continuePlacement(this._order,this._layers,l),this.pauseablePlacement.isDone()&&(this.placement=this.pauseablePlacement.commit(s.now()),n=!0),a&&this.pauseablePlacement.placement.setStale()),n||a)for(const e of this._order){const t=this._layers[e];"symbol"===t.type&&this.placement.updateLayerOpacities(t,l[t.source]);}return !this.pauseablePlacement.isDone()||this.placement.hasTransitions(s.now())}_releaseSymbolFadeTiles(){for(const e in this.sourceCaches)this.sourceCaches[e].releaseSymbolFadeTiles();}getImages(e,i){return t._(this,void 0,void 0,(function*(){const e=yield this.imageManager.getImages(i.icons);this._updateTilesForChangedImages();const t=this.sourceCaches[i.source];return t&&t.setDependencies(i.tileID.key,i.type,i.icons),e}))}getGlyphs(e,i){return t._(this,void 0,void 0,(function*(){const e=yield this.glyphManager.getGlyphs(i.stacks),t=this.sourceCaches[i.source];return t&&t.setDependencies(i.tileID.key,i.type,[""]),e}))}getGlyphsUrl(){return this.stylesheet.glyphs||null}setGlyphs(e,i={}){this._checkLoaded(),e&&this._validate(t.z.glyphs,"glyphs",e,null,i)||(this._glyphsDidChange=!0,this.stylesheet.glyphs=e,this.glyphManager.entries={},this.glyphManager.setURL(e));}addSprite(e,i,o={},r){this._checkLoaded();const a=[{id:e,url:i}],s=[...f(this.stylesheet.sprite),...a];this._validate(t.z.sprite,"sprite",s,null,o)||(this.stylesheet.sprite=s,this._loadSprite(a,!0,r));}removeSprite(e){this._checkLoaded();const i=f(this.stylesheet.sprite);if(i.find((t=>t.id===e))){if(this._spritesImagesIds[e])for(const t of this._spritesImagesIds[e])this.imageManager.removeImage(t),this._changedImages[t]=!0;i.splice(i.findIndex((t=>t.id===e)),1),this.stylesheet.sprite=i.length>0?i:void 0,delete this._spritesImagesIds[e],this._availableImages=this.imageManager.listImages(),this._changed=!0,this.dispatcher.broadcast("SI",this._availableImages),this.fire(new t.l("data",{dataType:"style"}));}else this.fire(new t.k(new Error(`Sprite "${e}" doesn't exists on this map.`)));}getSprite(){return f(this.stylesheet.sprite)}setSprite(e,i={},o){this._checkLoaded(),e&&this._validate(t.z.sprite,"sprite",e,null,i)||(this.stylesheet.sprite=e,e?this._loadSprite(e,!0,o):(this._unloadSprite(),o&&o(null)));}}var Ti=t.aJ([{name:"a_pos",type:"Int16",components:2},{name:"a_texture_pos",type:"Int16",components:2}]);class Pi{constructor(){this.boundProgram=null,this.boundLayoutVertexBuffer=null,this.boundPaintVertexBuffers=[],this.boundIndexBuffer=null,this.boundVertexOffset=null,this.boundDynamicVertexBuffer=null,this.vao=null;}bind(e,t,i,o,r,a,s,n,l){this.context=e;let c=this.boundPaintVertexBuffers.length!==o.length;for(let e=0;!c&&e({u_texture:0,u_ele_delta:e,u_fog_matrix:i,u_fog_color:o?o.properties.get("fog-color"):t.bf.white,u_fog_ground_blend:o?o.properties.get("fog-ground-blend"):1,u_fog_ground_blend_opacity:a?0:o?o.calculateFogBlendOpacity(r):0,u_horizon_color:o?o.properties.get("horizon-color"):t.bf.white,u_horizon_fog_blend:o?o.properties.get("horizon-fog-blend"):1,u_is_globe_mode:a?1:0}),Ii={mainMatrix:"u_projection_matrix",tileMercatorCoords:"u_projection_tile_mercator_coords",clippingPlane:"u_projection_clipping_plane",projectionTransition:"u_projection_transition",fallbackMatrix:"u_projection_fallback_matrix"};function Mi(e){const t=[];for(let i=0;i({u_depth:new t.bP(e,i.u_depth),u_terrain:new t.bP(e,i.u_terrain),u_terrain_dim:new t.bg(e,i.u_terrain_dim),u_terrain_matrix:new t.bR(e,i.u_terrain_matrix),u_terrain_unpack:new t.bS(e,i.u_terrain_unpack),u_terrain_exaggeration:new t.bg(e,i.u_terrain_exaggeration)}))(e,C),this.projectionUniforms=((e,i)=>({u_projection_matrix:new t.bR(e,i.u_projection_matrix),u_projection_tile_mercator_coords:new t.bS(e,i.u_projection_tile_mercator_coords),u_projection_clipping_plane:new t.bS(e,i.u_projection_clipping_plane),u_projection_transition:new t.bg(e,i.u_projection_transition),u_projection_fallback_matrix:new t.bR(e,i.u_projection_fallback_matrix)}))(e,C),this.binderUniforms=o?o.getUniforms(e,C):[];}draw(e,t,i,o,r,a,s,n,l,c,h,u,d,_,p,m,f,g,v){const b=e.gl;if(this.failedToCreate)return;if(e.program.set(this.program),e.setDepthMode(i),e.setStencilMode(o),e.setColorMode(r),e.setCullFace(a),n){e.activeTexture.set(b.TEXTURE2),b.bindTexture(b.TEXTURE_2D,n.depthTexture),e.activeTexture.set(b.TEXTURE3),b.bindTexture(b.TEXTURE_2D,n.texture);for(const e in this.terrainUniforms)this.terrainUniforms[e].set(n[e]);}if(l)for(const e in l)this.projectionUniforms[Ii[e]].set(l[e]);if(s)for(const e in this.fixedUniforms)this.fixedUniforms[e].set(s[e]);m&&m.setUniforms(e,this.binderUniforms,_,{zoom:p});let x=0;switch(t){case b.LINES:x=2;break;case b.TRIANGLES:x=3;break;case b.LINE_STRIP:x=1;}for(const i of d.get()){const o=i.vaos||(i.vaos={});(o[c]||(o[c]=new Pi)).bind(e,this,h,m?m.getPaintVertexBuffers():[],u,i.vertexOffset,f,g,v),b.drawElements(t,i.primitiveLength*x,b.UNSIGNED_SHORT,i.primitiveOffset*x*2);}}}function Ei(e,i,o){const r=1/t.aC(o,1,i.transform.tileZoom),a=Math.pow(2,o.tileID.overscaledZ),s=o.tileSize*Math.pow(2,i.transform.tileZoom)/a,n=s*(o.tileID.canonical.x+o.tileID.wrap*a),l=s*o.tileID.canonical.y;return {u_image:0,u_texsize:o.imageAtlasTexture.size,u_scale:[r,e.fromScale,e.toScale],u_fade:e.t,u_pixel_coord_upper:[n>>16,l>>16],u_pixel_coord_lower:[65535&n,65535&l]}}const Ri=(e,i,o,r)=>{const a=e.style.light,s=a.properties.get("position"),n=[s.x,s.y,s.z],l=t.bV();"viewport"===a.properties.get("anchor")&&t.bW(l,e.transform.bearingInRadians),t.bX(n,n,l);const c=e.transform.transformLightDirection(n),h=a.properties.get("color");return {u_lightpos:n,u_lightpos_globe:c,u_lightintensity:a.properties.get("intensity"),u_lightcolor:[h.r,h.g,h.b],u_vertical_gradient:+i,u_opacity:o,u_fill_translate:r}},zi=(e,i,o,r,a,s,n)=>t.e(Ri(e,i,o,r),Ei(s,e,n),{u_height_factor:-Math.pow(2,a.overscaledZ)/n.tileSize/8}),Di=(e,i,o,r)=>t.e(Ei(i,e,o),{u_fill_translate:r}),Ai=(e,t)=>({u_world:e,u_fill_translate:t}),Li=(e,i,o,r,a)=>t.e(Di(e,i,o,a),{u_world:r}),ki=(e,i,o,r,a)=>{const s=e.transform;let n,l,c=0;if("map"===o.paint.get("circle-pitch-alignment")){const e=t.aC(i,1,s.zoom);n=!0,l=[e,e],c=e/(t.$*Math.pow(2,i.tileID.overscaledZ))*2*Math.PI*a;}else n=!1,l=s.pixelsToGLUnits;return {u_camera_to_center_distance:s.cameraToCenterDistance,u_scale_with_map:+("map"===o.paint.get("circle-pitch-scale")),u_pitch_with_map:+n,u_device_pixel_ratio:e.pixelRatio,u_extrude_scale:l,u_globe_extrude_scale:c,u_translate:r}},Fi=e=>({u_pixel_extrude_scale:[1/e.width,1/e.height]}),Bi=e=>({u_viewport_size:[e.width,e.height]}),Oi=(e,t=1)=>({u_color:e,u_overlay:0,u_overlay_scale:t}),ji=(e,i,o,r)=>{const a=t.aC(e,1,i)/(t.$*Math.pow(2,e.tileID.overscaledZ))*2*Math.PI*r;return {u_extrude_scale:t.aC(e,1,i),u_intensity:o,u_globe_extrude_scale:a}},Ni=(e,i,o,r)=>{const a=t.L();t.bY(a,0,e.width,e.height,0,0,1);const s=e.context.gl;return {u_matrix:a,u_world:[s.drawingBufferWidth,s.drawingBufferHeight],u_image:o,u_color_ramp:r,u_opacity:i.paint.get("heatmap-opacity")}},Ui=(e,t,i)=>{const o=i.paint.get("hillshade-accent-color");let r;switch(i.paint.get("hillshade-method")){case "basic":r=4;break;case "combined":r=1;break;case "igor":r=2;break;case "multidirectional":r=3;break;default:r=0;}const a=i.getIlluminationProperties();for(let t=0;t{const o=i.stride,r=t.L();return t.bY(r,0,t.$,-t.$,0,0,1),t.M(r,r,[0,-t.$,0]),{u_matrix:r,u_image:1,u_dimension:[o,o],u_zoom:e.overscaledZ,u_unpack:i.getUnpackVector()}};function Gi(e,i){const o=Math.pow(2,i.canonical.z),r=i.canonical.y;return [new t.a1(0,r/o).toLngLat().lat,new t.a1(0,(r+1)/o).toLngLat().lat]}const Vi=(e,t,i=0)=>({u_image:0,u_unpack:t.getUnpackVector(),u_dimension:[t.stride,t.stride],u_elevation_stops:1,u_color_stops:4,u_color_ramp_size:i,u_opacity:e.paint.get("color-relief-opacity")}),$i=(e,i,o,r)=>{const a=e.transform;return {u_translation:Ki(e,i,o),u_ratio:r/t.aC(i,1,a.zoom),u_device_pixel_ratio:e.pixelRatio,u_units_to_pixels:[1/a.pixelsToGLUnits[0],1/a.pixelsToGLUnits[1]]}},qi=(e,i,o,r,a)=>t.e($i(e,i,o,r),{u_image:0,u_image_height:a}),Wi=(e,i,o,r,a)=>{const s=e.transform,n=Xi(i,s);return {u_translation:Ki(e,i,o),u_texsize:i.imageAtlasTexture.size,u_ratio:r/t.aC(i,1,s.zoom),u_device_pixel_ratio:e.pixelRatio,u_image:0,u_scale:[n,a.fromScale,a.toScale],u_fade:a.t,u_units_to_pixels:[1/s.pixelsToGLUnits[0],1/s.pixelsToGLUnits[1]]}},Hi=(e,i,o,r,a,s)=>{const n=e.lineAtlas,l=Xi(i,e.transform),c="round"===o.layout.get("line-cap"),h=n.getDash(a.from,c),u=n.getDash(a.to,c),d=h.width*s.fromScale,_=u.width*s.toScale;return t.e($i(e,i,o,r),{u_patternscale_a:[l/d,-h.height/2],u_patternscale_b:[l/_,-u.height/2],u_sdfgamma:n.width/(256*Math.min(d,_)*e.pixelRatio)/2,u_image:0,u_tex_y_a:h.y,u_tex_y_b:u.y,u_mix:s.t})};function Xi(e,i){return 1/t.aC(e,1,i.tileZoom)}function Ki(e,i,o){return t.aD(e.transform,i,o.paint.get("line-translate"),o.paint.get("line-translate-anchor"))}const Yi=(e,t,i,o,r)=>{return {u_tl_parent:e,u_scale_parent:t,u_buffer_scale:1,u_fade_t:i.mix,u_opacity:i.opacity*o.paint.get("raster-opacity"),u_image0:0,u_image1:1,u_brightness_low:o.paint.get("raster-brightness-min"),u_brightness_high:o.paint.get("raster-brightness-max"),u_saturation_factor:(s=o.paint.get("raster-saturation"),s>0?1-1/(1.001-s):-s),u_contrast_factor:(a=o.paint.get("raster-contrast"),a>0?1/(1-a):1+a),u_spin_weights:Qi(o.paint.get("raster-hue-rotate")),u_coords_top:[r[0].x,r[0].y,r[1].x,r[1].y],u_coords_bottom:[r[3].x,r[3].y,r[2].x,r[2].y]};var a,s;};function Qi(e){e*=Math.PI/180;const t=Math.sin(e),i=Math.cos(e);return [(2*i+1)/3,(-Math.sqrt(3)*t-i+1)/3,(Math.sqrt(3)*t-i+1)/3]}const Ji=(e,t,i,o,r,a,s,n,l,c,h,u,d)=>{const _=s.transform;return {u_is_size_zoom_constant:+("constant"===e||"source"===e),u_is_size_feature_constant:+("constant"===e||"camera"===e),u_size_t:t?t.uSizeT:0,u_size:t?t.uSize:0,u_camera_to_center_distance:_.cameraToCenterDistance,u_pitch:_.pitch/360*2*Math.PI,u_rotate_symbol:+i,u_aspect_ratio:_.width/_.height,u_fade_change:s.options.fadeDuration?s.symbolFadeChange:1,u_label_plane_matrix:n,u_coord_matrix:l,u_is_text:+h,u_pitch_with_map:+o,u_is_along_line:r,u_is_variable_anchor:a,u_texsize:u,u_texture:0,u_translation:c,u_pitched_scale:d}},eo=(e,i,o,r,a,s,n,l,c,h,u,d,_,p)=>{const m=n.transform;return t.e(Ji(e,i,o,r,a,s,n,l,c,h,u,d,p),{u_gamma_scale:r?Math.cos(m.pitch*Math.PI/180)*m.cameraToCenterDistance:1,u_device_pixel_ratio:n.pixelRatio,u_is_halo:1})},to=(e,i,o,r,a,s,n,l,c,h,u,d,_)=>t.e(eo(e,i,o,r,a,s,n,l,c,h,!0,u,0,_),{u_texsize_icon:d,u_texture_icon:1}),io=(e,t)=>({u_opacity:e,u_color:t}),oo=(e,i,o,r,a)=>t.e(function(e,i,o,r){const a=o.imageManager.getPattern(e.from.toString()),s=o.imageManager.getPattern(e.to.toString()),{width:n,height:l}=o.imageManager.getPixelSize(),c=Math.pow(2,r.tileID.overscaledZ),h=r.tileSize*Math.pow(2,o.transform.tileZoom)/c,u=h*(r.tileID.canonical.x+r.tileID.wrap*c),d=h*r.tileID.canonical.y;return {u_image:0,u_pattern_tl_a:a.tl,u_pattern_br_a:a.br,u_pattern_tl_b:s.tl,u_pattern_br_b:s.br,u_texsize:[n,l],u_mix:i.t,u_pattern_size_a:a.displaySize,u_pattern_size_b:s.displaySize,u_scale_a:i.fromScale,u_scale_b:i.toScale,u_tile_units_to_pixels:1/t.aC(r,1,o.transform.tileZoom),u_pixel_coord_upper:[u>>16,d>>16],u_pixel_coord_lower:[65535&u,65535&d]}}(o,a,i,r),{u_opacity:e}),ro=(e,t)=>{},ao={fillExtrusion:(e,i)=>({u_lightpos:new t.bT(e,i.u_lightpos),u_lightpos_globe:new t.bT(e,i.u_lightpos_globe),u_lightintensity:new t.bg(e,i.u_lightintensity),u_lightcolor:new t.bT(e,i.u_lightcolor),u_vertical_gradient:new t.bg(e,i.u_vertical_gradient),u_opacity:new t.bg(e,i.u_opacity),u_fill_translate:new t.bU(e,i.u_fill_translate)}),fillExtrusionPattern:(e,i)=>({u_lightpos:new t.bT(e,i.u_lightpos),u_lightpos_globe:new t.bT(e,i.u_lightpos_globe),u_lightintensity:new t.bg(e,i.u_lightintensity),u_lightcolor:new t.bT(e,i.u_lightcolor),u_vertical_gradient:new t.bg(e,i.u_vertical_gradient),u_height_factor:new t.bg(e,i.u_height_factor),u_opacity:new t.bg(e,i.u_opacity),u_fill_translate:new t.bU(e,i.u_fill_translate),u_image:new t.bP(e,i.u_image),u_texsize:new t.bU(e,i.u_texsize),u_pixel_coord_upper:new t.bU(e,i.u_pixel_coord_upper),u_pixel_coord_lower:new t.bU(e,i.u_pixel_coord_lower),u_scale:new t.bT(e,i.u_scale),u_fade:new t.bg(e,i.u_fade)}),fill:(e,i)=>({u_fill_translate:new t.bU(e,i.u_fill_translate)}),fillPattern:(e,i)=>({u_image:new t.bP(e,i.u_image),u_texsize:new t.bU(e,i.u_texsize),u_pixel_coord_upper:new t.bU(e,i.u_pixel_coord_upper),u_pixel_coord_lower:new t.bU(e,i.u_pixel_coord_lower),u_scale:new t.bT(e,i.u_scale),u_fade:new t.bg(e,i.u_fade),u_fill_translate:new t.bU(e,i.u_fill_translate)}),fillOutline:(e,i)=>({u_world:new t.bU(e,i.u_world),u_fill_translate:new t.bU(e,i.u_fill_translate)}),fillOutlinePattern:(e,i)=>({u_world:new t.bU(e,i.u_world),u_image:new t.bP(e,i.u_image),u_texsize:new t.bU(e,i.u_texsize),u_pixel_coord_upper:new t.bU(e,i.u_pixel_coord_upper),u_pixel_coord_lower:new t.bU(e,i.u_pixel_coord_lower),u_scale:new t.bT(e,i.u_scale),u_fade:new t.bg(e,i.u_fade),u_fill_translate:new t.bU(e,i.u_fill_translate)}),circle:(e,i)=>({u_camera_to_center_distance:new t.bg(e,i.u_camera_to_center_distance),u_scale_with_map:new t.bP(e,i.u_scale_with_map),u_pitch_with_map:new t.bP(e,i.u_pitch_with_map),u_extrude_scale:new t.bU(e,i.u_extrude_scale),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_globe_extrude_scale:new t.bg(e,i.u_globe_extrude_scale),u_translate:new t.bU(e,i.u_translate)}),collisionBox:(e,i)=>({u_pixel_extrude_scale:new t.bU(e,i.u_pixel_extrude_scale)}),collisionCircle:(e,i)=>({u_viewport_size:new t.bU(e,i.u_viewport_size)}),debug:(e,i)=>({u_color:new t.bQ(e,i.u_color),u_overlay:new t.bP(e,i.u_overlay),u_overlay_scale:new t.bg(e,i.u_overlay_scale)}),depth:ro,clippingMask:ro,heatmap:(e,i)=>({u_extrude_scale:new t.bg(e,i.u_extrude_scale),u_intensity:new t.bg(e,i.u_intensity),u_globe_extrude_scale:new t.bg(e,i.u_globe_extrude_scale)}),heatmapTexture:(e,i)=>({u_matrix:new t.bR(e,i.u_matrix),u_world:new t.bU(e,i.u_world),u_image:new t.bP(e,i.u_image),u_color_ramp:new t.bP(e,i.u_color_ramp),u_opacity:new t.bg(e,i.u_opacity)}),hillshade:(e,i)=>({u_image:new t.bP(e,i.u_image),u_latrange:new t.bU(e,i.u_latrange),u_exaggeration:new t.bg(e,i.u_exaggeration),u_altitudes:new t.b_(e,i.u_altitudes),u_azimuths:new t.b_(e,i.u_azimuths),u_accent:new t.bQ(e,i.u_accent),u_method:new t.bP(e,i.u_method),u_shadows:new t.bZ(e,i.u_shadows),u_highlights:new t.bZ(e,i.u_highlights)}),hillshadePrepare:(e,i)=>({u_matrix:new t.bR(e,i.u_matrix),u_image:new t.bP(e,i.u_image),u_dimension:new t.bU(e,i.u_dimension),u_zoom:new t.bg(e,i.u_zoom),u_unpack:new t.bS(e,i.u_unpack)}),colorRelief:(e,i)=>({u_image:new t.bP(e,i.u_image),u_unpack:new t.bS(e,i.u_unpack),u_dimension:new t.bU(e,i.u_dimension),u_elevation_stops:new t.bP(e,i.u_elevation_stops),u_color_stops:new t.bP(e,i.u_color_stops),u_color_ramp_size:new t.bP(e,i.u_color_ramp_size),u_opacity:new t.bg(e,i.u_opacity)}),line:(e,i)=>({u_translation:new t.bU(e,i.u_translation),u_ratio:new t.bg(e,i.u_ratio),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_units_to_pixels:new t.bU(e,i.u_units_to_pixels)}),lineGradient:(e,i)=>({u_translation:new t.bU(e,i.u_translation),u_ratio:new t.bg(e,i.u_ratio),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_units_to_pixels:new t.bU(e,i.u_units_to_pixels),u_image:new t.bP(e,i.u_image),u_image_height:new t.bg(e,i.u_image_height)}),linePattern:(e,i)=>({u_translation:new t.bU(e,i.u_translation),u_texsize:new t.bU(e,i.u_texsize),u_ratio:new t.bg(e,i.u_ratio),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_image:new t.bP(e,i.u_image),u_units_to_pixels:new t.bU(e,i.u_units_to_pixels),u_scale:new t.bT(e,i.u_scale),u_fade:new t.bg(e,i.u_fade)}),lineSDF:(e,i)=>({u_translation:new t.bU(e,i.u_translation),u_ratio:new t.bg(e,i.u_ratio),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_units_to_pixels:new t.bU(e,i.u_units_to_pixels),u_patternscale_a:new t.bU(e,i.u_patternscale_a),u_patternscale_b:new t.bU(e,i.u_patternscale_b),u_sdfgamma:new t.bg(e,i.u_sdfgamma),u_image:new t.bP(e,i.u_image),u_tex_y_a:new t.bg(e,i.u_tex_y_a),u_tex_y_b:new t.bg(e,i.u_tex_y_b),u_mix:new t.bg(e,i.u_mix)}),raster:(e,i)=>({u_tl_parent:new t.bU(e,i.u_tl_parent),u_scale_parent:new t.bg(e,i.u_scale_parent),u_buffer_scale:new t.bg(e,i.u_buffer_scale),u_fade_t:new t.bg(e,i.u_fade_t),u_opacity:new t.bg(e,i.u_opacity),u_image0:new t.bP(e,i.u_image0),u_image1:new t.bP(e,i.u_image1),u_brightness_low:new t.bg(e,i.u_brightness_low),u_brightness_high:new t.bg(e,i.u_brightness_high),u_saturation_factor:new t.bg(e,i.u_saturation_factor),u_contrast_factor:new t.bg(e,i.u_contrast_factor),u_spin_weights:new t.bT(e,i.u_spin_weights),u_coords_top:new t.bS(e,i.u_coords_top),u_coords_bottom:new t.bS(e,i.u_coords_bottom)}),symbolIcon:(e,i)=>({u_is_size_zoom_constant:new t.bP(e,i.u_is_size_zoom_constant),u_is_size_feature_constant:new t.bP(e,i.u_is_size_feature_constant),u_size_t:new t.bg(e,i.u_size_t),u_size:new t.bg(e,i.u_size),u_camera_to_center_distance:new t.bg(e,i.u_camera_to_center_distance),u_pitch:new t.bg(e,i.u_pitch),u_rotate_symbol:new t.bP(e,i.u_rotate_symbol),u_aspect_ratio:new t.bg(e,i.u_aspect_ratio),u_fade_change:new t.bg(e,i.u_fade_change),u_label_plane_matrix:new t.bR(e,i.u_label_plane_matrix),u_coord_matrix:new t.bR(e,i.u_coord_matrix),u_is_text:new t.bP(e,i.u_is_text),u_pitch_with_map:new t.bP(e,i.u_pitch_with_map),u_is_along_line:new t.bP(e,i.u_is_along_line),u_is_variable_anchor:new t.bP(e,i.u_is_variable_anchor),u_texsize:new t.bU(e,i.u_texsize),u_texture:new t.bP(e,i.u_texture),u_translation:new t.bU(e,i.u_translation),u_pitched_scale:new t.bg(e,i.u_pitched_scale)}),symbolSDF:(e,i)=>({u_is_size_zoom_constant:new t.bP(e,i.u_is_size_zoom_constant),u_is_size_feature_constant:new t.bP(e,i.u_is_size_feature_constant),u_size_t:new t.bg(e,i.u_size_t),u_size:new t.bg(e,i.u_size),u_camera_to_center_distance:new t.bg(e,i.u_camera_to_center_distance),u_pitch:new t.bg(e,i.u_pitch),u_rotate_symbol:new t.bP(e,i.u_rotate_symbol),u_aspect_ratio:new t.bg(e,i.u_aspect_ratio),u_fade_change:new t.bg(e,i.u_fade_change),u_label_plane_matrix:new t.bR(e,i.u_label_plane_matrix),u_coord_matrix:new t.bR(e,i.u_coord_matrix),u_is_text:new t.bP(e,i.u_is_text),u_pitch_with_map:new t.bP(e,i.u_pitch_with_map),u_is_along_line:new t.bP(e,i.u_is_along_line),u_is_variable_anchor:new t.bP(e,i.u_is_variable_anchor),u_texsize:new t.bU(e,i.u_texsize),u_texture:new t.bP(e,i.u_texture),u_gamma_scale:new t.bg(e,i.u_gamma_scale),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_is_halo:new t.bP(e,i.u_is_halo),u_translation:new t.bU(e,i.u_translation),u_pitched_scale:new t.bg(e,i.u_pitched_scale)}),symbolTextAndIcon:(e,i)=>({u_is_size_zoom_constant:new t.bP(e,i.u_is_size_zoom_constant),u_is_size_feature_constant:new t.bP(e,i.u_is_size_feature_constant),u_size_t:new t.bg(e,i.u_size_t),u_size:new t.bg(e,i.u_size),u_camera_to_center_distance:new t.bg(e,i.u_camera_to_center_distance),u_pitch:new t.bg(e,i.u_pitch),u_rotate_symbol:new t.bP(e,i.u_rotate_symbol),u_aspect_ratio:new t.bg(e,i.u_aspect_ratio),u_fade_change:new t.bg(e,i.u_fade_change),u_label_plane_matrix:new t.bR(e,i.u_label_plane_matrix),u_coord_matrix:new t.bR(e,i.u_coord_matrix),u_is_text:new t.bP(e,i.u_is_text),u_pitch_with_map:new t.bP(e,i.u_pitch_with_map),u_is_along_line:new t.bP(e,i.u_is_along_line),u_is_variable_anchor:new t.bP(e,i.u_is_variable_anchor),u_texsize:new t.bU(e,i.u_texsize),u_texsize_icon:new t.bU(e,i.u_texsize_icon),u_texture:new t.bP(e,i.u_texture),u_texture_icon:new t.bP(e,i.u_texture_icon),u_gamma_scale:new t.bg(e,i.u_gamma_scale),u_device_pixel_ratio:new t.bg(e,i.u_device_pixel_ratio),u_is_halo:new t.bP(e,i.u_is_halo),u_translation:new t.bU(e,i.u_translation),u_pitched_scale:new t.bg(e,i.u_pitched_scale)}),background:(e,i)=>({u_opacity:new t.bg(e,i.u_opacity),u_color:new t.bQ(e,i.u_color)}),backgroundPattern:(e,i)=>({u_opacity:new t.bg(e,i.u_opacity),u_image:new t.bP(e,i.u_image),u_pattern_tl_a:new t.bU(e,i.u_pattern_tl_a),u_pattern_br_a:new t.bU(e,i.u_pattern_br_a),u_pattern_tl_b:new t.bU(e,i.u_pattern_tl_b),u_pattern_br_b:new t.bU(e,i.u_pattern_br_b),u_texsize:new t.bU(e,i.u_texsize),u_mix:new t.bg(e,i.u_mix),u_pattern_size_a:new t.bU(e,i.u_pattern_size_a),u_pattern_size_b:new t.bU(e,i.u_pattern_size_b),u_scale_a:new t.bg(e,i.u_scale_a),u_scale_b:new t.bg(e,i.u_scale_b),u_pixel_coord_upper:new t.bU(e,i.u_pixel_coord_upper),u_pixel_coord_lower:new t.bU(e,i.u_pixel_coord_lower),u_tile_units_to_pixels:new t.bg(e,i.u_tile_units_to_pixels)}),terrain:(e,i)=>({u_texture:new t.bP(e,i.u_texture),u_ele_delta:new t.bg(e,i.u_ele_delta),u_fog_matrix:new t.bR(e,i.u_fog_matrix),u_fog_color:new t.bQ(e,i.u_fog_color),u_fog_ground_blend:new t.bg(e,i.u_fog_ground_blend),u_fog_ground_blend_opacity:new t.bg(e,i.u_fog_ground_blend_opacity),u_horizon_color:new t.bQ(e,i.u_horizon_color),u_horizon_fog_blend:new t.bg(e,i.u_horizon_fog_blend),u_is_globe_mode:new t.bg(e,i.u_is_globe_mode)}),terrainDepth:(e,i)=>({u_ele_delta:new t.bg(e,i.u_ele_delta)}),terrainCoords:(e,i)=>({u_texture:new t.bP(e,i.u_texture),u_terrain_coords_id:new t.bg(e,i.u_terrain_coords_id),u_ele_delta:new t.bg(e,i.u_ele_delta)}),projectionErrorMeasurement:(e,i)=>({u_input:new t.bg(e,i.u_input),u_output_expected:new t.bg(e,i.u_output_expected)}),atmosphere:(e,i)=>({u_sun_pos:new t.bT(e,i.u_sun_pos),u_atmosphere_blend:new t.bg(e,i.u_atmosphere_blend),u_globe_position:new t.bT(e,i.u_globe_position),u_globe_radius:new t.bg(e,i.u_globe_radius),u_inv_proj_matrix:new t.bR(e,i.u_inv_proj_matrix)}),sky:(e,i)=>({u_sky_color:new t.bQ(e,i.u_sky_color),u_horizon_color:new t.bQ(e,i.u_horizon_color),u_horizon:new t.bU(e,i.u_horizon),u_horizon_normal:new t.bU(e,i.u_horizon_normal),u_sky_horizon_blend:new t.bg(e,i.u_sky_horizon_blend),u_sky_blend:new t.bg(e,i.u_sky_blend)})};class so{constructor(e,t,i){this.context=e;const o=e.gl;this.buffer=o.createBuffer(),this.dynamicDraw=Boolean(i),this.context.unbindVAO(),e.bindElementBuffer.set(this.buffer),o.bufferData(o.ELEMENT_ARRAY_BUFFER,t.arrayBuffer,this.dynamicDraw?o.DYNAMIC_DRAW:o.STATIC_DRAW),this.dynamicDraw||delete t.arrayBuffer;}bind(){this.context.bindElementBuffer.set(this.buffer);}updateData(e){const t=this.context.gl;if(!this.dynamicDraw)throw new Error("Attempted to update data while not in dynamic mode.");this.context.unbindVAO(),this.bind(),t.bufferSubData(t.ELEMENT_ARRAY_BUFFER,0,e.arrayBuffer);}destroy(){this.buffer&&(this.context.gl.deleteBuffer(this.buffer),delete this.buffer);}}const no={Int8:"BYTE",Uint8:"UNSIGNED_BYTE",Int16:"SHORT",Uint16:"UNSIGNED_SHORT",Int32:"INT",Uint32:"UNSIGNED_INT",Float32:"FLOAT"};class lo{constructor(e,t,i,o){this.length=t.length,this.attributes=i,this.itemSize=t.bytesPerElement,this.dynamicDraw=o,this.context=e;const r=e.gl;this.buffer=r.createBuffer(),e.bindVertexBuffer.set(this.buffer),r.bufferData(r.ARRAY_BUFFER,t.arrayBuffer,this.dynamicDraw?r.DYNAMIC_DRAW:r.STATIC_DRAW),this.dynamicDraw||delete t.arrayBuffer;}bind(){this.context.bindVertexBuffer.set(this.buffer);}updateData(e){if(e.length!==this.length)throw new Error(`Length of new data is ${e.length}, which doesn't match current length of ${this.length}`);const t=this.context.gl;this.bind(),t.bufferSubData(t.ARRAY_BUFFER,0,e.arrayBuffer);}enableAttributes(e,t){for(let i=0;i0&&(h.push({circleArray:f,circleOffset:d,coord:_}),u+=f.length/4,d=u),m&&c.draw(s,l.LINES,Zt.disabled,Vt.disabled,e.colorModeForRenderPass(),Ut.disabled,Fi(e.transform),e.style.map.terrain&&e.style.map.terrain.getTerrainData(_),n.getProjectionData({overscaledTileID:_,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),o.id,m.layoutVertexBuffer,m.indexBuffer,m.segments,null,e.transform.zoom,null,null,m.collisionVertexBuffer);}if(!a||!h.length)return;const _=e.useProgram("collisionCircle"),p=new t.b$;p.resize(4*u),p._trim();let m=0;for(const e of h)for(let t=0;t=0&&(f[g.associatedIconIndex]={shiftedAnchor:S,angle:E});}else $e(g.numGlyphs,p);}if(c){m.clear();const i=e.icon.placedSymbolArray;for(let e=0;ee.style.map.terrain.getElevation(l,t,i):null,i="map"===o.layout.get("text-rotation-alignment");De(c,e,a,O,j,v,h,i,l.toUnwrapped(),f.width,f.height,U,t);}const $=a&&P||V,q=b||$?Yo:v?O:e.transform.clipSpaceToPixelsMatrix,W=p&&0!==o.paint.get(a?"text-halo-width":"icon-halo-width").constantOr(1);let H;H=p?c.iconsInText?to(T.kind,E,x,v,b,$,e,q,N,U,z,k,I):eo(T.kind,E,x,v,b,$,e,q,N,U,a,z,0,I):Ji(T.kind,E,x,v,b,$,e,q,N,U,a,z,I);const X={program:S,buffers:u,uniformValues:H,projectionData:Z,atlasTexture:D,atlasTextureIcon:F,atlasInterpolation:A,atlasInterpolationIcon:L,isSDF:p,hasHalo:W};if(y&&c.canOverlap){w=!0;const e=u.segments.get();for(const i of e)C.push({segments:new t.aM([i]),sortKey:i.sortKey,state:X,terrainData:R});}else C.push({segments:u.segments,sortKey:0,state:X,terrainData:R});}w&&C.sort(((e,t)=>e.sortKey-t.sortKey));for(const t of C){const i=t.state;if(p.activeTexture.set(m.TEXTURE0),i.atlasTexture.bind(i.atlasInterpolation,m.CLAMP_TO_EDGE),i.atlasTextureIcon&&(p.activeTexture.set(m.TEXTURE1),i.atlasTextureIcon&&i.atlasTextureIcon.bind(i.atlasInterpolationIcon,m.CLAMP_TO_EDGE)),i.isSDF){const r=i.uniformValues;i.hasHalo&&(r.u_is_halo=1,or(i.buffers,t.segments,o,e,i.program,T,u,d,r,i.projectionData,t.terrainData)),r.u_is_halo=0;}or(i.buffers,t.segments,o,e,i.program,T,u,d,i.uniformValues,i.projectionData,t.terrainData);}}function or(e,t,i,o,r,a,s,n,l,c,h){const u=o.context;r.draw(u,u.gl.TRIANGLES,a,s,n,Ut.backCCW,l,h,c,i.id,e.layoutVertexBuffer,e.indexBuffer,t,i.paint,o.transform.zoom,e.programConfigurations.get(i.id),e.dynamicLayoutVertexBuffer,e.opacityVertexBuffer);}function rr(e,i,o,r,a){const s=e.context,n=s.gl,l=Vt.disabled,c=new jt([n.ONE,n.ONE],t.bf.transparent,[!0,!0,!0,!0]),h=i.getBucket(o);if(!h)return;const u=r.key;let d=o.heatmapFbos.get(u);d||(d=sr(s,i.tileSize,i.tileSize),o.heatmapFbos.set(u,d)),s.bindFramebuffer.set(d.framebuffer),s.viewport.set([0,0,i.tileSize,i.tileSize]),s.clear({color:t.bf.transparent});const _=h.programConfigurations.get(o.id),p=e.useProgram("heatmap",_,!a),m=e.transform.getProjectionData({overscaledTileID:i.tileID,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),f=e.style.map.terrain.getTerrainData(r);p.draw(s,n.TRIANGLES,Zt.disabled,l,c,Ut.disabled,ji(i,e.transform.zoom,o.paint.get("heatmap-intensity"),1),f,m,o.id,h.layoutVertexBuffer,h.indexBuffer,h.segments,o.paint,e.transform.zoom,_);}function ar(e,t,i,o,r){const a=e.context,s=a.gl,n=e.transform;a.setColorMode(e.colorModeForRenderPass());const l=nr(a,t),c=i.key,h=t.heatmapFbos.get(c);if(!h)return;a.activeTexture.set(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,h.colorAttachment.get()),a.activeTexture.set(s.TEXTURE1),l.bind(s.LINEAR,s.CLAMP_TO_EDGE);const u=n.getProjectionData({overscaledTileID:i,applyTerrainMatrix:r,applyGlobeMatrix:!o});e.useProgram("heatmapTexture").draw(a,s.TRIANGLES,Zt.disabled,Vt.disabled,e.colorModeForRenderPass(),Ut.disabled,Ni(e,t,0,1),null,u,t.id,e.rasterBoundsBuffer,e.quadTriangleIndexBuffer,e.rasterBoundsSegments,t.paint,n.zoom),h.destroy(),t.heatmapFbos.delete(c);}function sr(e,t,i){var o,r;const a=e.gl,s=a.createTexture();a.bindTexture(a.TEXTURE_2D,s),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_WRAP_S,a.CLAMP_TO_EDGE),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_WRAP_T,a.CLAMP_TO_EDGE),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.LINEAR),a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.LINEAR);const n=null!==(o=e.HALF_FLOAT)&&void 0!==o?o:a.UNSIGNED_BYTE,l=null!==(r=e.RGBA16F)&&void 0!==r?r:a.RGBA;a.texImage2D(a.TEXTURE_2D,0,l,t,i,0,a.RGBA,n,null);const c=e.createFramebuffer(t,i,!1,!1);return c.colorAttachment.set(s),c}function nr(e,i){return i.colorRampTexture||(i.colorRampTexture=new t.T(e,i.colorRamp,e.gl.RGBA)),i.colorRampTexture}function lr(e,t,i,o,r){if(!i||!o||!o.imageAtlas)return;const a=o.imageAtlas.patternPositions;let s=a[i.to.toString()],n=a[i.from.toString()];if(!s&&n&&(s=n),!n&&s&&(n=s),!s||!n){const e=r.getPaintProperty(t);s=a[e],n=a[e];}s&&n&&e.setConstantPatternPositions(s,n);}function cr(e,i,o,r,a,s,n,l){const c=e.context.gl,h="fill-pattern",u=o.paint.get(h),d=u&&u.constantOr(1),_=o.getCrossfadeParameters();let p,m,f,g,v;const b=e.transform,x=o.paint.get("fill-translate"),y=o.paint.get("fill-translate-anchor");n?(m=d&&!o.getPaintProperty("fill-outline-color")?"fillOutlinePattern":"fillOutline",p=c.LINES):(m=d?"fillPattern":"fill",p=c.TRIANGLES);const w=u.constantOr(null);for(const u of r){const r=i.getTile(u);if(d&&!r.patternsLoaded())continue;const T=r.getBucket(o);if(!T)continue;const P=T.programConfigurations.get(o.id),C=e.useProgram(m,P),I=e.style.map.terrain&&e.style.map.terrain.getTerrainData(u);d&&(e.context.activeTexture.set(c.TEXTURE0),r.imageAtlasTexture.bind(c.LINEAR,c.CLAMP_TO_EDGE),P.updatePaintBuffers(_)),lr(P,h,w,r,o);const M=b.getProjectionData({overscaledTileID:u,applyGlobeMatrix:!l,applyTerrainMatrix:!0}),S=t.aD(b,r,x,y);if(n){g=T.indexBuffer2,v=T.segments2;const t=[c.drawingBufferWidth,c.drawingBufferHeight];f="fillOutlinePattern"===m&&d?Li(e,_,r,t,S):Ai(t,S);}else g=T.indexBuffer,v=T.segments,f=d?Di(e,_,r,S):{u_fill_translate:S};const E=e.stencilModeForClipping(u);C.draw(e.context,p,a,E,s,Ut.backCCW,f,I,M,o.id,T.layoutVertexBuffer,g,v,o.paint,e.transform.zoom,P);}}function hr(e,i,o,r,a,s,n,l){const c=e.context,h=c.gl,u="fill-extrusion-pattern",d=o.paint.get(u),_=d.constantOr(1),p=o.getCrossfadeParameters(),m=o.paint.get("fill-extrusion-opacity"),f=d.constantOr(null),g=e.transform;for(const d of r){const r=i.getTile(d),v=r.getBucket(o);if(!v)continue;const b=e.style.map.terrain&&e.style.map.terrain.getTerrainData(d),x=v.programConfigurations.get(o.id),y=e.useProgram(_?"fillExtrusionPattern":"fillExtrusion",x);_&&(e.context.activeTexture.set(h.TEXTURE0),r.imageAtlasTexture.bind(h.LINEAR,h.CLAMP_TO_EDGE),x.updatePaintBuffers(p));const w=g.getProjectionData({overscaledTileID:d,applyGlobeMatrix:!l,applyTerrainMatrix:!0});lr(x,u,f,r,o);const T=t.aD(g,r,o.paint.get("fill-extrusion-translate"),o.paint.get("fill-extrusion-translate-anchor")),P=o.paint.get("fill-extrusion-vertical-gradient"),C=_?zi(e,P,m,T,d,p,r):Ri(e,P,m,T);y.draw(c,c.gl.TRIANGLES,a,s,n,Ut.backCCW,C,b,w,o.id,v.layoutVertexBuffer,v.indexBuffer,v.segments,o.paint,e.transform.zoom,x,e.style.map.terrain&&v.centroidVertexBuffer);}}function ur(e,t,i,o,r,a,s,n,l){var c;const h=e.style.projection,u=e.context,d=e.transform,_=u.gl,p=[`#define NUM_ILLUMINATION_SOURCES ${i.paint.get("hillshade-highlight-color").values.length}`],m=e.useProgram("hillshade",null,!1,p),f=!e.options.moving;for(const p of o){const o=t.getTile(p),g=o.fbo;if(!g)continue;const v=h.getMeshFromTileID(u,p.canonical,n,!0,"raster"),b=null===(c=e.style.map.terrain)||void 0===c?void 0:c.getTerrainData(p);u.activeTexture.set(_.TEXTURE0),_.bindTexture(_.TEXTURE_2D,g.colorAttachment.get());const x=d.getProjectionData({overscaledTileID:p,aligned:f,applyGlobeMatrix:!l,applyTerrainMatrix:!0});m.draw(u,_.TRIANGLES,a,r[p.overscaledZ],s,Ut.backCCW,Ui(e,o,i),b,x,i.id,v.vertexBuffer,v.indexBuffer,v.segments);}}function dr(e,i,o,r,a,s,n,l,c){var h;const u=e.style.projection,d=e.context,_=e.transform,p=d.gl,m=e.useProgram("colorRelief"),f=!e.options.moving;let g=!0,v=0;for(const b of r){const r=i.getTile(b),x=r.dem;if(g){const e=p.getParameter(p.MAX_TEXTURE_SIZE),{elevationTexture:t,colorTexture:i}=o.getColorRampTextures(d,e,x.getUnpackVector());d.activeTexture.set(p.TEXTURE1),t.bind(p.NEAREST,p.CLAMP_TO_EDGE),d.activeTexture.set(p.TEXTURE4),i.bind(p.LINEAR,p.CLAMP_TO_EDGE),g=!1,v=t.size[0];}if(!x||!x.data)continue;const y=x.stride,w=x.getPixels();if(d.activeTexture.set(p.TEXTURE0),d.pixelStoreUnpackPremultiplyAlpha.set(!1),r.demTexture=r.demTexture||e.getTileTexture(y),r.demTexture){const e=r.demTexture;e.update(w,{premultiply:!1}),e.bind(p.LINEAR,p.CLAMP_TO_EDGE);}else r.demTexture=new t.T(d,w,p.RGBA,{premultiply:!1}),r.demTexture.bind(p.LINEAR,p.CLAMP_TO_EDGE);const T=u.getMeshFromTileID(d,b.canonical,l,!0,"raster"),P=null===(h=e.style.map.terrain)||void 0===h?void 0:h.getTerrainData(b),C=_.getProjectionData({overscaledTileID:b,aligned:f,applyGlobeMatrix:!c,applyTerrainMatrix:!0});m.draw(d,p.TRIANGLES,s,a[b.overscaledZ],n,Ut.backCCW,Vi(o,r.dem,v),P,C,o.id,T.vertexBuffer,T.indexBuffer,T.segments);}}const _r=[new t.P(0,0),new t.P(t.$,0),new t.P(t.$,t.$),new t.P(0,t.$)];function pr(e,t,i,o,r,a,s,n,l=!1,c=!1){const h=o[o.length-1].overscaledZ,u=e.context,d=u.gl,_=e.useProgram("raster"),p=e.transform,m=e.style.projection,f=e.colorModeForRenderPass(),g=!e.options.moving;for(const v of o){const o=e.getDepthModeForSublayer(v.overscaledZ-h,1===i.paint.get("raster-opacity")?Zt.ReadWrite:Zt.ReadOnly,d.LESS),b=t.getTile(v);b.registerFadeDuration(i.paint.get("raster-fade-duration"));const x=t.findLoadedParent(v,0),y=t.findLoadedSibling(v),w=mr(b,x||y||null,t,i,e.transform,e.style.map.terrain);let T,P;const C="nearest"===i.paint.get("raster-resampling")?d.NEAREST:d.LINEAR;u.activeTexture.set(d.TEXTURE0),b.texture.bind(C,d.CLAMP_TO_EDGE,d.LINEAR_MIPMAP_NEAREST),u.activeTexture.set(d.TEXTURE1),x?(x.texture.bind(C,d.CLAMP_TO_EDGE,d.LINEAR_MIPMAP_NEAREST),T=Math.pow(2,x.tileID.overscaledZ-b.tileID.overscaledZ),P=[b.tileID.canonical.x*T%1,b.tileID.canonical.y*T%1]):b.texture.bind(C,d.CLAMP_TO_EDGE,d.LINEAR_MIPMAP_NEAREST),b.texture.useMipmap&&u.extTextureFilterAnisotropic&&e.transform.pitch>20&&d.texParameterf(d.TEXTURE_2D,u.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,u.extTextureFilterAnisotropicMax);const I=e.style.map.terrain&&e.style.map.terrain.getTerrainData(v),M=p.getProjectionData({overscaledTileID:v,aligned:g,applyGlobeMatrix:!c,applyTerrainMatrix:!0}),S=Yi(P||[0,0],T||1,w,i,n),E=m.getMeshFromTileID(u,v.canonical,a,s,"raster");_.draw(u,d.TRIANGLES,o,r?r[v.overscaledZ]:Vt.disabled,f,l?Ut.frontCCW:Ut.backCCW,S,I,M,i.id,E.vertexBuffer,E.indexBuffer,E.segments);}}function mr(e,i,o,r,a,n){const l=r.paint.get("raster-fade-duration");if(!n&&l>0){const r=s.now(),n=(r-e.timeAdded)/l,c=i?(r-i.timeAdded)/l:-1,h=o.getSource(),u=ge(a,{tileSize:h.tileSize,roundZoom:h.roundZoom}),d=!i||Math.abs(i.tileID.overscaledZ-u)>Math.abs(e.tileID.overscaledZ-u),_=d&&e.refreshedUponExpiration?1:t.ah(d?n:1-c,0,1);return e.refreshedUponExpiration&&n>=1&&(e.refreshedUponExpiration=!1),i?{opacity:1,mix:1-_}:{opacity:_,mix:0}}return {opacity:1,mix:0}}const fr=new t.bf(1,0,0,1),gr=new t.bf(0,1,0,1),vr=new t.bf(0,0,1,1),br=new t.bf(1,0,1,1),xr=new t.bf(0,1,1,1);function yr(e,t,i,o){Tr(e,0,t+i/2,e.transform.width,i,o);}function wr(e,t,i,o){Tr(e,t-i/2,0,i,e.transform.height,o);}function Tr(e,t,i,o,r,a){const s=e.context,n=s.gl;n.enable(n.SCISSOR_TEST),n.scissor(t*e.pixelRatio,i*e.pixelRatio,o*e.pixelRatio,r*e.pixelRatio),s.clear({color:a}),n.disable(n.SCISSOR_TEST);}function Pr(e,i,o){const r=e.context,a=r.gl,s=e.useProgram("debug"),n=Zt.disabled,l=Vt.disabled,c=e.colorModeForRenderPass(),h="$debug",u=e.style.map.terrain&&e.style.map.terrain.getTerrainData(o);r.activeTexture.set(a.TEXTURE0);const d=i.getTileByID(o.key).latestRawTileData,_=Math.floor((d&&d.byteLength||0)/1024),p=i.getTile(o).tileSize,m=512/Math.min(p,512)*(o.overscaledZ/e.transform.zoom)*.5;let f=o.canonical.toString();o.overscaledZ!==o.canonical.z&&(f+=` => ${o.overscaledZ}`),function(e,t){e.initDebugOverlayCanvas();const i=e.debugOverlayCanvas,o=e.context.gl,r=e.debugOverlayCanvas.getContext("2d");r.clearRect(0,0,i.width,i.height),r.shadowColor="white",r.shadowBlur=2,r.lineWidth=1.5,r.strokeStyle="white",r.textBaseline="top",r.font="bold 36px Open Sans, sans-serif",r.fillText(t,5,5),r.strokeText(t,5,5),e.debugOverlayTexture.update(i),e.debugOverlayTexture.bind(o.LINEAR,o.CLAMP_TO_EDGE);}(e,`${f} ${_}kB`);const g=e.transform.getProjectionData({overscaledTileID:o,applyGlobeMatrix:!0,applyTerrainMatrix:!0});s.draw(r,a.TRIANGLES,n,l,jt.alphaBlended,Ut.disabled,Oi(t.bf.transparent,m),null,g,h,e.debugBuffer,e.quadTriangleIndexBuffer,e.debugSegments),s.draw(r,a.LINE_STRIP,n,l,c,Ut.disabled,Oi(t.bf.red),u,g,h,e.debugBuffer,e.tileBorderIndexBuffer,e.debugSegments);}function Cr(e,t,i,o){const{isRenderingGlobe:r}=o,a=e.context,s=a.gl,n=e.transform,l=e.colorModeForRenderPass(),c=e.getDepthModeFor3D(),h=e.useProgram("terrain");a.bindFramebuffer.set(null),a.viewport.set([0,0,e.width,e.height]);for(const o of i){const i=t.getTerrainMesh(o.tileID),u=e.renderToTexture.getTexture(o),d=t.getTerrainData(o.tileID);a.activeTexture.set(s.TEXTURE0),s.bindTexture(s.TEXTURE_2D,u.texture);const _=t.getMeshFrameDelta(n.zoom),p=n.calculateFogMatrix(o.tileID.toUnwrapped()),m=Ci(_,p,e.style.sky,n.pitch,r),f=n.getProjectionData({overscaledTileID:o.tileID,applyTerrainMatrix:!1,applyGlobeMatrix:!0});h.draw(a,s.TRIANGLES,c,Vt.disabled,l,Ut.backCCW,m,d,f,"terrain",i.vertexBuffer,i.indexBuffer,i.segments);}}function Ir(e,i){if(!i.mesh){const o=new t.aL;o.emplaceBack(-1,-1),o.emplaceBack(1,-1),o.emplaceBack(1,1),o.emplaceBack(-1,1);const r=new t.aN;r.emplaceBack(0,1,2),r.emplaceBack(0,2,3),i.mesh=new wt(e.createVertexBuffer(o,Tt.members),e.createIndexBuffer(r),t.aM.simpleSegment(0,0,o.length,r.length));}return i.mesh}class Mr{constructor(e,i){this.context=new Ho(e),this.transform=i,this._tileTextures={},this.terrainFacilitator={dirty:!0,matrix:t.ag(new Float64Array(16)),renderTime:0},this.setup(),this.numSublayers=xe.maxUnderzooming+xe.maxOverzooming+1,this.depthEpsilon=1/Math.pow(2,16),this.crossTileSymbolIndex=new vt;}resize(e,t,i){if(this.width=Math.floor(e*i),this.height=Math.floor(t*i),this.pixelRatio=i,this.context.viewport.set([0,0,this.width,this.height]),this.style)for(const e of this.style._order)this.style._layers[e].resize();}setup(){const e=this.context,i=new t.aL;i.emplaceBack(0,0),i.emplaceBack(t.$,0),i.emplaceBack(0,t.$),i.emplaceBack(t.$,t.$),this.tileExtentBuffer=e.createVertexBuffer(i,Tt.members),this.tileExtentSegments=t.aM.simpleSegment(0,0,4,2);const o=new t.aL;o.emplaceBack(0,0),o.emplaceBack(t.$,0),o.emplaceBack(0,t.$),o.emplaceBack(t.$,t.$),this.debugBuffer=e.createVertexBuffer(o,Tt.members),this.debugSegments=t.aM.simpleSegment(0,0,4,5);const r=new t.c6;r.emplaceBack(0,0,0,0),r.emplaceBack(t.$,0,t.$,0),r.emplaceBack(0,t.$,0,t.$),r.emplaceBack(t.$,t.$,t.$,t.$),this.rasterBoundsBuffer=e.createVertexBuffer(r,Ti.members),this.rasterBoundsSegments=t.aM.simpleSegment(0,0,4,2);const a=new t.aL;a.emplaceBack(0,0),a.emplaceBack(t.$,0),a.emplaceBack(0,t.$),a.emplaceBack(t.$,t.$),this.rasterBoundsBufferPosOnly=e.createVertexBuffer(a,Tt.members),this.rasterBoundsSegmentsPosOnly=t.aM.simpleSegment(0,0,4,5);const s=new t.aL;s.emplaceBack(0,0),s.emplaceBack(1,0),s.emplaceBack(0,1),s.emplaceBack(1,1),this.viewportBuffer=e.createVertexBuffer(s,Tt.members),this.viewportSegments=t.aM.simpleSegment(0,0,4,2);const n=new t.c7;n.emplaceBack(0),n.emplaceBack(1),n.emplaceBack(3),n.emplaceBack(2),n.emplaceBack(0),this.tileBorderIndexBuffer=e.createIndexBuffer(n);const l=new t.aN;l.emplaceBack(1,0,2),l.emplaceBack(1,2,3),this.quadTriangleIndexBuffer=e.createIndexBuffer(l);const c=this.context.gl;this.stencilClearMode=new Vt({func:c.ALWAYS,mask:0},0,255,c.ZERO,c.ZERO,c.ZERO),this.tileExtentMesh=new wt(this.tileExtentBuffer,this.quadTriangleIndexBuffer,this.tileExtentSegments);}clearStencil(){const e=this.context,i=e.gl;this.nextStencilID=1,this.currentStencilSource=void 0;const o=t.L();t.bY(o,0,this.width,this.height,0,0,1),t.N(o,o,[i.drawingBufferWidth,i.drawingBufferHeight,0]);const r={mainMatrix:o,tileMercatorCoords:[0,0,1,1],clippingPlane:[0,0,0,0],projectionTransition:0,fallbackMatrix:o};this.useProgram("clippingMask",null,!0).draw(e,i.TRIANGLES,Zt.disabled,this.stencilClearMode,jt.disabled,Ut.disabled,null,null,r,"$clipping",this.viewportBuffer,this.quadTriangleIndexBuffer,this.viewportSegments);}_renderTileClippingMasks(e,t,i){if(this.currentStencilSource===e.source||!e.isTileClipped()||!t||!t.length)return;this.currentStencilSource=e.source,this.nextStencilID+t.length>256&&this.clearStencil();const o=this.context;o.setColorMode(jt.disabled),o.setDepthMode(Zt.disabled);const r={};for(const e of t)r[e.key]=this.nextStencilID++;this._renderTileMasks(r,t,i,!0),this._renderTileMasks(r,t,i,!1),this._tileClippingMaskIDs=r;}_renderTileMasks(e,t,i,o){const r=this.context,a=r.gl,s=this.style.projection,n=this.transform,l=this.useProgram("clippingMask");for(const c of t){const t=e[c.key],h=this.style.map.terrain&&this.style.map.terrain.getTerrainData(c),u=s.getMeshFromTileID(this.context,c.canonical,o,!0,"stencil"),d=n.getProjectionData({overscaledTileID:c,applyGlobeMatrix:!i,applyTerrainMatrix:!0});l.draw(r,a.TRIANGLES,Zt.disabled,new Vt({func:a.ALWAYS,mask:0},t,255,a.KEEP,a.KEEP,a.REPLACE),jt.disabled,i?Ut.disabled:Ut.backCCW,null,h,d,"$clipping",u.vertexBuffer,u.indexBuffer,u.segments);}}_renderTilesDepthBuffer(){const e=this.context,t=e.gl,i=this.style.projection,o=this.transform,r=this.useProgram("depth"),a=this.getDepthModeFor3D(),s=ve(o,{tileSize:o.tileSize});for(const n of s){const s=this.style.map.terrain&&this.style.map.terrain.getTerrainData(n),l=i.getMeshFromTileID(this.context,n.canonical,!0,!0,"raster"),c=o.getProjectionData({overscaledTileID:n,applyGlobeMatrix:!0,applyTerrainMatrix:!0});r.draw(e,t.TRIANGLES,a,Vt.disabled,jt.disabled,Ut.backCCW,null,s,c,"$clipping",l.vertexBuffer,l.indexBuffer,l.segments);}}stencilModeFor3D(){this.currentStencilSource=void 0,this.nextStencilID+1>256&&this.clearStencil();const e=this.nextStencilID++,t=this.context.gl;return new Vt({func:t.NOTEQUAL,mask:255},e,255,t.KEEP,t.KEEP,t.REPLACE)}stencilModeForClipping(e){const t=this.context.gl;return new Vt({func:t.EQUAL,mask:255},this._tileClippingMaskIDs[e.key],0,t.KEEP,t.KEEP,t.REPLACE)}getStencilConfigForOverlapAndUpdateStencilID(e){const t=this.context.gl,i=e.sort(((e,t)=>t.overscaledZ-e.overscaledZ)),o=i[i.length-1].overscaledZ,r=i[0].overscaledZ-o+1;if(r>1){this.currentStencilSource=void 0,this.nextStencilID+r>256&&this.clearStencil();const e={};for(let i=0;it.overscaledZ-e.overscaledZ)),o=i[i.length-1].overscaledZ,r=i[0].overscaledZ-o+1;if(this.clearStencil(),r>1){const e={},a={};for(let i=0;i0};for(const e in n){const t=n[e];t.used&&t.prepare(this.context),l[e]=t.getVisibleCoordinates(!1),c[e]=l[e].slice().reverse(),h[e]=t.getVisibleCoordinates(!0).reverse();}this.opaquePassCutoff=1/0;for(let e=0;ethis.useProgram(e)}),this.context.viewport.set([0,0,this.width,this.height]),this.context.bindFramebuffer.set(null),this.context.clear({color:i.showOverdrawInspector?t.bf.black:t.bf.transparent,depth:1}),this.clearStencil(),this.style.sky&&function(e,t){const i=e.context,o=i.gl,r=((e,t,i)=>{const o=Math.cos(t.rollInRadians),r=Math.sin(t.rollInRadians),a=he(t),s=t.getProjectionData({overscaledTileID:null,applyGlobeMatrix:!0,applyTerrainMatrix:!0}).projectionTransition;return {u_sky_color:e.properties.get("sky-color"),u_horizon_color:e.properties.get("horizon-color"),u_horizon:[(t.width/2-a*r)*i,(t.height/2+a*o)*i],u_horizon_normal:[-r,o],u_sky_horizon_blend:e.properties.get("sky-horizon-blend")*t.height/2*i,u_sky_blend:s}})(t,e.style.map.transform,e.pixelRatio),a=new Zt(o.LEQUAL,Zt.ReadWrite,[0,1]),s=Vt.disabled,n=e.colorModeForRenderPass(),l=e.useProgram("sky"),c=Ir(i,t);l.draw(i,o.TRIANGLES,a,s,n,Ut.disabled,r,null,void 0,"sky",c.vertexBuffer,c.indexBuffer,c.segments);}(this,this.style.sky),this._showOverdrawInspector=i.showOverdrawInspector,this.depthRangeFor3D=[0,1-(e._order.length+2)*this.numSublayers*this.depthEpsilon],!this.renderToTexture)for(this.renderPass="opaque",this.currentLayer=a.length-1;this.currentLayer>=0;this.currentLayer--){const e=this.style._layers[a[this.currentLayer]],t=n[e.source],i=l[e.source];this._renderTileClippingMasks(e,i,!1),this.renderLayer(this,t,e,i,u);}this.renderPass="translucent";let d=!1;for(this.currentLayer=0;this.currentLayer({u_sun_pos:e,u_atmosphere_blend:t,u_globe_position:i,u_globe_radius:o,u_inv_proj_matrix:r}))(c,u,[p[0],p[1],p[2]],d,_),f=Ir(r,i);s.draw(r,a.TRIANGLES,n,Vt.disabled,jt.alphaBlended,Ut.disabled,m,null,null,"atmosphere",f.vertexBuffer,f.indexBuffer,f.segments);}(this,this.style.sky,this.style.light),this.options.showTileBoundaries){const e=function(e,t){let i=null;const o=Object.values(e._layers).flatMap((i=>i.source&&!i.isHidden(t)?[e.sourceCaches[i.source]]:[])),r=o.filter((e=>"vector"===e.getSource().type)),a=o.filter((e=>"vector"!==e.getSource().type)),s=e=>{(!i||i.getSource().maxzooms(e))),i||a.forEach((e=>s(e))),i}(this.style,this.transform.zoom);e&&function(e,t,i){for(let o=0;ou.getElevation(a,e,t):null;er(s,d,_,c,h,f,i,p,g,t.aD(h,e,n,l),a.toUnwrapped(),o);}}}(r,e,o,i,o.layout.get("text-rotation-alignment"),o.layout.get("text-pitch-alignment"),o.paint.get("text-translate"),o.paint.get("text-translate-anchor"),a),0!==o.paint.get("icon-opacity").constantOr(1)&&ir(e,i,o,r,!1,o.paint.get("icon-translate"),o.paint.get("icon-translate-anchor"),o.layout.get("icon-rotation-alignment"),o.layout.get("icon-pitch-alignment"),o.layout.get("icon-keep-upright"),l,c,n),0!==o.paint.get("text-opacity").constantOr(1)&&ir(e,i,o,r,!0,o.paint.get("text-translate"),o.paint.get("text-translate-anchor"),o.layout.get("text-rotation-alignment"),o.layout.get("text-pitch-alignment"),o.layout.get("text-keep-upright"),l,c,n),i.map.showCollisionBoxes&&(Ko(e,i,o,r,!0),Ko(e,i,o,r,!1));}(e,i,o,r,this.style.placement.variableOffsets,a):t.cc(o)?function(e,i,o,r,a){if("translucent"!==e.renderPass)return;const{isRenderingToTexture:s}=a,n=o.paint.get("circle-opacity"),l=o.paint.get("circle-stroke-width"),c=o.paint.get("circle-stroke-opacity"),h=!o.layout.get("circle-sort-key").isConstant();if(0===n.constantOr(1)&&(0===l.constantOr(1)||0===c.constantOr(1)))return;const u=e.context,d=u.gl,_=e.transform,p=e.getDepthModeForSublayer(0,Zt.ReadOnly),m=Vt.disabled,f=e.colorModeForRenderPass(),g=[],v=_.getCircleRadiusCorrection();for(let a=0;ae.sortKey-t.sortKey));for(const t of g){const{programConfiguration:i,program:r,layoutVertexBuffer:a,indexBuffer:s,uniformValues:n,terrainData:l,projectionData:c}=t.state;r.draw(u,d.TRIANGLES,p,m,f,Ut.backCCW,n,l,c,o.id,a,s,t.segments,o.paint,e.transform.zoom,i);}}(e,i,o,r,a):t.cd(o)?function(e,i,o,r,a){if(0===o.paint.get("heatmap-opacity"))return;const s=e.context,{isRenderingToTexture:n,isRenderingGlobe:l}=a;if(e.style.map.terrain){for(const t of r){const r=i.getTile(t);i.hasRenderableParent(t)||("offscreen"===e.renderPass?rr(e,r,o,t,l):"translucent"===e.renderPass&&ar(e,o,t,n,l));}s.viewport.set([0,0,e.width,e.height]);}else "offscreen"===e.renderPass?function(e,i,o,r){const a=e.context,s=a.gl,n=e.transform,l=Vt.disabled,c=new jt([s.ONE,s.ONE],t.bf.transparent,[!0,!0,!0,!0]);((function(e,i,o){const r=e.gl;e.activeTexture.set(r.TEXTURE1),e.viewport.set([0,0,i.width/4,i.height/4]);let a=o.heatmapFbos.get(t.c2);a?(r.bindTexture(r.TEXTURE_2D,a.colorAttachment.get()),e.bindFramebuffer.set(a.framebuffer)):(a=sr(e,i.width/4,i.height/4),o.heatmapFbos.set(t.c2,a));}))(a,e,o),a.clear({color:t.bf.transparent});for(let t=0;t0?t.pop():null}isPatternMissing(e){if(!e)return !1;if(!e.from||!e.to)return !0;const t=this.imageManager.getPattern(e.from.toString()),i=this.imageManager.getPattern(e.to.toString());return !t||!i}useProgram(e,t,i=!1,o=[]){this.cache=this.cache||{};const r=!!this.style.map.terrain,a=this.style.projection,s=i?xt.projectionMercator:a.shaderPreludeCode,n=i?Pt:a.shaderDefine,l=e+(t?t.cacheKey:"")+`/${i?Ct:a.shaderVariantName}`+(this._showOverdrawInspector?"/overdraw":"")+(r?"/terrain":"")+(o?`/${o.join("/")}`:"");return this.cache[l]||(this.cache[l]=new Si(this.context,xt[e],t,ao[e],this._showOverdrawInspector,r,s,n,o)),this.cache[l]}setCustomLayerDefaults(){this.context.unbindVAO(),this.context.cullFace.setDefault(),this.context.activeTexture.setDefault(),this.context.pixelStoreUnpack.setDefault(),this.context.pixelStoreUnpackPremultiplyAlpha.setDefault(),this.context.pixelStoreUnpackFlipY.setDefault();}setBaseState(){const e=this.context.gl;this.context.cullFace.set(!1),this.context.viewport.set([0,0,this.width,this.height]),this.context.blendEquation.set(e.FUNC_ADD);}initDebugOverlayCanvas(){null==this.debugOverlayCanvas&&(this.debugOverlayCanvas=document.createElement("canvas"),this.debugOverlayCanvas.width=512,this.debugOverlayCanvas.height=512,this.debugOverlayTexture=new t.T(this.context,this.debugOverlayCanvas,this.context.gl.RGBA));}destroy(){this.debugOverlayTexture&&this.debugOverlayTexture.destroy();}overLimit(){const{drawingBufferWidth:e,drawingBufferHeight:t}=this.context.gl;return this.width!==e||this.height!==t}}function Sr(e,t){let i,o=!1,r=null,a=null;const s=()=>{r=null,o&&(e.apply(a,i),r=setTimeout(s,t),o=!1);};return (...e)=>(o=!0,a=this,i=e,r||s(),r)}class Er{constructor(e){this._getCurrentHash=()=>{const e=window.location.hash.replace("#","");if(this._hashName){let t;return e.split("&").map((e=>e.split("="))).forEach((e=>{e[0]===this._hashName&&(t=e);})),(t&&t[1]||"").split("/")}return e.split("/")},this._onHashChange=()=>{const e=this._getCurrentHash();if(!this._isValidHash(e))return !1;const t=this._map.dragRotate.isEnabled()&&this._map.touchZoomRotate.isEnabled()?+(e[3]||0):this._map.getBearing();return this._map.jumpTo({center:[+e[2],+e[1]],zoom:+e[0],bearing:t,pitch:+(e[4]||0)}),!0},this._updateHashUnthrottled=()=>{const e=window.location.href.replace(/(#.*)?$/,this.getHashString());window.history.replaceState(window.history.state,null,e);},this._removeHash=()=>{const e=this._getCurrentHash();if(0===e.length)return;const t=e.join("/");let i=t;i.split("&").length>0&&(i=i.split("&")[0]),this._hashName&&(i=`${this._hashName}=${t}`);let o=window.location.hash.replace(i,"");o.startsWith("#&")?o=o.slice(0,1)+o.slice(2):"#"===o&&(o="");let r=window.location.href.replace(/(#.+)?$/,o);r=r.replace("&&","&"),window.history.replaceState(window.history.state,null,r);},this._updateHash=Sr(this._updateHashUnthrottled,300),this._hashName=e&&encodeURIComponent(e);}addTo(e){return this._map=e,addEventListener("hashchange",this._onHashChange,!1),this._map.on("moveend",this._updateHash),this}remove(){return removeEventListener("hashchange",this._onHashChange,!1),this._map.off("moveend",this._updateHash),clearTimeout(this._updateHash()),this._removeHash(),delete this._map,this}getHashString(e){const t=this._map.getCenter(),i=Math.round(100*this._map.getZoom())/100,o=Math.ceil((i*Math.LN2+Math.log(512/360/.5))/Math.LN10),r=Math.pow(10,o),a=Math.round(t.lng*r)/r,s=Math.round(t.lat*r)/r,n=this._map.getBearing(),l=this._map.getPitch();let c="";if(c+=e?`/${a}/${s}/${i}`:`${i}/${s}/${a}`,(n||l)&&(c+="/"+Math.round(10*n)/10),l&&(c+=`/${Math.round(l)}`),this._hashName){const e=this._hashName;let t=!1;const i=window.location.hash.slice(1).split("&").map((i=>{const o=i.split("=")[0];return o===e?(t=!0,`${o}=${c}`):i})).filter((e=>e));return t||i.push(`${e}=${c}`),`#${i.join("&")}`}return `#${c}`}_isValidHash(e){if(e.length<3||e.some(isNaN))return !1;try{new t.S(+e[2],+e[1]);}catch(e){return !1}const i=+e[0],o=+(e[3]||0),r=+(e[4]||0);return i>=this._map.getMinZoom()&&i<=this._map.getMaxZoom()&&o>=-180&&o<=180&&r>=this._map.getMinPitch()&&r<=this._map.getMaxPitch()}}const Rr={linearity:.3,easing:t.cm(0,0,.3,1)},zr=t.e({deceleration:2500,maxSpeed:1400},Rr),Dr=t.e({deceleration:20,maxSpeed:1400},Rr),Ar=t.e({deceleration:1e3,maxSpeed:360},Rr),Lr=t.e({deceleration:1e3,maxSpeed:90},Rr),kr=t.e({deceleration:1e3,maxSpeed:360},Rr);class Fr{constructor(e){this._map=e,this.clear();}clear(){this._inertiaBuffer=[];}record(e){this._drainInertiaBuffer(),this._inertiaBuffer.push({time:s.now(),settings:e});}_drainInertiaBuffer(){const e=this._inertiaBuffer,t=s.now();for(;e.length>0&&t-e[0].time>160;)e.shift();}_onMoveEnd(e){if(this._drainInertiaBuffer(),this._inertiaBuffer.length<2)return;const i={zoom:0,bearing:0,pitch:0,roll:0,pan:new t.P(0,0),pinchAround:void 0,around:void 0};for(const{settings:e}of this._inertiaBuffer)i.zoom+=e.zoomDelta||0,i.bearing+=e.bearingDelta||0,i.pitch+=e.pitchDelta||0,i.roll+=e.rollDelta||0,e.panDelta&&i.pan._add(e.panDelta),e.around&&(i.around=e.around),e.pinchAround&&(i.pinchAround=e.pinchAround);const o=this._inertiaBuffer[this._inertiaBuffer.length-1].time-this._inertiaBuffer[0].time,r={};if(i.pan.mag()){const a=Or(i.pan.mag(),o,t.e({},zr,e||{})),s=i.pan.mult(a.amount/i.pan.mag()),n=this._map.cameraHelper.handlePanInertia(s,this._map.transform);r.center=n.easingCenter,r.offset=n.easingOffset,Br(r,a);}if(i.zoom){const e=Or(i.zoom,o,Dr);r.zoom=this._map.transform.zoom+e.amount,Br(r,e);}if(i.bearing){const e=Or(i.bearing,o,Ar);r.bearing=this._map.transform.bearing+t.ah(e.amount,-179,179),Br(r,e);}if(i.pitch){const e=Or(i.pitch,o,Lr);r.pitch=this._map.transform.pitch+e.amount,Br(r,e);}if(i.roll){const e=Or(i.roll,o,kr);r.roll=this._map.transform.roll+t.ah(e.amount,-179,179),Br(r,e);}if(r.zoom||r.bearing){const e=void 0===i.pinchAround?i.around:i.pinchAround;r.around=e?this._map.unproject(e):this._map.getCenter();}return this.clear(),t.e(r,{noMoveStart:!0})}}function Br(e,t){(!e.duration||e.durationi.unproject(e))),l=a.reduce(((e,t,i,o)=>e.add(t.div(o.length))),new t.P(0,0));super(e,{points:a,point:l,lngLats:s,lngLat:i.unproject(l),originalEvent:o}),this._defaultPrevented=!1;}}class Ur extends t.l{preventDefault(){this._defaultPrevented=!0;}get defaultPrevented(){return this._defaultPrevented}constructor(e,t,i){super(e,{originalEvent:i}),this._defaultPrevented=!1;}}class Zr{constructor(e,t){this._map=e,this._clickTolerance=t.clickTolerance;}reset(){delete this._mousedownPos;}wheel(e){return this._firePreventable(new Ur(e.type,this._map,e))}mousedown(e,t){return this._mousedownPos=t,this._firePreventable(new jr(e.type,this._map,e))}mouseup(e){this._map.fire(new jr(e.type,this._map,e));}click(e,t){this._mousedownPos&&this._mousedownPos.dist(t)>=this._clickTolerance||this._map.fire(new jr(e.type,this._map,e));}dblclick(e){return this._firePreventable(new jr(e.type,this._map,e))}mouseover(e){this._map.fire(new jr(e.type,this._map,e));}mouseout(e){this._map.fire(new jr(e.type,this._map,e));}touchstart(e){return this._firePreventable(new Nr(e.type,this._map,e))}touchmove(e){this._map.fire(new Nr(e.type,this._map,e));}touchend(e){this._map.fire(new Nr(e.type,this._map,e));}touchcancel(e){this._map.fire(new Nr(e.type,this._map,e));}_firePreventable(e){if(this._map.fire(e),e.defaultPrevented)return {}}isEnabled(){return !0}isActive(){return !1}enable(){}disable(){}}class Gr{constructor(e){this._map=e;}reset(){this._delayContextMenu=!1,this._ignoreContextMenu=!0,delete this._contextMenuEvent;}mousemove(e){this._map.fire(new jr(e.type,this._map,e));}mousedown(){this._delayContextMenu=!0,this._ignoreContextMenu=!1;}mouseup(){this._delayContextMenu=!1,this._contextMenuEvent&&(this._map.fire(new jr("contextmenu",this._map,this._contextMenuEvent)),delete this._contextMenuEvent);}contextmenu(e){this._delayContextMenu?this._contextMenuEvent=e:this._ignoreContextMenu||this._map.fire(new jr(e.type,this._map,e)),this._map.listens("contextmenu")&&e.preventDefault();}isEnabled(){return !0}isActive(){return !1}enable(){}disable(){}}class Vr{constructor(e){this._map=e;}get transform(){return this._map._requestedCameraState||this._map.transform}get center(){return {lng:this.transform.center.lng,lat:this.transform.center.lat}}get zoom(){return this.transform.zoom}get pitch(){return this.transform.pitch}get bearing(){return this.transform.bearing}unproject(e){return this.transform.screenPointToLocation(t.P.convert(e),this._map.terrain)}}class $r{constructor(e,t){this._map=e,this._tr=new Vr(e),this._el=e.getCanvasContainer(),this._container=e.getContainer(),this._clickTolerance=t.clickTolerance||1;}isEnabled(){return !!this._enabled}isActive(){return !!this._active}enable(){this.isEnabled()||(this._enabled=!0);}disable(){this.isEnabled()&&(this._enabled=!1);}mousedown(e,t){this.isEnabled()&&e.shiftKey&&0===e.button&&(n.disableDrag(),this._startPos=this._lastPos=t,this._active=!0);}mousemoveWindow(e,t){if(!this._active)return;const i=t;if(this._lastPos.equals(i)||!this._box&&i.dist(this._startPos)e.fitScreenCoordinates(o,r,this._tr.bearing,{linear:!0})};this._fireEvent("boxzoomcancel",e);}keydown(e){this._active&&27===e.keyCode&&(this.reset(),this._fireEvent("boxzoomcancel",e));}reset(){this._active=!1,this._container.classList.remove("maplibregl-crosshair"),this._box&&(n.remove(this._box),this._box=null),n.enableDrag(),delete this._startPos,delete this._lastPos;}_fireEvent(e,i){return this._map.fire(new t.l(e,{originalEvent:i}))}}function qr(e,t){if(e.length!==t.length)throw new Error(`The number of touches and points are not equal - touches ${e.length}, points ${t.length}`);const i={};for(let o=0;othis.numTouches)&&(this.aborted=!0),this.aborted||(void 0===this.startTime&&(this.startTime=e.timeStamp),o.length===this.numTouches&&(this.centroid=function(e){const i=new t.P(0,0);for(const t of e)i._add(t);return i.div(e.length)}(i),this.touches=qr(o,i)));}touchmove(e,t,i){if(this.aborted||!this.centroid)return;const o=qr(i,t);for(const e in this.touches){const t=o[e];(!t||t.dist(this.touches[e])>30)&&(this.aborted=!0);}}touchend(e,t,i){if((!this.centroid||e.timeStamp-this.startTime>500)&&(this.aborted=!0),0===i.length){const e=!this.aborted&&this.centroid;if(this.reset(),e)return e}}}class Hr{constructor(e){this.singleTap=new Wr(e),this.numTaps=e.numTaps,this.reset();}reset(){this.lastTime=1/0,delete this.lastTap,this.count=0,this.singleTap.reset();}touchstart(e,t,i){this.singleTap.touchstart(e,t,i);}touchmove(e,t,i){this.singleTap.touchmove(e,t,i);}touchend(e,t,i){const o=this.singleTap.touchend(e,t,i);if(o){const t=e.timeStamp-this.lastTime<500,i=!this.lastTap||this.lastTap.dist(o)<30;if(t&&i||this.reset(),this.count++,this.lastTime=e.timeStamp,this.lastTap=o,this.count===this.numTaps)return this.reset(),o}}}class Xr{constructor(e){this._tr=new Vr(e),this._zoomIn=new Hr({numTouches:1,numTaps:2}),this._zoomOut=new Hr({numTouches:2,numTaps:1}),this.reset();}reset(){this._active=!1,this._zoomIn.reset(),this._zoomOut.reset();}touchstart(e,t,i){this._zoomIn.touchstart(e,t,i),this._zoomOut.touchstart(e,t,i);}touchmove(e,t,i){this._zoomIn.touchmove(e,t,i),this._zoomOut.touchmove(e,t,i);}touchend(e,t,i){const o=this._zoomIn.touchend(e,t,i),r=this._zoomOut.touchend(e,t,i),a=this._tr;return o?(this._active=!0,e.preventDefault(),setTimeout((()=>this.reset()),0),{cameraAnimation:t=>t.easeTo({duration:300,zoom:a.zoom+1,around:a.unproject(o)},{originalEvent:e})}):r?(this._active=!0,e.preventDefault(),setTimeout((()=>this.reset()),0),{cameraAnimation:t=>t.easeTo({duration:300,zoom:a.zoom-1,around:a.unproject(r)},{originalEvent:e})}):void 0}touchcancel(){this.reset();}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}}class Kr{constructor(e){this._enabled=!!e.enable,this._moveStateManager=e.moveStateManager,this._clickTolerance=e.clickTolerance||1,this._moveFunction=e.move,this._activateOnStart=!!e.activateOnStart,e.assignEvents(this),this.reset();}reset(e){this._active=!1,this._moved=!1,delete this._lastPoint,this._moveStateManager.endMove(e);}_move(...e){const t=this._moveFunction(...e);if(t.bearingDelta||t.pitchDelta||t.rollDelta||t.around||t.panDelta)return this._active=!0,t}dragStart(e,t){this.isEnabled()&&!this._lastPoint&&this._moveStateManager.isValidStartEvent(e)&&(this._moveStateManager.startMove(e),this._lastPoint=Array.isArray(t)?t[0]:t,this._activateOnStart&&this._lastPoint&&(this._active=!0));}dragMove(e,t){if(!this.isEnabled())return;const i=this._lastPoint;if(!i)return;if(e.preventDefault(),!this._moveStateManager.isValidMoveEvent(e))return void this.reset(e);const o=Array.isArray(t)?t[0]:t;return !this._moved&&o.dist(i)!0}),t=new ta){this.mouseMoveStateManager=e,this.oneFingerTouchMoveStateManager=t;}_executeRelevantHandler(e,t,i){return e instanceof MouseEvent?t(e):"undefined"!=typeof TouchEvent&&e instanceof TouchEvent?i(e):void 0}startMove(e){this._executeRelevantHandler(e,(e=>this.mouseMoveStateManager.startMove(e)),(e=>this.oneFingerTouchMoveStateManager.startMove(e)));}endMove(e){this._executeRelevantHandler(e,(e=>this.mouseMoveStateManager.endMove(e)),(e=>this.oneFingerTouchMoveStateManager.endMove(e)));}isValidStartEvent(e){return this._executeRelevantHandler(e,(e=>this.mouseMoveStateManager.isValidStartEvent(e)),(e=>this.oneFingerTouchMoveStateManager.isValidStartEvent(e)))}isValidMoveEvent(e){return this._executeRelevantHandler(e,(e=>this.mouseMoveStateManager.isValidMoveEvent(e)),(e=>this.oneFingerTouchMoveStateManager.isValidMoveEvent(e)))}isValidEndEvent(e){return this._executeRelevantHandler(e,(e=>this.mouseMoveStateManager.isValidEndEvent(e)),(e=>this.oneFingerTouchMoveStateManager.isValidEndEvent(e)))}}const oa=e=>{e.mousedown=e.dragStart,e.mousemoveWindow=e.dragMove,e.mouseup=e.dragEnd,e.contextmenu=e=>{e.preventDefault();};};class ra{constructor(e,t){this._clickTolerance=e.clickTolerance||1,this._map=t,this.reset();}reset(){this._active=!1,this._touches={},this._sum=new t.P(0,0);}_shouldBePrevented(e){return e<(this._map.cooperativeGestures.isEnabled()?2:1)}touchstart(e,t,i){return this._calculateTransform(e,t,i)}touchmove(e,t,i){if(this._active){if(!this._shouldBePrevented(i.length))return e.preventDefault(),this._calculateTransform(e,t,i);this._map.cooperativeGestures.notifyGestureBlocked("touch_pan",e);}}touchend(e,t,i){this._calculateTransform(e,t,i),this._active&&this._shouldBePrevented(i.length)&&this.reset();}touchcancel(){this.reset();}_calculateTransform(e,i,o){o.length>0&&(this._active=!0);const r=qr(o,i),a=new t.P(0,0),s=new t.P(0,0);let n=0;for(const e in r){const t=r[e],i=this._touches[e];i&&(a._add(t),s._add(t.sub(i)),n++,r[e]=t);}if(this._touches=r,this._shouldBePrevented(n)||!s.mag())return;const l=s.div(n);return this._sum._add(l),this._sum.mag()Math.abs(e.x)}class da extends aa{constructor(e){super(),this._currentTouchCount=0,this._map=e;}reset(){super.reset(),this._valid=void 0,delete this._firstMove,delete this._lastPoints;}touchstart(e,t,i){super.touchstart(e,t,i),this._currentTouchCount=i.length;}_start(e){this._lastPoints=e,ua(e[0].sub(e[1]))&&(this._valid=!1);}_move(e,t,i){if(this._map.cooperativeGestures.isEnabled()&&this._currentTouchCount<3)return;const o=e[0].sub(this._lastPoints[0]),r=e[1].sub(this._lastPoints[1]);return this._valid=this.gestureBeginsVertically(o,r,i.timeStamp),this._valid?(this._lastPoints=e,this._active=!0,{pitchDelta:(o.y+r.y)/2*-.5}):void 0}gestureBeginsVertically(e,t,i){if(void 0!==this._valid)return this._valid;const o=e.mag()>=2,r=t.mag()>=2;if(!o&&!r)return;if(!o||!r)return void 0===this._firstMove&&(this._firstMove=i),i-this._firstMove<100&&void 0;const a=e.y>0==t.y>0;return ua(e)&&ua(t)&&a}}const _a={panStep:100,bearingStep:15,pitchStep:10};class pa{constructor(e){this._tr=new Vr(e);const t=_a;this._panStep=t.panStep,this._bearingStep=t.bearingStep,this._pitchStep=t.pitchStep,this._rotationDisabled=!1;}reset(){this._active=!1;}keydown(e){if(e.altKey||e.ctrlKey||e.metaKey)return;let t=0,i=0,o=0,r=0,a=0;switch(e.keyCode){case 61:case 107:case 171:case 187:t=1;break;case 189:case 109:case 173:t=-1;break;case 37:e.shiftKey?i=-1:(e.preventDefault(),r=-1);break;case 39:e.shiftKey?i=1:(e.preventDefault(),r=1);break;case 38:e.shiftKey?o=1:(e.preventDefault(),a=-1);break;case 40:e.shiftKey?o=-1:(e.preventDefault(),a=1);break;default:return}return this._rotationDisabled&&(i=0,o=0),{cameraAnimation:s=>{const n=this._tr;s.easeTo({duration:300,easeId:"keyboardHandler",easing:ma,zoom:t?Math.round(n.zoom)+t*(e.shiftKey?2:1):n.zoom,bearing:n.bearing+i*this._bearingStep,pitch:n.pitch+o*this._pitchStep,offset:[-r*this._panStep,-a*this._panStep],center:n.center},{originalEvent:e});}}}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}disableRotation(){this._rotationDisabled=!0;}enableRotation(){this._rotationDisabled=!1;}}function ma(e){return e*(2-e)}const fa=4.000244140625,ga=1/450;class va{constructor(e,t){this._onTimeout=e=>{this._type="wheel",this._delta-=this._lastValue,this._active||this._start(e);},this._map=e,this._tr=new Vr(e),this._triggerRenderFrame=t,this._delta=0,this._defaultZoomRate=.01,this._wheelZoomRate=ga;}setZoomRate(e){this._defaultZoomRate=e;}setWheelZoomRate(e){this._wheelZoomRate=e;}isEnabled(){return !!this._enabled}isActive(){return !!this._active||void 0!==this._finishTimeout}isZooming(){return !!this._zooming}enable(e){this.isEnabled()||(this._enabled=!0,this._aroundCenter=!!e&&"center"===e.around);}disable(){this.isEnabled()&&(this._enabled=!1);}_shouldBePrevented(e){return !!this._map.cooperativeGestures.isEnabled()&&!(e.ctrlKey||this._map.cooperativeGestures.isBypassed(e))}wheel(e){if(!this.isEnabled())return;if(this._shouldBePrevented(e))return void this._map.cooperativeGestures.notifyGestureBlocked("wheel_zoom",e);let t=e.deltaMode===WheelEvent.DOM_DELTA_LINE?40*e.deltaY:e.deltaY;const i=s.now(),o=i-(this._lastWheelEventTime||0);this._lastWheelEventTime=i,0!==t&&t%fa==0?this._type="wheel":0!==t&&Math.abs(t)<4?this._type="trackpad":o>400?(this._type=null,this._lastValue=t,this._timeout=setTimeout(this._onTimeout,40,e)):this._type||(this._type=Math.abs(o*t)<200?"trackpad":"wheel",this._timeout&&(clearTimeout(this._timeout),this._timeout=null,t+=this._lastValue)),e.shiftKey&&t&&(t/=4),this._type&&(this._lastWheelEvent=e,this._delta-=t,this._active||this._start(e)),e.preventDefault();}_start(e){if(!this._delta)return;this._frameId&&(this._frameId=null),this._active=!0,this.isZooming()||(this._zooming=!0),this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout);const i=n.mousePos(this._map.getCanvas(),e),o=this._tr;this._aroundPoint=this._aroundCenter?o.transform.locationToScreenPoint(t.S.convert(o.center)):i,this._frameId||(this._frameId=!0,this._triggerRenderFrame());}renderFrame(){if(!this._frameId)return;if(this._frameId=null,!this.isActive())return;const e=this._tr.transform;if("number"==typeof this._lastExpectedZoom){const t=e.zoom-this._lastExpectedZoom;"number"==typeof this._startZoom&&(this._startZoom+=t),"number"==typeof this._targetZoom&&(this._targetZoom+=t);}if(0!==this._delta){const i="wheel"===this._type&&Math.abs(this._delta)>fa?this._wheelZoomRate:this._defaultZoomRate;let o=2/(1+Math.exp(-Math.abs(this._delta*i)));this._delta<0&&0!==o&&(o=1/o);const r="number"!=typeof this._targetZoom?e.scale:t.af(this._targetZoom);this._targetZoom=e.getConstrained(e.getCameraLngLat(),t.ak(r*o)).zoom,"wheel"===this._type&&(this._startZoom=e.zoom,this._easing=this._smoothOutEasing(200)),this._delta=0;}const i="number"!=typeof this._targetZoom?e.zoom:this._targetZoom,o=this._startZoom,r=this._easing;let a,n=!1;if("wheel"===this._type&&o&&r){const e=s.now()-this._lastWheelEventTime,l=Math.min((e+5)/200,1),c=r(l);a=t.C.number(o,i,c),l<1?this._frameId||(this._frameId=!0):n=!0;}else a=i,n=!0;return this._active=!0,n&&(this._active=!1,this._finishTimeout=setTimeout((()=>{this._zooming=!1,this._triggerRenderFrame(),delete this._targetZoom,delete this._lastExpectedZoom,delete this._finishTimeout;}),200)),this._lastExpectedZoom=a,{noInertia:!0,needsRenderFrame:!n,zoomDelta:a-e.zoom,around:this._aroundPoint,originalEvent:this._lastWheelEvent}}_smoothOutEasing(e){let i=t.co;if(this._prevEase){const e=this._prevEase,o=(s.now()-e.start)/e.duration,r=e.easing(o+.01)-e.easing(o),a=.27/Math.sqrt(r*r+1e-4)*.01,n=Math.sqrt(.0729-a*a);i=t.cm(a,n,.25,1);}return this._prevEase={start:s.now(),duration:e,easing:i},i}reset(){this._active=!1,this._zooming=!1,delete this._targetZoom,delete this._lastExpectedZoom,this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout);}}class ba{constructor(e,t){this._clickZoom=e,this._tapZoom=t;}enable(){this._clickZoom.enable(),this._tapZoom.enable();}disable(){this._clickZoom.disable(),this._tapZoom.disable();}isEnabled(){return this._clickZoom.isEnabled()&&this._tapZoom.isEnabled()}isActive(){return this._clickZoom.isActive()||this._tapZoom.isActive()}}class xa{constructor(e){this._tr=new Vr(e),this.reset();}reset(){this._active=!1;}dblclick(e,t){return e.preventDefault(),{cameraAnimation:i=>{i.easeTo({duration:300,zoom:this._tr.zoom+(e.shiftKey?-1:1),around:this._tr.unproject(t)},{originalEvent:e});}}}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}}class ya{constructor(){this._tap=new Hr({numTouches:1,numTaps:1}),this.reset();}reset(){this._active=!1,delete this._swipePoint,delete this._swipeTouch,delete this._tapTime,delete this._tapPoint,this._tap.reset();}touchstart(e,t,i){if(!this._swipePoint)if(this._tapTime){const o=t[0],r=e.timeStamp-this._tapTime<500,a=this._tapPoint.dist(o)<30;r&&a?i.length>0&&(this._swipePoint=o,this._swipeTouch=i[0].identifier):this.reset();}else this._tap.touchstart(e,t,i);}touchmove(e,t,i){if(this._tapTime){if(this._swipePoint){if(i[0].identifier!==this._swipeTouch)return;const o=t[0],r=o.y-this._swipePoint.y;return this._swipePoint=o,e.preventDefault(),this._active=!0,{zoomDelta:r/128}}}else this._tap.touchmove(e,t,i);}touchend(e,t,i){if(this._tapTime)this._swipePoint&&0===i.length&&this.reset();else {const o=this._tap.touchend(e,t,i);o&&(this._tapTime=e.timeStamp,this._tapPoint=o);}}touchcancel(){this.reset();}enable(){this._enabled=!0;}disable(){this._enabled=!1,this.reset();}isEnabled(){return this._enabled}isActive(){return this._active}}class wa{constructor(e,t,i){this._el=e,this._mousePan=t,this._touchPan=i;}enable(e){this._inertiaOptions=e||{},this._mousePan.enable(),this._touchPan.enable(),this._el.classList.add("maplibregl-touch-drag-pan");}disable(){this._mousePan.disable(),this._touchPan.disable(),this._el.classList.remove("maplibregl-touch-drag-pan");}isEnabled(){return this._mousePan.isEnabled()&&this._touchPan.isEnabled()}isActive(){return this._mousePan.isActive()||this._touchPan.isActive()}}class Ta{constructor(e,t,i,o){this._pitchWithRotate=e.pitchWithRotate,this._rollEnabled=e.rollEnabled,this._mouseRotate=t,this._mousePitch=i,this._mouseRoll=o;}enable(){this._mouseRotate.enable(),this._pitchWithRotate&&this._mousePitch.enable(),this._rollEnabled&&this._mouseRoll.enable();}disable(){this._mouseRotate.disable(),this._mousePitch.disable(),this._mouseRoll.disable();}isEnabled(){return this._mouseRotate.isEnabled()&&(!this._pitchWithRotate||this._mousePitch.isEnabled())&&(!this._rollEnabled||this._mouseRoll.isEnabled())}isActive(){return this._mouseRotate.isActive()||this._mousePitch.isActive()||this._mouseRoll.isActive()}}class Pa{constructor(e,t,i,o){this._el=e,this._touchZoom=t,this._touchRotate=i,this._tapDragZoom=o,this._rotationDisabled=!1,this._enabled=!0;}enable(e){this._touchZoom.enable(e),this._rotationDisabled||this._touchRotate.enable(e),this._tapDragZoom.enable(),this._el.classList.add("maplibregl-touch-zoom-rotate");}disable(){this._touchZoom.disable(),this._touchRotate.disable(),this._tapDragZoom.disable(),this._el.classList.remove("maplibregl-touch-zoom-rotate");}isEnabled(){return this._touchZoom.isEnabled()&&(this._rotationDisabled||this._touchRotate.isEnabled())&&this._tapDragZoom.isEnabled()}isActive(){return this._touchZoom.isActive()||this._touchRotate.isActive()||this._tapDragZoom.isActive()}disableRotation(){this._rotationDisabled=!0,this._touchRotate.disable();}enableRotation(){this._rotationDisabled=!1,this._touchZoom.isEnabled()&&this._touchRotate.enable();}}class Ca{constructor(e,t){this._bypassKey=-1!==navigator.userAgent.indexOf("Mac")?"metaKey":"ctrlKey",this._map=e,this._options=t,this._enabled=!1;}isActive(){return !1}reset(){}_setupUI(){if(this._container)return;const e=this._map.getCanvasContainer();e.classList.add("maplibregl-cooperative-gestures"),this._container=n.create("div","maplibregl-cooperative-gesture-screen",e);let t=this._map._getUIString("CooperativeGesturesHandler.WindowsHelpText");"metaKey"===this._bypassKey&&(t=this._map._getUIString("CooperativeGesturesHandler.MacHelpText"));const i=this._map._getUIString("CooperativeGesturesHandler.MobileHelpText"),o=document.createElement("div");o.className="maplibregl-desktop-message",o.textContent=t,this._container.appendChild(o);const r=document.createElement("div");r.className="maplibregl-mobile-message",r.textContent=i,this._container.appendChild(r),this._container.setAttribute("aria-hidden","true");}_destroyUI(){this._container&&(n.remove(this._container),this._map.getCanvasContainer().classList.remove("maplibregl-cooperative-gestures")),delete this._container;}enable(){this._setupUI(),this._enabled=!0;}disable(){this._enabled=!1,this._destroyUI();}isEnabled(){return this._enabled}isBypassed(e){return e[this._bypassKey]}notifyGestureBlocked(e,i){this._enabled&&(this._map.fire(new t.l("cooperativegestureprevented",{gestureType:e,originalEvent:i})),this._container.classList.add("maplibregl-show"),setTimeout((()=>{this._container.classList.remove("maplibregl-show");}),100));}}const Ia=e=>e.zoom||e.drag||e.roll||e.pitch||e.rotate;class Ma extends t.l{}function Sa(e){return e.panDelta&&e.panDelta.mag()||e.zoomDelta||e.bearingDelta||e.pitchDelta||e.rollDelta}class Ea{constructor(e,i){this.handleWindowEvent=e=>{this.handleEvent(e,`${e.type}Window`);},this.handleEvent=(e,i)=>{if("blur"===e.type)return void this.stop(!0);this._updatingCamera=!0;const o="renderFrame"===e.type?void 0:e,r={needsRenderFrame:!1},a={},s={};for(const{handlerName:l,handler:c,allowed:h}of this._handlers){if(!c.isEnabled())continue;let u;if(this._blockedByActive(s,h,l))c.reset();else if(c[i||e.type]){if(t.cp(e,i||e.type)){const t=n.mousePos(this._map.getCanvas(),e);u=c[i||e.type](e,t);}else if(t.cq(e,i||e.type)){const t=this._getMapTouches(e.touches),o=n.touchPos(this._map.getCanvas(),t);u=c[i||e.type](e,o,t);}else t.cr(i||e.type)||(u=c[i||e.type](e));this.mergeHandlerResult(r,a,u,l,o),u&&u.needsRenderFrame&&this._triggerRenderFrame();}(u||c.isActive())&&(s[l]=c);}const l={};for(const e in this._previousActiveHandlers)s[e]||(l[e]=o);this._previousActiveHandlers=s,(Object.keys(l).length||Sa(r))&&(this._changes.push([r,a,l]),this._triggerRenderFrame()),(Object.keys(s).length||Sa(r))&&this._map._stop(!0),this._updatingCamera=!1;const{cameraAnimation:c}=r;c&&(this._inertia.clear(),this._fireEvents({},{},!0),this._changes=[],c(this._map));},this._map=e,this._el=this._map.getCanvasContainer(),this._handlers=[],this._handlersById={},this._changes=[],this._inertia=new Fr(e),this._bearingSnap=i.bearingSnap,this._previousActiveHandlers={},this._eventsInProgress={},this._addDefaultHandlers(i);const o=this._el;this._listeners=[[o,"touchstart",{passive:!0}],[o,"touchmove",{passive:!1}],[o,"touchend",void 0],[o,"touchcancel",void 0],[o,"mousedown",void 0],[o,"mousemove",void 0],[o,"mouseup",void 0],[document,"mousemove",{capture:!0}],[document,"mouseup",void 0],[o,"mouseover",void 0],[o,"mouseout",void 0],[o,"dblclick",void 0],[o,"click",void 0],[o,"keydown",{capture:!1}],[o,"keyup",void 0],[o,"wheel",{passive:!1}],[o,"contextmenu",void 0],[window,"blur",void 0]];for(const[e,t,i]of this._listeners)n.addEventListener(e,t,e===document?this.handleWindowEvent:this.handleEvent,i);}destroy(){for(const[e,t,i]of this._listeners)n.removeEventListener(e,t,e===document?this.handleWindowEvent:this.handleEvent,i);}_addDefaultHandlers(e){const i=this._map,o=i.getCanvasContainer();this._add("mapEvent",new Zr(i,e));const r=i.boxZoom=new $r(i,e);this._add("boxZoom",r),e.interactive&&e.boxZoom&&r.enable();const a=i.cooperativeGestures=new Ca(i,e.cooperativeGestures);this._add("cooperativeGestures",a),e.cooperativeGestures&&a.enable();const s=new Xr(i),l=new xa(i);i.doubleClickZoom=new ba(l,s),this._add("tapZoom",s),this._add("clickZoom",l),e.interactive&&e.doubleClickZoom&&i.doubleClickZoom.enable();const c=new ya;this._add("tapDragZoom",c);const h=i.touchPitch=new da(i);this._add("touchPitch",h),e.interactive&&e.touchPitch&&i.touchPitch.enable(e.touchPitch);const u=()=>i.project(i.getCenter()),d=function({enable:e,clickTolerance:i,aroundCenter:o=!0,minPixelCenterThreshold:r=100,rotateDegreesPerPixelMoved:a=.8},s){const l=new ea({checkCorrectEvent:e=>0===n.mouseButton(e)&&e.ctrlKey||2===n.mouseButton(e)&&!e.ctrlKey});return new Kr({clickTolerance:i,move:(e,i)=>{const n=s();if(o&&Math.abs(n.y-e.y)>r)return {bearingDelta:t.cn(new t.P(e.x,i.y),i,n)};let l=(i.x-e.x)*a;return o&&i.y0===n.mouseButton(e)&&e.ctrlKey||2===n.mouseButton(e)});return new Kr({clickTolerance:t,move:(e,t)=>({pitchDelta:(t.y-e.y)*i}),moveStateManager:o,enable:e,assignEvents:oa})}(e),p=function({enable:e,clickTolerance:t,rollDegreesPerPixelMoved:i=.3},o){const r=new ea({checkCorrectEvent:e=>2===n.mouseButton(e)&&e.ctrlKey});return new Kr({clickTolerance:t,move:(e,t)=>{const r=o();let a=(t.x-e.x)*i;return t.y0===n.mouseButton(e)&&!e.ctrlKey});return new Kr({clickTolerance:t,move:(e,t)=>({around:t,panDelta:t.sub(e)}),activateOnStart:!0,moveStateManager:i,enable:e,assignEvents:oa})}(e),f=new ra(e,i);i.dragPan=new wa(o,m,f),this._add("mousePan",m),this._add("touchPan",f,["touchZoom","touchRotate"]),e.interactive&&e.dragPan&&i.dragPan.enable(e.dragPan);const g=new ha,v=new la;i.touchZoomRotate=new Pa(o,v,g,c),this._add("touchRotate",g,["touchPan","touchZoom"]),this._add("touchZoom",v,["touchPan","touchRotate"]),e.interactive&&e.touchZoomRotate&&i.touchZoomRotate.enable(e.touchZoomRotate);const b=i.scrollZoom=new va(i,(()=>this._triggerRenderFrame()));this._add("scrollZoom",b,["mousePan"]),e.interactive&&e.scrollZoom&&i.scrollZoom.enable(e.scrollZoom);const x=i.keyboard=new pa(i);this._add("keyboard",x),e.interactive&&e.keyboard&&i.keyboard.enable(),this._add("blockableMapEvent",new Gr(i));}_add(e,t,i){this._handlers.push({handlerName:e,handler:t,allowed:i}),this._handlersById[e]=t;}stop(e){if(!this._updatingCamera){for(const{handler:e}of this._handlers)e.reset();this._inertia.clear(),this._fireEvents({},{},e),this._changes=[];}}isActive(){for(const{handler:e}of this._handlers)if(e.isActive())return !0;return !1}isZooming(){return !!this._eventsInProgress.zoom||this._map.scrollZoom.isZooming()}isRotating(){return !!this._eventsInProgress.rotate}isMoving(){return Boolean(Ia(this._eventsInProgress))||this.isZooming()}_blockedByActive(e,t,i){for(const o in e)if(o!==i&&(!t||t.indexOf(o)<0))return !0;return !1}_getMapTouches(e){const t=[];for(const i of e)this._el.contains(i.target)&&t.push(i);return t}mergeHandlerResult(e,i,o,r,a){if(!o)return;t.e(e,o);const s={handlerName:r,originalEvent:o.originalEvent||a};void 0!==o.zoomDelta&&(i.zoom=s),void 0!==o.panDelta&&(i.drag=s),void 0!==o.rollDelta&&(i.roll=s),void 0!==o.pitchDelta&&(i.pitch=s),void 0!==o.bearingDelta&&(i.rotate=s);}_applyChanges(){const e={},i={},o={};for(const[r,a,s]of this._changes)r.panDelta&&(e.panDelta=(e.panDelta||new t.P(0,0))._add(r.panDelta)),r.zoomDelta&&(e.zoomDelta=(e.zoomDelta||0)+r.zoomDelta),r.bearingDelta&&(e.bearingDelta=(e.bearingDelta||0)+r.bearingDelta),r.pitchDelta&&(e.pitchDelta=(e.pitchDelta||0)+r.pitchDelta),r.rollDelta&&(e.rollDelta=(e.rollDelta||0)+r.rollDelta),void 0!==r.around&&(e.around=r.around),void 0!==r.pinchAround&&(e.pinchAround=r.pinchAround),r.noInertia&&(e.noInertia=r.noInertia),t.e(i,a),t.e(o,s);this._updateMapTransform(e,i,o),this._changes=[];}_updateMapTransform(e,t,i){const o=this._map,r=o._getTransformForUpdate(),a=o.terrain;if(!(Sa(e)||a&&this._terrainMovement))return this._fireEvents(t,i,!0);o._stop(!0);let{panDelta:s,zoomDelta:n,bearingDelta:l,pitchDelta:c,rollDelta:h,around:u,pinchAround:d}=e;void 0!==d&&(u=d),u=u||o.transform.centerPoint,a&&!r.isPointOnMapSurface(u)&&(u=r.centerPoint);const _={panDelta:s,zoomDelta:n,rollDelta:h,pitchDelta:c,bearingDelta:l,around:u};this._map.cameraHelper.useGlobeControls&&!r.isPointOnMapSurface(u)&&(u=r.centerPoint);const p=u.distSqr(r.centerPoint)<.01?r.center:r.screenPointToLocation(s?u.sub(s):u);a?(this._map.cameraHelper.handleMapControlsRollPitchBearingZoom(_,r),this._terrainMovement||!t.drag&&!t.zoom?t.drag&&this._terrainMovement?r.setCenter(r.screenPointToLocation(r.centerPoint.sub(s))):this._map.cameraHelper.handleMapControlsPan(_,r,p):(this._terrainMovement=!0,this._map._elevationFreeze=!0,this._map.cameraHelper.handleMapControlsPan(_,r,p))):(this._map.cameraHelper.handleMapControlsRollPitchBearingZoom(_,r),this._map.cameraHelper.handleMapControlsPan(_,r,p)),o._applyUpdatedTransform(r),this._map._update(),e.noInertia||this._inertia.record(e),this._fireEvents(t,i,!0);}_fireEvents(e,i,o){const r=Ia(this._eventsInProgress),a=Ia(e),n={};for(const t in e){const{originalEvent:i}=e[t];this._eventsInProgress[t]||(n[`${t}start`]=i),this._eventsInProgress[t]=e[t];}!r&&a&&this._fireEvent("movestart",a.originalEvent);for(const e in n)this._fireEvent(e,n[e]);a&&this._fireEvent("move",a.originalEvent);for(const t in e){const{originalEvent:i}=e[t];this._fireEvent(t,i);}const l={};let c;for(const e in this._eventsInProgress){const{handlerName:t,originalEvent:o}=this._eventsInProgress[e];this._handlersById[t].isActive()||(delete this._eventsInProgress[e],c=i[t]||o,l[`${e}end`]=c);}for(const e in l)this._fireEvent(e,l[e]);const h=Ia(this._eventsInProgress),u=(r||a)&&!h;if(u&&this._terrainMovement){this._map._elevationFreeze=!1,this._terrainMovement=!1;const e=this._map._getTransformForUpdate();this._map.getCenterClampedToGround()&&e.recalculateZoomAndCenter(this._map.terrain),this._map._applyUpdatedTransform(e);}if(o&&u){this._updatingCamera=!0;const e=this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions),i=e=>0!==e&&-this._bearingSnap{delete this._frameId,this.handleEvent(new Ma("renderFrame",{timeStamp:e})),this._applyChanges();}))}_triggerRenderFrame(){void 0===this._frameId&&(this._frameId=this._requestFrame());}}class Ra extends t.E{constructor(e,t,i){super(),this._renderFrameCallback=()=>{const e=Math.min((s.now()-this._easeStart)/this._easeOptions.duration,1);this._onEaseFrame(this._easeOptions.easing(e)),e<1&&this._easeFrameId?this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback):this.stop();},this._moving=!1,this._zooming=!1,this.transform=e,this._bearingSnap=i.bearingSnap,this.cameraHelper=t,this.on("moveend",(()=>{delete this._requestedCameraState;}));}migrateProjection(e,t){e.apply(this.transform),this.transform=e,this.cameraHelper=t;}getCenter(){return new t.S(this.transform.center.lng,this.transform.center.lat)}setCenter(e,t){return this.jumpTo({center:e},t)}getCenterElevation(){return this.transform.elevation}setCenterElevation(e,t){return this.jumpTo({elevation:e},t),this}getCenterClampedToGround(){return this._centerClampedToGround}setCenterClampedToGround(e){this._centerClampedToGround=e;}panBy(e,i,o){return e=t.P.convert(e).mult(-1),this.panTo(this.transform.center,t.e({offset:e},i),o)}panTo(e,i,o){return this.easeTo(t.e({center:e},i),o)}getZoom(){return this.transform.zoom}setZoom(e,t){return this.jumpTo({zoom:e},t),this}zoomTo(e,i,o){return this.easeTo(t.e({zoom:e},i),o)}zoomIn(e,t){return this.zoomTo(this.getZoom()+1,e,t),this}zoomOut(e,t){return this.zoomTo(this.getZoom()-1,e,t),this}getVerticalFieldOfView(){return this.transform.fov}setVerticalFieldOfView(e,i){return e!=this.transform.fov&&(this.transform.setFov(e),this.fire(new t.l("movestart",i)).fire(new t.l("move",i)).fire(new t.l("moveend",i))),this}getBearing(){return this.transform.bearing}setBearing(e,t){return this.jumpTo({bearing:e},t),this}getPadding(){return this.transform.padding}setPadding(e,t){return this.jumpTo({padding:e},t),this}rotateTo(e,i,o){return this.easeTo(t.e({bearing:e},i),o)}resetNorth(e,i){return this.rotateTo(0,t.e({duration:1e3},e),i),this}resetNorthPitch(e,i){return this.easeTo(t.e({bearing:0,pitch:0,roll:0,duration:1e3},e),i),this}snapToNorth(e,t){return Math.abs(this.getBearing()){f.easeFunc(t),this.terrain&&!e.freezeElevation&&this._updateElevation(t),this._applyUpdatedTransform(o),this._fireMoveEvents(i);}),(t=>{this.terrain&&e.freezeElevation&&this._finalizeElevation(),this._afterEase(i,t);}),e),this}_prepareEase(e,i,o={}){this._moving=!0,i||o.moving||this.fire(new t.l("movestart",e)),this._zooming&&!o.zooming&&this.fire(new t.l("zoomstart",e)),this._rotating&&!o.rotating&&this.fire(new t.l("rotatestart",e)),this._pitching&&!o.pitching&&this.fire(new t.l("pitchstart",e)),this._rolling&&!o.rolling&&this.fire(new t.l("rollstart",e));}_prepareElevation(e){this._elevationCenter=e,this._elevationStart=this.transform.elevation,this._elevationTarget=this.terrain.getElevationForLngLatZoom(e,this.transform.tileZoom),this._elevationFreeze=!0;}_updateElevation(e){void 0!==this._elevationStart&&void 0!==this._elevationCenter||this._prepareElevation(this.transform.center),this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom));const i=this.terrain.getElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom);if(e<1&&i!==this._elevationTarget){const t=this._elevationTarget-this._elevationStart;this._elevationStart+=e*(t-(i-(t*e+this._elevationStart))/(1-e)),this._elevationTarget=i;}this.transform.setElevation(t.C.number(this._elevationStart,this._elevationTarget,e));}_finalizeElevation(){this._elevationFreeze=!1,this.getCenterClampedToGround()&&this.transform.recalculateZoomAndCenter(this.terrain);}_getTransformForUpdate(){return this.transformCameraUpdate||this.terrain?(this._requestedCameraState||(this._requestedCameraState=this.transform.clone()),this._requestedCameraState):this.transform}_elevateCameraIfInsideTerrain(e){if(!this.terrain&&e.elevation>=0&&e.pitch<=90)return {};const t=e.getCameraLngLat(),i=e.getCameraAltitude(),o=this.terrain?this.terrain.getElevationForLngLatZoom(t,e.zoom):0;if(ithis._elevateCameraIfInsideTerrain(e))),this.transformCameraUpdate&&t.push((e=>this.transformCameraUpdate(e))),!t.length)return;const i=e.clone();for(const e of t){const t=i.clone(),{center:o,zoom:r,roll:a,pitch:s,bearing:n,elevation:l}=e(t);o&&t.setCenter(o),void 0!==l&&t.setElevation(l),void 0!==r&&t.setZoom(r),void 0!==a&&t.setRoll(a),void 0!==s&&t.setPitch(s),void 0!==n&&t.setBearing(n),i.apply(t);}this.transform.apply(i);}_fireMoveEvents(e){this.fire(new t.l("move",e)),this._zooming&&this.fire(new t.l("zoom",e)),this._rotating&&this.fire(new t.l("rotate",e)),this._pitching&&this.fire(new t.l("pitch",e)),this._rolling&&this.fire(new t.l("roll",e));}_afterEase(e,i){if(this._easeId&&i&&this._easeId===i)return;delete this._easeId;const o=this._zooming,r=this._rotating,a=this._pitching,s=this._rolling;this._moving=!1,this._zooming=!1,this._rotating=!1,this._pitching=!1,this._rolling=!1,this._padding=!1,o&&this.fire(new t.l("zoomend",e)),r&&this.fire(new t.l("rotateend",e)),a&&this.fire(new t.l("pitchend",e)),s&&this.fire(new t.l("rollend",e)),this.fire(new t.l("moveend",e));}flyTo(e,i){if(!e.essential&&s.prefersReducedMotion){const o=t.Q(e,["center","zoom","bearing","pitch","roll","elevation"]);return this.jumpTo(o,i)}this.stop(),e=t.e({offset:[0,0],speed:1.2,curve:1.42,easing:t.co},e);const o=this._getTransformForUpdate(),r=o.bearing,a=o.pitch,n=o.roll,l=o.padding,c="bearing"in e?this._normalizeBearing(e.bearing,r):r,h="pitch"in e?+e.pitch:a,u="roll"in e?this._normalizeBearing(e.roll,n):n,d="padding"in e?e.padding:o.padding,_=t.P.convert(e.offset);let p=o.centerPoint.add(_);const m=o.screenPointToLocation(p),f=this.cameraHelper.handleFlyTo(o,{bearing:c,pitch:h,roll:u,padding:d,locationAtOffset:m,offsetAsPoint:_,center:e.center,minZoom:e.minZoom,zoom:e.zoom});let g=e.curve;const v=Math.max(o.width,o.height),b=v/f.scaleOfZoom,x=f.pixelPathLength;"number"==typeof f.scaleOfMinZoom&&(g=Math.sqrt(v/f.scaleOfMinZoom/x*2));const y=g*g;function w(e){const t=(b*b-v*v+(e?-1:1)*y*y*x*x)/(2*(e?b:v)*y*x);return Math.log(Math.sqrt(t*t+1)-t)}function T(e){return (Math.exp(e)-Math.exp(-e))/2}function P(e){return (Math.exp(e)+Math.exp(-e))/2}const C=w(!1);let I=function(e){return P(C)/P(C+g*e)},M=function(e){return v*((P(C)*(T(t=C+g*e)/P(t))-T(C))/y)/x;var t;},S=(w(!0)-C)/g;if(Math.abs(x)<2e-6||!isFinite(S)){if(Math.abs(v-b)<1e-6)return this.easeTo(e,i);const t=b0,I=e=>Math.exp(t*g*e);}return e.duration="duration"in e?+e.duration:1e3*S/("screenSpeed"in e?+e.screenSpeed/g:+e.speed),e.maxDuration&&e.duration>e.maxDuration&&(e.duration=0),this._zooming=!0,this._rotating=r!==c,this._pitching=h!==a,this._rolling=u!==n,this._padding=!o.isPaddingEqual(d),this._prepareEase(i,!1),this.terrain&&this._prepareElevation(f.targetCenter),this._ease((s=>{const m=s*S,g=1/I(m),v=M(m);this._rotating&&o.setBearing(t.C.number(r,c,s)),this._pitching&&o.setPitch(t.C.number(a,h,s)),this._rolling&&o.setRoll(t.C.number(n,u,s)),this._padding&&(o.interpolatePadding(l,d,s),p=o.centerPoint.add(_)),f.easeFunc(s,g,v,p),this.terrain&&!e.freezeElevation&&this._updateElevation(s),this._applyUpdatedTransform(o),this._fireMoveEvents(i);}),(()=>{this.terrain&&e.freezeElevation&&this._finalizeElevation(),this._afterEase(i);}),e),this}isEasing(){return !!this._easeFrameId}stop(){return this._stop()}_stop(e,t){var i;if(this._easeFrameId&&(this._cancelRenderFrame(this._easeFrameId),delete this._easeFrameId,delete this._onEaseFrame),this._onEaseEnd){const e=this._onEaseEnd;delete this._onEaseEnd,e.call(this,t);}return e||null===(i=this.handlers)||void 0===i||i.stop(!1),this}_ease(e,t,i){!1===i.animate||0===i.duration?(e(1),t()):(this._easeStart=s.now(),this._easeOptions=i,this._onEaseFrame=e,this._onEaseEnd=t,this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback));}_normalizeBearing(e,i){e=t.aO(e,-180,180);const o=Math.abs(e-i);return Math.abs(e-360-i)MapLibre'};class Da{constructor(e=za){this._toggleAttribution=()=>{this._container.classList.contains("maplibregl-compact")&&(this._container.classList.contains("maplibregl-compact-show")?(this._container.setAttribute("open",""),this._container.classList.remove("maplibregl-compact-show")):(this._container.classList.add("maplibregl-compact-show"),this._container.removeAttribute("open")));},this._updateData=e=>{!e||"metadata"!==e.sourceDataType&&"visibility"!==e.sourceDataType&&"style"!==e.dataType&&"terrain"!==e.type||this._updateAttributions();},this._updateCompact=()=>{this._map.getCanvasContainer().offsetWidth<=640||this._compact?!1===this._compact?this._container.setAttribute("open",""):this._container.classList.contains("maplibregl-compact")||this._container.classList.contains("maplibregl-attrib-empty")||(this._container.setAttribute("open",""),this._container.classList.add("maplibregl-compact","maplibregl-compact-show")):(this._container.setAttribute("open",""),this._container.classList.contains("maplibregl-compact")&&this._container.classList.remove("maplibregl-compact","maplibregl-compact-show"));},this._updateCompactMinimize=()=>{this._container.classList.contains("maplibregl-compact")&&this._container.classList.contains("maplibregl-compact-show")&&this._container.classList.remove("maplibregl-compact-show");},this.options=e;}getDefaultPosition(){return "bottom-right"}onAdd(e){return this._map=e,this._compact=this.options.compact,this._container=n.create("details","maplibregl-ctrl maplibregl-ctrl-attrib"),this._compactButton=n.create("summary","maplibregl-ctrl-attrib-button",this._container),this._compactButton.addEventListener("click",this._toggleAttribution),this._setElementTitle(this._compactButton,"ToggleAttribution"),this._innerContainer=n.create("div","maplibregl-ctrl-attrib-inner",this._container),this._updateAttributions(),this._updateCompact(),this._map.on("styledata",this._updateData),this._map.on("sourcedata",this._updateData),this._map.on("terrain",this._updateData),this._map.on("resize",this._updateCompact),this._map.on("drag",this._updateCompactMinimize),this._container}onRemove(){n.remove(this._container),this._map.off("styledata",this._updateData),this._map.off("sourcedata",this._updateData),this._map.off("terrain",this._updateData),this._map.off("resize",this._updateCompact),this._map.off("drag",this._updateCompactMinimize),this._map=void 0,this._compact=void 0,this._attribHTML=void 0;}_setElementTitle(e,t){const i=this._map._getUIString(`AttributionControl.${t}`);e.title=i,e.setAttribute("aria-label",i);}_updateAttributions(){if(!this._map.style)return;let e=[];if(this.options.customAttribution&&(Array.isArray(this.options.customAttribution)?e=e.concat(this.options.customAttribution.map((e=>"string"!=typeof e?"":e))):"string"==typeof this.options.customAttribution&&e.push(this.options.customAttribution)),this._map.style.stylesheet){const e=this._map.style.stylesheet;this.styleOwner=e.owner,this.styleId=e.id;}const t=this._map.style.sourceCaches;for(const i in t){const o=t[i];if(o.used||o.usedForTerrain){const t=o.getSource();t.attribution&&e.indexOf(t.attribution)<0&&e.push(t.attribution);}}e=e.filter((e=>String(e).trim())),e.sort(((e,t)=>e.length-t.length)),e=e.filter(((t,i)=>{for(let o=i+1;o=0)return !1;return !0}));const i=e.join(" | ");i!==this._attribHTML&&(this._attribHTML=i,e.length?(this._innerContainer.innerHTML=n.sanitize(i),this._container.classList.remove("maplibregl-attrib-empty")):this._container.classList.add("maplibregl-attrib-empty"),this._updateCompact(),this._editLink=null);}}class Aa{constructor(e={}){this._updateCompact=()=>{const e=this._container.children;if(e.length){const t=e[0];this._map.getCanvasContainer().offsetWidth<=640||this._compact?!1!==this._compact&&t.classList.add("maplibregl-compact"):t.classList.remove("maplibregl-compact");}},this.options=e;}getDefaultPosition(){return "bottom-left"}onAdd(e){this._map=e,this._compact=this.options&&this.options.compact,this._container=n.create("div","maplibregl-ctrl");const t=n.create("a","maplibregl-ctrl-logo");return t.target="_blank",t.rel="noopener nofollow",t.href="https://maplibre.org/",t.setAttribute("aria-label",this._map._getUIString("LogoControl.Title")),t.setAttribute("rel","noopener nofollow"),this._container.appendChild(t),this._container.style.display="block",this._map.on("resize",this._updateCompact),this._updateCompact(),this._container}onRemove(){n.remove(this._container),this._map.off("resize",this._updateCompact),this._map=void 0,this._compact=void 0;}}class La{constructor(){this._queue=[],this._id=0,this._cleared=!1,this._currentlyRunning=!1;}add(e){const t=++this._id;return this._queue.push({callback:e,id:t,cancelled:!1}),t}remove(e){const t=this._currentlyRunning,i=t?this._queue.concat(t):this._queue;for(const t of i)if(t.id===e)return void(t.cancelled=!0)}run(e=0){if(this._currentlyRunning)throw new Error("Attempting to run(), but is already running.");const t=this._currentlyRunning=this._queue;this._queue=[];for(const i of t)if(!i.cancelled&&(i.callback(e),this._cleared))break;this._cleared=!1,this._currentlyRunning=!1;}clear(){this._currentlyRunning&&(this._cleared=!0),this._queue=[];}}var ka=t.aJ([{name:"a_pos3d",type:"Int16",components:3}]);class Fa extends t.E{constructor(e){super(),this._lastTilesetChange=s.now(),this.sourceCache=e,this._tiles={},this._renderableTilesKeys=[],this._sourceTileCache={},this.minzoom=0,this.maxzoom=22,this.deltaZoom=1,this.tileSize=e._source.tileSize*2**this.deltaZoom,e.usedForTerrain=!0,e.tileSize=this.tileSize;}destruct(){this.sourceCache.usedForTerrain=!1,this.sourceCache.tileSize=null;}update(e,i){this.sourceCache.update(e,i),this._renderableTilesKeys=[];const o={};for(const r of ve(e,{tileSize:this.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,reparseOverscaled:!1,terrain:i,calculateTileZoom:this.sourceCache._source.calculateTileZoom}))o[r.key]=!0,this._renderableTilesKeys.push(r.key),this._tiles[r.key]||(r.terrainRttPosMatrix32f=new Float64Array(16),t.bY(r.terrainRttPosMatrix32f,0,t.$,t.$,0,0,1),this._tiles[r.key]=new re(r,this.tileSize),this._lastTilesetChange=s.now());for(const e in this._tiles)o[e]||delete this._tiles[e];}freeRtt(e){for(const t in this._tiles){const i=this._tiles[t];(!e||i.tileID.equals(e)||i.tileID.isChildOf(e)||e.isChildOf(i.tileID))&&(i.rtt=[]);}}getRenderableTiles(){return this._renderableTilesKeys.map((e=>this.getTileByID(e)))}getTileByID(e){return this._tiles[e]}getTerrainCoords(e,t){return t?this._getTerrainCoordsForTileRanges(e,t):this._getTerrainCoordsForRegularTile(e)}_getTerrainCoordsForRegularTile(e){const i={};for(const o of this._renderableTilesKeys){const r=this._tiles[o].tileID,a=e.clone(),s=t.ba();if(r.canonical.equals(e.canonical))t.bY(s,0,t.$,t.$,0,0,1);else if(r.canonical.isChildOf(e.canonical)){const i=r.canonical.z-e.canonical.z,o=r.canonical.x-(r.canonical.x>>i<>i<>i;t.bY(s,0,n,n,0,0,1),t.M(s,s,[-o*n,-a*n,0]);}else {if(!e.canonical.isChildOf(r.canonical))continue;{const i=e.canonical.z-r.canonical.z,o=e.canonical.x-(e.canonical.x>>i<>i<>i;t.bY(s,0,t.$,t.$,0,0,1),t.M(s,s,[o*n,a*n,0]),t.N(s,s,[1/2**i,1/2**i,0]);}}a.terrainRttPosMatrix32f=new Float32Array(s),i[o]=a;}return i}_getTerrainCoordsForTileRanges(e,i){const o={};for(const r of this._renderableTilesKeys){const a=this._tiles[r].tileID;if(!this._isWithinTileRanges(a,i))continue;const s=e.clone(),n=t.ba();if(a.canonical.z===e.canonical.z){const i=e.canonical.x-a.canonical.x,o=e.canonical.y-a.canonical.y;t.bY(n,0,t.$,t.$,0,0,1),t.M(n,n,[i*t.$,o*t.$,0]);}else if(a.canonical.z>e.canonical.z){const i=a.canonical.z-e.canonical.z,o=a.canonical.x-(a.canonical.x>>i<>i<>i),l=e.canonical.y-(a.canonical.y>>i),c=t.$>>i;t.bY(n,0,c,c,0,0,1),t.M(n,n,[-o*c+s*t.$,-r*c+l*t.$,0]);}else {const i=e.canonical.z-a.canonical.z,o=e.canonical.x-(e.canonical.x>>i<>i<>i)-a.canonical.x,l=(e.canonical.y>>i)-a.canonical.y,c=t.$<i.maxzoom&&(o=i.maxzoom),o=i.minzoom&&(!r||!r.dem);)r=this.sourceCache.getTileByID(e.scaledTo(o--).key);return r}anyTilesAfterTime(e=Date.now()){return this._lastTilesetChange>=e}_isWithinTileRanges(e,t){return t[e.canonical.z]&&e.canonical.x>=t[e.canonical.z].minTileX&&e.canonical.x<=t[e.canonical.z].maxTileX&&e.canonical.y>=t[e.canonical.z].minTileY&&e.canonical.y<=t[e.canonical.z].maxTileY}}class Ba{constructor(e,t,i){this._meshCache={},this.painter=e,this.sourceCache=new Fa(t),this.options=i,this.exaggeration="number"==typeof i.exaggeration?i.exaggeration:1,this.qualityFactor=2,this.meshSize=128,this._demMatrixCache={},this.coordsIndex=[],this._coordsTextureSize=1024;}getDEMElevation(e,i,o,r=t.$){var a;if(!(i>=0&&i=0&&oe.canonical.z&&(e.canonical.z>=o?r=e.canonical.z-o:t.w("cannot calculate elevation if elevation maxzoom > source.maxzoom"));const a=e.canonical.x-(e.canonical.x>>r<>r<>8<<4|e>>8,i[t+3]=0;const o=new t.R({width:this._coordsTextureSize,height:this._coordsTextureSize},new Uint8Array(i.buffer)),r=new t.T(e,o,e.gl.RGBA,{premultiply:!1});return r.bind(e.gl.NEAREST,e.gl.CLAMP_TO_EDGE),this._coordsTexture=r,r}pointCoordinate(e){this.painter.maybeDrawDepthAndCoords(!0);const i=new Uint8Array(4),o=this.painter.context,r=o.gl,a=Math.round(e.x*this.painter.pixelRatio/devicePixelRatio),s=Math.round(e.y*this.painter.pixelRatio/devicePixelRatio),n=Math.round(this.painter.height/devicePixelRatio);o.bindFramebuffer.set(this.getFramebuffer("coords").framebuffer),r.readPixels(a,n-s-1,1,1,r.RGBA,r.UNSIGNED_BYTE,i),o.bindFramebuffer.set(null);const l=i[0]+(i[2]>>4<<8),c=i[1]+((15&i[2])<<8),h=this.coordsIndex[255-i[3]],u=h&&this.sourceCache.getTileByID(h);if(!u)return null;const d=this._coordsTextureSize,_=(1<0,r=o&&0===e.canonical.y,a=o&&e.canonical.y===(1<e.id!==t)),this._recentlyUsed.push(e.id);}stampObject(e){e.stamp=++this._stamp;}getOrCreateFreeObject(){for(const e of this._recentlyUsed)if(!this._objects[e].inUse)return this._objects[e];if(this._objects.length>=this._size)throw new Error("No free RenderPool available, call freeAllObjects() required!");const e=this._createObject(this._objects.length);return this._objects.push(e),e}freeObject(e){e.inUse=!1;}freeAllObjects(){for(const e of this._objects)this.freeObject(e);}isFull(){return !(this._objects.length!e.inUse))}}const ja={background:!0,fill:!0,line:!0,raster:!0,hillshade:!0,"color-relief":!0};class Na{constructor(e,t){this.painter=e,this.terrain=t,this.pool=new Oa(e.context,30,t.sourceCache.tileSize*t.qualityFactor);}destruct(){this.pool.destruct();}getTexture(e){return this.pool.getObjectForId(e.rtt[this._stacks.length-1].id).texture}prepareForRender(e,t){this._stacks=[],this._prevType=null,this._rttTiles=[],this._renderableTiles=this.terrain.sourceCache.getRenderableTiles(),this._renderableLayerIds=e._order.filter((i=>!e._layers[i].isHidden(t))),this._coordsAscending={};for(const t in e.sourceCaches){this._coordsAscending[t]={};const i=e.sourceCaches[t].getVisibleCoordinates(),o=e.sourceCaches[t].getSource(),r=o instanceof X?o.terrainTileRanges:null;for(const e of i){const i=this.terrain.sourceCache.getTerrainCoords(e,r);for(const e in i)this._coordsAscending[t][e]||(this._coordsAscending[t][e]=[]),this._coordsAscending[t][e].push(i[e]);}}this._coordsAscendingStr={};for(const t of e._order){const i=e._layers[t],o=i.source;if(ja[i.type]&&!this._coordsAscendingStr[o]){this._coordsAscendingStr[o]={};for(const e in this._coordsAscending[o])this._coordsAscendingStr[o][e]=this._coordsAscending[o][e].map((e=>e.key)).sort().join();}}for(const e of this._renderableTiles)for(const t in this._coordsAscendingStr){const i=this._coordsAscendingStr[t][e.tileID.key];i&&i!==e.rttCoords[t]&&(e.rtt=[]);}}renderLayer(e,i){if(e.isHidden(this.painter.transform.zoom))return !1;const o=Object.assign(Object.assign({},i),{isRenderingToTexture:!0}),r=e.type,a=this.painter,s=this._renderableLayerIds[this._renderableLayerIds.length-1]===e.id;if(ja[r]&&(this._prevType&&ja[this._prevType]||this._stacks.push([]),this._prevType=r,this._stacks[this._stacks.length-1].push(e.id),!s))return !0;if(ja[this._prevType]||ja[r]&&s){this._prevType=r;const e=this._stacks.length-1,i=this._stacks[e]||[];for(const r of this._renderableTiles){if(this.pool.isFull()&&(Cr(this.painter,this.terrain,this._rttTiles,o),this._rttTiles=[],this.pool.freeAllObjects()),this._rttTiles.push(r),r.rtt[e]){const t=this.pool.getObjectForId(r.rtt[e].id);if(t.stamp===r.rtt[e].stamp){this.pool.useObject(t);continue}}const s=this.pool.getOrCreateFreeObject();this.pool.useObject(s),this.pool.stampObject(s),r.rtt[e]={id:s.id,stamp:s.stamp},a.context.bindFramebuffer.set(s.fbo.framebuffer),a.context.clear({color:t.bf.transparent,stencil:0}),a.currentStencilSource=void 0;for(let e=0;e{this.startMove(e,n.mousePos(this.element,e)),n.addEventListener(window,"mousemove",this.mousemove),n.addEventListener(window,"mouseup",this.mouseup);},this.mousemove=e=>{this.move(e,n.mousePos(this.element,e));},this.mouseup=e=>{this._rotatePitchHandler.dragEnd(e),this.offTemp();},this.touchstart=e=>{1!==e.targetTouches.length?this.reset():(this._startPos=this._lastPos=n.touchPos(this.element,e.targetTouches)[0],this.startMove(e,this._startPos),n.addEventListener(window,"touchmove",this.touchmove,{passive:!1}),n.addEventListener(window,"touchend",this.touchend));},this.touchmove=e=>{1!==e.targetTouches.length?this.reset():(this._lastPos=n.touchPos(this.element,e.targetTouches)[0],this.move(e,this._lastPos));},this.touchend=e=>{0===e.targetTouches.length&&this._startPos&&this._lastPos&&this._startPos.dist(this._lastPos){this._rotatePitchHandler.reset(),delete this._startPos,delete this._lastPos,this.offTemp();},this._clickTolerance=10,this.element=i;const r=new ia;this._rotatePitchHandler=new Kr({clickTolerance:3,move:(e,r)=>{const a=i.getBoundingClientRect(),s=new t.P((a.bottom-a.top)/2,(a.right-a.left)/2);return {bearingDelta:t.cn(new t.P(e.x,r.y),r,s),pitchDelta:o?-.5*(r.y-e.y):void 0}},moveStateManager:r,enable:!0,assignEvents:()=>{}}),this.map=e,n.addEventListener(i,"mousedown",this.mousedown),n.addEventListener(i,"touchstart",this.touchstart,{passive:!1}),n.addEventListener(i,"touchcancel",this.reset);}startMove(e,t){this._rotatePitchHandler.dragStart(e,t),n.disableDrag();}move(e,t){const i=this.map,{bearingDelta:o,pitchDelta:r}=this._rotatePitchHandler.dragMove(e,t)||{};o&&i.setBearing(i.getBearing()+o),r&&i.setPitch(i.getPitch()+r);}off(){const e=this.element;n.removeEventListener(e,"mousedown",this.mousedown),n.removeEventListener(e,"touchstart",this.touchstart,{passive:!1}),n.removeEventListener(window,"touchmove",this.touchmove,{passive:!1}),n.removeEventListener(window,"touchend",this.touchend),n.removeEventListener(e,"touchcancel",this.reset),this.offTemp();}offTemp(){n.enableDrag(),n.removeEventListener(window,"mousemove",this.mousemove),n.removeEventListener(window,"mouseup",this.mouseup),n.removeEventListener(window,"touchmove",this.touchmove,{passive:!1}),n.removeEventListener(window,"touchend",this.touchend);}}let qa;function Wa(e,i,o,r=!1){if(r||!o.getCoveringTilesDetailsProvider().allowWorldCopies())return null==e?void 0:e.wrap();const a=new t.S(e.lng,e.lat);if(e=new t.S(e.lng,e.lat),i){const r=new t.S(e.lng-360,e.lat),a=new t.S(e.lng+360,e.lat),s=o.locationToScreenPoint(e).distSqr(i);o.locationToScreenPoint(r).distSqr(i)180;){const t=o.locationToScreenPoint(e);if(t.x>=0&&t.y>=0&&t.x<=o.width&&t.y<=o.height)break;e.lng>o.center.lng?e.lng-=360:e.lng+=360;}return e.lng!==a.lng&&o.isPointOnMapSurface(o.locationToScreenPoint(e))?e:a}const Ha={center:"translate(-50%,-50%)",top:"translate(-50%,0)","top-left":"translate(0,0)","top-right":"translate(-100%,0)",bottom:"translate(-50%,-100%)","bottom-left":"translate(0,-100%)","bottom-right":"translate(-100%,-100%)",left:"translate(0,-50%)",right:"translate(-100%,-50%)"};function Xa(e,t,i){const o=e.classList;for(const e in Ha)o.remove(`maplibregl-${i}-anchor-${e}`);o.add(`maplibregl-${i}-anchor-${t}`);}class Ka extends t.E{constructor(e){if(super(),this._onKeyPress=e=>{const t=e.code,i=e.charCode||e.keyCode;"Space"!==t&&"Enter"!==t&&32!==i&&13!==i||this.togglePopup();},this._onMapClick=e=>{const t=e.originalEvent.target,i=this._element;this._popup&&(t===i||i.contains(t))&&this.togglePopup();},this._update=e=>{if(!this._map)return;const t=this._map.loaded()&&!this._map.isMoving();("terrain"===(null==e?void 0:e.type)||"render"===(null==e?void 0:e.type)&&!t)&&this._map.once("render",this._update),this._lngLat=Wa(this._lngLat,this._flatPos,this._map.transform),this._flatPos=this._pos=this._map.project(this._lngLat)._add(this._offset),this._map.terrain&&(this._flatPos=this._map.transform.locationToScreenPoint(this._lngLat)._add(this._offset));let i="";"viewport"===this._rotationAlignment||"auto"===this._rotationAlignment?i=`rotateZ(${this._rotation}deg)`:"map"===this._rotationAlignment&&(i=`rotateZ(${this._rotation-this._map.getBearing()}deg)`);let o="";"viewport"===this._pitchAlignment||"auto"===this._pitchAlignment?o="rotateX(0deg)":"map"===this._pitchAlignment&&(o=`rotateX(${this._map.getPitch()}deg)`),this._subpixelPositioning||e&&"moveend"!==e.type||(this._pos=this._pos.round()),n.setTransform(this._element,`${Ha[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${o} ${i}`),s.frameAsync(new AbortController).then((()=>{this._updateOpacity(e&&"moveend"===e.type);})).catch((()=>{}));},this._onMove=e=>{if(!this._isDragging){const t=this._clickTolerance||this._map._clickTolerance;this._isDragging=e.point.dist(this._pointerdownPos)>=t;}this._isDragging&&(this._pos=e.point.sub(this._positionDelta),this._lngLat=this._map.unproject(this._pos),this.setLngLat(this._lngLat),this._element.style.pointerEvents="none","pending"===this._state&&(this._state="active",this.fire(new t.l("dragstart"))),this.fire(new t.l("drag")));},this._onUp=()=>{this._element.style.pointerEvents="auto",this._positionDelta=null,this._pointerdownPos=null,this._isDragging=!1,this._map.off("mousemove",this._onMove),this._map.off("touchmove",this._onMove),"active"===this._state&&this.fire(new t.l("dragend")),this._state="inactive";},this._addDragHandler=e=>{this._element.contains(e.originalEvent.target)&&(e.preventDefault(),this._positionDelta=e.point.sub(this._pos).add(this._offset),this._pointerdownPos=e.point,this._state="pending",this._map.on("mousemove",this._onMove),this._map.on("touchmove",this._onMove),this._map.once("mouseup",this._onUp),this._map.once("touchend",this._onUp));},this._anchor=e&&e.anchor||"center",this._color=e&&e.color||"#3FB1CE",this._scale=e&&e.scale||1,this._draggable=e&&e.draggable||!1,this._clickTolerance=e&&e.clickTolerance||0,this._subpixelPositioning=e&&e.subpixelPositioning||!1,this._isDragging=!1,this._state="inactive",this._rotation=e&&e.rotation||0,this._rotationAlignment=e&&e.rotationAlignment||"auto",this._pitchAlignment=e&&e.pitchAlignment&&"auto"!==e.pitchAlignment?e.pitchAlignment:this._rotationAlignment,this.setOpacity(null==e?void 0:e.opacity,null==e?void 0:e.opacityWhenCovered),e&&e.element)this._element=e.element,this._offset=t.P.convert(e&&e.offset||[0,0]);else {this._defaultMarker=!0,this._element=n.create("div");const i=n.createNS("http://www.w3.org/2000/svg","svg"),o=41,r=27;i.setAttributeNS(null,"display","block"),i.setAttributeNS(null,"height",`${o}px`),i.setAttributeNS(null,"width",`${r}px`),i.setAttributeNS(null,"viewBox",`0 0 ${r} ${o}`);const a=n.createNS("http://www.w3.org/2000/svg","g");a.setAttributeNS(null,"stroke","none"),a.setAttributeNS(null,"stroke-width","1"),a.setAttributeNS(null,"fill","none"),a.setAttributeNS(null,"fill-rule","evenodd");const s=n.createNS("http://www.w3.org/2000/svg","g");s.setAttributeNS(null,"fill-rule","nonzero");const l=n.createNS("http://www.w3.org/2000/svg","g");l.setAttributeNS(null,"transform","translate(3.0, 29.0)"),l.setAttributeNS(null,"fill","#000000");const c=[{rx:"10.5",ry:"5.25002273"},{rx:"10.5",ry:"5.25002273"},{rx:"9.5",ry:"4.77275007"},{rx:"8.5",ry:"4.29549936"},{rx:"7.5",ry:"3.81822308"},{rx:"6.5",ry:"3.34094679"},{rx:"5.5",ry:"2.86367051"},{rx:"4.5",ry:"2.38636864"}];for(const e of c){const t=n.createNS("http://www.w3.org/2000/svg","ellipse");t.setAttributeNS(null,"opacity","0.04"),t.setAttributeNS(null,"cx","10.5"),t.setAttributeNS(null,"cy","5.80029008"),t.setAttributeNS(null,"rx",e.rx),t.setAttributeNS(null,"ry",e.ry),l.appendChild(t);}const h=n.createNS("http://www.w3.org/2000/svg","g");h.setAttributeNS(null,"fill",this._color);const u=n.createNS("http://www.w3.org/2000/svg","path");u.setAttributeNS(null,"d","M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z"),h.appendChild(u);const d=n.createNS("http://www.w3.org/2000/svg","g");d.setAttributeNS(null,"opacity","0.25"),d.setAttributeNS(null,"fill","#000000");const _=n.createNS("http://www.w3.org/2000/svg","path");_.setAttributeNS(null,"d","M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z"),d.appendChild(_);const p=n.createNS("http://www.w3.org/2000/svg","g");p.setAttributeNS(null,"transform","translate(6.0, 7.0)"),p.setAttributeNS(null,"fill","#FFFFFF");const m=n.createNS("http://www.w3.org/2000/svg","g");m.setAttributeNS(null,"transform","translate(8.0, 8.0)");const f=n.createNS("http://www.w3.org/2000/svg","circle");f.setAttributeNS(null,"fill","#000000"),f.setAttributeNS(null,"opacity","0.25"),f.setAttributeNS(null,"cx","5.5"),f.setAttributeNS(null,"cy","5.5"),f.setAttributeNS(null,"r","5.4999962");const g=n.createNS("http://www.w3.org/2000/svg","circle");g.setAttributeNS(null,"fill","#FFFFFF"),g.setAttributeNS(null,"cx","5.5"),g.setAttributeNS(null,"cy","5.5"),g.setAttributeNS(null,"r","5.4999962"),m.appendChild(f),m.appendChild(g),s.appendChild(l),s.appendChild(h),s.appendChild(d),s.appendChild(p),s.appendChild(m),i.appendChild(s),i.setAttributeNS(null,"height",o*this._scale+"px"),i.setAttributeNS(null,"width",r*this._scale+"px"),this._element.appendChild(i),this._offset=t.P.convert(e&&e.offset||[0,-14]);}if(this._element.classList.add("maplibregl-marker"),this._element.addEventListener("dragstart",(e=>{e.preventDefault();})),this._element.addEventListener("mousedown",(e=>{e.preventDefault();})),Xa(this._element,this._anchor,"marker"),e&&e.className)for(const t of e.className.split(" "))this._element.classList.add(t);this._popup=null;}addTo(e){return this.remove(),this._map=e,this._element.hasAttribute("aria-label")||this._element.setAttribute("aria-label",e._getUIString("Marker.Title")),e.getCanvasContainer().appendChild(this._element),e.on("move",this._update),e.on("moveend",this._update),e.on("terrain",this._update),e.on("projectiontransition",this._update),this.setDraggable(this._draggable),this._update(),this._map.on("click",this._onMapClick),this}remove(){return this._opacityTimeout&&(clearTimeout(this._opacityTimeout),delete this._opacityTimeout),this._map&&(this._map.off("click",this._onMapClick),this._map.off("move",this._update),this._map.off("moveend",this._update),this._map.off("terrain",this._update),this._map.off("projectiontransition",this._update),this._map.off("mousedown",this._addDragHandler),this._map.off("touchstart",this._addDragHandler),this._map.off("mouseup",this._onUp),this._map.off("touchend",this._onUp),this._map.off("mousemove",this._onMove),this._map.off("touchmove",this._onMove),delete this._map),n.remove(this._element),this._popup&&this._popup.remove(),this}getLngLat(){return this._lngLat}setLngLat(e){return this._lngLat=t.S.convert(e),this._pos=null,this._popup&&this._popup.setLngLat(this._lngLat),this._update(),this}getElement(){return this._element}setPopup(e){if(this._popup&&(this._popup.remove(),this._popup=null,this._element.removeEventListener("keypress",this._onKeyPress),this._originalTabIndex||this._element.removeAttribute("tabindex")),e){if(!("offset"in e.options)){const t=38.1,i=13.5,o=Math.abs(i)/Math.SQRT2;e.options.offset=this._defaultMarker?{top:[0,0],"top-left":[0,0],"top-right":[0,0],bottom:[0,-t],"bottom-left":[o,-1*(t-i+o)],"bottom-right":[-o,-1*(t-i+o)],left:[i,-1*(t-i)],right:[-i,-1*(t-i)]}:this._offset;}this._popup=e,this._originalTabIndex=this._element.getAttribute("tabindex"),this._originalTabIndex||this._element.setAttribute("tabindex","0"),this._element.addEventListener("keypress",this._onKeyPress);}return this}setSubpixelPositioning(e){return this._subpixelPositioning=e,this}getPopup(){return this._popup}togglePopup(){const e=this._popup;return this._element.style.opacity===this._opacityWhenCovered?this:e?(e.isOpen()?e.remove():(e.setLngLat(this._lngLat),e.addTo(this._map)),this):this}_updateOpacity(e=!1){var i,o;const r=null===(i=this._map)||void 0===i?void 0:i.terrain,a=this._map.transform.isLocationOccluded(this._lngLat);if(!r||a){const e=a?this._opacityWhenCovered:this._opacity;return void(this._element.style.opacity!==e&&(this._element.style.opacity=e))}if(e)this._opacityTimeout=null;else {if(this._opacityTimeout)return;this._opacityTimeout=setTimeout((()=>{this._opacityTimeout=null;}),100);}const s=this._map,n=s.terrain.depthAtPoint(this._pos),l=s.terrain.getElevationForLngLatZoom(this._lngLat,s.transform.tileZoom);if(s.transform.lngLatToCameraDepth(this._lngLat,l)-n<.006)return void(this._element.style.opacity=this._opacity);const c=-this._offset.y/s.transform.pixelsPerMeter,h=Math.sin(s.getPitch()*Math.PI/180)*c,u=s.terrain.depthAtPoint(new t.P(this._pos.x,this._pos.y-this._offset.y)),d=s.transform.lngLatToCameraDepth(this._lngLat,l+h)-u>.006;(null===(o=this._popup)||void 0===o?void 0:o.isOpen())&&d&&this._popup.remove(),this._element.style.opacity=d?this._opacityWhenCovered:this._opacity;}getOffset(){return this._offset}setOffset(e){return this._offset=t.P.convert(e),this._update(),this}addClassName(e){this._element.classList.add(e);}removeClassName(e){this._element.classList.remove(e);}toggleClassName(e){return this._element.classList.toggle(e)}setDraggable(e){return this._draggable=!!e,this._map&&(e?(this._map.on("mousedown",this._addDragHandler),this._map.on("touchstart",this._addDragHandler)):(this._map.off("mousedown",this._addDragHandler),this._map.off("touchstart",this._addDragHandler))),this}isDraggable(){return this._draggable}setRotation(e){return this._rotation=e||0,this._update(),this}getRotation(){return this._rotation}setRotationAlignment(e){return this._rotationAlignment=e||"auto",this._update(),this}getRotationAlignment(){return this._rotationAlignment}setPitchAlignment(e){return this._pitchAlignment=e&&"auto"!==e?e:this._rotationAlignment,this._update(),this}getPitchAlignment(){return this._pitchAlignment}setOpacity(e,t){return (void 0===this._opacity||void 0===e&&void 0===t)&&(this._opacity="1",this._opacityWhenCovered="0.2"),void 0!==e&&(this._opacity=e),void 0!==t&&(this._opacityWhenCovered=t),this._map&&this._updateOpacity(!0),this}}const Ya={positionOptions:{enableHighAccuracy:!1,maximumAge:0,timeout:6e3},fitBoundsOptions:{maxZoom:15},trackUserLocation:!1,showAccuracyCircle:!0,showUserLocation:!0};let Qa=0,Ja=!1;const es={maxWidth:100,unit:"metric"};function ts(e,t,i){const o=i&&i.maxWidth||100,r=e._container.clientHeight/2,a=e._container.clientWidth/2,s=e.unproject([a-o/2,r]),n=e.unproject([a+o/2,r]),l=Math.round(e.project(n).x-e.project(s).x),c=Math.min(o,l,e._container.clientWidth),h=s.distanceTo(n);if(i&&"imperial"===i.unit){const i=3.2808*h;i>5280?is(t,c,i/5280,e._getUIString("ScaleControl.Miles")):is(t,c,i,e._getUIString("ScaleControl.Feet"));}else i&&"nautical"===i.unit?is(t,c,h/1852,e._getUIString("ScaleControl.NauticalMiles")):h>=1e3?is(t,c,h/1e3,e._getUIString("ScaleControl.Kilometers")):is(t,c,h,e._getUIString("ScaleControl.Meters"));}function is(e,t,i,o){const r=function(e){const t=Math.pow(10,`${Math.floor(e)}`.length-1);let i=e/t;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:i>=1?1:function(e){const t=Math.pow(10,Math.ceil(-Math.log(e)/Math.LN10));return Math.round(e*t)/t}(i),t*i}(i);e.style.width=t*(r/i)+"px",e.innerHTML=`${r} ${o}`;}const os={closeButton:!0,closeOnClick:!0,focusAfterOpen:!0,className:"",maxWidth:"240px",subpixelPositioning:!1,locationOccludedOpacity:void 0},rs=["a[href]","[tabindex]:not([tabindex='-1'])","[contenteditable]:not([contenteditable='false'])","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].join(", ");function as(e){if(e){if("number"==typeof e){const i=Math.round(Math.abs(e)/Math.SQRT2);return {center:new t.P(0,0),top:new t.P(0,e),"top-left":new t.P(i,i),"top-right":new t.P(-i,i),bottom:new t.P(0,-e),"bottom-left":new t.P(i,-i),"bottom-right":new t.P(-i,-i),left:new t.P(e,0),right:new t.P(-e,0)}}if(e instanceof t.P||Array.isArray(e)){const i=t.P.convert(e);return {center:i,top:i,"top-left":i,"top-right":i,bottom:i,"bottom-left":i,"bottom-right":i,left:i,right:i}}return {center:t.P.convert(e.center||[0,0]),top:t.P.convert(e.top||[0,0]),"top-left":t.P.convert(e["top-left"]||[0,0]),"top-right":t.P.convert(e["top-right"]||[0,0]),bottom:t.P.convert(e.bottom||[0,0]),"bottom-left":t.P.convert(e["bottom-left"]||[0,0]),"bottom-right":t.P.convert(e["bottom-right"]||[0,0]),left:t.P.convert(e.left||[0,0]),right:t.P.convert(e.right||[0,0])}}return as(new t.P(0,0))}const ss=i;e.AJAXError=t.cz,e.Event=t.l,e.Evented=t.E,e.LngLat=t.S,e.MercatorCoordinate=t.a1,e.Point=t.P,e.addProtocol=t.cA,e.config=t.a,e.removeProtocol=t.cB,e.AttributionControl=Da,e.BoxZoomHandler=$r,e.CanvasSource=Y,e.CooperativeGesturesHandler=Ca,e.DoubleClickZoomHandler=ba,e.DragPanHandler=wa,e.DragRotateHandler=Ta,e.EdgeInsets=Mt,e.FullscreenControl=class extends t.E{constructor(e={}){super(),this._onFullscreenChange=()=>{var e;let t=window.document.fullscreenElement||window.document.mozFullScreenElement||window.document.webkitFullscreenElement||window.document.msFullscreenElement;for(;null===(e=null==t?void 0:t.shadowRoot)||void 0===e?void 0:e.fullscreenElement;)t=t.shadowRoot.fullscreenElement;t===this._container!==this._fullscreen&&this._handleFullscreenChange();},this._onClickFullscreen=()=>{this._isFullscreen()?this._exitFullscreen():this._requestFullscreen();},this._fullscreen=!1,e&&e.container&&(e.container instanceof HTMLElement?this._container=e.container:t.w("Full screen control 'container' must be a DOM element.")),"onfullscreenchange"in document?this._fullscreenchange="fullscreenchange":"onmozfullscreenchange"in document?this._fullscreenchange="mozfullscreenchange":"onwebkitfullscreenchange"in document?this._fullscreenchange="webkitfullscreenchange":"onmsfullscreenchange"in document&&(this._fullscreenchange="MSFullscreenChange");}onAdd(e){return this._map=e,this._container||(this._container=this._map.getContainer()),this._controlContainer=n.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._setupUI(),this._controlContainer}onRemove(){n.remove(this._controlContainer),this._map=null,window.document.removeEventListener(this._fullscreenchange,this._onFullscreenChange);}_setupUI(){const e=this._fullscreenButton=n.create("button","maplibregl-ctrl-fullscreen",this._controlContainer);n.create("span","maplibregl-ctrl-icon",e).setAttribute("aria-hidden","true"),e.type="button",this._updateTitle(),this._fullscreenButton.addEventListener("click",this._onClickFullscreen),window.document.addEventListener(this._fullscreenchange,this._onFullscreenChange);}_updateTitle(){const e=this._getTitle();this._fullscreenButton.setAttribute("aria-label",e),this._fullscreenButton.title=e;}_getTitle(){return this._map._getUIString(this._isFullscreen()?"FullscreenControl.Exit":"FullscreenControl.Enter")}_isFullscreen(){return this._fullscreen}_handleFullscreenChange(){this._fullscreen=!this._fullscreen,this._fullscreenButton.classList.toggle("maplibregl-ctrl-shrink"),this._fullscreenButton.classList.toggle("maplibregl-ctrl-fullscreen"),this._updateTitle(),this._fullscreen?(this.fire(new t.l("fullscreenstart")),this._prevCooperativeGesturesEnabled=this._map.cooperativeGestures.isEnabled(),this._map.cooperativeGestures.disable()):(this.fire(new t.l("fullscreenend")),this._prevCooperativeGesturesEnabled&&this._map.cooperativeGestures.enable());}_exitFullscreen(){window.document.exitFullscreen?window.document.exitFullscreen():window.document.mozCancelFullScreen?window.document.mozCancelFullScreen():window.document.msExitFullscreen?window.document.msExitFullscreen():window.document.webkitCancelFullScreen?window.document.webkitCancelFullScreen():this._togglePseudoFullScreen();}_requestFullscreen(){this._container.requestFullscreen?this._container.requestFullscreen():this._container.mozRequestFullScreen?this._container.mozRequestFullScreen():this._container.msRequestFullscreen?this._container.msRequestFullscreen():this._container.webkitRequestFullscreen?this._container.webkitRequestFullscreen():this._togglePseudoFullScreen();}_togglePseudoFullScreen(){this._container.classList.toggle("maplibregl-pseudo-fullscreen"),this._handleFullscreenChange(),this._map.resize();}},e.GeoJSONSource=H,e.GeolocateControl=class extends t.E{constructor(e){super(),this._onSuccess=e=>{if(this._map){if(this._isOutOfMapMaxBounds(e))return this._setErrorState(),this.fire(new t.l("outofmaxbounds",e)),this._updateMarker(),void this._finish();if(this.options.trackUserLocation)switch(this._lastKnownPosition=e,this._watchState){case "WAITING_ACTIVE":case "ACTIVE_LOCK":case "ACTIVE_ERROR":this._watchState="ACTIVE_LOCK",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active");break;case "BACKGROUND":case "BACKGROUND_ERROR":this._watchState="BACKGROUND",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background");break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}this.options.showUserLocation&&"OFF"!==this._watchState&&this._updateMarker(e),this.options.trackUserLocation&&"ACTIVE_LOCK"!==this._watchState||this._updateCamera(e),this.options.showUserLocation&&this._dotElement.classList.remove("maplibregl-user-location-dot-stale"),this.fire(new t.l("geolocate",e)),this._finish();}},this._updateCamera=e=>{const i=new t.S(e.coords.longitude,e.coords.latitude),o=e.coords.accuracy,r=this._map.getBearing(),a=t.e({bearing:r},this.options.fitBoundsOptions),s=G.fromLngLat(i,o);this._map.fitBounds(s,a,{geolocateSource:!0});},this._updateMarker=e=>{if(e){const i=new t.S(e.coords.longitude,e.coords.latitude);this._accuracyCircleMarker.setLngLat(i).addTo(this._map),this._userLocationDotMarker.setLngLat(i).addTo(this._map),this._accuracy=e.coords.accuracy,this.options.showUserLocation&&this.options.showAccuracyCircle&&this._updateCircleRadius();}else this._userLocationDotMarker.remove(),this._accuracyCircleMarker.remove();},this._onZoom=()=>{this.options.showUserLocation&&this.options.showAccuracyCircle&&this._updateCircleRadius();},this._onError=e=>{if(this._map){if(1===e.code){this._watchState="OFF",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"),this._geolocateButton.disabled=!0;const e=this._map._getUIString("GeolocateControl.LocationNotAvailable");this._geolocateButton.title=e,this._geolocateButton.setAttribute("aria-label",e),void 0!==this._geolocationWatchID&&this._clearWatch();}else {if(3===e.code&&Ja)return;this.options.trackUserLocation&&this._setErrorState();}"OFF"!==this._watchState&&this.options.showUserLocation&&this._dotElement.classList.add("maplibregl-user-location-dot-stale"),this.fire(new t.l("error",e)),this._finish();}},this._finish=()=>{this._timeoutId&&clearTimeout(this._timeoutId),this._timeoutId=void 0;},this._setupUI=()=>{this._map&&(this._container.addEventListener("contextmenu",(e=>e.preventDefault())),this._geolocateButton=n.create("button","maplibregl-ctrl-geolocate",this._container),n.create("span","maplibregl-ctrl-icon",this._geolocateButton).setAttribute("aria-hidden","true"),this._geolocateButton.type="button",this._geolocateButton.disabled=!0);},this._finishSetupUI=e=>{if(this._map){if(!1===e){t.w("Geolocation support is not available so the GeolocateControl will be disabled.");const e=this._map._getUIString("GeolocateControl.LocationNotAvailable");this._geolocateButton.disabled=!0,this._geolocateButton.title=e,this._geolocateButton.setAttribute("aria-label",e);}else {const e=this._map._getUIString("GeolocateControl.FindMyLocation");this._geolocateButton.disabled=!1,this._geolocateButton.title=e,this._geolocateButton.setAttribute("aria-label",e);}this.options.trackUserLocation&&(this._geolocateButton.setAttribute("aria-pressed","false"),this._watchState="OFF"),this.options.showUserLocation&&(this._dotElement=n.create("div","maplibregl-user-location-dot"),this._userLocationDotMarker=new Ka({element:this._dotElement}),this._circleElement=n.create("div","maplibregl-user-location-accuracy-circle"),this._accuracyCircleMarker=new Ka({element:this._circleElement,pitchAlignment:"map"}),this.options.trackUserLocation&&(this._watchState="OFF"),this._map.on("zoom",this._onZoom)),this._geolocateButton.addEventListener("click",(()=>this.trigger())),this._setup=!0,this.options.trackUserLocation&&this._map.on("movestart",(e=>{const i=(null==e?void 0:e[0])instanceof ResizeObserverEntry;e.geolocateSource||"ACTIVE_LOCK"!==this._watchState||i||this._map.isZooming()||(this._watchState="BACKGROUND",this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this.fire(new t.l("trackuserlocationend")),this.fire(new t.l("userlocationlostfocus")));}));}},this.options=t.e({},Ya,e);}onAdd(e){return this._map=e,this._container=n.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._setupUI(),function(){return t._(this,arguments,void 0,(function*(e=!1){if(void 0!==qa&&!e)return qa;if(void 0===window.navigator.permissions)return qa=!!window.navigator.geolocation,qa;try{const e=yield window.navigator.permissions.query({name:"geolocation"});qa="denied"!==e.state;}catch(e){qa=!!window.navigator.geolocation;}return qa}))}().then((e=>this._finishSetupUI(e))),this._container}onRemove(){void 0!==this._geolocationWatchID&&(window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0),this.options.showUserLocation&&this._userLocationDotMarker&&this._userLocationDotMarker.remove(),this.options.showAccuracyCircle&&this._accuracyCircleMarker&&this._accuracyCircleMarker.remove(),n.remove(this._container),this._map.off("zoom",this._onZoom),this._map=void 0,Qa=0,Ja=!1;}_isOutOfMapMaxBounds(e){const t=this._map.getMaxBounds(),i=e.coords;return t&&(i.longitudet.getEast()||i.latitudet.getNorth())}_setErrorState(){switch(this._watchState){case "WAITING_ACTIVE":this._watchState="ACTIVE_ERROR",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active-error");break;case "ACTIVE_LOCK":this._watchState="ACTIVE_ERROR",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting");break;case "BACKGROUND":this._watchState="BACKGROUND_ERROR",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-background-error"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting");break;case "ACTIVE_ERROR":break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}}_updateCircleRadius(){const e=this._map.getBounds(),t=e.getSouthEast(),i=e.getNorthEast(),o=t.distanceTo(i),r=Math.ceil(this._accuracy/(o/this._map._container.clientHeight)*2);this._circleElement.style.width=`${r}px`,this._circleElement.style.height=`${r}px`;}trigger(){if(!this._setup)return t.w("Geolocate control triggered before added to a map"),!1;if(this.options.trackUserLocation){switch(this._watchState){case "OFF":this._watchState="WAITING_ACTIVE",this.fire(new t.l("trackuserlocationstart"));break;case "WAITING_ACTIVE":case "ACTIVE_LOCK":case "ACTIVE_ERROR":case "BACKGROUND_ERROR":Qa--,Ja=!1,this._watchState="OFF",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-active-error"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background-error"),this.fire(new t.l("trackuserlocationend"));break;case "BACKGROUND":this._watchState="ACTIVE_LOCK",this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-background"),this._lastKnownPosition&&this._updateCamera(this._lastKnownPosition),this.fire(new t.l("trackuserlocationstart")),this.fire(new t.l("userlocationfocus"));break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}switch(this._watchState){case "WAITING_ACTIVE":this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active");break;case "ACTIVE_LOCK":this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-active");break;case "OFF":break;default:throw new Error(`Unexpected watchState ${this._watchState}`)}if("OFF"===this._watchState&&void 0!==this._geolocationWatchID)this._clearWatch();else if(void 0===this._geolocationWatchID){let e;this._geolocateButton.classList.add("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.setAttribute("aria-pressed","true"),Qa++,Qa>1?(e={maximumAge:6e5,timeout:0},Ja=!0):(e=this.options.positionOptions,Ja=!1),this._geolocationWatchID=window.navigator.geolocation.watchPosition(this._onSuccess,this._onError,e);}}else window.navigator.geolocation.getCurrentPosition(this._onSuccess,this._onError,this.options.positionOptions),this._timeoutId=setTimeout(this._finish,1e4);return !0}_clearWatch(){window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0,this._geolocateButton.classList.remove("maplibregl-ctrl-geolocate-waiting"),this._geolocateButton.setAttribute("aria-pressed","false"),this.options.showUserLocation&&this._updateMarker(null);}},e.GlobeControl=class{constructor(){this._toggleProjection=()=>{var e;const t=null===(e=this._map.getProjection())||void 0===e?void 0:e.type;this._map.setProjection("mercator"!==t&&t?{type:"mercator"}:{type:"globe"}),this._updateGlobeIcon();},this._updateGlobeIcon=()=>{var e;this._globeButton.classList.remove("maplibregl-ctrl-globe"),this._globeButton.classList.remove("maplibregl-ctrl-globe-enabled"),"globe"===(null===(e=this._map.getProjection())||void 0===e?void 0:e.type)?(this._globeButton.classList.add("maplibregl-ctrl-globe-enabled"),this._globeButton.title=this._map._getUIString("GlobeControl.Disable")):(this._globeButton.classList.add("maplibregl-ctrl-globe"),this._globeButton.title=this._map._getUIString("GlobeControl.Enable"));};}onAdd(e){return this._map=e,this._container=n.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._globeButton=n.create("button","maplibregl-ctrl-globe",this._container),n.create("span","maplibregl-ctrl-icon",this._globeButton).setAttribute("aria-hidden","true"),this._globeButton.type="button",this._globeButton.addEventListener("click",this._toggleProjection),this._updateGlobeIcon(),this._map.on("styledata",this._updateGlobeIcon),this._container}onRemove(){n.remove(this._container),this._map.off("styledata",this._updateGlobeIcon),this._globeButton.removeEventListener("click",this._toggleProjection),this._map=void 0;}},e.Hash=Er,e.ImageSource=X,e.KeyboardHandler=pa,e.LngLatBounds=G,e.LogoControl=Aa,e.Map=class extends Ra{constructor(e){var i,o;t.cw.mark(t.cx.create);const r=Object.assign(Object.assign(Object.assign({},Ga),e),{canvasContextAttributes:Object.assign(Object.assign({},Ga.canvasContextAttributes),e.canvasContextAttributes)});if(null!=r.minZoom&&null!=r.maxZoom&&r.minZoom>r.maxZoom)throw new Error("maxZoom must be greater than or equal to minZoom");if(null!=r.minPitch&&null!=r.maxPitch&&r.minPitch>r.maxPitch)throw new Error("maxPitch must be greater than or equal to minPitch");if(null!=r.minPitch&&r.minPitch<0)throw new Error("minPitch must be greater than or equal to 0");if(null!=r.maxPitch&&r.maxPitch>180)throw new Error("maxPitch must be less than or equal to 180");const a=new Lt,s=new Ot;if(void 0!==r.minZoom&&a.setMinZoom(r.minZoom),void 0!==r.maxZoom&&a.setMaxZoom(r.maxZoom),void 0!==r.minPitch&&a.setMinPitch(r.minPitch),void 0!==r.maxPitch&&a.setMaxPitch(r.maxPitch),void 0!==r.renderWorldCopies&&a.setRenderWorldCopies(r.renderWorldCopies),super(a,s,{bearingSnap:r.bearingSnap}),this._idleTriggered=!1,this._crossFadingFactor=1,this._renderTaskQueue=new La,this._controls=[],this._mapId=t.a7(),this._contextLost=e=>{e.preventDefault(),this._frameRequest&&(this._frameRequest.abort(),this._frameRequest=null),this.fire(new t.l("webglcontextlost",{originalEvent:e}));},this._contextRestored=e=>{this._setupPainter(),this.resize(),this._update(),this.fire(new t.l("webglcontextrestored",{originalEvent:e}));},this._onMapScroll=e=>{if(e.target===this._container)return this._container.scrollTop=0,this._container.scrollLeft=0,!1},this._onWindowOnline=()=>{this._update();},this._interactive=r.interactive,this._maxTileCacheSize=r.maxTileCacheSize,this._maxTileCacheZoomLevels=r.maxTileCacheZoomLevels,this._canvasContextAttributes=Object.assign({},r.canvasContextAttributes),this._trackResize=!0===r.trackResize,this._bearingSnap=r.bearingSnap,this._centerClampedToGround=r.centerClampedToGround,this._refreshExpiredTiles=!0===r.refreshExpiredTiles,this._fadeDuration=r.fadeDuration,this._crossSourceCollisions=!0===r.crossSourceCollisions,this._collectResourceTiming=!0===r.collectResourceTiming,this._locale=Object.assign(Object.assign({},Ua),r.locale),this._clickTolerance=r.clickTolerance,this._overridePixelRatio=r.pixelRatio,this._maxCanvasSize=r.maxCanvasSize,this.transformCameraUpdate=r.transformCameraUpdate,this.cancelPendingTileRequestsWhileZooming=!0===r.cancelPendingTileRequestsWhileZooming,this._imageQueueHandle=p.addThrottleControl((()=>this.isMoving())),this._requestManager=new m(r.transformRequest),"string"==typeof r.container){if(this._container=document.getElementById(r.container),!this._container)throw new Error(`Container '${r.container}' not found.`)}else {if(!(r.container instanceof HTMLElement))throw new Error("Invalid type: 'container' must be a String or HTMLElement.");this._container=r.container;}if(r.maxBounds&&this.setMaxBounds(r.maxBounds),this._setupContainer(),this._setupPainter(),this.on("move",(()=>this._update(!1))),this.on("moveend",(()=>this._update(!1))),this.on("zoom",(()=>this._update(!0))),this.on("terrain",(()=>{this.painter.terrainFacilitator.dirty=!0,this._update(!0);})),this.once("idle",(()=>{this._idleTriggered=!0;})),"undefined"!=typeof window){addEventListener("online",this._onWindowOnline,!1);let e=!1;const t=Sr((e=>{this._trackResize&&!this._removed&&(this.resize(e),this.redraw());}),50);this._resizeObserver=new ResizeObserver((i=>{e?t(i):e=!0;})),this._resizeObserver.observe(this._container);}this.handlers=new Ea(this,r),this._hash=r.hash&&new Er("string"==typeof r.hash&&r.hash||void 0).addTo(this),this._hash&&this._hash._onHashChange()||(this.jumpTo({center:r.center,elevation:r.elevation,zoom:r.zoom,bearing:r.bearing,pitch:r.pitch,roll:r.roll}),r.bounds&&(this.resize(),this.fitBounds(r.bounds,t.e({},r.fitBoundsOptions,{duration:0}))));const n="string"==typeof r.style||!("globe"===(null===(o=null===(i=r.style)||void 0===i?void 0:i.projection)||void 0===o?void 0:o.type));this.resize(null,n),this._localIdeographFontFamily=r.localIdeographFontFamily,this._validateStyle=r.validateStyle,r.style&&this.setStyle(r.style,{localIdeographFontFamily:r.localIdeographFontFamily}),r.attributionControl&&this.addControl(new Da("boolean"==typeof r.attributionControl?void 0:r.attributionControl)),r.maplibreLogo&&this.addControl(new Aa,r.logoPosition),this.on("style.load",(()=>{if(n||this._resizeTransform(),this.transform.unmodified){const e=t.Q(this.style.stylesheet,["center","zoom","bearing","pitch","roll"]);this.jumpTo(e);}})),this.on("data",(e=>{this._update("style"===e.dataType),this.fire(new t.l(`${e.dataType}data`,e));})),this.on("dataloading",(e=>{this.fire(new t.l(`${e.dataType}dataloading`,e));})),this.on("dataabort",(e=>{this.fire(new t.l("sourcedataabort",e));}));}_getMapId(){return this._mapId}setGlobalStateProperty(e,t){return this.style.setGlobalStateProperty(e,t),this._update(!0)}getGlobalState(){return this.style.getGlobalState()}addControl(e,i){if(void 0===i&&(i=e.getDefaultPosition?e.getDefaultPosition():"top-right"),!e||!e.onAdd)return this.fire(new t.k(new Error("Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.")));const o=e.onAdd(this);this._controls.push(e);const r=this._controlPositions[i];return -1!==i.indexOf("bottom")?r.insertBefore(o,r.firstChild):r.appendChild(o),this}removeControl(e){if(!e||!e.onRemove)return this.fire(new t.k(new Error("Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.")));const i=this._controls.indexOf(e);return i>-1&&this._controls.splice(i,1),e.onRemove(this),this}hasControl(e){return this._controls.indexOf(e)>-1}coveringTiles(e){return ve(this.transform,e)}calculateCameraOptionsFromTo(e,t,i,o){return null==o&&this.terrain&&(o=this.terrain.getElevationForLngLatZoom(i,this.transform.tileZoom)),super.calculateCameraOptionsFromTo(e,t,i,o)}resize(e,i=!0){const[o,r]=this._containerDimensions(),a=this._getClampedPixelRatio(o,r);if(this._resizeCanvas(o,r,a),this.painter.resize(o,r,a),this.painter.overLimit()){const e=this.painter.context.gl;this._maxCanvasSize=[e.drawingBufferWidth,e.drawingBufferHeight];const t=this._getClampedPixelRatio(o,r);this._resizeCanvas(o,r,t),this.painter.resize(o,r,t);}this._resizeTransform(i);const s=!this._moving;return s&&(this.stop(),this.fire(new t.l("movestart",e)).fire(new t.l("move",e))),this.fire(new t.l("resize",e)),s&&this.fire(new t.l("moveend",e)),this}_resizeTransform(e=!0){var t;const[i,o]=this._containerDimensions();this.transform.resize(i,o,e),null===(t=this._requestedCameraState)||void 0===t||t.resize(i,o,e);}_getClampedPixelRatio(e,t){const{0:i,1:o}=this._maxCanvasSize,r=this.getPixelRatio(),a=e*r,s=t*r;return Math.min(a>i?i/a:1,s>o?o/s:1)*r}getPixelRatio(){var e;return null!==(e=this._overridePixelRatio)&&void 0!==e?e:devicePixelRatio}setPixelRatio(e){this._overridePixelRatio=e,this.resize();}getBounds(){return this.transform.getBounds()}getMaxBounds(){return this.transform.getMaxBounds()}setMaxBounds(e){return this.transform.setMaxBounds(G.convert(e)),this._update()}setMinZoom(e){if((e=null==e?-2:e)>=-2&&e<=this.transform.maxZoom)return this.transform.setMinZoom(e),this._update(),this.getZoom()=this.transform.minZoom)return this.transform.setMaxZoom(e),this._update(),this.getZoom()>e&&this.setZoom(e),this;throw new Error("maxZoom must be greater than the current minZoom")}getMaxZoom(){return this.transform.maxZoom}setMinPitch(e){if((e=null==e?0:e)<0)throw new Error("minPitch must be greater than or equal to 0");if(e>=0&&e<=this.transform.maxPitch)return this.transform.setMinPitch(e),this._update(),this.getPitch()180)throw new Error("maxPitch must be less than or equal to 180");if(e>=this.transform.minPitch)return this.transform.setMaxPitch(e),this._update(),this.getPitch()>e&&this.setPitch(e),this;throw new Error("maxPitch must be greater than the current minPitch")}getMaxPitch(){return this.transform.maxPitch}getRenderWorldCopies(){return this.transform.renderWorldCopies}setRenderWorldCopies(e){return this.transform.setRenderWorldCopies(e),this._update()}project(e){return this.transform.locationToScreenPoint(t.S.convert(e),this.style&&this.terrain)}unproject(e){return this.transform.screenPointToLocation(t.P.convert(e),this.terrain)}isMoving(){var e;return this._moving||(null===(e=this.handlers)||void 0===e?void 0:e.isMoving())}isZooming(){var e;return this._zooming||(null===(e=this.handlers)||void 0===e?void 0:e.isZooming())}isRotating(){var e;return this._rotating||(null===(e=this.handlers)||void 0===e?void 0:e.isRotating())}_createDelegatedListener(e,t,i){if("mouseenter"===e||"mouseover"===e){let o=!1;const r=r=>{const a=t.filter((e=>this.getLayer(e))),s=0!==a.length?this.queryRenderedFeatures(r.point,{layers:a}):[];s.length?o||(o=!0,i.call(this,new jr(e,this,r.originalEvent,{features:s}))):o=!1;};return {layers:t,listener:i,delegates:{mousemove:r,mouseout:()=>{o=!1;}}}}if("mouseleave"===e||"mouseout"===e){let o=!1;const r=r=>{const a=t.filter((e=>this.getLayer(e)));(0!==a.length?this.queryRenderedFeatures(r.point,{layers:a}):[]).length?o=!0:o&&(o=!1,i.call(this,new jr(e,this,r.originalEvent)));},a=t=>{o&&(o=!1,i.call(this,new jr(e,this,t.originalEvent)));};return {layers:t,listener:i,delegates:{mousemove:r,mouseout:a}}}{const o=e=>{const o=t.filter((e=>this.getLayer(e))),r=0!==o.length?this.queryRenderedFeatures(e.point,{layers:o}):[];r.length&&(e.features=r,i.call(this,e),delete e.features);};return {layers:t,listener:i,delegates:{[e]:o}}}}_saveDelegatedListener(e,t){this._delegatedListeners=this._delegatedListeners||{},this._delegatedListeners[e]=this._delegatedListeners[e]||[],this._delegatedListeners[e].push(t);}_removeDelegatedListener(e,t,i){if(!this._delegatedListeners||!this._delegatedListeners[e])return;const o=this._delegatedListeners[e];for(let e=0;et.includes(e)))){for(const e in r.delegates)this.off(e,r.delegates[e]);return void o.splice(e,1)}}}on(e,t,i){if(void 0===i)return super.on(e,t);const o="string"==typeof t?[t]:t,r=this._createDelegatedListener(e,o,i);this._saveDelegatedListener(e,r);for(const e in r.delegates)this.on(e,r.delegates[e]);return {unsubscribe:()=>{this._removeDelegatedListener(e,o,i);}}}once(e,t,i){if(void 0===i)return super.once(e,t);const o="string"==typeof t?[t]:t,r=this._createDelegatedListener(e,o,i);for(const t in r.delegates){const a=r.delegates[t];r.delegates[t]=(...t)=>{this._removeDelegatedListener(e,o,i),a(...t);};}this._saveDelegatedListener(e,r);for(const e in r.delegates)this.once(e,r.delegates[e]);return this}off(e,t,i){return void 0===i?super.off(e,t):(this._removeDelegatedListener(e,"string"==typeof t?[t]:t,i),this)}queryRenderedFeatures(e,i){if(!this.style)return [];let o;const r=e instanceof t.P||Array.isArray(e),a=r?e:[[0,0],[this.transform.width,this.transform.height]];if(i=i||(r?{}:e)||{},a instanceof t.P||"number"==typeof a[0])o=[t.P.convert(a)];else {const e=t.P.convert(a[0]),i=t.P.convert(a[1]);o=[e,new t.P(i.x,e.y),i,new t.P(e.x,i.y),e];}return this.style.queryRenderedFeatures(o,i,this.transform)}querySourceFeatures(e,t){return this.style.querySourceFeatures(e,t)}setStyle(e,i){return !1!==(i=t.e({},{localIdeographFontFamily:this._localIdeographFontFamily,validate:this._validateStyle},i)).diff&&i.localIdeographFontFamily===this._localIdeographFontFamily&&this.style&&e?(this._diffStyle(e,i),this):(this._localIdeographFontFamily=i.localIdeographFontFamily,this._updateStyle(e,i))}setTransformRequest(e){return this._requestManager.setTransformRequest(e),this}_getUIString(e){const t=this._locale[e];if(null==t)throw new Error(`Missing UI string '${e}'`);return t}_updateStyle(e,t){var i,o;if(t.transformStyle&&this.style&&!this.style._loaded)return void this.style.once("style.load",(()=>this._updateStyle(e,t)));const r=this.style&&t.transformStyle?this.style.serialize():void 0;return this.style&&(this.style.setEventedParent(null),this.style._remove(!e)),e?(this.style=new wi(this,t||{}),this.style.setEventedParent(this,{style:this.style}),"string"==typeof e?this.style.loadURL(e,t,r):this.style.loadJSON(e,t,r),this):(null===(o=null===(i=this.style)||void 0===i?void 0:i.projection)||void 0===o||o.destroy(),delete this.style,this)}_lazyInitEmptyStyle(){this.style||(this.style=new wi(this,{}),this.style.setEventedParent(this,{style:this.style}),this.style.loadEmpty());}_diffStyle(e,i){if("string"==typeof e){const o=this._requestManager.transformRequest(e,"Style");t.j(o,new AbortController).then((e=>{this._updateDiff(e.data,i);})).catch((e=>{e&&this.fire(new t.k(e));}));}else "object"==typeof e&&this._updateDiff(e,i);}_updateDiff(e,i){try{this.style.setState(e,i)&&this._update(!0);}catch(o){t.w(`Unable to perform style diff: ${o.message||o.error||o}. Rebuilding the style from scratch.`),this._updateStyle(e,i);}}getStyle(){if(this.style)return this.style.serialize()}isStyleLoaded(){return this.style?this.style.loaded():t.w("There is no style added to the map.")}addSource(e,t){return this._lazyInitEmptyStyle(),this.style.addSource(e,t),this._update(!0)}isSourceLoaded(e){const i=this.style&&this.style.sourceCaches[e];if(void 0!==i)return i.loaded();this.fire(new t.k(new Error(`There is no source with ID '${e}'`)));}setTerrain(e){if(this.style._checkLoaded(),this._terrainDataCallback&&this.style.off("data",this._terrainDataCallback),e){const i=this.style.sourceCaches[e.source];if(!i)throw new Error(`cannot load terrain, because there exists no source with ID: ${e.source}`);null===this.terrain&&i.reload();for(const i in this.style._layers){const o=this.style._layers[i];"hillshade"===o.type&&o.source===e.source&&t.w("You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality."),"color-relief"===o.type&&o.source===e.source&&t.w("You are using the same source for a color-relief layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.");}this.terrain=new Ba(this.painter,i,e),this.painter.renderToTexture=new Na(this.painter,this.terrain),this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this.transform.center,this.transform.tileZoom)),this.transform.setElevation(this.terrain.getElevationForLngLatZoom(this.transform.center,this.transform.tileZoom)),this._terrainDataCallback=t=>{var i;"style"===t.dataType?this.terrain.sourceCache.freeRtt():"source"===t.dataType&&t.tile&&(t.sourceId!==e.source||this._elevationFreeze||(this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this.transform.center,this.transform.tileZoom)),this._centerClampedToGround&&this.transform.setElevation(this.terrain.getElevationForLngLatZoom(this.transform.center,this.transform.tileZoom))),"image"===(null===(i=t.source)||void 0===i?void 0:i.type)?this.terrain.sourceCache.freeRtt():this.terrain.sourceCache.freeRtt(t.tile.tileID));},this.style.on("data",this._terrainDataCallback);}else this.terrain&&this.terrain.sourceCache.destruct(),this.terrain=null,this.painter.renderToTexture&&this.painter.renderToTexture.destruct(),this.painter.renderToTexture=null,this.transform.setMinElevationForCurrentTile(0),this._centerClampedToGround&&this.transform.setElevation(0);return this.fire(new t.l("terrain",{terrain:e})),this}getTerrain(){var e,t;return null!==(t=null===(e=this.terrain)||void 0===e?void 0:e.options)&&void 0!==t?t:null}areTilesLoaded(){const e=this.style&&this.style.sourceCaches;for(const t in e){const i=e[t]._tiles;for(const e in i){const t=i[e];if("loaded"!==t.state&&"errored"!==t.state)return !1}}return !0}removeSource(e){return this.style.removeSource(e),this._update(!0)}getSource(e){return this.style.getSource(e)}setSourceTileLodParams(e,t,i){if(i){const o=this.getSource(i);if(!o)throw new Error(`There is no source with ID "${i}", cannot set LOD parameters`);o.calculateTileZoom=me(Math.max(1,e),Math.max(1,t));}else for(const i in this.style.sourceCaches)this.style.sourceCaches[i].getSource().calculateTileZoom=me(Math.max(1,e),Math.max(1,t));return this._update(!0),this}refreshTiles(e,i){const o=this.style.sourceCaches[e];if(!o)throw new Error(`There is no source cache with ID "${e}", cannot refresh tile`);void 0===i?o.reload(!0):o.refreshTiles(i.map((e=>new t.a4(e.z,e.x,e.y))));}addImage(e,i,o={}){const{pixelRatio:r=1,sdf:a=!1,stretchX:n,stretchY:l,content:c,textFitWidth:h,textFitHeight:u}=o;if(this._lazyInitEmptyStyle(),!(i instanceof HTMLImageElement||t.b(i))){if(void 0===i.width||void 0===i.height)return this.fire(new t.k(new Error("Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`")));{const{width:o,height:s,data:d}=i,_=i;return this.style.addImage(e,{data:new t.R({width:o,height:s},new Uint8Array(d)),pixelRatio:r,stretchX:n,stretchY:l,content:c,textFitWidth:h,textFitHeight:u,sdf:a,version:0,userImage:_}),_.onAdd&&_.onAdd(this,e),this}}{const{width:o,height:d,data:_}=s.getImageData(i);this.style.addImage(e,{data:new t.R({width:o,height:d},_),pixelRatio:r,stretchX:n,stretchY:l,content:c,textFitWidth:h,textFitHeight:u,sdf:a,version:0});}}updateImage(e,i){const o=this.style.getImage(e);if(!o)return this.fire(new t.k(new Error("The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.")));const r=i instanceof HTMLImageElement||t.b(i)?s.getImageData(i):i,{width:a,height:n,data:l}=r;if(void 0===a||void 0===n)return this.fire(new t.k(new Error("Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`")));if(a!==o.data.width||n!==o.data.height)return this.fire(new t.k(new Error("The width and height of the updated image must be that same as the previous version of the image")));const c=!(i instanceof HTMLImageElement||t.b(i));return o.data.replace(l,c),this.style.updateImage(e,o),this}getImage(e){return this.style.getImage(e)}hasImage(e){return e?!!this.style.getImage(e):(this.fire(new t.k(new Error("Missing required image id"))),!1)}removeImage(e){this.style.removeImage(e);}loadImage(e){return p.getImage(this._requestManager.transformRequest(e,"Image"),new AbortController)}listImages(){return this.style.listImages()}addLayer(e,t){return this._lazyInitEmptyStyle(),this.style.addLayer(e,t),this._update(!0)}moveLayer(e,t){return this.style.moveLayer(e,t),this._update(!0)}removeLayer(e){return this.style.removeLayer(e),this._update(!0)}getLayer(e){return this.style.getLayer(e)}getLayersOrder(){return this.style.getLayersOrder()}setLayerZoomRange(e,t,i){return this.style.setLayerZoomRange(e,t,i),this._update(!0)}setFilter(e,t,i={}){return this.style.setFilter(e,t,i),this._update(!0)}getFilter(e){return this.style.getFilter(e)}setPaintProperty(e,t,i,o={}){return this.style.setPaintProperty(e,t,i,o),this._update(!0)}getPaintProperty(e,t){return this.style.getPaintProperty(e,t)}setLayoutProperty(e,t,i,o={}){return this.style.setLayoutProperty(e,t,i,o),this._update(!0)}getLayoutProperty(e,t){return this.style.getLayoutProperty(e,t)}setGlyphs(e,t={}){return this._lazyInitEmptyStyle(),this.style.setGlyphs(e,t),this._update(!0)}getGlyphs(){return this.style.getGlyphsUrl()}addSprite(e,t,i={}){return this._lazyInitEmptyStyle(),this.style.addSprite(e,t,i,(e=>{e||this._update(!0);})),this}removeSprite(e){return this._lazyInitEmptyStyle(),this.style.removeSprite(e),this._update(!0)}getSprite(){return this.style.getSprite()}setSprite(e,t={}){return this._lazyInitEmptyStyle(),this.style.setSprite(e,t,(e=>{e||this._update(!0);})),this}setLight(e,t={}){return this._lazyInitEmptyStyle(),this.style.setLight(e,t),this._update(!0)}getLight(){return this.style.getLight()}setSky(e,t={}){return this._lazyInitEmptyStyle(),this.style.setSky(e,t),this._update(!0)}getSky(){return this.style.getSky()}setFeatureState(e,t){return this.style.setFeatureState(e,t),this._update()}removeFeatureState(e,t){return this.style.removeFeatureState(e,t),this._update()}getFeatureState(e){return this.style.getFeatureState(e)}getContainer(){return this._container}getCanvasContainer(){return this._canvasContainer}getCanvas(){return this._canvas}_containerDimensions(){let e=0,t=0;return this._container&&(e=this._container.clientWidth||400,t=this._container.clientHeight||300),[e,t]}_setupContainer(){const e=this._container;e.classList.add("maplibregl-map");const t=this._canvasContainer=n.create("div","maplibregl-canvas-container",e);this._interactive&&t.classList.add("maplibregl-interactive"),this._canvas=n.create("canvas","maplibregl-canvas",t),this._canvas.addEventListener("webglcontextlost",this._contextLost,!1),this._canvas.addEventListener("webglcontextrestored",this._contextRestored,!1),this._canvas.setAttribute("tabindex",this._interactive?"0":"-1"),this._canvas.setAttribute("aria-label",this._getUIString("Map.Title")),this._canvas.setAttribute("role","region");const i=this._containerDimensions(),o=this._getClampedPixelRatio(i[0],i[1]);this._resizeCanvas(i[0],i[1],o);const r=this._controlContainer=n.create("div","maplibregl-control-container",e),a=this._controlPositions={};["top-left","top-right","bottom-left","bottom-right"].forEach((e=>{a[e]=n.create("div",`maplibregl-ctrl-${e} `,r);})),this._container.addEventListener("scroll",this._onMapScroll,!1);}_resizeCanvas(e,t,i){this._canvas.width=Math.floor(i*e),this._canvas.height=Math.floor(i*t),this._canvas.style.width=`${e}px`,this._canvas.style.height=`${t}px`;}_setupPainter(){const e=Object.assign(Object.assign({},this._canvasContextAttributes),{alpha:!0,depth:!0,stencil:!0,premultipliedAlpha:!0});let t=null;this._canvas.addEventListener("webglcontextcreationerror",(i=>{t={requestedAttributes:e},i&&(t.statusMessage=i.statusMessage,t.type=i.type);}),{once:!0});let i=null;if(i=this._canvasContextAttributes.contextType?this._canvas.getContext(this._canvasContextAttributes.contextType,e):this._canvas.getContext("webgl2",e)||this._canvas.getContext("webgl",e),!i){const e="Failed to initialize WebGL";throw t?(t.message=e,new Error(JSON.stringify(t))):new Error(e)}this.painter=new Mr(i,this.transform),l.testSupport(i);}migrateProjection(e,i){super.migrateProjection(e,i),this.painter.transform=e,this.fire(new t.l("projectiontransition",{newProjection:this.style.projection.name}));}loaded(){return !this._styleDirty&&!this._sourcesDirty&&!!this.style&&this.style.loaded()}_update(e){return this.style&&this.style._loaded?(this._styleDirty=this._styleDirty||e,this._sourcesDirty=!0,this.triggerRepaint(),this):this}_requestRenderFrame(e){return this._update(),this._renderTaskQueue.add(e)}_cancelRenderFrame(e){this._renderTaskQueue.remove(e);}_render(e){var i,o,r,a,n;const l=this._idleTriggered?this._fadeDuration:0,c=(null===(i=this.style.projection)||void 0===i?void 0:i.transitionState)>0;if(this.painter.context.setDirty(),this.painter.setBaseState(),this._renderTaskQueue.run(e),this._removed)return;let h=!1;if(this.style&&this._styleDirty){this._styleDirty=!1;const e=this.transform.zoom,i=s.now();this.style.zoomHistory.update(e,i);const o=new t.F(e,{now:i,fadeDuration:l,zoomHistory:this.style.zoomHistory,transition:this.style.getTransition(),globalState:this.style.getGlobalState()}),r=o.crossFadingFactor();1===r&&r===this._crossFadingFactor||(h=!0,this._crossFadingFactor=r),this.style.update(o);}const u=(null===(o=this.style.projection)||void 0===o?void 0:o.transitionState)>0!==c;null===(r=this.style.projection)||void 0===r||r.setErrorQueryLatitudeDegrees(this.transform.center.lat),this.transform.setTransitionState(null===(a=this.style.projection)||void 0===a?void 0:a.transitionState,null===(n=this.style.projection)||void 0===n?void 0:n.latitudeErrorCorrectionRadians),this.style&&(this._sourcesDirty||u)&&(this._sourcesDirty=!1,this.style._updateSources(this.transform)),this.terrain?(this.terrain.sourceCache.update(this.transform,this.terrain),this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this.transform.center,this.transform.tileZoom)),!this._elevationFreeze&&this._centerClampedToGround&&this.transform.setElevation(this.terrain.getElevationForLngLatZoom(this.transform.center,this.transform.tileZoom))):(this.transform.setMinElevationForCurrentTile(0),this._centerClampedToGround&&this.transform.setElevation(0)),this._placementDirty=this.style&&this.style._updatePlacement(this.transform,this.showCollisionBoxes,l,this._crossSourceCollisions,u),this.painter.render(this.style,{showTileBoundaries:this.showTileBoundaries,showOverdrawInspector:this._showOverdrawInspector,rotating:this.isRotating(),zooming:this.isZooming(),moving:this.isMoving(),fadeDuration:l,showPadding:this.showPadding}),this.fire(new t.l("render")),this.loaded()&&!this._loaded&&(this._loaded=!0,t.cw.mark(t.cx.load),this.fire(new t.l("load"))),this.style&&(this.style.hasTransitions()||h)&&(this._styleDirty=!0),this.style&&!this._placementDirty&&this.style._releaseSymbolFadeTiles();const d=this._sourcesDirty||this._styleDirty||this._placementDirty;return d||this._repaint?this.triggerRepaint():!this.isMoving()&&this.loaded()&&this.fire(new t.l("idle")),!this._loaded||this._fullyLoaded||d||(this._fullyLoaded=!0,t.cw.mark(t.cx.fullLoad)),this}redraw(){return this.style&&(this._frameRequest&&(this._frameRequest.abort(),this._frameRequest=null),this._render(0)),this}remove(){var e;this._hash&&this._hash.remove();for(const e of this._controls)e.onRemove(this);this._controls=[],this._frameRequest&&(this._frameRequest.abort(),this._frameRequest=null),this._renderTaskQueue.clear(),this.painter.destroy(),this.handlers.destroy(),delete this.handlers,this.setStyle(null),"undefined"!=typeof window&&removeEventListener("online",this._onWindowOnline,!1),p.removeThrottleControl(this._imageQueueHandle),null===(e=this._resizeObserver)||void 0===e||e.disconnect();const i=this.painter.context.gl.getExtension("WEBGL_lose_context");(null==i?void 0:i.loseContext)&&i.loseContext(),this._canvas.removeEventListener("webglcontextrestored",this._contextRestored,!1),this._canvas.removeEventListener("webglcontextlost",this._contextLost,!1),n.remove(this._canvasContainer),n.remove(this._controlContainer),this._container.removeEventListener("scroll",this._onMapScroll,!1),this._container.classList.remove("maplibregl-map"),t.cw.clearMetrics(),this._removed=!0,this.fire(new t.l("remove"));}triggerRepaint(){this.style&&!this._frameRequest&&(this._frameRequest=new AbortController,s.frame(this._frameRequest,(e=>{t.cw.frame(e),this._frameRequest=null;try{this._render(e);}catch(e){if(!t.cy(e)&&!function(e){return e.message===qo}(e))throw e}}),(()=>{})));}get showTileBoundaries(){return !!this._showTileBoundaries}set showTileBoundaries(e){this._showTileBoundaries!==e&&(this._showTileBoundaries=e,this._update());}get showPadding(){return !!this._showPadding}set showPadding(e){this._showPadding!==e&&(this._showPadding=e,this._update());}get showCollisionBoxes(){return !!this._showCollisionBoxes}set showCollisionBoxes(e){this._showCollisionBoxes!==e&&(this._showCollisionBoxes=e,e?this.style._generateCollisionBoxes():this._update());}get showOverdrawInspector(){return !!this._showOverdrawInspector}set showOverdrawInspector(e){this._showOverdrawInspector!==e&&(this._showOverdrawInspector=e,this._update());}get repaint(){return !!this._repaint}set repaint(e){this._repaint!==e&&(this._repaint=e,this.triggerRepaint());}get vertices(){return !!this._vertices}set vertices(e){this._vertices=e,this._update();}get version(){return Za}getCameraTargetElevation(){return this.transform.elevation}getProjection(){return this.style.getProjection()}setProjection(e){return this._lazyInitEmptyStyle(),this.style.setProjection(e),this._update(!0)}},e.MapMouseEvent=jr,e.MapTouchEvent=Nr,e.MapWheelEvent=Ur,e.Marker=Ka,e.NavigationControl=class{constructor(e){this._updateZoomButtons=()=>{const e=this._map.getZoom(),t=e===this._map.getMaxZoom(),i=e===this._map.getMinZoom();this._zoomInButton.disabled=t,this._zoomOutButton.disabled=i,this._zoomInButton.setAttribute("aria-disabled",t.toString()),this._zoomOutButton.setAttribute("aria-disabled",i.toString());},this._rotateCompassArrow=()=>{this._compassIcon.style.transform=this.options.visualizePitch&&this.options.visualizeRoll?`scale(${1/Math.pow(Math.cos(this._map.transform.pitchInRadians),.5)}) rotateZ(${-this._map.transform.roll}deg) rotateX(${this._map.transform.pitch}deg) rotateZ(${-this._map.transform.bearing}deg)`:this.options.visualizePitch?`scale(${1/Math.pow(Math.cos(this._map.transform.pitchInRadians),.5)}) rotateX(${this._map.transform.pitch}deg) rotateZ(${-this._map.transform.bearing}deg)`:this.options.visualizeRoll?`rotate(${-this._map.transform.bearing-this._map.transform.roll}deg)`:`rotate(${-this._map.transform.bearing}deg)`;},this._setButtonTitle=(e,t)=>{const i=this._map._getUIString(`NavigationControl.${t}`);e.title=i,e.setAttribute("aria-label",i);},this.options=t.e({},Va,e),this._container=n.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._container.addEventListener("contextmenu",(e=>e.preventDefault())),this.options.showZoom&&(this._zoomInButton=this._createButton("maplibregl-ctrl-zoom-in",(e=>this._map.zoomIn({},{originalEvent:e}))),n.create("span","maplibregl-ctrl-icon",this._zoomInButton).setAttribute("aria-hidden","true"),this._zoomOutButton=this._createButton("maplibregl-ctrl-zoom-out",(e=>this._map.zoomOut({},{originalEvent:e}))),n.create("span","maplibregl-ctrl-icon",this._zoomOutButton).setAttribute("aria-hidden","true")),this.options.showCompass&&(this._compass=this._createButton("maplibregl-ctrl-compass",(e=>{this.options.visualizePitch?this._map.resetNorthPitch({},{originalEvent:e}):this._map.resetNorth({},{originalEvent:e});})),this._compassIcon=n.create("span","maplibregl-ctrl-icon",this._compass),this._compassIcon.setAttribute("aria-hidden","true"));}onAdd(e){return this._map=e,this.options.showZoom&&(this._setButtonTitle(this._zoomInButton,"ZoomIn"),this._setButtonTitle(this._zoomOutButton,"ZoomOut"),this._map.on("zoom",this._updateZoomButtons),this._updateZoomButtons()),this.options.showCompass&&(this._setButtonTitle(this._compass,"ResetBearing"),this.options.visualizePitch&&this._map.on("pitch",this._rotateCompassArrow),this.options.visualizeRoll&&this._map.on("roll",this._rotateCompassArrow),this._map.on("rotate",this._rotateCompassArrow),this._rotateCompassArrow(),this._handler=new $a(this._map,this._compass,this.options.visualizePitch)),this._container}onRemove(){n.remove(this._container),this.options.showZoom&&this._map.off("zoom",this._updateZoomButtons),this.options.showCompass&&(this.options.visualizePitch&&this._map.off("pitch",this._rotateCompassArrow),this.options.visualizeRoll&&this._map.off("roll",this._rotateCompassArrow),this._map.off("rotate",this._rotateCompassArrow),this._handler.off(),delete this._handler),delete this._map;}_createButton(e,t){const i=n.create("button",e,this._container);return i.type="button",i.addEventListener("click",t),i}},e.Popup=class extends t.E{constructor(e){super(),this._updateOpacity=()=>{void 0!==this.options.locationOccludedOpacity&&(this._container.style.opacity=this._map.transform.isLocationOccluded(this.getLngLat())?`${this.options.locationOccludedOpacity}`:"");},this.remove=()=>(this._content&&n.remove(this._content),this._container&&(n.remove(this._container),delete this._container),this._map&&(this._map.off("move",this._update),this._map.off("move",this._onClose),this._map.off("click",this._onClose),this._map.off("remove",this.remove),this._map.off("mousemove",this._onMouseMove),this._map.off("mouseup",this._onMouseUp),this._map.off("drag",this._onDrag),this._map._canvasContainer.classList.remove("maplibregl-track-pointer"),delete this._map,this.fire(new t.l("close"))),this),this._onMouseUp=e=>{this._update(e.point);},this._onMouseMove=e=>{this._update(e.point);},this._onDrag=e=>{this._update(e.point);},this._update=e=>{if(!this._map||!this._lngLat&&!this._trackPointer||!this._content)return;if(!this._container){if(this._container=n.create("div","maplibregl-popup",this._map.getContainer()),this._tip=n.create("div","maplibregl-popup-tip",this._container),this._container.appendChild(this._content),this.options.className)for(const e of this.options.className.split(" "))this._container.classList.add(e);this._closeButton&&this._closeButton.setAttribute("aria-label",this._map._getUIString("Popup.Close")),this._trackPointer&&this._container.classList.add("maplibregl-popup-track-pointer");}if(this.options.maxWidth&&this._container.style.maxWidth!==this.options.maxWidth&&(this._container.style.maxWidth=this.options.maxWidth),this._lngLat=Wa(this._lngLat,this._flatPos,this._map.transform,this._trackPointer),this._trackPointer&&!e)return;const t=this._flatPos=this._pos=this._trackPointer&&e?e:this._map.project(this._lngLat);this._map.terrain&&(this._flatPos=this._trackPointer&&e?e:this._map.transform.locationToScreenPoint(this._lngLat));let i=this.options.anchor;const o=as(this.options.offset);if(!i){const e=this._container.offsetWidth,r=this._container.offsetHeight;let a;a=t.y+o.bottom.ythis._map.transform.height-r?["bottom"]:[],t.xthis._map.transform.width-e/2&&a.push("right"),i=0===a.length?"bottom":a.join("-");}let r=t.add(o[i]);this.options.subpixelPositioning||(r=r.round()),n.setTransform(this._container,`${Ha[i]} translate(${r.x}px,${r.y}px)`),Xa(this._container,i,"popup"),this._updateOpacity();},this._onClose=()=>{this.remove();},this.options=t.e(Object.create(os),e);}addTo(e){return this._map&&this.remove(),this._map=e,this.options.closeOnClick&&this._map.on("click",this._onClose),this.options.closeOnMove&&this._map.on("move",this._onClose),this._map.on("remove",this.remove),this._update(),this._focusFirstElement(),this._trackPointer?(this._map.on("mousemove",this._onMouseMove),this._map.on("mouseup",this._onMouseUp),this._container&&this._container.classList.add("maplibregl-popup-track-pointer"),this._map._canvasContainer.classList.add("maplibregl-track-pointer")):this._map.on("move",this._update),this.fire(new t.l("open")),this}isOpen(){return !!this._map}getLngLat(){return this._lngLat}setLngLat(e){return this._lngLat=t.S.convert(e),this._pos=null,this._flatPos=null,this._trackPointer=!1,this._update(),this._map&&(this._map.on("move",this._update),this._map.off("mousemove",this._onMouseMove),this._container&&this._container.classList.remove("maplibregl-popup-track-pointer"),this._map._canvasContainer.classList.remove("maplibregl-track-pointer")),this}trackPointer(){return this._trackPointer=!0,this._pos=null,this._flatPos=null,this._update(),this._map&&(this._map.off("move",this._update),this._map.on("mousemove",this._onMouseMove),this._map.on("drag",this._onDrag),this._container&&this._container.classList.add("maplibregl-popup-track-pointer"),this._map._canvasContainer.classList.add("maplibregl-track-pointer")),this}getElement(){return this._container}setText(e){return this.setDOMContent(document.createTextNode(e))}setHTML(e){const t=document.createDocumentFragment(),i=document.createElement("body");let o;for(i.innerHTML=e;o=i.firstChild,o;)t.appendChild(o);return this.setDOMContent(t)}getMaxWidth(){var e;return null===(e=this._container)||void 0===e?void 0:e.style.maxWidth}setMaxWidth(e){return this.options.maxWidth=e,this._update(),this}setDOMContent(e){if(this._content)for(;this._content.hasChildNodes();)this._content.firstChild&&this._content.removeChild(this._content.firstChild);else this._content=n.create("div","maplibregl-popup-content",this._container);return this._content.appendChild(e),this._createCloseButton(),this._update(),this._focusFirstElement(),this}addClassName(e){return this._container&&this._container.classList.add(e),this}removeClassName(e){return this._container&&this._container.classList.remove(e),this}setOffset(e){return this.options.offset=e,this._update(),this}toggleClassName(e){if(this._container)return this._container.classList.toggle(e)}setSubpixelPositioning(e){this.options.subpixelPositioning=e;}_createCloseButton(){this.options.closeButton&&(this._closeButton=n.create("button","maplibregl-popup-close-button",this._content),this._closeButton.type="button",this._closeButton.innerHTML="×",this._closeButton.addEventListener("click",this._onClose));}_focusFirstElement(){if(!this.options.focusAfterOpen||!this._container)return;const e=this._container.querySelector(rs);e&&e.focus();}},e.RasterDEMTileSource=W,e.RasterTileSource=q,e.ScaleControl=class{constructor(e){this._onMove=()=>{ts(this._map,this._container,this.options);},this.setUnit=e=>{this.options.unit=e,ts(this._map,this._container,this.options);},this.options=Object.assign(Object.assign({},es),e);}getDefaultPosition(){return "bottom-left"}onAdd(e){return this._map=e,this._container=n.create("div","maplibregl-ctrl maplibregl-ctrl-scale",e.getContainer()),this._map.on("move",this._onMove),this._onMove(),this._container}onRemove(){n.remove(this._container),this._map.off("move",this._onMove),this._map=void 0;}},e.ScrollZoomHandler=va,e.Style=wi,e.TerrainControl=class{constructor(e){this._toggleTerrain=()=>{this._map.getTerrain()?this._map.setTerrain(null):this._map.setTerrain(this.options),this._updateTerrainIcon();},this._updateTerrainIcon=()=>{this._terrainButton.classList.remove("maplibregl-ctrl-terrain"),this._terrainButton.classList.remove("maplibregl-ctrl-terrain-enabled"),this._map.terrain?(this._terrainButton.classList.add("maplibregl-ctrl-terrain-enabled"),this._terrainButton.title=this._map._getUIString("TerrainControl.Disable")):(this._terrainButton.classList.add("maplibregl-ctrl-terrain"),this._terrainButton.title=this._map._getUIString("TerrainControl.Enable"));},this.options=e;}onAdd(e){return this._map=e,this._container=n.create("div","maplibregl-ctrl maplibregl-ctrl-group"),this._terrainButton=n.create("button","maplibregl-ctrl-terrain",this._container),n.create("span","maplibregl-ctrl-icon",this._terrainButton).setAttribute("aria-hidden","true"),this._terrainButton.type="button",this._terrainButton.addEventListener("click",this._toggleTerrain),this._updateTerrainIcon(),this._map.on("terrain",this._updateTerrainIcon),this._container}onRemove(){n.remove(this._container),this._map.off("terrain",this._updateTerrainIcon),this._map=void 0;}},e.TwoFingersTouchPitchHandler=da,e.TwoFingersTouchRotateHandler=ha,e.TwoFingersTouchZoomHandler=la,e.TwoFingersTouchZoomRotateHandler=Pa,e.VectorTileSource=$,e.VideoSource=K,e.addSourceType=(e,i)=>t._(void 0,void 0,void 0,(function*(){if(J(e))throw new Error(`A source type called "${e}" already exists.`);((e,t)=>{Q[e]=t;})(e,i);})),e.clearPrewarmedResources=function(){const e=A;e&&(e.isPreloaded()&&1===e.numActive()?(e.release(R),A=null):console.warn("Could not clear WebWorkers since there are active Map instances that still reference it. The pre-warmed WebWorker pool can only be cleared when all map instances have been removed with map.remove()"));},e.createTileMesh=Xt,e.getMaxParallelImageRequests=function(){return t.a.MAX_PARALLEL_IMAGE_REQUESTS},e.getRTLTextPluginStatus=function(){return oe().getRTLTextPluginStatus()},e.getVersion=function(){return ss},e.getWorkerCount=function(){return z.workerCount},e.getWorkerUrl=function(){return t.a.WORKER_URL},e.importScriptInWorkers=function(e){return B().broadcast("IS",e)},e.prewarm=function(){k().acquire(R);},e.setMaxParallelImageRequests=function(e){t.a.MAX_PARALLEL_IMAGE_REQUESTS=e;},e.setRTLTextPlugin=function(e,t){return oe().setRTLTextPlugin(e,t)},e.setWorkerCount=function(e){z.workerCount=e;},e.setWorkerUrl=function(e){t.a.WORKER_URL=e;};})); + +// +// Our custom intro provides a specialized "define()" function, called by the +// AMD modules below, that sets up the worker blob URL and then executes the +// main module, storing its exported value as 'maplibregl' + + +var maplibregl$1 = maplibregl; + +return maplibregl$1; + +})); diff --git a/streetup/common/static/common/maskable-192.png b/streetup/common/static/common/maskable-192.png new file mode 100644 index 0000000..19a5442 Binary files /dev/null and b/streetup/common/static/common/maskable-192.png differ diff --git a/streetup/common/static/common/maskable-512.png b/streetup/common/static/common/maskable-512.png new file mode 100644 index 0000000..6562dee Binary files /dev/null and b/streetup/common/static/common/maskable-512.png differ diff --git a/streetup/common/static/common/proj4.min.js b/streetup/common/static/common/proj4.min.js new file mode 100644 index 0000000..9387d17 --- /dev/null +++ b/streetup/common/static/common/proj4.min.js @@ -0,0 +1 @@ +!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):t.proj4=s()}(this,function(){"use strict";function m(t,s){if(t[s])return t[s];for(var i,a=Object.keys(t),h=s.toLowerCase().replace(gt,""),e=-1;++eMath.PI&&(a-=2*Math.PI),t=Math.sin(h),h=Math.cos(h),{x:((i=i/Math.sqrt(1-s*(t*t)))+e)*h*Math.cos(a),y:(i+e)*h*Math.sin(a),z:(i*(1-s)+e)*t}}function W(t,s,i,a){var h,e,n,r,o,l,u,M,c,f,d,m=t.x,p=t.y,y=t.z||0,_=Math.sqrt(m*m+p*p),x=Math.sqrt(m*m+p*p+y*y);if(_/i<1e-12){if(f=0,x/i<1e-12)return d=-a,{x:t.x,y:t.y,z:t.z}}else f=Math.atan2(p,m);for(h=y/x,r=(e=_/x)*(1-s)*(n=1/Math.sqrt(1-s*(2-s)*e*e)),o=h*n,c=0;c++,M=s*(M=i/Math.sqrt(1-s*o*o))/(M+(d=_*r+y*o-M*(1-s*o*o))),M=(u=h*(n=1/Math.sqrt(1-M*(2-M)*e*e)))*r-(l=e*(1-M)*n)*o,r=l,o=u,1e-24=s.lim[0])return e;if(a<0||a>=s.lim[1])return e;var a=a*s.lim[0]+i,i=s.cvs[a][0],n=s.cvs[a][1],r=s.cvs[++a][0],o=s.cvs[a][1];a+=s.lim[0];var l=s.cvs[a][0],u=s.cvs[a][1],M=s.cvs[--a][0],s=s.cvs[a][1],a=h*t,c=h*(1-t),f=(1-h)*(1-t),h=(1-h)*t;return e.x=f*i+c*r+h*M+a*l,e.y=f*n+c*o+h*s+a*u,e}function K(t){if("function"==typeof Number.isFinite){if(Number.isFinite(t))return;throw new TypeError("coordinates must be finite numbers")}if("number"!=typeof t||t!=t||!isFinite(t))throw new TypeError("coordinates must be finite numbers")}function V(t,s,i,a){var h,e,n=void 0!==(i=Array.isArray(i)?Wt(i):{x:i.x,y:i.y,z:i.z,m:i.m}).z;if(Ht(i),t.datum&&s.datum&&(e=s,((h=t).datum.datum_type===y||h.datum.datum_type===_||h.datum.datum_type===x)&&"WGS84"!==e.datumCode||(e.datum.datum_type===y||e.datum.datum_type===_||e.datum.datum_type===x)&&"WGS84"!==h.datumCode)&&(i=V(t,e=new p("WGS84"),i,a),t=e),a&&"enu"!==t.axis&&(i=Qt(t,!1,i)),"longlat"===t.projName)i={x:i.x*v,y:i.y*v,z:i.z||0};else if(t.to_meter&&(i={x:i.x*t.to_meter,y:i.y*t.to_meter,z:i.z||0}),!(i=t.inverse(i)))return;if(t.from_greenwich&&(i.x+=t.from_greenwich),i=Ut(t.datum,s.datum,i))return s.from_greenwich&&(i={x:i.x-s.from_greenwich,y:i.y,z:i.z||0}),"longlat"===s.projName?i={x:i.x*w,y:i.y*w,z:i.z||0}:(i=s.forward(i),s.to_meter&&(i={x:i.x/s.to_meter,y:i.y/s.to_meter,z:i.z||0})),a&&"enu"!==s.axis?Qt(s,!0,i):(i&&!n&&delete i.z,i)}function Z(s,i,a,t){var h,e;return Array.isArray(a)?(e=V(s,i,a,t)||{x:NaN,y:NaN},2mi?(t.x=a*Math.sin(s=s*Math.cos(i)/a),t.y=this.cphi1-a*Math.cos(s)):t.x=t.y=0,t.x=this.a*t.x+(this.x0||0),t.y=this.a*t.y+(this.y0||0),t}function ut(t){t.x=(t.x-(this.x0||0))/this.a,t.y=(t.y-(this.y0||0))/this.a;var s=d(t.x,t.y=this.cphi1-t.y),i=this.cphi1+this.phi1-s;if(Math.abs(i)>g)throw new Error;return s=Math.abs(Math.abs(i)-g)<=mi?0:s*Math.atan2(t.x,t.y)/Math.cos(i),t.x=S(s+(this.long0||0)),t.y=E(i),t}var y=1,_=2,x=3,Mt=4,ct=5,ft=484813681109536e-20,g=Math.PI/2,dt=.16666666666666666,mt=.04722222222222222,pt=.022156084656084655,b=1e-10,v=.017453292519943295,w=57.29577951308232,N=Math.PI/4,yt=2*Math.PI,M=3.14159265359,_t={greenwich:0,lisbon:-9.131906111111,paris:2.337229166667,bogota:-74.080916666667,madrid:-3.687938888889,rome:12.452333333333,bern:7.439583333333,jakarta:106.807719444444,ferro:-17.666666666667,brussels:4.367975,stockholm:18.058277777778,athens:23.7163375,oslo:10.722916666667},xt={mm:{to_meter:.001},cm:{to_meter:.01},ft:{to_meter:.3048},"us-ft":{to_meter:1200/3937},fath:{to_meter:1.8288},kmi:{to_meter:1852},"us-ch":{to_meter:20.1168402336805},"us-mi":{to_meter:1609.34721869444},km:{to_meter:1e3},"ind-ft":{to_meter:.30479841},"ind-yd":{to_meter:.91439523},mi:{to_meter:1609.344},yd:{to_meter:.9144},ch:{to_meter:20.1168},link:{to_meter:.201168},dm:{to_meter:.01},in:{to_meter:.0254},"ind-ch":{to_meter:20.11669506},"us-in":{to_meter:.025400050800101},"us-yd":{to_meter:.914401828803658}},gt=/[\s_\-\/\(\)]/g,bt=function(t){var s,i,a,h={},e=t.split("+").map(function(t){return t.trim()}).filter(function(t){return t}).reduce(function(t,s){s=s.split("=");return s.push(!0),t[s[0].toLowerCase()]=s[1],t},{}),n={proj:"projName",datum:"datumCode",rf:function(t){h.rf=parseFloat(t)},lat_0:function(t){h.lat0=t*v},lat_1:function(t){h.lat1=t*v},lat_2:function(t){h.lat2=t*v},lat_ts:function(t){h.lat_ts=t*v},lon_0:function(t){h.long0=t*v},lon_1:function(t){h.long1=t*v},lon_2:function(t){h.long2=t*v},alpha:function(t){h.alpha=parseFloat(t)*v},gamma:function(t){h.rectified_grid_angle=parseFloat(t)},lonc:function(t){h.longc=t*v},x_0:function(t){h.x0=parseFloat(t)},y_0:function(t){h.y0=parseFloat(t)},k_0:function(t){h.k0=parseFloat(t)},k:function(t){h.k0=parseFloat(t)},a:function(t){h.a=parseFloat(t)},b:function(t){h.b=parseFloat(t)},r:function(t){h.a=h.b=parseFloat(t)},r_a:function(){h.R_A=!0},zone:function(t){h.zone=parseInt(t,10)},south:function(){h.utmSouth=!0},towgs84:function(t){h.datum_params=t.split(",").map(function(t){return parseFloat(t)})},to_meter:function(t){h.to_meter=parseFloat(t)},units:function(t){h.units=t;t=m(xt,t);t&&(h.to_meter=t.to_meter)},from_greenwich:function(t){h.from_greenwich=t*v},pm:function(t){var s=m(_t,t);h.from_greenwich=(s||parseFloat(t))*v},nadgrids:function(t){"@null"===t?h.datumCode="none":h.nadgrids=t},axis:function(t){3===t.length&&-1!=="ewnsud".indexOf(t.substr(0,1))&&-1!=="ewnsud".indexOf(t.substr(1,1))&&-1!=="ewnsud".indexOf(t.substr(2,1))&&(h.axis=t)},approx:function(){h.approx=!0}};for(s in e)i=e[s],s in n?"function"==typeof(a=n[s])?a(i):h[a]=i:h[s]=i;return"string"==typeof h.datumCode&&"WGS84"!==h.datumCode&&(h.datumCode=h.datumCode.toLowerCase()),h},vt=/\s/,wt=/[A-Za-z]/,Nt=/[A-Za-z84_]/,Ct=/[,\]]/,St=/[\d\.E\-\+]/,Pt=(a.prototype.readCharicter=function(){var t=this.text[this.place++];if(4!==this.state)for(;vt.test(t);){if(this.place>=this.text.length)return;t=this.text[this.place++]}switch(this.state){case 1:return this.neutral(t);case 2:return this.keyword(t);case 4:return this.quoted(t);case 5:return this.afterquote(t);case 3:return this.number(t);case-1:return}},a.prototype.afterquote=function(t){if('"'===t)return this.word+='"',void(this.state=4);if(Ct.test(t))return this.word=this.word.trim(),void this.afterItem(t);throw new Error("havn't handled \""+t+'" in afterquote yet, index '+this.place)},a.prototype.afterItem=function(t){return","===t?(null!==this.word&&this.currentObject.push(this.word),this.word=null,void(this.state=1)):"]"===t?(this.level--,null!==this.word&&(this.currentObject.push(this.word),this.word=null),this.state=1,this.currentObject=this.stack.pop(),void(this.currentObject||(this.state=-1))):void 0},a.prototype.number=function(t){if(!St.test(t)){if(Ct.test(t))return this.word=parseFloat(this.word),void this.afterItem(t);throw new Error("havn't handled \""+t+'" in number yet, index '+this.place)}this.word+=t},a.prototype.quoted=function(t){'"'!==t?this.word+=t:this.state=5},a.prototype.keyword=function(t){if(Nt.test(t))this.word+=t;else{var s;if("["===t)return(s=[]).push(this.word),this.level++,null===this.root?this.root=s:this.currentObject.push(s),this.stack.push(this.currentObject),this.currentObject=s,void(this.state=1);if(!Ct.test(t))throw new Error("havn't handled \""+t+'" in keyword yet, index '+this.place);this.afterItem(t)}},a.prototype.neutral=function(t){if(wt.test(t))return this.word=t,void(this.state=2);if('"'===t)return this.word="",void(this.state=4);if(St.test(t))return this.word=t,void(this.state=3);if(!Ct.test(t))throw new Error("havn't handled \""+t+'" in neutral yet, index '+this.place);this.afterItem(t)},a.prototype.output=function(){for(;this.placeb?Math.tan(i):0,l=Math.pow(l,2),u=Math.pow(l,2),M=1-this.es*Math.pow(a,2),M=(e/=Math.sqrt(M),f(i,a,h,this.en)),c=this.a*(this.k0*e*(1+n/6*(1-l+r+n/20*(5-18*l+u+14*r-58*l*r+n/42*(61+179*u-u*l-479*l)))))+this.x0,M=this.a*(this.k0*(M-this.ml0+a*s*e/2*(1+n/12*(5-l+9*r+4*o+n/30*(61+u-58*l+270*r-330*l*r+n/56*(1385+543*u-u*l-3111*l))))))+this.y0;else{a=h*Math.sin(s);if(Math.abs(Math.abs(a)-1)b?Math.tan(l):0,o=this.ep2*Math.pow(s,2),n=Math.pow(o,2),i=Math.pow(h,2),e=Math.pow(i,2),a=1-this.es*Math.pow(r,2),r=u*Math.sqrt(a)/this.k0,h=l-(a*=h)*(l=Math.pow(r,2))/(1-this.es)*.5*(1-l/12*(5+3*i-9*o*i+o-4*n-l/30*(61+90*i-252*o*i+45*e+46*o-l/56*(1385+3633*i+4095*e+1574*e*i)))),S(this.long0+r*(1-l/6*(1+2*i+o-l/20*(5+28*i+24*e+8*o*i+6*o-l/42*(61+662*i+1320*e+720*e*i))))/s)):(h=g*qt(M),0)):(r=.5*((n=Math.exp(u/this.k0))-1/n),o=this.lat0+M/this.k0,l=Math.cos(o),a=Math.sqrt((1-Math.pow(l,2))/(1+Math.pow(r,2))),h=Math.asin(a),M<0&&(h=-h),0==r&&0===l?0:S(Math.atan2(r,l)+this.long0)),t.x=e,t.y=h,t},names:["Fast_Transverse_Mercator","Fast Transverse Mercator"]},ys={init:function(){if(!this.approx&&(isNaN(this.es)||this.es<=0))throw new Error('Incorrect elliptical usage. Try using the +approx option in the proj string, or PROJECTION["Fast_Transverse_Mercator"] in the WKT.');this.approx&&(ps.init.apply(this),this.forward=ps.forward,this.inverse=ps.inverse),this.x0=void 0!==this.x0?this.x0:0,this.y0=void 0!==this.y0?this.y0:0,this.long0=void 0!==this.long0?this.long0:0,this.lat0=void 0!==this.lat0?this.lat0:0,this.cgb=[],this.cbg=[],this.utg=[],this.gtu=[];var t=this.es/(1+Math.sqrt(1-this.es)),t=t/(2-t),s=t,t=(this.cgb[0]=t*(2+t*(-2/3+t*(t*(116/45+t*(26/45+-2854/675*t))-2))),this.cbg[0]=t*(t*(2/3+t*(4/3+t*(-82/45+t*(32/45+4642/4725*t))))-2),this.cgb[1]=(s*=t)*(7/3+t*(t*(-227/45+t*(2704/315+2323/945*t))-1.6)),this.cbg[1]=s*(5/3+t*(-16/15+t*(-13/9+t*(904/315+-1522/945*t)))),this.cgb[2]=(s*=t)*(56/15+t*(-136/35+t*(-1262/105+73814/2835*t))),this.cbg[2]=s*(-26/15+t*(34/21+t*(1.6+-12686/2835*t))),this.cgb[3]=(s*=t)*(4279/630+t*(-332/35+-399572/14175*t)),this.cbg[3]=s*(1237/630+t*(-24832/14175*t-2.4)),this.cgb[4]=(s*=t)*(4174/315+-144838/6237*t),this.cbg[4]=s*(-734/315+109598/31185*t),this.cgb[5]=601676/22275*(s*=t),this.cbg[5]=444337/155925*s,s=Math.pow(t,2),this.Qn=this.k0/(1+t)*(1+s*(.25+s*(1/64+s/256))),this.utg[0]=t*(t*(2/3+t*(-37/96+t*(1/360+t*(81/512+-96199/604800*t))))-.5),this.gtu[0]=t*(.5+t*(-2/3+t*(5/16+t*(41/180+t*(-127/288+7891/37800*t))))),this.utg[1]=s*(-1/48+t*(-1/15+t*(437/1440+t*(-46/105+1118711/3870720*t)))),this.gtu[1]=s*(13/48+t*(t*(557/1440+t*(281/630+-1983433/1935360*t))-.6)),this.utg[2]=(s*=t)*(-17/480+t*(37/840+t*(209/4480+-5569/90720*t))),this.gtu[2]=s*(61/240+t*(-103/140+t*(15061/26880+167603/181440*t))),this.utg[3]=(s*=t)*(-4397/161280+t*(11/504+830251/7257600*t)),this.gtu[3]=s*(49561/161280+t*(-179/168+6601661/7257600*t)),this.utg[4]=(s*=t)*(-4583/161280+108847/3991680*t),this.gtu[4]=s*(34729/80640+-3418889/1995840*t),this.utg[5]=-.03233083094085698*(s*=t),this.gtu[5]=.6650675310896665*s,Zt(this.cbg,this.lat0));this.Zb=-this.Qn*(t+function(t,s){for(var i,a=2*Math.cos(s),h=t.length-1,e=t[h],n=0;0<=--h;)i=a*e-n+t[h],n=e,e=i;return Math.sin(s)*i}(this.gtu,2*t))},forward:function(t){var s,i=S(t.x-this.long0),a=t.y,a=Zt(this.cbg,a),h=Math.sin(a),e=Math.cos(a),n=Math.sin(i),r=Math.cos(i),n=(a=Math.atan2(h,r*e),i=Math.atan2(n*e,d(h,e*r)),i=Vt(Math.tan(i)),Yt(this.gtu,2*a,2*i));return a+=n[0],i+=n[1],h=Math.abs(i)<=2.623395162778?(s=this.a*(this.Qn*i)+this.x0,this.a*(this.Qn*a+this.Zb)+this.y0):s=1/0,t.x=s,t.y=h,t},inverse:function(t){var s,i,a,h,e=(t.x-this.x0)*(1/this.a),n=(t.y-this.y0)*(1/this.a);return n=(n-this.Zb)/this.Qn,e/=this.Qn,h=Math.abs(e)<=2.623395162778?(n+=(a=Yt(this.utg,2*n,2*e))[0],e+=a[1],e=Math.atan(Kt(e)),a=Math.sin(n),s=Math.cos(n),h=Math.sin(e),i=Math.cos(e),n=Math.atan2(a*i,d(h,i*s)),e=Math.atan2(h,i*s),a=S(e+this.long0),Zt(this.cgb,n)):a=1/0,t.x=a,t.y=h,t},names:["Extended_Transverse_Mercator","Extended Transverse Mercator","etmerc","Transverse_Mercator","Transverse Mercator","Gauss Kruger","Gauss_Kruger","tmerc"]},_s={init:function(){var t=function(t,s){if(void 0===t){if((t=Math.floor(30*(S(s)+Math.PI)/Math.PI)+1)<0)return 0;if(60b&&(this.k0=.5*this.cons*o(this.e,Math.sin(this.lat_ts),Math.cos(this.lat_ts))/C(this.e,this.con*this.lat_ts,this.con*Math.sin(this.lat_ts))),this.ms1=o(this.e,this.sinlat0,this.coslat0),this.X0=2*Math.atan(this.ssfn_(this.lat0,this.sinlat0,this.e))-g,this.cosX0=Math.cos(this.X0),this.sinX0=Math.sin(this.X0))},forward:function(t){var s,i,a=t.x,h=t.y,e=Math.sin(h),n=Math.cos(h),r=S(a-this.long0);return Math.abs(Math.abs(a-this.long0)-Math.PI)<=b&&Math.abs(h+this.lat0)<=b?(t.x=NaN,t.y=NaN):this.sphere?(s=2*this.k0/(1+this.sinlat0*e+this.coslat0*n*Math.cos(r)),t.x=this.a*s*n*Math.sin(r)+this.x0,t.y=this.a*s*(this.coslat0*e-this.sinlat0*n*Math.cos(r))+this.y0):(n=2*Math.atan(this.ssfn_(h,e,this.e))-g,i=Math.cos(n),n=Math.sin(n),Math.abs(this.coslat0)<=b?(h=C(this.e,h*this.con,this.con*e),e=2*this.a*this.k0*h/this.cons,t.x=this.x0+e*Math.sin(a-this.long0),t.y=this.y0-this.con*e*Math.cos(a-this.long0)):(Math.abs(this.sinlat0)b?(a=Math.sin(this.lat0),s=Math.cos(this.lat0),t=1-this.es*a*a,this.B=s*s,this.B=Math.sqrt(1+this.es*this.B*this.B/d),this.A=this.B*this.k0*m/t,(i=(s=this.B*m/(s*Math.sqrt(t)))*s-1)<=0?i=0:(i=Math.sqrt(i),this.lat0<0&&(i=-i)),this.E=i+=s,this.E*=Math.pow(C(this.e,this.lat0,a),this.B)):(this.B=1/m,this.A=this.k0,this.E=s=i=1),c||f?(c?(h=Math.asin(Math.sin(M)/s),f||(e=M)):(h=e,M=Math.asin(s*Math.sin(h))),this.lam0=n-Math.asin(.5*(i-1/i)*Math.tan(h))/this.B):(d=Math.pow(C(this.e,l,Math.sin(l)),this.B),a=Math.pow(C(this.e,u,Math.sin(u)),this.B),i=this.E/d,m=(a-d)/(a+d),c=((c=this.E*this.E)-a*d)/(c+a*d),(t=r-o)<-Math.pi?o-=yt:t>Math.pi&&(o+=yt),this.lam0=S(.5*(r+o)-Math.atan(c*Math.tan(.5*this.B*(r-o))/m)/this.B),h=Math.atan(2*Math.sin(this.B*S(r-this.lam0))/(i-1/i)),e=M=Math.asin(s*Math.sin(h))),this.singam=Math.sin(h),this.cosgam=Math.cos(h),this.sinrot=Math.sin(e),this.cosrot=Math.cos(e),this.rB=1/this.B,this.ArB=this.A*this.rB,this.BrA=1/this.ArB,this.no_off?this.u_0=0:(this.u_0=Math.abs(this.ArB*Math.atan(Math.sqrt(s*s-1)/Math.cos(M))),this.lat0<0&&(this.u_0=-this.u_0)),i=.5*h,this.v_pole_n=this.ArB*Math.log(Math.tan(N-i)),this.v_pole_s=this.ArB*Math.log(Math.tan(N+i))},forward:function(t){var s,i,a,h,e={};if(t.x=t.x-this.lam0,Math.abs(Math.abs(t.y)-g)>b){if(a=.5*((h=this.E/Math.pow(C(this.e,t.y,Math.sin(t.y)),this.B))-(i=1/h)),h=.5*(h+i),s=Math.sin(this.B*t.x),h=(a*this.singam-s*this.cosgam)/h,Math.abs(Math.abs(h)-1)b?this.ns=Math.log(s/a)/Math.log(i/h):this.ns=t,isNaN(this.ns)&&(this.ns=t),this.f0=s/(this.ns*Math.pow(i,this.ns)),this.rh=this.a*this.f0*Math.pow(e,this.ns),this.title||(this.title="Lambert Conformal Conic"))},forward:function(t){var s=t.x,i=t.y;Math.abs(2*Math.abs(i)-Math.PI)<=b&&(i=qt(i)*(g-2*b));var a,h=Math.abs(Math.abs(i)-g);if(bb?this.ns0=(this.ms1*this.ms1-this.ms2*this.ms2)/(this.qs2-this.qs1):this.ns0=this.con,this.c=this.ms1*this.ms1+this.ns0*this.qs1,this.rh=this.a*Math.sqrt(this.c-this.ns0*this.qs0)/this.ns0)},forward:function(t){var s=t.x,i=t.y,i=(this.sin_phi=Math.sin(i),this.cos_phi=Math.cos(i),k(this.e3,this.sin_phi)),i=this.a*Math.sqrt(this.c-this.ns0*i)/this.ns0,s=this.ns0*S(s-this.long0),a=i*Math.sin(s)+this.x0,i=this.rh-i*Math.cos(s)+this.y0;return t.x=a,t.y=i,t},inverse:function(t){var s,i,a;return t.x-=this.x0,t.y=this.rh-t.y+this.y0,a=0<=this.ns0?(i=Math.sqrt(t.x*t.x+t.y*t.y),1):(i=-Math.sqrt(t.x*t.x+t.y*t.y),-1),(s=0)!==i&&(s=Math.atan2(a*t.x,a*t.y)),a=i*this.ns0/this.a,a=this.sphere?Math.asin((this.c-a*a)/(2*this.ns0)):(i=(this.c-a*a)/this.ns0,this.phi1z(this.e3,i)),i=S(s/this.ns0+this.long0),t.x=i,t.y=a,t},names:["Albers_Conic_Equal_Area","Albers","aea"],phi1z:function(t,s){var i,a,h,e=I(.5*s);if(tMath.PI&&(i=Math.PI),s=(2*a+Math.sin(2*a))/Math.PI,12*g*this.a?void 0:(h=o/this.a,r=Math.sin(h),h=Math.cos(h),s=this.long0,Math.abs(o)<=b?i=this.lat0:(i=I(h*this.sin_p12+t.y*r*this.cos_p12/o),n=Math.abs(this.lat0)-g,s=S(Math.abs(n)<=b?0<=this.lat0?this.long0+Math.atan2(t.x,-t.y):this.long0-Math.atan2(-t.x,t.y):this.long0+Math.atan2(t.x*r,o*this.cos_p12*h-t.y*this.sin_p12*r))),t.x=s,t.y=i,t):(n=ts(this.es),h=ss(this.es),r=is(this.es),e=as(this.es),Math.abs(this.sin_p12-1)<=b?(a=this.a*P(n,h,r,e,g),o=Math.sqrt(t.x*t.x+t.y*t.y),i=es((a-o)/this.a,n,h,r,e),s=S(this.long0+Math.atan2(t.x,-1*t.y))):Math.abs(this.sin_p12+1)<=b?(a=this.a*P(n,h,r,e,g),o=Math.sqrt(t.x*t.x+t.y*t.y),i=es((o-a)/this.a,n,h,r,e),s=S(this.long0+Math.atan2(t.x,t.y))):(o=Math.sqrt(t.x*t.x+t.y*t.y),a=Math.atan2(t.x,t.y),n=hs(this.a,this.e,this.sin_p12),h=Math.cos(a),e=-(r=this.e*this.cos_p12*h)*r/(1-this.es),r=3*this.es*(1-e)*this.sin_p12*this.cos_p12*h/(1-this.es),r=1-e*(n=(o=o/n)-e*(1+e)*Math.pow(o,3)/6-r*(1+3*e)*Math.pow(o,4)/24)*n/2-o*n*n*n/6,e=Math.asin(this.sin_p12*Math.cos(n)+this.cos_p12*Math.sin(n)*h),s=S(this.long0+Math.asin(Math.sin(a)*Math.sin(n)/Math.cos(e))),o=Math.sin(e),i=Math.atan2((o-this.es*r*this.sin_p12)*Math.tan(e),o*(1-this.es))),t.x=s,t.y=i,t)},names:["Azimuthal_Equidistant","aeqd"]},Ds={init:function(){this.sin_p14=Math.sin(this.lat0),this.cos_p14=Math.cos(this.lat0)},forward:function(t){var s,i,a,h=t.x,e=t.y,h=S(h-this.long0),n=Math.sin(e),e=Math.cos(e),r=Math.cos(h);return(0<(s=this.sin_p14*n+this.cos_p14*e*r)||Math.abs(s)<=b)&&(i=+this.a*e*Math.sin(h),a=this.y0+ +this.a*(this.cos_p14*n-this.sin_p14*e*r)),t.x=i,t.y=a,t},inverse:function(t){var s,i,a,h,e,n;return t.x-=this.x0,t.y-=this.y0,s=Math.sqrt(t.x*t.x+t.y*t.y),a=I(s/this.a),i=Math.sin(a),a=Math.cos(a),e=this.long0,Math.abs(s)<=b?n=this.lat0:(n=I(a*this.sin_p14+t.y*i*this.cos_p14/s),h=Math.abs(this.lat0)-g,e=Math.abs(h)<=b?S(0<=this.lat0?this.long0+Math.atan2(t.x,-t.y):this.long0-Math.atan2(-t.x,t.y)):S(this.long0+Math.atan2(t.x*i,s*this.cos_p14*a-t.y*this.sin_p14*i))),t.x=e,t.y=n,t},names:["ortho"]},Fs=1,Us=2,Qs=3,Ws=4,Hs=5,Js=6,A=1,G=2,j=3,Xs=4,Ks={init:function(){this.x0=this.x0||0,this.y0=this.y0||0,this.lat0=this.lat0||0,this.long0=this.long0||0,this.lat_ts=this.lat_ts||0,this.title=this.title||"Quadrilateralized Spherical Cube",this.lat0>=g-N/2?this.face=Hs:this.lat0<=-(g-N/2)?this.face=Js:Math.abs(this.long0)<=N?this.face=Fs:Math.abs(this.long0)<=g+N?this.face=0=Math.abs(t.y)?r.value=A:0<=t.y&&t.y>=Math.abs(t.x)?(r.value=G,s-=g):t.x<0&&-t.x>=Math.abs(t.y)?(r.value=j,s=s<0?s+M:s-M):(r.value=Xs,s+=g),e=M/12*Math.tan(s),h=Math.sin(e)/(Math.cos(e)-1/Math.sqrt(2)),h=Math.atan(h),(a=1-(s=Math.cos(s))*s*(s=Math.tan(a))*s*(1-Math.cos(Math.atan(1/Math.cos(h)))))<-1?a=-1:1s.y)--i;else{if(!(Zs[i+1][0]<=s.y))break;++i}var a=Zs[i],h=function(t,s,i,a){for(var h=s;a;--a){var e=t(h);if(h-=e,Math.abs(e)=g?this.cphi1=0:this.cphi1=1/Math.tan(this.phi1),this.inverse=ut,this.forward=lt)},names:["bonne","Bonne (Werner lat_1=90)"]};return t.defaultDatum="WGS84",t.Proj=p,t.WGS84=new t.Proj("WGS84"),t.Point=n,t.toPoint=Wt,t.defs=e,t.nadgrid=function(t,s){var s=new DataView(s),i=11!==(i=s).getInt32(8,!1)&&(11!==i.getInt32(8,!0)&&console.warn("Failed to detect nadgrid endian-ness, defaulting to little-endian"),!0),a=(h=i,{nFields:(a=s).getInt32(8,h),nSubgridFields:a.getInt32(24,h),nSubgrids:a.getInt32(40,h),shiftType:F(a,56,64).trim(),fromSemiMajorAxis:a.getFloat64(120,h),fromSemiMinorAxis:a.getFloat64(136,h),toSemiMajorAxis:a.getFloat64(152,h),toSemiMinorAxis:a.getFloat64(168,h)}),h={header:a,subgrids:U(s,a,i)};return rs[t]=h},t.transform=V,t.mgrs=c,t.version="2.15.0",(c=t).Proj.projections.add(ps),c.Proj.projections.add(ys),c.Proj.projections.add(_s),c.Proj.projections.add(gs),c.Proj.projections.add(bs),c.Proj.projections.add(vs),c.Proj.projections.add(Ns),c.Proj.projections.add(Cs),c.Proj.projections.add(Ss),c.Proj.projections.add(Ps),c.Proj.projections.add(Es),c.Proj.projections.add(ks),c.Proj.projections.add(Is),c.Proj.projections.add(qs),c.Proj.projections.add(Os),c.Proj.projections.add(As),c.Proj.projections.add(Gs),c.Proj.projections.add(js),c.Proj.projections.add(Ls),c.Proj.projections.add(Rs),c.Proj.projections.add(Ts),c.Proj.projections.add(zs),c.Proj.projections.add(Bs),c.Proj.projections.add(Ds),c.Proj.projections.add(Ks),c.Proj.projections.add(ti),c.Proj.projections.add(si),c.Proj.projections.add(ri),c.Proj.projections.add(oi),c.Proj.projections.add(di),c.Proj.projections.add(pi),t}); \ No newline at end of file diff --git a/streetup/common/static/common/select2-bootstrap-5-theme.min.css b/streetup/common/static/common/select2-bootstrap-5-theme.min.css new file mode 100644 index 0000000..de0abcb --- /dev/null +++ b/streetup/common/static/common/select2-bootstrap-5-theme.min.css @@ -0,0 +1,3 @@ +/*! + * Select2 v4 Bootstrap 5 theme v1.3.0 +*/.select2-container--bootstrap-5{display:block}select+.select2-container--bootstrap-5{z-index:1}.select2-container--bootstrap-5 :focus{outline:0}.select2-container--bootstrap-5 .select2-selection{width:100%;min-height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-family:inherit;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.select2-container--bootstrap-5 .select2-selection{transition:none}}.select2-container--bootstrap-5.select2-container--focus .select2-selection,.select2-container--bootstrap-5.select2-container--open .select2-selection{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection{border-bottom:0 solid transparent;border-bottom-right-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection{border-top:0 solid transparent;border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap-5 .select2-search{width:100%}.select2-container--bootstrap-5 .select2-search--inline .select2-search__field{vertical-align:top}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear,.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear{position:absolute;top:50%;right:2.25rem;width:.75rem;height:.75rem;padding:.25em;overflow:hidden;text-indent:100%;white-space:nowrap;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.75rem auto no-repeat;transform:translateY(-50%)}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover,.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.75rem auto no-repeat}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear>span,.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear>span{display:none}.select2-container--bootstrap-5+.select2-container--bootstrap-5{z-index:1056}.select2-container--bootstrap-5 .select2-dropdown{z-index:1056;overflow:hidden;color:#212529;background-color:#fff;border-color:#86b7fe;border-radius:.25rem}.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--below{border-top:0 solid transparent;border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--above{border-bottom:0 solid transparent;border-bottom-right-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap-5 .select2-dropdown .select2-search{padding:.375rem .75rem}.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field{display:block;width:100%;padding:.375rem .75rem;font-family:inherit;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field{transition:none}}.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field:focus{border-color:#86b7fe;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options:not(.select2-results__options--nested){max-height:15rem;overflow-y:auto}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option{padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__message{color:#6c757d}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--highlighted{color:#000;background-color:#e9ecef}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--selected,.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-selected=true]:not(.select2-results__option--highlighted){color:#fff;background-color:#0d6efd}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--disabled,.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-disabled=true]{color:#6c757d}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group]{padding:0}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group{padding:.375rem;font-weight:500;line-height:1.5;color:#6c757d}.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option{padding:.375rem .75rem}.select2-container--bootstrap-5 .select2-selection--single{padding:.375rem 2.25rem .375rem .75rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px}.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered{padding:0;font-weight:400;line-height:1.5;color:#212529}.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder{font-weight:400;line-height:1.5;color:#6c757d}.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__arrow{display:none}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered{display:flex;flex-direction:row;flex-wrap:wrap;padding-left:0;margin:0;list-style:none}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice{display:flex;flex-direction:row;align-items:center;padding:.35em .65em;margin-right:.375rem;margin-bottom:.375rem;font-size:1rem;color:#212529;cursor:auto;border:1px solid #ced4da;border-radius:.25rem}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove{width:.75rem;height:.75rem;padding:.25em;margin-right:.25rem;overflow:hidden;text-indent:100%;white-space:nowrap;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.75rem auto no-repeat;border:0}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.75rem auto no-repeat}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove>span{display:none}.select2-container--bootstrap-5 .select2-selection--multiple .select2-search{display:block;width:100%;height:1.5rem}.select2-container--bootstrap-5 .select2-selection--multiple .select2-search .select2-search__field{width:100%;height:1.5rem;margin-top:0;margin-left:0;font-family:inherit;line-height:1.5;background-color:transparent}.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear{right:.75rem}.select2-container--bootstrap-5.select2-container--disabled .select2-selection,.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection{color:#6c757d;cursor:not-allowed;background-color:#e9ecef;border-color:#ced4da;box-shadow:none}.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__clear,.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice,.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice{cursor:not-allowed}.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove,.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove{display:none}.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty),.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty){padding-bottom:0}.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty)+.select2-search,.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty)+.select2-search{display:none}.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.btn~.select2-container--bootstrap-5 .select2-selection,.input-group>.dropdown-menu~.select2-container--bootstrap-5 .select2-selection,.input-group>.input-group-text~.select2-container--bootstrap-5 .select2-selection{border-top-left-radius:0;border-bottom-left-radius:0}.input-group .select2-container--bootstrap-5{flex-grow:1}.input-group .select2-container--bootstrap-5 .select2-selection{height:100%}.is-valid+.select2-container--bootstrap-5 .select2-selection,.was-validated select:valid+.select2-container--bootstrap-5 .select2-selection{border-color:#198754}.is-valid+.select2-container--bootstrap-5.select2-container--focus .select2-selection,.is-valid+.select2-container--bootstrap-5.select2-container--open .select2-selection,.was-validated select:valid+.select2-container--bootstrap-5.select2-container--focus .select2-selection,.was-validated select:valid+.select2-container--bootstrap-5.select2-container--open .select2-selection{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.is-valid+.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection,.was-validated select:valid+.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection{border-bottom:0 solid transparent}.is-valid+.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection,.was-validated select:valid+.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection{border-top:0 solid transparent;border-top-left-radius:0;border-top-right-radius:0}.is-invalid+.select2-container--bootstrap-5 .select2-selection,.was-validated select:invalid+.select2-container--bootstrap-5 .select2-selection{border-color:#dc3545}.is-invalid+.select2-container--bootstrap-5.select2-container--focus .select2-selection,.is-invalid+.select2-container--bootstrap-5.select2-container--open .select2-selection,.was-validated select:invalid+.select2-container--bootstrap-5.select2-container--focus .select2-selection,.was-validated select:invalid+.select2-container--bootstrap-5.select2-container--open .select2-selection{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.is-invalid+.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection,.was-validated select:invalid+.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection{border-bottom:0 solid transparent}.is-invalid+.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection,.was-validated select:invalid+.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection{border-top:0 solid transparent;border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap-5 .select2--small.select2-selection{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-selection__clear,.select2-container--bootstrap-5 .select2--small.select2-selection--single .select2-selection__clear{width:.5rem;height:.5rem;padding:.125rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-selection__clear:hover,.select2-container--bootstrap-5 .select2--small.select2-selection--single .select2-selection__clear:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-search,.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-search .select2-search__field,.select2-container--bootstrap-5 .select2--small.select2-selection--single .select2-search,.select2-container--bootstrap-5 .select2--small.select2-selection--single .select2-search .select2-search__field{height:1.5em}.select2-container--bootstrap-5 .select2--small.select2-dropdown{border-radius:.2rem}.select2-container--bootstrap-5 .select2--small.select2-dropdown.select2-dropdown--below{border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap-5 .select2--small.select2-dropdown.select2-dropdown--above{border-bottom-right-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap-5 .select2--small.select2-dropdown .select2-results__options .select2-results__option,.select2-container--bootstrap-5 .select2--small.select2-dropdown .select2-search .select2-search__field{padding:.25rem .5rem;font-size:.875rem}.select2-container--bootstrap-5 .select2--small.select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group{padding:.25rem}.select2-container--bootstrap-5 .select2--small.select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option{padding:.25rem .5rem}.select2-container--bootstrap-5 .select2--small.select2-selection--single{padding:.25rem 2.25rem .25rem .5rem}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-selection__rendered .select2-selection__choice{padding:.35em .65em;font-size:.875rem}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove{width:.5rem;height:.5rem;padding:.125rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.select2-container--bootstrap-5 .select2--small.select2-selection--multiple .select2-selection__clear{right:.5rem}.select2-container--bootstrap-5 .select2--large.select2-selection{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-selection__clear,.select2-container--bootstrap-5 .select2--large.select2-selection--single .select2-selection__clear{width:1rem;height:1rem;padding:.5rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-selection__clear:hover,.select2-container--bootstrap-5 .select2--large.select2-selection--single .select2-selection__clear:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-search,.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-search .select2-search__field,.select2-container--bootstrap-5 .select2--large.select2-selection--single .select2-search,.select2-container--bootstrap-5 .select2--large.select2-selection--single .select2-search .select2-search__field{height:1.5em}.select2-container--bootstrap-5 .select2--large.select2-dropdown{border-radius:.3rem}.select2-container--bootstrap-5 .select2--large.select2-dropdown.select2-dropdown--below{border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap-5 .select2--large.select2-dropdown.select2-dropdown--above{border-bottom-right-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap-5 .select2--large.select2-dropdown .select2-results__options .select2-results__option,.select2-container--bootstrap-5 .select2--large.select2-dropdown .select2-search .select2-search__field{padding:.5rem 1rem;font-size:1.25rem}.select2-container--bootstrap-5 .select2--large.select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group{padding:.5rem}.select2-container--bootstrap-5 .select2--large.select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option{padding:.5rem 1rem}.select2-container--bootstrap-5 .select2--large.select2-selection--single{padding:.5rem 2.25rem .5rem 1rem}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-selection__rendered .select2-selection__choice{padding:.35em .65em;font-size:1.25rem}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove{width:1rem;height:1rem;padding:.5rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.select2-container--bootstrap-5 .select2--large.select2-selection--multiple .select2-selection__clear{right:1rem}.form-select-sm~.select2-container--bootstrap-5 .select2-selection{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear,.form-select-sm~.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear{width:.5rem;height:.5rem;padding:.125rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover,.form-select-sm~.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-search,.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-search .select2-search__field,.form-select-sm~.select2-container--bootstrap-5 .select2-selection--single .select2-search,.form-select-sm~.select2-container--bootstrap-5 .select2-selection--single .select2-search .select2-search__field{height:1.5em}.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown{border-radius:.2rem}.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--below{border-top-left-radius:0;border-top-right-radius:0}.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--above{border-bottom-right-radius:0;border-bottom-left-radius:0}.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option,.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field{padding:.25rem .5rem;font-size:.875rem}.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group{padding:.25rem}.form-select-sm~.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option{padding:.25rem .5rem}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--single{padding:.25rem 2.25rem .25rem .5rem}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice{padding:.35em .65em;font-size:.875rem}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove{width:.5rem;height:.5rem;padding:.125rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/.5rem auto no-repeat}.form-select-sm~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear{right:.5rem}.form-select-lg~.select2-container--bootstrap-5 .select2-selection{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear,.form-select-lg~.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear{width:1rem;height:1rem;padding:.5rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover,.form-select-lg~.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-search,.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-search .select2-search__field,.form-select-lg~.select2-container--bootstrap-5 .select2-selection--single .select2-search,.form-select-lg~.select2-container--bootstrap-5 .select2-selection--single .select2-search .select2-search__field{height:1.5em}.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown{border-radius:.3rem}.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--below{border-top-left-radius:0;border-top-right-radius:0}.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--above{border-bottom-right-radius:0;border-bottom-left-radius:0}.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option,.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field{padding:.5rem 1rem;font-size:1.25rem}.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group{padding:.5rem}.form-select-lg~.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option{padding:.5rem 1rem}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--single{padding:.5rem 2.25rem .5rem 1rem}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice{padding:.35em .65em;font-size:1.25rem}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove{width:1rem;height:1rem;padding:.5rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover{background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") 50%/1rem auto no-repeat}.form-select-lg~.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear{right:1rem} \ No newline at end of file diff --git a/streetup/common/static/common/select2.min.css b/streetup/common/static/common/select2.min.css new file mode 100644 index 0000000..39a4547 --- /dev/null +++ b/streetup/common/static/common/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline;list-style:none;padding:0}.select2-container .select2-selection--multiple .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;margin-left:5px;padding:0;max-width:100%;resize:none;height:18px;vertical-align:bottom;font-family:sans-serif;overflow:hidden;word-break:keep-all}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option--selectable{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px;padding-right:0px}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;padding-bottom:5px;padding-right:5px;position:relative}.select2-container--default .select2-selection--multiple.select2-selection--clearable{padding-right:25px}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;font-weight:bold;height:20px;margin-right:10px;margin-top:5px;position:absolute;right:0;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:inline-block;margin-left:5px;margin-top:5px;padding:0;padding-left:20px;position:relative;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}.select2-container--default .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-right:1px solid #aaa;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#999;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px;position:absolute;left:0;top:0}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus{background-color:#f1f1f1;color:#333;outline:none}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-left:1px solid #aaa;border-right:none;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear{float:left;margin-left:10px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--group{padding:0}.select2-container--default .select2-results__option--disabled{color:#999}.select2-container--default .select2-results__option--selected{background-color:#ddd}.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;padding-bottom:5px;padding-right:5px}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--classic .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#888;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;outline:none}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option--group{padding:0}.select2-container--classic .select2-results__option--disabled{color:grey}.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/streetup/common/static/common/select2.min.js b/streetup/common/static/common/select2.min.js new file mode 100644 index 0000000..cc9a83f --- /dev/null +++ b/streetup/common/static/common/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.1.0-rc.0 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(t){var e,n,s,p,r,o,h,f,g,m,y,v,i,a,_,s=((u=t&&t.fn&&t.fn.select2&&t.fn.select2.amd?t.fn.select2.amd:u)&&u.requirejs||(u?n=u:u={},g={},m={},y={},v={},i=Object.prototype.hasOwnProperty,a=[].slice,_=/\.js$/,h=function(e,t){var n,s,i=c(e),r=i[0],t=t[1];return e=i[1],r&&(n=x(r=l(r,t))),r?e=n&&n.normalize?n.normalize(e,(s=t,function(e){return l(e,s)})):l(e,t):(r=(i=c(e=l(e,t)))[0],e=i[1],r&&(n=x(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},f={require:function(e){return w(e)},exports:function(e){var t=g[e];return void 0!==t?t:g[e]={}},module:function(e){return{id:e,uri:"",exports:g[e],config:(t=e,function(){return y&&y.config&&y.config[t]||{}})};var t}},r=function(e,t,n,s){var i,r,o,a,l,c=[],u=typeof n,d=A(s=s||e);if("undefined"==u||"function"==u){for(t=!t.length&&n.length?["require","exports","module"]:t,a=0;a":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},s.__cache={};var n=0;return s.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null!=t||(t=e.id?"select2-data-"+e.id:"select2-data-"+(++n).toString()+"-"+s.generateChars(4),e.setAttribute("data-select2-id",t)),t},s.StoreData=function(e,t,n){e=s.GetUniqueElementId(e);s.__cache[e]||(s.__cache[e]={}),s.__cache[e][t]=n},s.GetData=function(e,t){var n=s.GetUniqueElementId(e);return t?s.__cache[n]&&null!=s.__cache[n][t]?s.__cache[n][t]:r(e).data(t):s.__cache[n]},s.RemoveData=function(e){var t=s.GetUniqueElementId(e);null!=s.__cache[t]&&delete s.__cache[t],e.removeAttribute("data-select2-id")},s.copyNonInternalCssClasses=function(e,t){var n=(n=e.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0===e.indexOf("select2-")}),t=(t=t.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0!==e.indexOf("select2-")}),t=n.concat(t);e.setAttribute("class",t.join(" "))},s}),u.define("select2/results",["jquery","./utils"],function(d,p){function s(e,t,n){this.$element=e,this.data=n,this.options=t,s.__super__.constructor.call(this)}return p.Extend(s,p.Observable),s.prototype.render=function(){var e=d('
          ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},s.prototype.clear=function(){this.$results.empty()},s.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=d('
        • '),s=this.options.get("translations").get(e.message);n.append(t(s(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},s.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},s.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n
        ",{class:"select2-results__options select2-results__options--nested",role:"none"});i.append(l),o.append(a),o.append(i)}else this.template(e,t);return p.StoreData(t,"data",e),t},s.prototype.bind=function(t,e){var i=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){i.clear(),i.append(e.data),t.isOpen()&&(i.setClasses(),i.highlightFirstItem())}),t.on("results:append",function(e){i.append(e.data),t.isOpen()&&i.setClasses()}),t.on("query",function(e){i.hideMessages(),i.showLoading(e)}),t.on("select",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("open",function(){i.$results.attr("aria-expanded","true"),i.$results.attr("aria-hidden","false"),i.setClasses(),i.ensureHighlightVisible()}),t.on("close",function(){i.$results.attr("aria-expanded","false"),i.$results.attr("aria-hidden","true"),i.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=i.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e,t=i.getHighlightedResults();0!==t.length&&(e=p.GetData(t[0],"data"),t.hasClass("select2-results__option--selected")?i.trigger("close",{}):i.trigger("select",{data:e}))}),t.on("results:previous",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t);s<=0||(e=s-1,0===t.length&&(e=0),(s=n.eq(e)).trigger("mouseenter"),t=i.$results.offset().top,n=s.offset().top,s=i.$results.scrollTop()+(n-t),0===e?i.$results.scrollTop(0):n-t<0&&i.$results.scrollTop(s))}),t.on("results:next",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t)+1;s>=n.length||((e=n.eq(s)).trigger("mouseenter"),t=i.$results.offset().top+i.$results.outerHeight(!1),n=e.offset().top+e.outerHeight(!1),e=i.$results.scrollTop()+n-t,0===s?i.$results.scrollTop(0):tthis.$results.outerHeight()||s<0)&&this.$results.scrollTop(n))},s.prototype.template=function(e,t){var n=this.options.get("templateResult"),s=this.options.get("escapeMarkup"),e=n(e,t);null==e?t.style.display="none":"string"==typeof e?t.innerHTML=s(e):d(t).append(e)},s}),u.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),u.define("select2/selection/base",["jquery","../utils","../keys"],function(n,s,i){function r(e,t){this.$element=e,this.options=t,r.__super__.constructor.call(this)}return s.Extend(r,s.Observable),r.prototype.render=function(){var e=n('');return this._tabindex=0,null!=s.GetData(this.$element[0],"old-tabindex")?this._tabindex=s.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},r.prototype.bind=function(e,t){var n=this,s=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",s),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},r.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},r.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&s.GetData(this,"element").select2("close")})})},r.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},r.prototype.position=function(e,t){t.find(".selection").append(e)},r.prototype.destroy=function(){this._detachCloseHandler(this.container)},r.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},r.prototype.isEnabled=function(){return!this.isDisabled()},r.prototype.isDisabled=function(){return this.options.get("disabled")},r}),u.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,s){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e[0].classList.add("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var s=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",s),this.$selection.attr("aria-controls",s),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){var t,n;0!==e.length?(n=e[0],t=this.$selection.find(".select2-selection__rendered"),e=this.display(n,t),t.empty().append(e),(n=n.title||n.text)?t.attr("title",n):t.removeAttr("title")):this.clear()},i}),u.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,c){function r(e,t){r.__super__.constructor.apply(this,arguments)}return c.Extend(r,e),r.prototype.render=function(){var e=r.__super__.render.call(this);return e[0].classList.add("select2-selection--multiple"),e.html('
          '),e},r.prototype.bind=function(e,t){var n=this;r.__super__.bind.apply(this,arguments);var s=e.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s),this.$selection.on("click",function(e){n.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){var t;n.isDisabled()||(t=i(this).parent(),t=c.GetData(t[0],"data"),n.trigger("unselect",{originalEvent:e,data:t}))}),this.$selection.on("keydown",".select2-selection__choice__remove",function(e){n.isDisabled()||e.stopPropagation()})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return i('
        • ')},r.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=this.$selection.find(".select2-selection__rendered").attr("id")+"-choice-",s=0;s')).attr("title",s()),e.attr("aria-label",s()),e.attr("aria-describedby",n),a.StoreData(e[0],"data",t),this.$selection.prepend(e),this.$selection[0].classList.add("select2-selection--clearable"))},e}),u.define("select2/selection/search",["jquery","../utils","../keys"],function(s,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=this.options.get("translations").get("search"),n=s('');this.$searchContainer=n,this.$search=n.find("textarea"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",t());e=e.call(this);return this._transferTabIndex(),e.append(this.$searchContainer),e},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results",r=t.id+"-container";e.call(this,t,n),s.$search.attr("aria-describedby",r),t.on("open",function(){s.$search.attr("aria-controls",i),s.$search.trigger("focus")}),t.on("close",function(){s.$search.val(""),s.resizeSearch(),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.trigger("focus")}),t.on("enable",function(){s.$search.prop("disabled",!1),s._transferTabIndex()}),t.on("disable",function(){s.$search.prop("disabled",!0)}),t.on("focus",function(e){s.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){s.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){s._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){var t;e.stopPropagation(),s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented(),e.which!==l.BACKSPACE||""!==s.$search.val()||0<(t=s.$selection.find(".select2-selection__choice").last()).length&&(t=a.GetData(t[0],"data"),s.searchRemoveChoice(t),e.preventDefault())}),this.$selection.on("click",".select2-search--inline",function(e){s.$search.val()&&e.stopPropagation()});var t=document.documentMode,o=t&&t<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){o?s.$selection.off("input.search input.searchcheck"):s.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){var t;o&&"input"===e.type?s.$selection.off("input.search input.searchcheck"):(t=e.which)!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&s.handleSearch(e)})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){var e;this.resizeSearch(),this._keyUpPrevented||(e=this.$search.val(),this.trigger("query",{term:e})),this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="100%";""===this.$search.attr("placeholder")&&(e=.75*(this.$search.val().length+1)+"em"),this.$search.css("width",e)},e}),u.define("select2/selection/selectionCss",["../utils"],function(n){function e(){}return e.prototype.render=function(e){var t=e.call(this),e=this.options.get("selectionCssClass")||"";return-1!==e.indexOf(":all:")&&(e=e.replace(":all:",""),n.copyNonInternalCssClasses(t[0],this.$element[0])),t.addClass(e),t},e}),u.define("select2/selection/eventRelay",["jquery"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var s=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],r=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){var n;-1!==i.indexOf(e)&&(t=t||{},n=o.Event("select2:"+e,{params:t}),s.$element.trigger(n),-1!==r.indexOf(e)&&(t.prevented=n.isDefaultPrevented()))})},e}),u.define("select2/translation",["jquery","require"],function(t,n){function s(e){this.dict=e||{}}return s.prototype.all=function(){return this.dict},s.prototype.get=function(e){return this.dict[e]},s.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},s._cache={},s.loadPath=function(e){var t;return e in s._cache||(t=n(e),s._cache[e]=t),new s(s._cache[e])},s}),u.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),u.define("select2/data/base",["../utils"],function(n){function s(e,t){s.__super__.constructor.call(this)}return n.Extend(s,n.Observable),s.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},s.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},s.prototype.bind=function(e,t){},s.prototype.destroy=function(){},s.prototype.generateResultId=function(e,t){e=e.id+"-result-";return e+=n.generateChars(4),null!=t.id?e+="-"+t.id.toString():e+="-"+n.generateChars(4),e},s}),u.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var t=this;e(Array.prototype.map.call(this.$element[0].querySelectorAll(":checked"),function(e){return t.item(l(e))}))},n.prototype.select=function(i){var e,r=this;if(i.selected=!0,null!=i.element&&"option"===i.element.tagName.toLowerCase())return i.element.selected=!0,void this.$element.trigger("input").trigger("change");this.$element.prop("multiple")?this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;nthis.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),u.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("select",function(){s._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var s=this;this._checkIfMaximumSelected(function(){e.call(s,t,n)})},e.prototype._checkIfMaximumSelected=function(e,t){var n=this;this.current(function(e){e=null!=e?e.length:0;0=n.maximumSelectionLength?n.trigger("results:message",{message:"maximumSelected",args:{maximum:n.maximumSelectionLength}}):t&&t()})},e}),u.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),u.define("select2/dropdown/search",["jquery"],function(r){function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("translations").get("search"),e=r('');return this.$searchContainer=e,this.$search=e.find("input"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",n()),t.prepend(e),t},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){r(this).off("keyup")}),this.$search.on("keyup input",function(e){s.handleSearch(e)}),t.on("open",function(){s.$search.attr("tabindex",0),s.$search.attr("aria-controls",i),s.$search.trigger("focus"),window.setTimeout(function(){s.$search.trigger("focus")},0)}),t.on("close",function(){s.$search.attr("tabindex",-1),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.val(""),s.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||s.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(s.showSearch(e)?s.$searchContainer[0].classList.remove("select2-search--hide"):s.$searchContainer[0].classList.add("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")})},e.prototype.handleSearch=function(e){var t;this._keyUpPrevented||(t=this.$search.val(),this.trigger("query",{term:t})),this._keyUpPrevented=!1},e.prototype.showSearch=function(e,t){return!0},e}),u.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,s){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,s)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return t="string"==typeof t?{id:"",text:t}:t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),s=t.length-1;0<=s;s--){var i=t[s];this.placeholder.id===i.id&&n.splice(s,1)}return n},e}),u.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,s){this.lastParams={},e.call(this,t,n,s),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("query",function(e){s.lastParams=e,s.loading=!0}),t.on("query:append",function(e){s.lastParams=e,s.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);!this.loading&&e&&(e=this.$results.offset().top+this.$results.outerHeight(!1),this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=e+50&&this.loadMore())},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
        • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),u.define("select2/dropdown/attachBody",["jquery","../utils"],function(u,o){function e(e,t,n){this.$dropdownParent=u(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("open",function(){s._showDropdown(),s._attachPositioningHandler(t),s._bindContainerResultHandlers(t)}),t.on("close",function(){s._hideDropdown(),s._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t[0].classList.remove("select2"),t[0].classList.add("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=u(""),e=e.call(this);return t.append(e),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){var n;this._containerResultsHandlersBound||(n=this,t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0)},e.prototype._attachPositioningHandler=function(e,t){var n=this,s="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id,t=this.$container.parents().filter(o.hasScroll);t.each(function(){o.StoreData(this,"select2-scroll-position",{x:u(this).scrollLeft(),y:u(this).scrollTop()})}),t.on(s,function(e){var t=o.GetData(this,"select2-scroll-position");u(this).scrollTop(t.y)}),u(window).on(s+" "+i+" "+r,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,s="resize.select2."+t.id,t="orientationchange.select2."+t.id;this.$container.parents().filter(o.hasScroll).off(n),u(window).off(n+" "+s+" "+t)},e.prototype._positionDropdown=function(){var e=u(window),t=this.$dropdown[0].classList.contains("select2-dropdown--above"),n=this.$dropdown[0].classList.contains("select2-dropdown--below"),s=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var r={height:this.$container.outerHeight(!1)};r.top=i.top,r.bottom=i.top+r.height;var o=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+o,a={left:i.left,top:r.bottom},l=this.$dropdownParent;"static"===l.css("position")&&(l=l.offsetParent());i={top:0,left:0};(u.contains(document.body,l[0])||l[0].isConnected)&&(i=l.offset()),a.top-=i.top,a.left-=i.left,t||n||(s="below"),e||!c||t?!c&&e&&t&&(s="below"):s="above",("above"==s||t&&"below"!==s)&&(a.top=r.top-i.top-o),null!=s&&(this.$dropdown[0].classList.remove("select2-dropdown--below"),this.$dropdown[0].classList.remove("select2-dropdown--above"),this.$dropdown[0].classList.add("select2-dropdown--"+s),this.$container[0].classList.remove("select2-container--below"),this.$container[0].classList.remove("select2-container--above"),this.$container[0].classList.add("select2-container--"+s)),this.$dropdownContainer.css(a)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),u.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,s){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,s)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,s=0;s');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container[0].classList.add("select2-container--"+this.options.get("theme")),r.StoreData(e[0],"element",this.$element),e},o}),u.define("jquery-mousewheel",["jquery"],function(e){return e}),u.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,r,t,o){var a;return null==i.fn.select2&&(a=["open","close","destroy"],i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new r(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,s=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=o.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,s)}),-1 \ No newline at end of file diff --git a/streetup/common/static/common/streetup_favicon.png b/streetup/common/static/common/streetup_favicon.png new file mode 100644 index 0000000..6216d3d Binary files /dev/null and b/streetup/common/static/common/streetup_favicon.png differ diff --git a/streetup/common/static/common/streetup_logo.png b/streetup/common/static/common/streetup_logo.png new file mode 100644 index 0000000..5ccd57b Binary files /dev/null and b/streetup/common/static/common/streetup_logo.png differ diff --git a/streetup/common/static/common/streetup_logo.svg b/streetup/common/static/common/streetup_logo.svg new file mode 100644 index 0000000..21a5529 --- /dev/null +++ b/streetup/common/static/common/streetup_logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/streetup/common/static/common/streetup_logo_1.png b/streetup/common/static/common/streetup_logo_1.png new file mode 100644 index 0000000..8d61086 Binary files /dev/null and b/streetup/common/static/common/streetup_logo_1.png differ diff --git a/streetup/common/static/common/turf.min.js b/streetup/common/static/common/turf.min.js new file mode 100644 index 0000000..a85d8bd --- /dev/null +++ b/streetup/common/static/common/turf.min.js @@ -0,0 +1,88 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).turf={})}(this,(function(t){"use strict";var e=6371008.8,n={centimeters:637100880,centimetres:637100880,degrees:57.22891354143274,feet:20902260.511392,inches:39.37*e,kilometers:6371.0088,kilometres:6371.0088,meters:e,metres:e,miles:3958.761333810546,millimeters:6371008800,millimetres:6371008800,nauticalmiles:e/1852,radians:1,yards:6967335.223679999},r={centimeters:100,centimetres:100,degrees:1/111325,feet:3.28084,inches:39.37,kilometers:.001,kilometres:.001,meters:1,metres:1,miles:1/1609.344,millimeters:1e3,millimetres:1e3,nauticalmiles:1/1852,radians:1/e,yards:1.0936133},i={acres:247105e-9,centimeters:1e4,centimetres:1e4,feet:10.763910417,hectares:1e-4,inches:1550.003100006,kilometers:1e-6,kilometres:1e-6,meters:1,metres:1,miles:386e-9,millimeters:1e6,millimetres:1e6,yards:1.195990046};function o(t,e,n){void 0===n&&(n={});var r={type:"Feature"};return(0===n.id||n.id)&&(r.id=n.id),n.bbox&&(r.bbox=n.bbox),r.properties=e||{},r.geometry=t,r}function s(t,e,n){switch(t){case"Point":return a(e).geometry;case"LineString":return h(e).geometry;case"Polygon":return l(e).geometry;case"MultiPoint":return d(e).geometry;case"MultiLineString":return g(e).geometry;case"MultiPolygon":return y(e).geometry;default:throw new Error(t+" is invalid")}}function a(t,e,n){if(void 0===n&&(n={}),!t)throw new Error("coordinates is required");if(!Array.isArray(t))throw new Error("coordinates must be an Array");if(t.length<2)throw new Error("coordinates must be at least 2 numbers long");if(!C(t[0])||!C(t[1]))throw new Error("coordinates must contain numbers");return o({type:"Point",coordinates:t},e,n)}function u(t,e,n){return void 0===n&&(n={}),f(t.map((function(t){return a(t,e)})),n)}function l(t,e,n){void 0===n&&(n={});for(var r=0,i=t;r=0))throw new Error("precision must be a positive number");var n=Math.pow(10,e||0);return Math.round(t*n)/n}function m(t,e){void 0===e&&(e="kilometers");var r=n[e];if(!r)throw new Error(e+" units is invalid");return t*r}function x(t,e){void 0===e&&(e="kilometers");var r=n[e];if(!r)throw new Error(e+" units is invalid");return t/r}function E(t,e){return w(x(t,e))}function b(t){var e=t%360;return e<0&&(e+=360),e}function w(t){return 180*(t%(2*Math.PI))/Math.PI}function I(t){return t%360*Math.PI/180}function N(t,e,n){if(void 0===e&&(e="kilometers"),void 0===n&&(n="kilometers"),!(t>=0))throw new Error("length must be a positive number");return m(x(t,e),n)}function S(t,e,n){if(void 0===e&&(e="meters"),void 0===n&&(n="kilometers"),!(t>=0))throw new Error("area must be a positive number");var r=i[e];if(!r)throw new Error("invalid original units");var o=i[n];if(!o)throw new Error("invalid final units");return t/r*o}function C(t){return!isNaN(t)&&null!==t&&!Array.isArray(t)}function P(t){return!!t&&t.constructor===Object}function M(t){if(!t)throw new Error("bbox is required");if(!Array.isArray(t))throw new Error("bbox must be an Array");if(4!==t.length&&6!==t.length)throw new Error("bbox must be an Array of 4 or 6 numbers");t.forEach((function(t){if(!C(t))throw new Error("bbox must only contain numbers")}))}function L(t){if(!t)throw new Error("id is required");if(-1===["string","number"].indexOf(typeof t))throw new Error("id must be a number or a string")}var O=Object.freeze({__proto__:null,earthRadius:e,factors:n,unitsFactors:r,areaFactors:i,feature:o,geometry:s,point:a,points:u,polygon:l,polygons:c,lineString:h,lineStrings:p,featureCollection:f,multiLineString:g,multiPoint:d,multiPolygon:y,geometryCollection:v,round:_,radiansToLength:m,lengthToRadians:x,lengthToDegrees:E,bearingToAzimuth:b,radiansToDegrees:w,degreesToRadians:I,convertLength:N,convertArea:S,isNumber:C,isObject:P,validateBBox:M,validateId:L});function R(t,e,n){if(null!==t)for(var r,i,o,s,a,u,l,c,h=0,p=0,f=t.type,g="FeatureCollection"===f,d="Feature"===f,y=g?t.features.length:1,v=0;va||f>u||g>l)return s=o,a=n,u=f,l=g,void(i=0);var d=h([s,o],t.properties);if(!1===e(d,n,r,g,i))return!1;i++,s=o}))&&void 0}}}))}function V(t,e,n){var r=n,i=!1;return U(t,(function(t,o,s,a,u){r=!1===i&&void 0===n?t:e(r,t,o,s,a,u),i=!0})),r}function X(t,e){if(!t)throw new Error("geojson is required");z(t,(function(t,n,r){if(null!==t.geometry){var i=t.geometry.type,o=t.geometry.coordinates;switch(i){case"LineString":if(!1===e(t,n,r,0,0))return!1;break;case"Polygon":for(var s=0;st[0]&&(e[0]=t[0]),e[1]>t[1]&&(e[1]=t[1]),e[2]=2&&!Array.isArray(t[0])&&!Array.isArray(t[1]))return t;throw new Error("coord must be GeoJSON Point or an Array of numbers")}function Q(t){if(Array.isArray(t))return t;if("Feature"===t.type){if(null!==t.geometry)return t.geometry.coordinates}else if(t.coordinates)return t.coordinates;throw new Error("coords must be GeoJSON Feature, Geometry Object or an Array")}function $(t){if(t.length>1&&C(t[0])&&C(t[1]))return!0;if(Array.isArray(t[0])&&t[0].length)return $(t[0]);throw new Error("coordinates must only contain numbers")}function tt(t,e,n){if(!e||!n)throw new Error("type and name required");if(!t||t.type!==e)throw new Error("Invalid input to "+n+": must be a "+e+", given "+t.type)}function et(t,e,n){if(!t)throw new Error("No feature passed");if(!n)throw new Error(".featureOf() requires a name");if(!t||"Feature"!==t.type||!t.geometry)throw new Error("Invalid input to "+n+", Feature with geometry required");if(!t.geometry||t.geometry.type!==e)throw new Error("Invalid input to "+n+": must be a "+e+", given "+t.geometry.type)}function nt(t,e,n){if(!t)throw new Error("No featureCollection passed");if(!n)throw new Error(".collectionOf() requires a name");if(!t||"FeatureCollection"!==t.type)throw new Error("Invalid input to "+n+", FeatureCollection required");for(var r=0,i=t.features;r + * v. 1.2.0 + * https://github.com/RaumZeit/MarchingSquares.js + * + * MarchingSquaresJS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MarchingSquaresJS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * As additional permission under GNU Affero General Public License version 3 + * section 7, third-party projects (personal or commercial) may distribute, + * include, or link against UNMODIFIED VERSIONS of MarchingSquaresJS without the + * requirement that said third-party project for that reason alone becomes + * subject to any requirement of the GNU Affero General Public License version 3. + * Any modifications to MarchingSquaresJS, however, must be shared with the public + * and made available. + * + * In summary this: + * - allows you to use MarchingSquaresJS at no cost + * - allows you to use MarchingSquaresJS for both personal and commercial purposes + * - allows you to distribute UNMODIFIED VERSIONS of MarchingSquaresJS under any + * license as long as this license notice is included + * - enables you to keep the source code of your program that uses MarchingSquaresJS + * undisclosed + * - forces you to share any modifications you have made to MarchingSquaresJS, + * e.g. bug-fixes + * + * You should have received a copy of the GNU Affero General Public License + * along with MarchingSquaresJS. If not, see . + */function ft(t,e,n){n=n||{};for(var r=Object.keys(ht),i=0;i=0&&y>=0&&y=0;h--)if(Math.abs(e[h][0][0]-l)<=r&&Math.abs(e[h][0][1]-c)<=r){for(var p=a.path.length-2;p>=0;--p)e[h].unshift(a.path[p]);u=!0;break}u||(e[n++]=a.path)}var f}))})),e}(function(t,e){for(var n=t.length-1,r=t[0].length-1,i={rows:n,cols:r,cells:[]},o=0;o=e?8:0,a|=l>=e?4:0,a|=c>=e?2:0;var p,f,g,d,y=!1;if(5===(a|=h>=e?1:0)||10===a){var v=(u+l+c+h)/4;5===a&&vn;){if(r-n>600){var o=r-n+1,s=e-n+1,a=Math.log(o),u=.5*Math.exp(2*a/3),l=.5*Math.sqrt(a*u*(o-u)/o)*(s-o/2<0?-1:1);bt(t,e,Math.max(n,Math.floor(e-s*u/o+l)),Math.min(r,Math.floor(e+(o-s)*u/o+l)),i)}var c=t[e],h=n,p=r;for(wt(t,n,e),i(t[r],c)>0&&wt(t,n,r);h0;)p--}0===i(t[n],c)?wt(t,n,p):wt(t,++p,r),p<=e&&(n=p+1),e<=p&&(r=p-1)}}function wt(t,e,n){var r=t[e];t[e]=t[n],t[n]=r}function It(t,e){return te?1:0}mt.default=xt;var Nt=Ct,St=Ct;function Ct(t,e){if(!(this instanceof Ct))return new Ct(t,e);this._maxEntries=Math.max(4,t||9),this._minEntries=Math.max(2,Math.ceil(.4*this._maxEntries)),e&&this._initFormat(e),this.clear()}function Pt(t,e,n){if(!n)return e.indexOf(t);for(var r=0;r=t.minX&&e.maxY>=t.minY}function Gt(t){return{children:t,height:1,leaf:!0,minX:1/0,minY:1/0,maxX:-1/0,maxY:-1/0}}function qt(t,e,n,r,i){for(var o,s=[e,n];s.length;)(n=s.pop())-(e=s.pop())<=r||(o=e+Math.ceil((n-e)/r/2)*r,mt(t,o,e,n,i),s.push(e,o,o,n))}function Bt(t){var e={exports:{}};return t(e,e.exports),e.exports}Ct.prototype={all:function(){return this._all(this.data,[])},search:function(t){var e=this.data,n=[],r=this.toBBox;if(!kt(t,e))return n;for(var i,o,s,a,u=[];e;){for(i=0,o=e.children.length;i=0&&o[e].children.length>this._maxEntries;)this._split(o,e),e--;this._adjustParentBBoxes(i,o,e)},_split:function(t,e){var n=t[e],r=n.children.length,i=this._minEntries;this._chooseSplitAxis(n,i,r);var o=this._chooseSplitIndex(n,i,r),s=Gt(n.children.splice(o,n.children.length-o));s.height=n.height,s.leaf=n.leaf,Mt(n,this.toBBox),Mt(s,this.toBBox),e?t[e-1].children.push(s):this._splitRoot(n,s)},_splitRoot:function(t,e){this.data=Gt([t,e]),this.data.height=t.height+1,this.data.leaf=!1,Mt(this.data,this.toBBox)},_chooseSplitIndex:function(t,e,n){var r,i,o,s,a,u,l,c,h,p,f,g,d,y;for(u=l=1/0,r=e;r<=n-e;r++)i=Lt(t,0,r,this.toBBox),o=Lt(t,r,n,this.toBBox),h=i,p=o,f=void 0,g=void 0,d=void 0,y=void 0,f=Math.max(h.minX,p.minX),g=Math.max(h.minY,p.minY),d=Math.min(h.maxX,p.maxX),y=Math.min(h.maxY,p.maxY),s=Math.max(0,d-f)*Math.max(0,y-g),a=At(i)+At(o),s=e;i--)o=t.children[i],Ot(u,t.leaf?s(o):o),l+=Dt(u);return l},_adjustParentBBoxes:function(t,e,n){for(var r=n;r>=0;r--)Ot(e[r],t)},_condense:function(t){for(var e,n=t.length-1;n>=0;n--)0===t[n].children.length?n>0?(e=t[n-1].children).splice(e.indexOf(t[n]),1):this.clear():Mt(t[n],this.toBBox)},_initFormat:function(t){var e=["return a"," - b",";"];this.compareMinX=new Function("a","b",e.join(t[0])),this.compareMinY=new Function("a","b",e.join(t[1])),this.toBBox=new Function("a","return {minX: a"+t[0]+", minY: a"+t[1]+", maxX: a"+t[2]+", maxY: a"+t[3]+"};")}},Nt.default=St;var zt=function(t,e,n){var r=t*e,i=jt*t,o=i-(i-t),s=t-o,a=jt*e,u=a-(a-e),l=e-u,c=s*l-(r-o*u-s*u-o*l);if(n)return n[0]=c,n[1]=r,n;return[c,r]},jt=+(Math.pow(2,27)+1);var Ut=function(t,e){var n=0|t.length,r=0|e.length;if(1===n&&1===r)return function(t,e){var n=t+e,r=n-t,i=t-(n-r)+(e-r);if(i)return[i,n];return[n]}(t[0],e[0]);var i,o,s=new Array(n+r),a=0,u=0,l=0,c=Math.abs,h=t[u],p=c(h),f=e[l],g=c(f);p=r?(i=h,(u+=1)=r?(i=h,(u+=1)>1;return["sum(",n(t.slice(0,e)),",",n(t.slice(e)),")"].join("")}function r(t){if(2===t.length)return[["sum(prod(",t[0][0],",",t[1][1],"),prod(-",t[0][1],",",t[1][0],"))"].join("")];for(var i=[],o=0;o0){if(s<=0)return a;r=i+s}else{if(!(i<0))return a;if(s>=0)return a;r=-(i+s)}var u=33306690738754716e-32*r;return a>=u||a<=-u?a:o(t,e,n)},function(t,e,n,r){var i=t[0]-r[0],o=e[0]-r[0],a=n[0]-r[0],u=t[1]-r[1],l=e[1]-r[1],c=n[1]-r[1],h=t[2]-r[2],p=e[2]-r[2],f=n[2]-r[2],g=o*c,d=a*l,y=a*u,v=i*c,_=i*l,m=o*u,x=h*(g-d)+p*(y-v)+f*(_-m),E=7771561172376103e-31*((Math.abs(g)+Math.abs(d))*Math.abs(h)+(Math.abs(y)+Math.abs(v))*Math.abs(p)+(Math.abs(_)+Math.abs(m))*Math.abs(f));return x>E||-x>E?x:s(t,e,n,r)}];function u(t){var e=a[t.length];return e||(e=a[t.length]=i(t.length)),e.apply(void 0,t)}!function(){for(;a.length<=5;)a.push(i(a.length));for(var e=[],n=["slow"],r=0;r<=5;++r)e.push("a"+r),n.push("o"+r);var o=["function getOrientation(",e.join(),"){switch(arguments.length){case 0:case 1:return 0;"];for(r=2;r<=5;++r)o.push("case ",r,":return o",r,"(",e.slice(0,r).join(),");");o.push("}var s=new Array(arguments.length);for(var i=0;i1&&Jt(t[o[l-2]],t[o[l-1]],u)<=0;)l-=1,o.pop();for(o.push(a),l=s.length;l>1&&Jt(t[s[l-2]],t[s[l-1]],u)>=0;)l-=1,s.pop();s.push(a)}n=new Array(s.length+o.length-2);for(var c=0,h=(r=0,o.length);r0;--p)n[c++]=s[p];return n},Jt=Ht[3];var Zt=Qt,Kt=Qt;function Qt(t,e){if(!(this instanceof Qt))return new Qt(t,e);if(this.data=t||[],this.length=this.data.length,this.compare=e||$t,this.length>0)for(var n=(this.length>>1)-1;n>=0;n--)this._down(n)}function $t(t,e){return te?1:0}Qt.prototype={push:function(t){this.data.push(t),this.length++,this._up(this.length-1)},pop:function(){if(0!==this.length){var t=this.data[0];return this.length--,this.length>0&&(this.data[0]=this.data[this.length],this._down(0)),this.data.pop(),t}},peek:function(){return this.data[0]},_up:function(t){for(var e=this.data,n=this.compare,r=e[t];t>0;){var i=t-1>>1,o=e[i];if(n(r,o)>=0)break;e[t]=o,t=i}e[t]=r},_down:function(t){for(var e=this.data,n=this.compare,r=this.length>>1,i=e[t];t=0)break;e[t]=a,t=o}e[t]=i}},Zt.default=Kt;var te=function(t,e){for(var n=t[0],r=t[1],i=!1,o=0,s=e.length-1;or!=c>r&&n<(l-a)*(r-u)/(c-u)+a&&(i=!i)}return i},ee=Ht[3],ne=ie,re=ie;function ie(t,e,n){e=Math.max(0,void 0===e?2:e),n=n||0;for(var r,i=function(t){for(var e=t[0],n=t[0],r=t[0],i=t[0],o=0;or[0]&&(r=s),s[1]i[1]&&(i=s)}var a=[e,n,r,i],u=a.slice();for(o=0;oo||a.push({node:c,dist:h})}for(;a.length&&!a.peek().node.children;){var p=a.pop(),f=p.node,g=fe(f,e,n),d=fe(f,r,i);if(p.dist=e.minX&&t[0]<=e.maxX&&t[1]>=e.minY&&t[1]<=e.maxY}function le(t,e,n){for(var r,i,o,s,a=Math.min(t[0],e[0]),u=Math.min(t[1],e[1]),l=Math.max(t[0],e[0]),c=Math.max(t[1],e[1]),h=n.search({minX:a,minY:u,maxX:l,maxY:c}),p=0;p0!=ee(r,i,s)>0&&ee(o,s,r)>0!=ee(o,s,i)>0)return!1;return!0}function ce(t){var e=t.p,n=t.next.p;return t.minX=Math.min(e[0],n[0]),t.minY=Math.min(e[1],n[1]),t.maxX=Math.max(e[0],n[0]),t.maxY=Math.max(e[1],n[1]),t}function he(t,e){var n={p:t,prev:null,next:null,minX:0,minY:0,maxX:0,maxY:0};return e?(n.next=e.next,n.prev=e,e.next.prev=n,e.next=n):(n.prev=n,n.next=n),n}function pe(t,e){var n=t[0]-e[0],r=t[1]-e[1];return n*n+r*r}function fe(t,e,n){var r=e[0],i=e[1],o=n[0]-r,s=n[1]-i;if(0!==o||0!==s){var a=((t[0]-r)*o+(t[1]-i)*s)/(o*o+s*s);a>1?(r=n[0],i=n[1]):a>0&&(r+=o*a,i+=s*a)}return(o=t[0]-r)*o+(s=t[1]-i)*s}function ge(t,e,n,r,i,o,s,a){var u,l,c,h,p=n-t,f=r-e,g=s-i,d=a-o,y=t-i,v=e-o,_=p*p+f*f,m=p*g+f*d,x=g*g+d*d,E=p*y+f*v,b=g*y+d*v,w=_*x-m*m,I=w,N=w;0===w?(l=0,I=1,h=b,N=x):(h=_*b-m*E,(l=m*b-x*E)<0?(l=0,h=b,N=x):l>I&&(l=I,h=b+m,N=x)),h<0?(h=0,-E<0?l=0:-E>_?l=I:(l=-E,I=_)):h>N&&(h=N,-E+m<0?l=0:-E+m>_?l=I:(l=-E+m,I=_));var S=(1-(c=0===h?0:h/N))*i+c*s-((1-(u=0===l?0:l/I))*t+u*n),C=(1-c)*o+c*a-((1-u)*e+u*r);return S*S+C*C}function de(t,e){void 0===e&&(e={}),e.concavity=e.concavity||1/0;var n=[];if(R(t,(function(t){n.push([t[0],t[1]])})),!n.length)return null;var r=ne(n,e.concavity);return r.length>3?l([r]):null}function ye(t,e,n){if(void 0===n&&(n={}),!t)throw new Error("point is required");if(!e)throw new Error("polygon is required");var r=K(t),i=rt(e),o=i.type,s=e.bbox,a=i.coordinates;if(s&&!1===function(t,e){return e[0]<=t[0]&&e[1]<=t[1]&&e[2]>=t[0]&&e[3]>=t[1]}(r,s))return!1;"Polygon"===o&&(a=[a]);for(var u=!1,l=0;lt[1]!=l>t[1]&&t[0]<(u-s)*(t[1]-a)/(l-a)+s&&(r=!r)}return r}function _e(t,e){var n=[];return F(t,(function(t){var r=!1;if("Point"===t.geometry.type)q(e,(function(e){ye(t,e)&&(r=!0)})),r&&n.push(t);else{if("MultiPoint"!==t.geometry.type)throw new Error("Input geometry must be a Point or MultiPoint");var i=[];q(e,(function(e){R(t,(function(t){ye(t,e)&&(r=!0,i.push(t))}))})),r&&n.push(d(i))}})),f(n)}function me(t,e,n){void 0===n&&(n={});var r=K(t),i=K(e),o=I(i[1]-r[1]),s=I(i[0]-r[0]),a=I(r[1]),u=I(i[1]),l=Math.pow(Math.sin(o/2),2)+Math.pow(Math.sin(s/2),2)*Math.cos(a)*Math.cos(u);return m(2*Math.atan2(Math.sqrt(l),Math.sqrt(1-l)),n.units)}function xe(t,e){var n=!1;return f(function(t){if(t.length<3)return[];t.sort(be);var e,n,r,i,o,s,a=t.length-1,u=t[a].x,l=t[0].x,c=t[a].y,h=c,p=1e-12;for(;a--;)t[a].yh&&(h=t[a].y);var f,g=l-u,d=h-c,y=g>d?g:d,v=.5*(l+u),_=.5*(h+c),m=[new Ee({__sentinel:!0,x:v-20*y,y:_-y},{__sentinel:!0,x:v,y:_+20*y},{__sentinel:!0,x:v+20*y,y:_-y})],x=[],E=[];a=t.length;for(;a--;){for(E.length=0,f=m.length;f--;)(g=t[a].x-m[f].x)>0&&g*g>m[f].r?(x.push(m[f]),m.splice(f,1)):g*g+(d=t[a].y-m[f].y)*d>m[f].r||(E.push(m[f].a,m[f].b,m[f].b,m[f].c,m[f].c,m[f].a),m.splice(f,1));for(we(E),f=E.length;f;)n=E[--f],e=E[--f],r=t[a],i=n.x-e.x,o=n.y-e.y,s=2*(i*(r.y-n.y)-o*(r.x-n.x)),Math.abs(s)>p&&m.push(new Ee(e,n,r))}Array.prototype.push.apply(x,m),a=x.length;for(;a--;)(x[a].a.__sentinel||x[a].b.__sentinel||x[a].c.__sentinel)&&x.splice(a,1);return x}(t.features.map((function(t){var r={x:t.geometry.coordinates[0],y:t.geometry.coordinates[1]};return e?r.z=t.properties[e]:3===t.geometry.coordinates.length&&(n=!0,r.z=t.geometry.coordinates[2]),r}))).map((function(t){var e=[t.a.x,t.a.y],r=[t.b.x,t.b.y],i=[t.c.x,t.c.y],o={};return n?(e.push(t.a.z),r.push(t.b.z),i.push(t.c.z)):o={a:t.a.z,b:t.b.z,c:t.c.z},l([[e,r,i,e]],o)})))}ne.default=re;var Ee=function(t,e,n){this.a=t,this.b=e,this.c=n;var r,i,o=e.x-t.x,s=e.y-t.y,a=n.x-t.x,u=n.y-t.y,l=o*(t.x+e.x)+s*(t.y+e.y),c=a*(t.x+n.x)+u*(t.y+n.y),h=2*(o*(n.y-e.y)-s*(n.x-e.x));this.x=(u*l-s*c)/h,this.y=(o*c-a*l)/h,r=this.x-t.x,i=this.y-t.y,this.r=r*r+i*i};function be(t,e){return e.x-t.x}function we(t){var e,n,r,i,o,s=t.length;t:for(;s;)for(n=t[--s],e=t[--s],r=s;r;)if(o=t[--r],e===(i=t[--r])&&n===o||e===o&&n===i){t.splice(s,2),t.splice(r,2),s-=2;continue t}}function Ie(t){if(!t)throw new Error("geojson is required");switch(t.type){case"Feature":return Ne(t);case"FeatureCollection":return function(t){var e={type:"FeatureCollection"};return Object.keys(t).forEach((function(n){switch(n){case"type":case"features":return;default:e[n]=t[n]}})),e.features=t.features.map((function(t){return Ne(t)})),e}(t);case"Point":case"LineString":case"Polygon":case"MultiPoint":case"MultiLineString":case"MultiPolygon":case"GeometryCollection":return Ce(t);default:throw new Error("unknown GeoJSON type")}}function Ne(t){var e={type:"Feature"};return Object.keys(t).forEach((function(n){switch(n){case"type":case"properties":case"geometry":return;default:e[n]=t[n]}})),e.properties=Se(t.properties),e.geometry=Ce(t.geometry),e}function Se(t){var e={};return t?(Object.keys(t).forEach((function(n){var r=t[n];"object"==typeof r?null===r?e[n]=null:Array.isArray(r)?e[n]=r.map((function(t){return t})):e[n]=Se(r):e[n]=r})),e):e}function Ce(t){var e={type:t.type};return t.bbox&&(e.bbox=t.bbox),"GeometryCollection"===t.type?(e.geometries=t.geometries.map((function(t){return Ce(t)})),e):(e.coordinates=Pe(t.coordinates),e)}function Pe(t){var e=t;return"object"!=typeof e[0]?e.slice():e.map((function(t){return Pe(t)}))}function Me(t,e){if(void 0===e&&(e={}),!P(e=e||{}))throw new Error("options is invalid");var n=e.mutate;if("FeatureCollection"!==it(t))throw new Error("geojson must be a FeatureCollection");if(!t.features.length)throw new Error("geojson is empty");!1!==n&&void 0!==n||(t=Ie(t));var r=[],i=Y(t,(function(t,e){var n=function(t,e){var n,r=t.geometry.coordinates,i=e.geometry.coordinates,o=Le(r[0]),s=Le(r[r.length-1]),a=Le(i[0]),u=Le(i[i.length-1]);if(o===u)n=i.concat(r.slice(1));else if(a===s)n=r.concat(i.slice(1));else if(o===a)n=r.slice(1).reverse().concat(i);else{if(s!==u)return null;n=r.concat(i.reverse().slice(1))}return h(n)}(t,e);return n||(r.push(t),e)}));return i&&r.push(i),r.length?1===r.length?r[0]:g(r.map((function(t){return t.coordinates}))):null}function Le(t){return t[0].toString()+","+t[1].toString()}function Oe(t){return t}function Re(t,e){var n=function(t){if(null==t)return Oe;var e,n,r=t.scale[0],i=t.scale[1],o=t.translate[0],s=t.translate[1];return function(t,a){a||(e=n=0);var u=2,l=t.length,c=new Array(l);for(c[0]=(e+=t[0])*r+o,c[1]=(n+=t[1])*i+s;u1)for(var o,a,u=1,l=s(i[0]);ul&&(a=i[0],i[0]=i[u],i[u]=a,l=o);return i})).filter((function(t){return t.length>0}))}}var De=Object.prototype.hasOwnProperty;function Fe(t,e,n,r,i,o){3===arguments.length&&(r=o=Array,i=null);for(var s=new r(t=1<=t)throw new Error("full hashmap");c=s[l=l+1&u]}return s[l]=r,a[l]=o,o}function h(r,o){for(var l=e(r)&u,c=s[l],h=0;c!=i;){if(n(c,r))return a[l];if(++h>=t)throw new Error("full hashmap");c=s[l=l+1&u]}return s[l]=r,a[l]=o,o}function p(r,o){for(var l=e(r)&u,c=s[l],h=0;c!=i;){if(n(c,r))return a[l];if(++h>=t)break;c=s[l=l+1&u]}return o}function f(){for(var t=[],e=0,n=s.length;e>7^Be[2]^Be[3])}function je(t){var e,n,r,i,o=t.coordinates,s=t.lines,a=t.rings,u=function(){for(var t=Fe(1.4*o.length,E,b,Int32Array,-1,Int32Array),e=new Int32Array(o.length),n=0,r=o.length;n=0){var o=h[n];i===e&&o===r||i===r&&o===e||(++f,p[n]=1)}else c[n]=e,h[n]=r}}function E(t){return ze(o[t])}function b(t,e){return ke(o[t],o[e])}l=c=h=null;var w,I=function(t,e,n,r,i){3===arguments.length&&(r=Array,i=null);for(var o=new r(t=1<=t)throw new Error("full hashset");u=o[a=a+1&s]}return o[a]=r,!0}function l(r){for(var a=e(r)&s,u=o[a],l=0;u!=i;){if(n(u,r))return!0;if(++l>=t)break;u=o[a=a+1&s]}return!1}function c(){for(var t=[],e=0,n=o.length;e>1);er&&(r=o),si&&(i=s)}function u(t){t.forEach(a)}function l(t){t.forEach(u)}for(var c in t)o(t[c]);return r>=e&&i>=n?[e,n,r,i]:void 0}(t=Xe(t)),r=e>0&&n&&function(t,e,n){var r=e[0],i=e[1],o=e[2],s=e[3],a=o-r?(n-1)/(o-r):1,u=s-i?(n-1)/(s-i):1;function l(t){return[Math.round((t[0]-r)*a),Math.round((t[1]-i)*u)]}function c(t,e){for(var n,o,s,l,c,h=-1,p=0,f=t.length,g=new Array(f);++h2&&rn(n[i-3],n[i-1],n[i-2])&&n.splice(n.length-2,1))}if(n.push(e[e.length-1]),i=n.length,nn(e[0],e[e.length-1])&&i<4)throw new Error("invalid polygon");return rn(n[i-3],n[i-1],n[i-2])&&n.splice(n.length-2,1),n}function nn(t,e){return t[0]===e[0]&&t[1]===e[1]}function rn(t,e,n){var r=n[0],i=n[1],o=t[0],s=t[1],a=e[0],u=e[1],l=a-o,c=u-s;return 0===(r-o)*c-(i-s)*l&&(Math.abs(l)>=Math.abs(c)?l>0?o<=r&&r<=a:a<=r&&r<=o:c>0?s<=i&&i<=u:u<=i&&i<=s)}function on(t,e,n){var r=e.x,i=e.y,o=n.x-r,s=n.y-i;if(0!==o||0!==s){var a=((t.x-r)*o+(t.y-i)*s)/(o*o+s*s);a>1?(r=n.x,i=n.y):a>0&&(r+=o*a,i+=s*a)}return(o=t.x-r)*o+(s=t.y-i)*s}function sn(t,e,n,r,i){for(var o,s=r,a=e+1;as&&(o=a,s=u)}s>r&&(o-e>1&&sn(t,e,o,r,i),i.push(t[o]),n-o>1&&sn(t,o,n,r,i))}function an(t,e){var n=t.length-1,r=[t[0]];return sn(t,0,n,e,r),r.push(t[n]),r}function un(t,e,n){if(t.length<=2)return t;var r=void 0!==e?e*e:1;return t=an(t=n?t:function(t,e){for(var n,r,i,o,s,a=t[0],u=[a],l=1,c=t.length;le&&(u.push(n),a=n);return a!==n&&u.push(n),u}(t,r),r)}function ln(t,e,n){return un(t.map((function(t){return{x:t[0],y:t[1],z:t[2]}})),e,n).map((function(t){return t.z?[t.x,t.y,t.z]:[t.x,t.y]}))}function cn(t,e,n){return t.map((function(t){var r=t.map((function(t){return{x:t[0],y:t[1]}}));if(r.length<4)throw new Error("invalid polygon");for(var i=un(r,e,n).map((function(t){return[t.x,t.y]}));!hn(i);)i=un(r,e-=.01*e,n).map((function(t){return[t.x,t.y]}));return i[i.length-1][0]===i[0][0]&&i[i.length-1][1]===i[0][1]||i.push(i[0]),i}))}function hn(t){return!(t.length<3)&&!(3===t.length&&t[2][0]===t[0][0]&&t[2][1]===t[0][1])}var pn=function(){function t(t){this.points=t.points||[],this.duration=t.duration||1e4,this.sharpness=t.sharpness||.85,this.centers=[],this.controls=[],this.stepLength=t.stepLength||60,this.length=this.points.length,this.delay=0;for(var e=0;et&&(e.push(r),n=i)}return e},t.prototype.vector=function(t){var e=this.pos(t+10),n=this.pos(t-10);return{angle:180*Math.atan2(e.y-n.y,e.x-n.x)/3.14,speed:Math.sqrt((n.x-e.x)*(n.x-e.x)+(n.y-e.y)*(n.y-e.y)+(n.z-e.z)*(n.z-e.z))}},t.prototype.pos=function(t){var e=t-this.delay;e<0&&(e=0),e>this.duration&&(e=this.duration-1);var n=e/this.duration;if(n>=1)return this.points[this.length-1];var r=Math.floor((this.points.length-1)*n);return function(t,e,n,r,i){var o=function(t){var e=t*t;return[e*t,3*e*(1-t),3*t*(1-t)*(1-t),(1-t)*(1-t)*(1-t)]}(t);return{x:i.x*o[0]+r.x*o[1]+n.x*o[2]+e.x*o[3],y:i.y*o[0]+r.y*o[1]+n.y*o[2]+e.y*o[3],z:i.z*o[0]+r.z*o[1]+n.z*o[2]+e.z*o[3]}}((this.length-1)*n-r,this.points[r],this.controls[r][1],this.controls[r+1][0],this.points[r+1])},t}();function fn(t,e){void 0===e&&(e={});for(var n=e.resolution||1e4,r=e.sharpness||.85,i=[],o=rt(t).coordinates.map((function(t){return{x:t[0],y:t[1]}})),s=new pn({duration:n,points:o,sharpness:r}),a=function(t){var e=s.pos(t);Math.floor(t/100)%2==0&&i.push([e.x,e.y])},u=0;u=me(t.slice(0,2),[e,i])){var o=(n+i)/2;return[e,o-(r-e)/2,r,o+(r-e)/2]}var s=(e+r)/2;return[s-(i-n)/2,n,s+(i-n)/2,i]}function vn(t,e,n,r){void 0===r&&(r={});var i=K(t),o=I(i[0]),s=I(i[1]),u=I(n),l=x(e,r.units),c=Math.asin(Math.sin(s)*Math.cos(l)+Math.cos(s)*Math.sin(l)*Math.cos(u));return a([w(o+Math.atan2(Math.sin(u)*Math.sin(l)*Math.cos(s),Math.cos(l)-Math.sin(s)*Math.sin(c))),w(c)],r.properties)}function _n(t,e,n){void 0===n&&(n={});for(var r=n.steps||64,i=n.properties?n.properties:!Array.isArray(t)&&"Feature"===t.type&&t.properties?t.properties:{},o=[],s=0;s80*n){r=o=t[0],i=s=t[1];for(var g=n;go&&(o=a),u>s&&(s=u);l=0!==(l=Math.max(o-r,s-i))?1/l:0}return Pn(p,f,n,r,i,l),f}function Sn(t,e,n,r,i){var o,s;if(i===Hn(t,e,n,r)>0)for(o=e;o=e;o-=r)s=Vn(o,t[o],t[o+1],s);return s&&Bn(s,s.next)&&(Xn(s),s=s.next),s}function Cn(t,e){if(!t)return t;e||(e=t);var n,r=t;do{if(n=!1,r.steiner||!Bn(r,r.next)&&0!==qn(r.prev,r,r.next))r=r.next;else{if(Xn(r),(r=e=r.prev)===r.next)break;n=!0}}while(n||r!==e);return e}function Pn(t,e,n,r,i,o,s){if(t){!s&&o&&function(t,e,n,r){var i=t;do{null===i.z&&(i.z=Dn(i.x,i.y,e,n,r)),i.prevZ=i.prev,i.nextZ=i.next,i=i.next}while(i!==t);i.prevZ.nextZ=null,i.prevZ=null,function(t){var e,n,r,i,o,s,a,u,l=1;do{for(n=t,t=null,o=null,s=0;n;){for(s++,r=n,a=0,e=0;e0||u>0&&r;)0!==a&&(0===u||!r||n.z<=r.z)?(i=n,n=n.nextZ,a--):(i=r,r=r.nextZ,u--),o?o.nextZ=i:t=i,i.prevZ=o,o=i;n=r}o.nextZ=null,l*=2}while(s>1)}(i)}(t,r,i,o);for(var a,u,l=t;t.prev!==t.next;)if(a=t.prev,u=t.next,o?Ln(t,r,i,o):Mn(t))e.push(a.i/n),e.push(t.i/n),e.push(u.i/n),Xn(t),t=u.next,l=u.next;else if((t=u)===l){s?1===s?Pn(t=On(t,e,n),e,n,r,i,o,2):2===s&&Rn(t,e,n,r,i,o):Pn(Cn(t),e,n,r,i,o,1);break}}}function Mn(t){var e=t.prev,n=t,r=t.next;if(qn(e,n,r)>=0)return!1;for(var i=t.next.next;i!==t.prev;){if(kn(e.x,e.y,n.x,n.y,r.x,r.y,i.x,i.y)&&qn(i.prev,i,i.next)>=0)return!1;i=i.next}return!0}function Ln(t,e,n,r){var i=t.prev,o=t,s=t.next;if(qn(i,o,s)>=0)return!1;for(var a=i.xo.x?i.x>s.x?i.x:s.x:o.x>s.x?o.x:s.x,c=i.y>o.y?i.y>s.y?i.y:s.y:o.y>s.y?o.y:s.y,h=Dn(a,u,e,n,r),p=Dn(l,c,e,n,r),f=t.prevZ,g=t.nextZ;f&&f.z>=h&&g&&g.z<=p;){if(f!==t.prev&&f!==t.next&&kn(i.x,i.y,o.x,o.y,s.x,s.y,f.x,f.y)&&qn(f.prev,f,f.next)>=0)return!1;if(f=f.prevZ,g!==t.prev&&g!==t.next&&kn(i.x,i.y,o.x,o.y,s.x,s.y,g.x,g.y)&&qn(g.prev,g,g.next)>=0)return!1;g=g.nextZ}for(;f&&f.z>=h;){if(f!==t.prev&&f!==t.next&&kn(i.x,i.y,o.x,o.y,s.x,s.y,f.x,f.y)&&qn(f.prev,f,f.next)>=0)return!1;f=f.prevZ}for(;g&&g.z<=p;){if(g!==t.prev&&g!==t.next&&kn(i.x,i.y,o.x,o.y,s.x,s.y,g.x,g.y)&&qn(g.prev,g,g.next)>=0)return!1;g=g.nextZ}return!0}function On(t,e,n){var r=t;do{var i=r.prev,o=r.next.next;!Bn(i,o)&&zn(i,r,r.next,o)&&jn(i,o)&&jn(o,i)&&(e.push(i.i/n),e.push(r.i/n),e.push(o.i/n),Xn(r),Xn(r.next),r=t=o),r=r.next}while(r!==t);return r}function Rn(t,e,n,r,i,o){var s=t;do{for(var a=s.next.next;a!==s.prev;){if(s.i!==a.i&&Gn(s,a)){var u=Un(s,a);return s=Cn(s,s.next),u=Cn(u,u.next),Pn(s,e,n,r,i,o),void Pn(u,e,n,r,i,o)}a=a.next}s=s.next}while(s!==t)}function Tn(t,e){return t.x-e.x}function An(t,e){if(e=function(t,e){var n,r=e,i=t.x,o=t.y,s=-1/0;do{if(o<=r.y&&o>=r.next.y&&r.next.y!==r.y){var a=r.x+(o-r.y)*(r.next.x-r.x)/(r.next.y-r.y);if(a<=i&&a>s){if(s=a,a===i){if(o===r.y)return r;if(o===r.next.y)return r.next}n=r.x=r.x&&r.x>=c&&i!==r.x&&kn(on.x)&&jn(r,t)&&(n=r,p=u),r=r.next;return n}(t,e)){var n=Un(e,t);Cn(n,n.next)}}function Dn(t,e,n,r,i){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=32767*(t-n)*i)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=32767*(e-r)*i)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function Fn(t){var e=t,n=t;do{e.x=0&&(t-s)*(r-a)-(n-s)*(e-a)>=0&&(n-s)*(o-a)-(i-s)*(r-a)>=0}function Gn(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){var n=t;do{if(n.i!==t.i&&n.next.i!==t.i&&n.i!==e.i&&n.next.i!==e.i&&zn(n,n.next,t,e))return!0;n=n.next}while(n!==t);return!1}(t,e)&&jn(t,e)&&jn(e,t)&&function(t,e){var n=t,r=!1,i=(t.x+e.x)/2,o=(t.y+e.y)/2;do{n.y>o!=n.next.y>o&&n.next.y!==n.y&&i<(n.next.x-n.x)*(o-n.y)/(n.next.y-n.y)+n.x&&(r=!r),n=n.next}while(n!==t);return r}(t,e)}function qn(t,e,n){return(e.y-t.y)*(n.x-e.x)-(e.x-t.x)*(n.y-e.y)}function Bn(t,e){return t.x===e.x&&t.y===e.y}function zn(t,e,n,r){return!!(Bn(t,e)&&Bn(n,r)||Bn(t,r)&&Bn(n,e))||qn(t,e,n)>0!=qn(t,e,r)>0&&qn(n,r,t)>0!=qn(n,r,e)>0}function jn(t,e){return qn(t.prev,t,t.next)<0?qn(t,e,t.next)>=0&&qn(t,t.prev,e)>=0:qn(t,e,t.prev)<0||qn(t,t.next,e)<0}function Un(t,e){var n=new Yn(t.i,t.x,t.y),r=new Yn(e.i,e.x,e.y),i=t.next,o=e.prev;return t.next=e,e.prev=t,n.next=i,i.prev=n,r.next=n,n.prev=r,o.next=r,r.prev=o,r}function Vn(t,e,n,r){var i=new Yn(t,e,n);return r?(i.next=r.next,i.prev=r,r.next.prev=i,r.next=i):(i.prev=i,i.next=i),i}function Xn(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ)}function Yn(t,e,n){this.i=t,this.x=e,this.y=n,this.prev=null,this.next=null,this.z=null,this.prevZ=null,this.nextZ=null,this.steiner=!1}function Hn(t,e,n,r){for(var i=0,o=e,s=n-r;o0&&(r+=t[i-1].length,n.holes.push(r))}return n}(t),n=wn(e.vertices,e.holes,2),r=[],i=[];n.forEach((function(t,r){var o=n[r];i.push([e.vertices[2*o],e.vertices[2*o+1]])}));for(var o=0;oi?n:i,r>o?r:o]}(t,r),n.push(i),r})),n})(n,t.properties).forEach((function(t){t.id=e.length,e.push(t)}))}))}}(t,e)})),f(e)}Nn.deviation=function(t,e,n,r){var i=e&&e.length,o=i?e[0]*n:t.length,s=Math.abs(Hn(t,0,o,n));if(i)for(var a=0,u=e.length;a0&&(r+=t[i-1].length,n.holes.push(r))}return n},wn.default=In;var Kn=Bt((function(t,e){function n(t,e,n){void 0===n&&(n={});var r={type:"Feature"};return(0===n.id||n.id)&&(r.id=n.id),n.bbox&&(r.bbox=n.bbox),r.properties=e||{},r.geometry=t,r}function r(t,e,r){if(void 0===r&&(r={}),!t)throw new Error("coordinates is required");if(!Array.isArray(t))throw new Error("coordinates must be an Array");if(t.length<2)throw new Error("coordinates must be at least 2 numbers long");if(!f(t[0])||!f(t[1]))throw new Error("coordinates must contain numbers");return n({type:"Point",coordinates:t},e,r)}function i(t,e,r){void 0===r&&(r={});for(var i=0,o=t;i=0))throw new Error("precision must be a positive number");var n=Math.pow(10,e||0);return Math.round(t*n)/n},e.radiansToLength=c,e.lengthToRadians=h,e.lengthToDegrees=function(t,e){return p(h(t,e))},e.bearingToAzimuth=function(t){var e=t%360;return e<0&&(e+=360),e},e.radiansToDegrees=p,e.degreesToRadians=function(t){return t%360*Math.PI/180},e.convertLength=function(t,e,n){if(void 0===e&&(e="kilometers"),void 0===n&&(n="kilometers"),!(t>=0))throw new Error("length must be a positive number");return c(h(t,e),n)},e.convertArea=function(t,n,r){if(void 0===n&&(n="meters"),void 0===r&&(r="kilometers"),!(t>=0))throw new Error("area must be a positive number");var i=e.areaFactors[n];if(!i)throw new Error("invalid original units");var o=e.areaFactors[r];if(!o)throw new Error("invalid final units");return t/i*o},e.isNumber=f,e.isObject=function(t){return!!t&&t.constructor===Object},e.validateBBox=function(t){if(!t)throw new Error("bbox is required");if(!Array.isArray(t))throw new Error("bbox must be an Array");if(4!==t.length&&6!==t.length)throw new Error("bbox must be an Array of 4 or 6 numbers");t.forEach((function(t){if(!f(t))throw new Error("bbox must only contain numbers")}))},e.validateId=function(t){if(!t)throw new Error("id is required");if(-1===["string","number"].indexOf(typeof t))throw new Error("id must be a number or a string")}}));function Qn(t,e,n){if(null!==t)for(var r,i,o,s,a,u,l,c,h=0,p=0,f=t.type,g="FeatureCollection"===f,d="Feature"===f,y=g?t.features.length:1,v=0;va||p>u||f>l)return s=o,a=n,u=p,l=f,void(i=0);var g=Kn.lineString([s,o],t.properties);if(!1===e(g,n,r,f,i))return!1;i++,s=o}))&&void 0}}}))}function ir(t,e){if(!t)throw new Error("geojson is required");nr(t,(function(t,n,r){if(null!==t.geometry){var i=t.geometry.type,o=t.geometry.coordinates;switch(i){case"LineString":if(!1===e(t,n,r,0,0))return!1;break;case"Polygon":for(var s=0;st[0]&&(e[0]=t[0]),e[1]>t[1]&&(e[1]=t[1]),e[2] line1 must only contain 2 coordinates");if(2!==r.length)throw new Error(" line2 must only contain 2 coordinates");var i=n[0][0],o=n[0][1],s=n[1][0],u=n[1][1],l=r[0][0],c=r[0][1],h=r[1][0],p=r[1][1],f=(p-c)*(s-i)-(h-l)*(u-o),g=(h-l)*(o-c)-(p-c)*(i-l),d=(s-i)*(o-c)-(u-o)*(i-l);if(0===f)return null;var y=g/f,v=d/f;return y>=0&&y<=1&&v>=0&&v<=1?a([i+y*(s-i),o+y*(u-o)]):null}function Tr(t,e,n){void 0===n&&(n={});var r=a([1/0,1/0],{dist:1/0}),i=0;return z(t,(function(t){for(var o=Q(t),s=0;s0&&((v=y.features[0]).properties.dist=me(e,v,n),v.properties.location=i+me(u,v,n)),u.properties.dist180?-360:i[0]-o[0]>180?360:0,N(function(t,n,r){var i=r=void 0===r?e:Number(r),o=t[1]*Math.PI/180,s=n[1]*Math.PI/180,a=s-o,u=Math.abs(n[0]-t[0])*Math.PI/180;u>Math.PI&&(u-=2*Math.PI);var l=Math.log(Math.tan(s/2+Math.PI/4)/Math.tan(o/2+Math.PI/4)),c=Math.abs(l)>1e-11?a/l:Math.cos(o);return Math.sqrt(a*a+c*c*u*u)*i}(i,o),"meters",r.units)}function Dr(t,e,n){if(void 0===n&&(n={}),n.method||(n.method="geodesic"),n.units||(n.units="kilometers"),!t)throw new Error("pt is required");if(Array.isArray(t)?t=a(t):"Point"===t.type?t=o(t):et(t,"Point","point"),!e)throw new Error("line is required");Array.isArray(e)?e=h(e):"LineString"===e.type?e=o(e):et(e,"LineString","line");var r=1/0,i=t.geometry.coordinates;return U(e,(function(t){var e=t.geometry.coordinates[0],o=t.geometry.coordinates[1],s=function(t,e,n,r){var i=[n[0]-e[0],n[1]-e[1]],o=Fr([t[0]-e[0],t[1]-e[1]],i);if(o<=0)return kr(t,e,{method:r.method,units:"degrees"});var s=Fr(i,i);if(s<=o)return kr(t,n,{method:r.method,units:"degrees"});var a=o/s,u=[e[0]+a*i[0],e[1]+a*i[1]];return kr(t,u,{method:r.method,units:"degrees"})}(i,e,o,n);s=0&&l<=1&&(p.onLine1=!0),c>=0&&c<=1&&(p.onLine2=!0),!(!p.onLine1||!p.onLine2)&&[p.x,p.y])}function qr(t){for(var e=function(t){if("FeatureCollection"!==t.type)return"Feature"!==t.type?f([o(t)]):f([t]);return t}(t),n=xn(e),r=!1,i=0;!r&&i0){e+=Math.abs(Vr(t[0]));for(var n=1;n2){for(s=0;s=c&&p===i.length-1);p++){if(c>e&&0===o.length){if(!(s=e-c))return o.push(i[p]),h(o);a=mn(i[p],i[p-1])-180,u=vn(i[p],s,a,r),o.push(u.geometry.coordinates)}if(c>=n)return(s=n-c)?(a=mn(i[p],i[p-1])-180,u=vn(i[p],s,a,r),o.push(u.geometry.coordinates),h(o)):(o.push(i[p]),h(o));if(c>=e&&o.push(i[p]),p===i.length-1)return h(o);c+=me(i[p],i[p+1],r)}if(ci)return!1}else if(0!==f)return!1;return r?"start"===r?Math.abs(h)>=Math.abs(p)?h>0?a0?u=Math.abs(p)?h>0?a<=o&&o0?u<=s&&s=Math.abs(p)?h>0?a0?u=Math.abs(p)?h>0?a<=o&&o<=l:l<=o&&o<=a:p>0?u<=s&&s<=c:c<=s&&s<=u}function Zr(t,e){var n=rt(t),r=rt(e),i=n.type,o=r.type;switch(i){case"Point":switch(o){case"MultiPoint":return function(t,e){var n,r=!1;for(n=0;ne[0])&&(!(t[2]e[1])&&!(t[3] is required");if("number"!=typeof n)throw new Error(" must be a number");if("number"!=typeof r)throw new Error(" must be a number");!1!==i&&void 0!==i||(t=JSON.parse(JSON.stringify(t)));var o=Math.pow(10,n);return R(t,(function(t){!function(t,e,n){t.length>n&&t.splice(n,t.length);for(var r=0;r=1||u<=0||l>=1||l<=0))){var d=g,y=!o[d];y&&(o[d]=!0),e?i.push(e(g,t,n,c,h,u,s,a,p,f,l,y)):i.push(g)}}function d(t,e){var n,i,o,s,a=r[t][e],u=r[t][e+1];return a[0]p[e.isect].coord?-1:1}));for(u=[];b.length>0;){var C=b.pop(),P=C.isect,M=C.parent,L=C.winding,O=u.length,R=[p[P].coord],T=P;if(p[P].ringAndEdge1Walkable)var A=p[P].ringAndEdge1,D=p[P].nxtIsectAlongRingAndEdge1;else A=p[P].ringAndEdge2,D=p[P].nxtIsectAlongRingAndEdge2;for(;!ci(p[P].coord,p[D].coord);){R.push(p[D].coord);var F=void 0;for(r=0;r1)for(e=0;e=0==e}function li(t){for(var e=0,n=0;n1&&n.push(h(l)),f(n)}function xi(t,e){if(!e.features.length)throw new Error("lines must contain features");if(1===e.features.length)return e.features[0];var n,r=1/0;return F(e,(function(e){var i=Tr(e,t).properties.dist;ic&&f.push(vn(t,e,c,i).geometry.coordinates),h(f,u)}function wi(t){var e=t%360;return e<0&&(e+=360),e}function Ii(t,e){void 0===e&&(e={});var n=rt(t);switch(e.properties||"Feature"!==t.type||(e.properties=t.properties),n.type){case"Polygon":return Ni(n,e);case"MultiPolygon":return function(t,e){void 0===e&&(e={});var n=rt(t).coordinates,r=e.properties?e.properties:"Feature"===t.type?t.properties:{},i=[];return n.forEach((function(t){i.push(Si(t,r))})),f(i)}(n,e);default:throw new Error("invalid poly")}}function Ni(t,e){return void 0===e&&(e={}),Si(rt(t).coordinates,e.properties?e.properties:"Feature"===t.type?t.properties:{})}function Si(t,e){return t.length>1?g(t,e):h(t[0],e)}function Ci(t,e){var n,r,i;void 0===e&&(e={});var o=e.properties,s=null===(n=e.autoComplete)||void 0===n||n,a=null===(r=e.orderCoords)||void 0===r||r;switch(null!==(i=e.mutate)&&void 0!==i&&i||(t=Ie(t)),t.type){case"FeatureCollection":var u=[];return t.features.forEach((function(t){u.push(Q(Pi(t,{},s,a)))})),y(u,o);default:return Pi(t,o,s,a)}}function Pi(t,e,n,r){e=e||("Feature"===t.type?t.properties:{});var i=rt(t),o=i.coordinates,s=i.type;if(!o.length)throw new Error("line must contain coordinates");switch(s){case"LineString":return n&&(o=Mi(o)),l([o],e);case"MultiLineString":var a=[],u=0;return o.forEach((function(t){if(n&&(t=Mi(t)),r){var e=function(t){var e=t[0],n=t[1],r=t[2],i=t[3];return Math.abs(e-r)*Math.abs(n-i)}(Z(h(t)));e>u?(a.unshift(t),u=e):a.push(t)}else a.push(t)})),l(a,e);default:throw new Error("geometry type "+s+" is not supported")}}function Mi(t){var e=t[0],n=e[0],r=e[1],i=t[t.length-1],o=i[0],s=i[1];return n===o&&r===s||t.push(e),t}function Li(t,e){var n,r,i,o,s,a,u;for(r=1;r<=8;r*=2){for(n=[],o=!(Ri(i=t[t.length-1],e)&r),s=0;se[2]&&(n|=2),t[1]e[3]&&(n|=8),n}function Ti(t,e){for(var n=[],r=0,i=t;r0&&(o[0][0]===o[o.length-1][0]&&o[0][1]===o[o.length-1][1]||o.push(o[0]),o.length>=4&&n.push(o))}return n}vi.prototype.interpolate=function(t){var e=Math.sin((1-t)*this.g)/Math.sin(this.g),n=Math.sin(t*this.g)/Math.sin(this.g),r=e*Math.cos(this.start.y)*Math.cos(this.start.x)+n*Math.cos(this.end.y)*Math.cos(this.end.x),i=e*Math.cos(this.start.y)*Math.sin(this.start.x)+n*Math.cos(this.end.y)*Math.sin(this.end.x),o=e*Math.sin(this.start.y)+n*Math.sin(this.end.y),s=fi*Math.atan2(o,Math.sqrt(Math.pow(r,2)+Math.pow(i,2)));return[fi*Math.atan2(i,r),s]},vi.prototype.Arc=function(t,e){var n=[];if(!t||t<=2)n.push([this.start.lon,this.start.lat]),n.push([this.end.lon,this.end.lat]);else for(var r=1/(t-1),i=0;ip&&(d>c&&gc&&du&&(u=y)}var v=[];if(a&&u0&&Math.abs(x-n[m-1][0])>p){var E=parseFloat(n[m-1][0]),b=parseFloat(n[m-1][1]),w=parseFloat(n[m][0]),I=parseFloat(n[m][1]);if(E>-180&&E-180&&n[m-1][0]c&&E<180&&-180===w&&m+1c&&n[m-1][0]<180){_.push([180,n[m][1]]),m++,_.push([n[m][0],n[m][1]]);continue}if(Ec){var N=E;E=w,w=N;var S=b;b=I,I=S}if(E>c&&w=180&&Ec?180:-180,P]),(_=[]).push([n[m-1][0]>c?-180:180,P]),v.push(_)}else _=[],v.push(_);_.push([x,n[m][1]])}else _.push([n[m][0],n[m][1]])}}else{var M=[];v.push(M);for(var L=0;L=0;a--)if(l[a]!=c[a])return!1;for(a=l.length-1;a>=0;a--)if(u=l[a],!n(t[u],o[u],s))return!1;return typeof t==typeof o}(t,o,s))};function r(t){return null==t}function i(t){return!(!t||"object"!=typeof t||"number"!=typeof t.length)&&("function"==typeof t.copy&&"function"==typeof t.slice&&!(t.length>0&&"number"!=typeof t[0]))}}));function ki(t,e,n){if(void 0===n&&(n={}),!P(n=n||{}))throw new Error("options is invalid");var r,i=n.tolerance||0,o=[],s=Mr(),a=Zn(t);return s.load(a),U(e,(function(t){var e=!1;t&&(F(s.search(t),(function(n){if(!1===e){var o=Q(t).sort(),s=Q(n).sort();Fi(o,s)||(0===i?Wr(o[0],n)&&Wr(o[1],n):Tr(n,o[0]).properties.dist<=i&&Tr(n,o[1]).properties.dist<=i)?(e=!0,r=r?Gi(r,t):t):(0===i?Wr(s[0],t)&&Wr(s[1],t):Tr(t,s[0]).properties.dist<=i&&Tr(t,s[1]).properties.dist<=i)&&(r=r?Gi(r,n):n)}})),!1===e&&r&&(o.push(r),r=void 0))})),r&&o.push(r),f(o)}function Gi(t,e){var n=Q(e),r=Q(t),i=r[0],o=r[r.length-1],s=t.geometry.coordinates;return Fi(n[0],i)?s.unshift(n[1]):Fi(n[0],o)?s.push(n[1]):Fi(n[1],i)?s.unshift(n[0]):Fi(n[1],o)&&s.push(n[0]),t}function qi(t){var e=t%360;return e<0&&(e+=360),e}function Bi(t,e,n){var r;return void 0===n&&(n={}),(r=n.final?zi(K(e),K(t)):zi(K(t),K(e)))>180?-(360-r):r}function zi(t,e){var n=I(t[1]),r=I(e[1]),i=I(e[0]-t[0]);i>Math.PI&&(i-=2*Math.PI),i<-Math.PI&&(i+=2*Math.PI);var o=Math.log(Math.tan(r/2+Math.PI/4)/Math.tan(n/2+Math.PI/4));return(w(Math.atan2(i,o))+360)%360}function ji(t,n,r,i){void 0===i&&(i={});var o=n<0,s=N(Math.abs(n),i.units,"meters");o&&(s=-Math.abs(s));var u=K(t),l=function(t,n,r,i){i=void 0===i?e:Number(i);var o=n/i,s=t[0]*Math.PI/180,a=I(t[1]),u=I(r),l=o*Math.cos(u),c=a+l;Math.abs(c)>Math.PI/2&&(c=c>0?Math.PI-c:-Math.PI-c);var h=Math.log(Math.tan(c/2+Math.PI/4)/Math.tan(a/2+Math.PI/4)),p=Math.abs(h)>1e-11?l/h:Math.cos(a),f=o*Math.sin(u)/p;return[(180*(s+f)/Math.PI+540)%360-180,180*c/Math.PI]}(u,s,r);return l[0]+=l[0]-u[0]>180?-360:u[0]-l[0]>180?360:0,a(l,i.properties)}function Ui(t,e,n,r,i,o){for(var s=0;s0?Xi(e,a,i)<0||(i=a):n>0&&r<=0&&(Vi(e,a,o)||(o=a)),n=r}return[i,o]}function Vi(t,e,n){return Xi(t,e,n)>0}function Xi(t,e,n){return(e[0]-t[0])*(n[1]-t[1])-(n[0]-t[0])*(e[1]-t[1])}function Yi(t){for(var e,n,r=Q(t),i=0,o=1;o0}function Hi(t,e){switch("Feature"===t.type?t.geometry.type:t.type){case"GeometryCollection":return q(t,(function(t){Hi(t,e)})),t;case"LineString":return Wi(Q(t),e),t;case"Polygon":return Ji(Q(t),e),t;case"MultiLineString":return Q(t).forEach((function(t){Wi(t,e)})),t;case"MultiPolygon":return Q(t).forEach((function(t){Ji(t,e)})),t;case"Point":case"MultiPoint":return t}}function Wi(t,e){Yi(t)===e&&t.reverse()}function Ji(t,e){Yi(t[0])!==e&&t[0].reverse();for(var n=1;n + * v. 1.2.0 + * https://github.com/RaumZeit/MarchingSquares.js + * + * MarchingSquaresJS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MarchingSquaresJS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * As additional permission under GNU Affero General Public License version 3 + * section 7, third-party projects (personal or commercial) may distribute, + * include, or link against UNMODIFIED VERSIONS of MarchingSquaresJS without the + * requirement that said third-party project for that reason alone becomes + * subject to any requirement of the GNU Affero General Public License version 3. + * Any modifications to MarchingSquaresJS, however, must be shared with the public + * and made available. + * + * In summary this: + * - allows you to use MarchingSquaresJS at no cost + * - allows you to use MarchingSquaresJS for both personal and commercial purposes + * - allows you to distribute UNMODIFIED VERSIONS of MarchingSquaresJS under any + * license as long as this license notice is included + * - enables you to keep the source code of your program that uses MarchingSquaresJS + * undisclosed + * - forces you to share any modifications you have made to MarchingSquaresJS, + * e.g. bug-fixes + * + * You should have received a copy of the GNU Affero General Public License + * along with MarchingSquaresJS. If not, see . + */(t,r),s=[],a=0;as?128:64,l|=hs?32:16,l|=ps?8:4;var g=+(l|=fs?2:1),d=0;if(17===l||18===l||33===l||34===l||38===l||68===l||72===l||98===l||102===l||132===l||136===l||137===l||152===l||153===l){var y=(c+h+p+f)/4;d=y>s?2:y0?(l=156,d=4):l=152:33===l?d>0?(l=139,d=4):l=137:72===l?d>0?(l=99,d=4):l=98:132===l&&(d>0?(l=39,d=4):l=38)}if(0!=l&&170!=l){var v,_,m,x,E,b,w,I;v=_=m=x=E=b=w=I=.5;var N=[];1===l?(m=1-Vo(e,p,f),I=1-Vo(e,c,f),N.push(Go[l])):169===l?(m=Vo(s,f,p),I=Vo(s,f,c),N.push(Go[l])):4===l?(b=1-Vo(e,h,p),x=Vo(e,f,p),N.push(Fo[l])):166===l?(b=Vo(s,p,h),x=1-Vo(s,p,f),N.push(Fo[l])):16===l?(E=Vo(e,p,h),_=Vo(e,c,h),N.push(Do[l])):154===l?(E=1-Vo(s,h,p),_=1-Vo(s,h,c),N.push(Do[l])):64===l?(w=Vo(e,f,c),v=1-Vo(e,h,c),N.push(Bo[l])):106===l?(w=1-Vo(s,c,f),v=Vo(s,c,h),N.push(Bo[l])):168===l?(x=Vo(s,f,p),m=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),N.push(ko[l]),N.push(Go[l])):2===l?(x=1-Vo(e,p,f),m=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),N.push(ko[l]),N.push(Go[l])):162===l?(E=Vo(s,p,h),b=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),N.push(ko[l]),N.push(Go[l])):8===l?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),N.push(Do[l]),N.push(Fo[l])):138===l?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),v=1-Vo(s,h,c),_=1-Vo(e,h,c),N.push(Do[l]),N.push(Fo[l])):32===l?(E=Vo(s,p,h),b=Vo(e,p,h),v=Vo(e,c,h),_=Vo(s,c,h),N.push(Do[l]),N.push(Fo[l])):42===l?(I=1-Vo(s,c,f),w=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h),N.push(qo[l]),N.push(Bo[l])):128===l&&(I=Vo(e,f,c),w=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c),N.push(qo[l]),N.push(Bo[l])),5===l?(b=1-Vo(e,h,p),I=1-Vo(e,c,f),N.push(Fo[l])):165===l?(b=Vo(s,p,h),I=Vo(s,f,c),N.push(Fo[l])):20===l?(x=Vo(e,f,p),_=Vo(e,c,h),N.push(ko[l])):150===l?(x=1-Vo(s,p,f),_=1-Vo(s,h,c),N.push(ko[l])):80===l?(E=Vo(e,p,h),w=Vo(e,f,c),N.push(Do[l])):90===l?(E=1-Vo(s,h,p),w=1-Vo(s,c,f),N.push(Do[l])):65===l?(m=1-Vo(e,p,f),v=1-Vo(e,h,c),N.push(Go[l])):105===l?(m=Vo(s,f,p),v=Vo(s,c,h),N.push(Go[l])):160===l?(E=Vo(s,p,h),b=Vo(e,p,h),I=Vo(e,f,c),w=Vo(s,f,c),N.push(Do[l]),N.push(Fo[l])):10===l?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),I=1-Vo(s,c,f),w=1-Vo(e,c,f),N.push(Do[l]),N.push(Fo[l])):130===l?(x=1-Vo(e,p,f),m=1-Vo(s,p,f),v=1-Vo(s,h,c),_=1-Vo(e,h,c),N.push(ko[l]),N.push(Go[l])):40===l?(x=Vo(s,f,p),m=Vo(e,f,p),v=Vo(e,c,h),_=Vo(s,c,h),N.push(ko[l]),N.push(Go[l])):101===l?(b=Vo(s,p,h),v=Vo(s,c,h),N.push(Fo[l])):69===l?(b=1-Vo(e,h,p),v=1-Vo(e,h,c),N.push(Fo[l])):149===l?(I=Vo(s,f,c),_=1-Vo(s,h,c),N.push(qo[l])):21===l?(I=1-Vo(e,c,f),_=Vo(e,c,h),N.push(qo[l])):86===l?(x=1-Vo(s,p,f),w=1-Vo(s,c,f),N.push(ko[l])):84===l?(x=Vo(e,f,p),w=Vo(e,f,c),N.push(ko[l])):89===l?(E=1-Vo(s,h,p),m=Vo(s,f,p),N.push(Go[l])):81===l?(E=Vo(e,p,h),m=1-Vo(e,p,f),N.push(Go[l])):96===l?(E=Vo(s,p,h),b=Vo(e,p,h),w=Vo(e,f,c),v=Vo(s,c,h),N.push(Do[l]),N.push(Fo[l])):74===l?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),w=1-Vo(s,c,f),v=1-Vo(e,h,c),N.push(Do[l]),N.push(Fo[l])):24===l?(E=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),_=Vo(e,c,h),N.push(Do[l]),N.push(Go[l])):146===l?(E=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),_=1-Vo(s,h,c),N.push(Do[l]),N.push(Go[l])):6===l?(b=1-Vo(e,h,p),x=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),N.push(Fo[l]),N.push(ko[l])):164===l?(b=Vo(s,p,h),x=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),N.push(Fo[l]),N.push(ko[l])):129===l?(m=1-Vo(e,p,f),I=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c),N.push(Go[l]),N.push(qo[l])):41===l?(m=Vo(s,f,p),I=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h),N.push(Go[l]),N.push(qo[l])):66===l?(x=1-Vo(e,p,f),m=1-Vo(s,p,f),w=1-Vo(s,c,f),v=1-Vo(e,h,c),N.push(ko[l]),N.push(Go[l])):104===l?(x=Vo(s,f,p),m=Vo(e,f,p),w=Vo(e,f,c),v=Vo(s,c,h),N.push(Go[l]),N.push(zo[l])):144===l?(E=Vo(e,p,h),I=Vo(e,f,c),w=Vo(s,f,c),_=1-Vo(s,h,c),N.push(Do[l]),N.push(Bo[l])):26===l?(E=1-Vo(s,h,p),I=1-Vo(s,c,f),w=1-Vo(e,c,f),_=Vo(e,c,h),N.push(Do[l]),N.push(Bo[l])):36===l?(b=Vo(s,p,h),x=Vo(e,f,p),v=Vo(e,c,h),_=Vo(s,c,h),N.push(Fo[l]),N.push(ko[l])):134===l?(b=1-Vo(e,h,p),x=1-Vo(s,p,f),v=1-Vo(s,h,c),_=1-Vo(e,h,c),N.push(Fo[l]),N.push(ko[l])):9===l?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),m=Vo(s,f,p),I=1-Vo(e,c,f),N.push(Do[l]),N.push(Fo[l])):161===l?(E=Vo(s,p,h),b=Vo(e,p,h),m=1-Vo(e,p,f),I=Vo(s,f,c),N.push(Do[l]),N.push(Fo[l])):37===l?(b=Vo(s,p,h),I=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h),N.push(Fo[l]),N.push(qo[l])):133===l?(b=1-Vo(e,h,p),I=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c),N.push(Fo[l]),N.push(qo[l])):148===l?(x=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),_=1-Vo(s,h,c),N.push(ko[l]),N.push(Bo[l])):22===l?(x=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),_=Vo(e,c,h),N.push(ko[l]),N.push(Bo[l])):82===l?(E=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),w=1-Vo(s,c,f),N.push(Do[l]),N.push(Go[l])):88===l?(E=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),w=Vo(e,f,c),N.push(Do[l]),N.push(Go[l])):73===l?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),m=Vo(s,f,p),v=1-Vo(e,h,c),N.push(Do[l]),N.push(Fo[l])):97===l?(E=Vo(s,p,h),b=Vo(e,p,h),m=1-Vo(e,p,f),v=Vo(s,c,h),N.push(Do[l]),N.push(Fo[l])):145===l?(E=Vo(e,p,h),m=1-Vo(e,p,f),I=Vo(s,f,c),_=1-Vo(s,h,c),N.push(Do[l]),N.push(qo[l])):25===l?(E=1-Vo(s,h,p),m=Vo(s,f,p),I=1-Vo(e,c,f),_=Vo(e,c,h),N.push(Do[l]),N.push(qo[l])):70===l?(b=1-Vo(e,h,p),x=1-Vo(s,p,f),w=1-Vo(s,c,f),v=1-Vo(e,h,c),N.push(Fo[l]),N.push(ko[l])):100===l?(b=Vo(s,p,h),x=Vo(e,f,p),w=Vo(e,f,c),v=Vo(s,c,h),N.push(Fo[l]),N.push(ko[l])):34===l?(0===d?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)):(E=Vo(s,p,h),b=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)),N.push(Do[l]),N.push(Fo[l]),N.push(qo[l]),N.push(Bo[l])):35===l?(4===d?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)):(E=Vo(s,p,h),b=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)),N.push(Do[l]),N.push(Fo[l]),N.push(Go[l]),N.push(Bo[l])):136===l?(0===d?(E=Vo(s,p,h),b=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)):(E=1-Vo(e,h,p),b=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)),N.push(Do[l]),N.push(Fo[l]),N.push(qo[l]),N.push(Bo[l])):153===l?(0===d?(E=Vo(e,p,h),m=1-Vo(e,p,f),I=1-Vo(e,c,f),_=Vo(e,c,h)):(E=1-Vo(s,h,p),m=Vo(s,f,p),I=Vo(s,f,c),_=1-Vo(s,h,c)),N.push(Do[l]),N.push(Go[l])):102===l?(0===d?(b=1-Vo(e,h,p),x=Vo(e,f,p),w=Vo(e,f,c),v=1-Vo(e,h,c)):(b=Vo(s,p,h),x=1-Vo(s,p,f),w=1-Vo(s,c,f),v=Vo(s,c,h)),N.push(Fo[l]),N.push(Bo[l])):155===l?(4===d?(E=Vo(e,p,h),m=1-Vo(e,p,f),I=1-Vo(e,c,f),_=Vo(e,c,h)):(E=1-Vo(s,h,p),m=Vo(s,f,p),I=Vo(s,f,c),_=1-Vo(s,h,c)),N.push(Do[l]),N.push(qo[l])):103===l?(4===d?(b=1-Vo(e,h,p),x=Vo(e,f,p),w=Vo(e,f,c),v=1-Vo(e,h,c)):(b=Vo(s,p,h),x=1-Vo(s,p,f),w=1-Vo(s,c,f),v=Vo(s,c,h)),N.push(Fo[l]),N.push(ko[l])):152===l?(0===d?(E=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),_=Vo(e,c,h)):(E=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),_=1-Vo(s,h,c)),N.push(Do[l]),N.push(ko[l]),N.push(Go[l])):156===l?(4===d?(E=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),_=Vo(e,c,h)):(E=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),_=1-Vo(s,h,c)),N.push(Do[l]),N.push(Go[l]),N.push(Bo[l])):137===l?(0===d?(E=Vo(s,p,h),b=Vo(e,p,h),m=1-Vo(e,p,f),I=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)):(E=1-Vo(e,h,p),b=1-Vo(s,h,p),m=Vo(s,f,p),I=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)),N.push(Do[l]),N.push(Fo[l]),N.push(Go[l])):139===l?(4===d?(E=Vo(s,p,h),b=Vo(e,p,h),m=1-Vo(e,p,f),I=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)):(E=1-Vo(e,h,p),b=1-Vo(s,h,p),m=Vo(s,f,p),I=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)),N.push(Do[l]),N.push(Fo[l]),N.push(qo[l])):98===l?(0===d?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),w=Vo(e,f,c),v=1-Vo(e,h,c)):(E=Vo(s,p,h),b=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),w=1-Vo(s,c,f),v=Vo(s,c,h)),N.push(Do[l]),N.push(Fo[l]),N.push(Bo[l])):99===l?(4===d?(E=1-Vo(e,h,p),b=1-Vo(s,h,p),x=Vo(s,f,p),m=Vo(e,f,p),w=Vo(e,f,c),v=1-Vo(e,h,c)):(E=Vo(s,p,h),b=Vo(e,p,h),x=1-Vo(e,p,f),m=1-Vo(s,p,f),w=1-Vo(s,c,f),v=Vo(s,c,h)),N.push(Do[l]),N.push(Fo[l]),N.push(Go[l])):38===l?(0===d?(b=1-Vo(e,h,p),x=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)):(b=Vo(s,p,h),x=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)),N.push(Fo[l]),N.push(qo[l]),N.push(Bo[l])):39===l?(4===d?(b=1-Vo(e,h,p),x=Vo(e,f,p),I=Vo(e,f,c),w=Vo(s,f,c),v=1-Vo(s,h,c),_=1-Vo(e,h,c)):(b=Vo(s,p,h),x=1-Vo(s,p,f),I=1-Vo(s,c,f),w=1-Vo(e,c,f),v=Vo(e,c,h),_=Vo(s,c,h)),N.push(Fo[l]),N.push(ko[l]),N.push(Bo[l])):85===l&&(E=1,b=0,x=1,m=0,I=0,w=1,v=0,_=1),(v<0||v>1||_<0||_>1||E<0||E>1||x<0||x>1||I<0||I>1||w<0||w>1)&&console.log("MarchingSquaresJS-isoBands: "+l+" "+g+" "+c+","+h+","+p+","+f+" "+d+" "+v+" "+_+" "+E+" "+b+" "+x+" "+m+" "+I+" "+w),o.cells[a][u]={cval:l,cval_real:g,flipped:d,topleft:v,topright:_,righttop:E,rightbottom:b,bottomright:x,bottomleft:m,leftbottom:I,lefttop:w,edges:N}}}}}return o}(t,e,n);return Qi.polygons?(Qi.verbose&&console.log("MarchingSquaresJS-isoBands: returning single polygons for each grid cell"),u=function(t){var e=[],n=0;return t.cells.forEach((function(t,r){t.forEach((function(t,i){if(void 0!==t){var o=Uo[t.cval](t);"object"==typeof o&&Xo(o)?"object"==typeof o[0]&&Xo(o[0])?"object"==typeof o[0][0]&&Xo(o[0][0])?o.forEach((function(t){t.forEach((function(t){t[0]+=i,t[1]+=r})),e[n++]=t})):(o.forEach((function(t){t[0]+=i,t[1]+=r})),e[n++]=o):console.log("MarchingSquaresJS-isoBands: bandcell polygon with malformed coordinates"):console.log("MarchingSquaresJS-isoBands: bandcell polygon with null coordinates")}}))})),e}(l)):(Qi.verbose&&console.log("MarchingSquaresJS-isoBands: returning polygon paths for entire data grid"),u=function(t){for(var e=[],n=t.rows,r=t.cols,i=[],o=0;o0){var a=Ho(t.cells[o][s]),u=null,l=s,c=o;null!==a&&i.push([a.p[0]+l,a.p[1]+c]);do{if(null===(u=Wo(t.cells[c][l],a.x,a.y,a.o)))break;if(i.push([u.p[0]+l,u.p[1]+c]),l+=u.x,a=u,(c+=u.y)<0||c>=n||l<0||l>=r||void 0===t.cells[c][l]){var h=Yo(t,l-=u.x,c-=u.y,u.x,u.y,u.o);if(null===h)break;h.path.forEach((function(t){i.push(t)})),l=h.i,c=h.j,a=h}}while(void 0!==t.cells[c][l]&&t.cells[c][l].edges.length>0);e.push(i),i=[],t.cells[o][s].edges.length>0&&s--}return e}(l)),"function"==typeof Qi.successCallback&&Qi.successCallback(u),u}var to=64,eo=16,no=[],ro=[],io=[],oo=[],so=[],ao=[],uo=[],lo=[],co=[],ho=[],po=[],fo=[],go=[],yo=[],vo=[],_o=[],mo=[],xo=[],Eo=[],bo=[],wo=[],Io=[],No=[],So=[];uo[85]=ho[85]=-1,lo[85]=po[85]=0,co[85]=fo[85]=1,Eo[85]=Io[85]=1,bo[85]=No[85]=0,wo[85]=So[85]=1,no[85]=oo[85]=0,ro[85]=so[85]=-1,io[85]=vo[85]=0,_o[85]=go[85]=0,mo[85]=yo[85]=1,ao[85]=xo[85]=1,Io[1]=Io[169]=0,No[1]=No[169]=-1,So[1]=So[169]=0,go[1]=go[169]=-1,yo[1]=yo[169]=0,vo[1]=vo[169]=0,ho[4]=ho[166]=0,po[4]=po[166]=-1,fo[4]=fo[166]=1,_o[4]=_o[166]=1,mo[4]=mo[166]=0,xo[4]=xo[166]=0,uo[16]=uo[154]=0,lo[16]=lo[154]=1,co[16]=co[154]=1,oo[16]=oo[154]=1,so[16]=so[154]=0,ao[16]=ao[154]=1,Eo[64]=Eo[106]=0,bo[64]=bo[106]=1,wo[64]=wo[106]=0,no[64]=no[106]=-1,ro[64]=ro[106]=0,io[64]=io[106]=1,Eo[2]=Eo[168]=0,bo[2]=bo[168]=-1,wo[2]=wo[168]=1,Io[2]=Io[168]=0,No[2]=No[168]=-1,So[2]=So[168]=0,go[2]=go[168]=-1,yo[2]=yo[168]=0,vo[2]=vo[168]=0,_o[2]=_o[168]=-1,mo[2]=mo[168]=0,xo[2]=xo[168]=1,uo[8]=uo[162]=0,lo[8]=lo[162]=-1,co[8]=co[162]=0,ho[8]=ho[162]=0,po[8]=po[162]=-1,fo[8]=fo[162]=1,go[8]=go[162]=1,yo[8]=yo[162]=0,vo[8]=vo[162]=1,_o[8]=_o[162]=1,mo[8]=mo[162]=0,xo[8]=xo[162]=0,uo[32]=uo[138]=0,lo[32]=lo[138]=1,co[32]=co[138]=1,ho[32]=ho[138]=0,po[32]=po[138]=1,fo[32]=fo[138]=0,no[32]=no[138]=1,ro[32]=ro[138]=0,io[32]=io[138]=0,oo[32]=oo[138]=1,so[32]=so[138]=0,ao[32]=ao[138]=1,Io[128]=Io[42]=0,No[128]=No[42]=1,So[128]=So[42]=1,Eo[128]=Eo[42]=0,bo[128]=bo[42]=1,wo[128]=wo[42]=0,no[128]=no[42]=-1,ro[128]=ro[42]=0,io[128]=io[42]=1,oo[128]=oo[42]=-1,so[128]=so[42]=0,ao[128]=ao[42]=0,ho[5]=ho[165]=-1,po[5]=po[165]=0,fo[5]=fo[165]=0,Io[5]=Io[165]=1,No[5]=No[165]=0,So[5]=So[165]=0,_o[20]=_o[150]=0,mo[20]=mo[150]=1,xo[20]=xo[150]=1,oo[20]=oo[150]=0,so[20]=so[150]=-1,ao[20]=ao[150]=1,uo[80]=uo[90]=-1,lo[80]=lo[90]=0,co[80]=co[90]=1,Eo[80]=Eo[90]=1,bo[80]=bo[90]=0,wo[80]=wo[90]=1,go[65]=go[105]=0,yo[65]=yo[105]=1,vo[65]=vo[105]=0,no[65]=no[105]=0,ro[65]=ro[105]=-1,io[65]=io[105]=0,uo[160]=uo[10]=-1,lo[160]=lo[10]=0,co[160]=co[10]=1,ho[160]=ho[10]=-1,po[160]=po[10]=0,fo[160]=fo[10]=0,Io[160]=Io[10]=1,No[160]=No[10]=0,So[160]=So[10]=0,Eo[160]=Eo[10]=1,bo[160]=bo[10]=0,wo[160]=wo[10]=1,_o[130]=_o[40]=0,mo[130]=mo[40]=1,xo[130]=xo[40]=1,go[130]=go[40]=0,yo[130]=yo[40]=1,vo[130]=vo[40]=0,no[130]=no[40]=0,ro[130]=ro[40]=-1,io[130]=io[40]=0,oo[130]=oo[40]=0,so[130]=so[40]=-1,ao[130]=ao[40]=1,ho[37]=ho[133]=0,po[37]=po[133]=1,fo[37]=fo[133]=1,Io[37]=Io[133]=0,No[37]=No[133]=1,So[37]=So[133]=0,no[37]=no[133]=-1,ro[37]=ro[133]=0,io[37]=io[133]=0,oo[37]=oo[133]=1,so[37]=so[133]=0,ao[37]=ao[133]=0,_o[148]=_o[22]=-1,mo[148]=mo[22]=0,xo[148]=xo[22]=0,Io[148]=Io[22]=0,No[148]=No[22]=-1,So[148]=So[22]=1,Eo[148]=Eo[22]=0,bo[148]=bo[22]=1,wo[148]=wo[22]=1,oo[148]=oo[22]=-1,so[148]=so[22]=0,ao[148]=ao[22]=1,uo[82]=uo[88]=0,lo[82]=lo[88]=-1,co[82]=co[88]=1,_o[82]=_o[88]=1,mo[82]=mo[88]=0,xo[82]=xo[88]=1,go[82]=go[88]=-1,yo[82]=yo[88]=0,vo[82]=vo[88]=1,Eo[82]=Eo[88]=0,bo[82]=bo[88]=-1,wo[82]=wo[88]=0,uo[73]=uo[97]=0,lo[73]=lo[97]=1,co[73]=co[97]=0,ho[73]=ho[97]=0,po[73]=po[97]=-1,fo[73]=fo[97]=0,go[73]=go[97]=1,yo[73]=yo[97]=0,vo[73]=vo[97]=0,no[73]=no[97]=1,ro[73]=ro[97]=0,io[73]=io[97]=1,uo[145]=uo[25]=0,lo[145]=lo[25]=-1,co[145]=co[25]=0,go[145]=go[25]=1,yo[145]=yo[25]=0,vo[145]=vo[25]=1,Io[145]=Io[25]=0,No[145]=No[25]=1,So[145]=So[25]=1,oo[145]=oo[25]=-1,so[145]=so[25]=0,ao[145]=ao[25]=0,ho[70]=ho[100]=0,po[70]=po[100]=1,fo[70]=fo[100]=0,_o[70]=_o[100]=-1,mo[70]=mo[100]=0,xo[70]=xo[100]=1,Eo[70]=Eo[100]=0,bo[70]=bo[100]=-1,wo[70]=wo[100]=1,no[70]=no[100]=1,ro[70]=ro[100]=0,io[70]=io[100]=0,ho[101]=ho[69]=0,po[101]=po[69]=1,fo[101]=fo[69]=0,no[101]=no[69]=1,ro[101]=ro[69]=0,io[101]=io[69]=0,Io[149]=Io[21]=0,No[149]=No[21]=1,So[149]=So[21]=1,oo[149]=oo[21]=-1,so[149]=so[21]=0,ao[149]=ao[21]=0,_o[86]=_o[84]=-1,mo[86]=mo[84]=0,xo[86]=xo[84]=1,Eo[86]=Eo[84]=0,bo[86]=bo[84]=-1,wo[86]=wo[84]=1,uo[89]=uo[81]=0,lo[89]=lo[81]=-1,co[89]=co[81]=0,go[89]=go[81]=1,yo[89]=yo[81]=0,vo[89]=vo[81]=1,uo[96]=uo[74]=0,lo[96]=lo[74]=1,co[96]=co[74]=0,ho[96]=ho[74]=-1,po[96]=po[74]=0,fo[96]=fo[74]=1,Eo[96]=Eo[74]=1,bo[96]=bo[74]=0,wo[96]=wo[74]=0,no[96]=no[74]=1,ro[96]=ro[74]=0,io[96]=io[74]=1,uo[24]=uo[146]=0,lo[24]=lo[146]=-1,co[24]=co[146]=1,_o[24]=_o[146]=1,mo[24]=mo[146]=0,xo[24]=xo[146]=1,go[24]=go[146]=0,yo[24]=yo[146]=1,vo[24]=vo[146]=1,oo[24]=oo[146]=0,so[24]=so[146]=-1,ao[24]=ao[146]=0,ho[6]=ho[164]=-1,po[6]=po[164]=0,fo[6]=fo[164]=1,_o[6]=_o[164]=-1,mo[6]=mo[164]=0,xo[6]=xo[164]=0,Io[6]=Io[164]=0,No[6]=No[164]=-1,So[6]=So[164]=1,Eo[6]=Eo[164]=1,bo[6]=bo[164]=0,wo[6]=wo[164]=0,go[129]=go[41]=0,yo[129]=yo[41]=1,vo[129]=vo[41]=1,Io[129]=Io[41]=0,No[129]=No[41]=1,So[129]=So[41]=0,no[129]=no[41]=-1,ro[129]=ro[41]=0,io[129]=io[41]=0,oo[129]=oo[41]=0,so[129]=so[41]=-1,ao[129]=ao[41]=0,_o[66]=_o[104]=0,mo[66]=mo[104]=1,xo[66]=xo[104]=0,go[66]=go[104]=-1,yo[66]=yo[104]=0,vo[66]=vo[104]=1,Eo[66]=Eo[104]=0,bo[66]=bo[104]=-1,wo[66]=wo[104]=0,no[66]=no[104]=0,ro[66]=ro[104]=-1,io[66]=io[104]=1,uo[144]=uo[26]=-1,lo[144]=lo[26]=0,co[144]=co[26]=0,Io[144]=Io[26]=1,No[144]=No[26]=0,So[144]=So[26]=1,Eo[144]=Eo[26]=0,bo[144]=bo[26]=1,wo[144]=wo[26]=1,oo[144]=oo[26]=-1,so[144]=so[26]=0,ao[144]=ao[26]=1,ho[36]=ho[134]=0,po[36]=po[134]=1,fo[36]=fo[134]=1,_o[36]=_o[134]=0,mo[36]=mo[134]=1,xo[36]=xo[134]=0,no[36]=no[134]=0,ro[36]=ro[134]=-1,io[36]=io[134]=1,oo[36]=oo[134]=1,so[36]=so[134]=0,ao[36]=ao[134]=0,uo[9]=uo[161]=-1,lo[9]=lo[161]=0,co[9]=co[161]=0,ho[9]=ho[161]=0,po[9]=po[161]=-1,fo[9]=fo[161]=0,go[9]=go[161]=1,yo[9]=yo[161]=0,vo[9]=vo[161]=0,Io[9]=Io[161]=1,No[9]=No[161]=0,So[9]=So[161]=1,uo[136]=0,lo[136]=1,co[136]=1,ho[136]=0,po[136]=1,fo[136]=0,_o[136]=-1,mo[136]=0,xo[136]=1,go[136]=-1,yo[136]=0,vo[136]=0,Io[136]=0,No[136]=-1,So[136]=0,Eo[136]=0,bo[136]=-1,wo[136]=1,no[136]=1,ro[136]=0,io[136]=0,oo[136]=1,so[136]=0,ao[136]=1,uo[34]=0,lo[34]=-1,co[34]=0,ho[34]=0,po[34]=-1,fo[34]=1,_o[34]=1,mo[34]=0,xo[34]=0,go[34]=1,yo[34]=0,vo[34]=1,Io[34]=0,No[34]=1,So[34]=1,Eo[34]=0,bo[34]=1,wo[34]=0,no[34]=-1,ro[34]=0,io[34]=1,oo[34]=-1,so[34]=0,ao[34]=0,uo[35]=0,lo[35]=1,co[35]=1,ho[35]=0,po[35]=-1,fo[35]=1,_o[35]=1,mo[35]=0,xo[35]=0,go[35]=-1,yo[35]=0,vo[35]=0,Io[35]=0,No[35]=-1,So[35]=0,Eo[35]=0,bo[35]=1,wo[35]=0,no[35]=-1,ro[35]=0,io[35]=1,oo[35]=1,so[35]=0,ao[35]=1,uo[153]=0,lo[153]=1,co[153]=1,go[153]=-1,yo[153]=0,vo[153]=0,Io[153]=0,No[153]=-1,So[153]=0,oo[153]=1,so[153]=0,ao[153]=1,ho[102]=0,po[102]=-1,fo[102]=1,_o[102]=1,mo[102]=0,xo[102]=0,Eo[102]=0,bo[102]=1,wo[102]=0,no[102]=-1,ro[102]=0,io[102]=1,uo[155]=0,lo[155]=-1,co[155]=0,go[155]=1,yo[155]=0,vo[155]=1,Io[155]=0,No[155]=1,So[155]=1,oo[155]=-1,so[155]=0,ao[155]=0,ho[103]=0,po[103]=1,fo[103]=0,_o[103]=-1,mo[103]=0,xo[103]=1,Eo[103]=0,bo[103]=-1,wo[103]=1,no[103]=1,ro[103]=0,io[103]=0,uo[152]=0,lo[152]=1,co[152]=1,_o[152]=-1,mo[152]=0,xo[152]=1,go[152]=-1,yo[152]=0,vo[152]=0,Io[152]=0,No[152]=-1,So[152]=0,Eo[152]=0,bo[152]=-1,wo[152]=1,oo[152]=1,so[152]=0,ao[152]=1,uo[156]=0,lo[156]=-1,co[156]=1,_o[156]=1,mo[156]=0,xo[156]=1,go[156]=-1,yo[156]=0,vo[156]=0,Io[156]=0,No[156]=-1,So[156]=0,Eo[156]=0,bo[156]=1,wo[156]=1,oo[156]=-1,so[156]=0,ao[156]=1,uo[137]=0,lo[137]=1,co[137]=1,ho[137]=0,po[137]=1,fo[137]=0,go[137]=-1,yo[137]=0,vo[137]=0,Io[137]=0,No[137]=-1,So[137]=0,no[137]=1,ro[137]=0,io[137]=0,oo[137]=1,so[137]=0,ao[137]=1,uo[139]=0,lo[139]=1,co[139]=1,ho[139]=0,po[139]=-1,fo[139]=0,go[139]=1,yo[139]=0,vo[139]=0,Io[139]=0,No[139]=1,So[139]=0,no[139]=-1,ro[139]=0,io[139]=0,oo[139]=1,so[139]=0,ao[139]=1,uo[98]=0,lo[98]=-1,co[98]=0,ho[98]=0,po[98]=-1,fo[98]=1,_o[98]=1,mo[98]=0,xo[98]=0,go[98]=1,yo[98]=0,vo[98]=1,Eo[98]=0,bo[98]=1,wo[98]=0,no[98]=-1,ro[98]=0,io[98]=1,uo[99]=0,lo[99]=1,co[99]=0,ho[99]=0,po[99]=-1,fo[99]=1,_o[99]=1,mo[99]=0,xo[99]=0,go[99]=-1,yo[99]=0,vo[99]=1,Eo[99]=0,bo[99]=-1,wo[99]=0,no[99]=1,ro[99]=0,io[99]=1,ho[38]=0,po[38]=-1,fo[38]=1,_o[38]=1,mo[38]=0,xo[38]=0,Io[38]=0,No[38]=1,So[38]=1,Eo[38]=0,bo[38]=1,wo[38]=0,no[38]=-1,ro[38]=0,io[38]=1,oo[38]=-1,so[38]=0,ao[38]=0,ho[39]=0,po[39]=1,fo[39]=1,_o[39]=-1,mo[39]=0,xo[39]=0,Io[39]=0,No[39]=-1,So[39]=1,Eo[39]=0,bo[39]=1,wo[39]=0,no[39]=-1,ro[39]=0,io[39]=1,oo[39]=1,so[39]=0,ao[39]=0;var Co=function(t){return[[t.bottomleft,0],[0,0],[0,t.leftbottom]]},Po=function(t){return[[1,t.rightbottom],[1,0],[t.bottomright,0]]},Mo=function(t){return[[t.topright,1],[1,1],[1,t.righttop]]},Lo=function(t){return[[0,t.lefttop],[0,1],[t.topleft,1]]},Oo=function(t){return[[t.bottomright,0],[t.bottomleft,0],[0,t.leftbottom],[0,t.lefttop]]},Ro=function(t){return[[t.bottomright,0],[t.bottomleft,0],[1,t.righttop],[1,t.rightbottom]]},To=function(t){return[[1,t.righttop],[1,t.rightbottom],[t.topleft,1],[t.topright,1]]},Ao=function(t){return[[0,t.leftbottom],[0,t.lefttop],[t.topleft,1],[t.topright,1]]},Do=[],Fo=[],ko=[],Go=[],qo=[],Bo=[],zo=[],jo=[];Go[1]=qo[1]=18,Go[169]=qo[169]=18,ko[4]=Fo[4]=12,ko[166]=Fo[166]=12,Do[16]=jo[16]=4,Do[154]=jo[154]=4,Bo[64]=zo[64]=22,Bo[106]=zo[106]=22,ko[2]=Bo[2]=17,Go[2]=qo[2]=18,ko[168]=Bo[168]=17,Go[168]=qo[168]=18,Do[8]=Go[8]=9,Fo[8]=ko[8]=12,Do[162]=Go[162]=9,Fo[162]=ko[162]=12,Do[32]=jo[32]=4,Fo[32]=zo[32]=1,Do[138]=jo[138]=4,Fo[138]=zo[138]=1,qo[128]=jo[128]=21,Bo[128]=zo[128]=22,qo[42]=jo[42]=21,Bo[42]=zo[42]=22,Fo[5]=qo[5]=14,Fo[165]=qo[165]=14,ko[20]=jo[20]=6,ko[150]=jo[150]=6,Do[80]=Bo[80]=11,Do[90]=Bo[90]=11,Go[65]=zo[65]=3,Go[105]=zo[105]=3,Do[160]=Bo[160]=11,Fo[160]=qo[160]=14,Do[10]=Bo[10]=11,Fo[10]=qo[10]=14,ko[130]=jo[130]=6,Go[130]=zo[130]=3,ko[40]=jo[40]=6,Go[40]=zo[40]=3,Fo[101]=zo[101]=1,Fo[69]=zo[69]=1,qo[149]=jo[149]=21,qo[21]=jo[21]=21,ko[86]=Bo[86]=17,ko[84]=Bo[84]=17,Do[89]=Go[89]=9,Do[81]=Go[81]=9,Do[96]=zo[96]=0,Fo[96]=Bo[96]=15,Do[74]=zo[74]=0,Fo[74]=Bo[74]=15,Do[24]=ko[24]=8,Go[24]=jo[24]=7,Do[146]=ko[146]=8,Go[146]=jo[146]=7,Fo[6]=Bo[6]=15,ko[6]=qo[6]=16,Fo[164]=Bo[164]=15,ko[164]=qo[164]=16,Go[129]=jo[129]=7,qo[129]=zo[129]=20,Go[41]=jo[41]=7,qo[41]=zo[41]=20,ko[66]=zo[66]=2,Go[66]=Bo[66]=19,ko[104]=zo[104]=2,Go[104]=Bo[104]=19,Do[144]=qo[144]=10,Bo[144]=jo[144]=23,Do[26]=qo[26]=10,Bo[26]=jo[26]=23,Fo[36]=jo[36]=5,ko[36]=zo[36]=2,Fo[134]=jo[134]=5,ko[134]=zo[134]=2,Do[9]=qo[9]=10,Fo[9]=Go[9]=13,Do[161]=qo[161]=10,Fo[161]=Go[161]=13,Fo[37]=jo[37]=5,qo[37]=zo[37]=20,Fo[133]=jo[133]=5,qo[133]=zo[133]=20,ko[148]=qo[148]=16,Bo[148]=jo[148]=23,ko[22]=qo[22]=16,Bo[22]=jo[22]=23,Do[82]=ko[82]=8,Go[82]=Bo[82]=19,Do[88]=ko[88]=8,Go[88]=Bo[88]=19,Do[73]=zo[73]=0,Fo[73]=Go[73]=13,Do[97]=zo[97]=0,Fo[97]=Go[97]=13,Do[145]=Go[145]=9,qo[145]=jo[145]=21,Do[25]=Go[25]=9,qo[25]=jo[25]=21,Fo[70]=zo[70]=1,ko[70]=Bo[70]=17,Fo[100]=zo[100]=1,ko[100]=Bo[100]=17,Do[34]=Go[34]=9,Fo[34]=ko[34]=12,qo[34]=jo[34]=21,Bo[34]=zo[34]=22,Do[136]=jo[136]=4,Fo[136]=zo[136]=1,ko[136]=Bo[136]=17,Go[136]=qo[136]=18,Do[35]=jo[35]=4,Fo[35]=ko[35]=12,Go[35]=qo[35]=18,Bo[35]=zo[35]=22,Do[153]=jo[153]=4,Go[153]=qo[153]=18,Fo[102]=ko[102]=12,Bo[102]=zo[102]=22,Do[155]=Go[155]=9,qo[155]=jo[155]=23,Fo[103]=zo[103]=1,ko[103]=Bo[103]=17,Do[152]=jo[152]=4,ko[152]=Bo[152]=17,Go[152]=qo[152]=18,Do[156]=ko[156]=8,Go[156]=qo[156]=18,Bo[156]=jo[156]=23,Do[137]=jo[137]=4,Fo[137]=zo[137]=1,Go[137]=qo[137]=18,Do[139]=jo[139]=4,Fo[139]=Go[139]=13,qo[139]=zo[139]=20,Do[98]=Go[98]=9,Fo[98]=ko[98]=12,Bo[98]=zo[98]=22,Do[99]=zo[99]=0,Fo[99]=ko[99]=12,Go[99]=Bo[99]=19,Fo[38]=ko[38]=12,qo[38]=jo[38]=21,Bo[38]=zo[38]=22,Fo[39]=jo[39]=5,ko[39]=qo[39]=16,Bo[39]=zo[39]=22;var Uo=[];function Vo(t,e,n){return(t-e)/(n-e)}function Xo(t){return t.constructor.toString().indexOf("Array")>-1}function Yo(t,e,n,r,i,o){for(var s=t.cells[n][e],a=s.cval_real,u=e+r,l=n+i,c=[],h=!1;!h;){if(void 0===t.cells[l]||void 0===t.cells[l][u])if(l-=i,u-=r,a=(s=t.cells[l][u]).cval_real,-1===i)if(0===o)if(1&a)c.push([u,l]),r=-1,i=0,o=0;else{if(!(4&a)){c.push([u+s.bottomright,l]),r=0,i=1,o=1,h=!0;break}c.push([u+1,l]),r=1,i=0,o=0}else{if(!(1&a)){if(4&a){c.push([u+s.bottomright,l]),r=0,i=1,o=1,h=!0;break}c.push([u+s.bottomleft,l]),r=0,i=1,o=0,h=!0;break}c.push([u,l]),r=-1,i=0,o=0}else if(1===i)if(0===o){if(!(a&eo)){if(a&to){c.push([u+s.topleft,l+1]),r=0,i=-1,o=0,h=!0;break}c.push([u+s.topright,l+1]),r=0,i=-1,o=1,h=!0;break}c.push([u+1,l+1]),r=1,i=0,o=1}else c.push([u+1,l+1]),r=1,i=0,o=1;else if(-1===r)if(0===o){if(!(a&to)){if(1&a){c.push([u,l+s.leftbottom]),r=1,i=0,o=0,h=!0;break}c.push([u,l+s.lefttop]),r=1,i=0,o=1,h=!0;break}c.push([u,l+1]),r=0,i=1,o=0}else{if(!(a&to)){console.log("MarchingSquaresJS-isoBands: wtf");break}c.push([u,l+1]),r=0,i=1,o=0}else{if(1!==r){console.log("MarchingSquaresJS-isoBands: we came from nowhere!");break}if(0===o){if(!(4&a)){c.push([u+1,l+s.rightbottom]),r=-1,i=0,o=0,h=!0;break}c.push([u+1,l]),r=0,i=-1,o=1}else{if(!(4&a)){if(a&eo){c.push([u+1,l+s.righttop]),r=-1,i=0,o=1;break}c.push([u+1,l+s.rightbottom]),r=-1,i=0,o=0,h=!0;break}c.push([u+1,l]),r=0,i=-1,o=1}}else if(a=(s=t.cells[l][u]).cval_real,-1===r)if(0===o)if(void 0!==t.cells[l-1]&&void 0!==t.cells[l-1][u])r=0,i=-1,o=1;else{if(!(1&a)){c.push([u+s.bottomright,l]),r=0,i=1,o=1,h=!0;break}c.push([u,l])}else{if(!(a&to)){console.log("MarchingSquaresJS-isoBands: found entry from top at "+u+","+l);break}console.log("MarchingSquaresJS-isoBands: proceeding in x-direction!")}else if(1===r){if(0===o){console.log("MarchingSquaresJS-isoBands: wtf");break}if(void 0!==t.cells[l+1]&&void 0!==t.cells[l+1][u])r=0,i=1,o=0;else{if(!(a&eo)){c.push([u+s.topleft,l+1]),r=0,i=-1,o=0,h=!0;break}c.push([u+1,l+1]),r=1,i=0,o=1}}else if(-1===i){if(1!==o){console.log("MarchingSquaresJS-isoBands: wtf");break}if(void 0!==t.cells[l][u+1])r=1,i=0,o=1;else{if(!(4&a)){c.push([u+1,l+s.righttop]),r=-1,i=0,o=1,h=!0;break}c.push([u+1,l]),r=0,i=-1,o=1}}else{if(1!==i){console.log("MarchingSquaresJS-isoBands: where did we came from???");break}if(0!==o){console.log("MarchingSquaresJS-isoBands: wtf");break}if(void 0!==t.cells[l][u-1])r=-1,i=0,o=0;else{if(!(a&to)){c.push([u,l+s.leftbottom]),r=1,i=0,o=0,h=!0;break}c.push([u,l+1]),r=0,i=1,o=0}}if(l+=i,(u+=r)===e&&l===n)break}return{path:c,i:u,j:l,x:r,y:i,o:o}}function Ho(t){if(t.edges.length>0){var e=t.edges[t.edges.length-1],n=t.cval_real;switch(e){case 0:return n&eo?{p:[1,t.righttop],x:-1,y:0,o:1}:{p:[t.topleft,1],x:0,y:-1,o:0};case 1:return 4&n?{p:[t.topleft,1],x:0,y:-1,o:0}:{p:[1,t.rightbottom],x:-1,y:0,o:0};case 2:return 4&n?{p:[t.bottomright,0],x:0,y:1,o:1}:{p:[t.topleft,1],x:0,y:-1,o:0};case 3:return 1&n?{p:[t.topleft,1],x:0,y:-1,o:0}:{p:[t.bottomleft,0],x:0,y:1,o:0};case 4:return n&eo?{p:[1,t.righttop],x:-1,y:0,o:1}:{p:[t.topright,1],x:0,y:-1,o:1};case 5:return 4&n?{p:[t.topright,1],x:0,y:-1,o:1}:{p:[1,t.rightbottom],x:-1,y:0,o:0};case 6:return 4&n?{p:[t.bottomright,0],x:0,y:1,o:1}:{p:[t.topright,1],x:0,y:-1,o:1};case 7:return 1&n?{p:[t.topright,1],x:0,y:-1,o:1}:{p:[t.bottomleft,0],x:0,y:1,o:0};case 8:return 4&n?{p:[t.bottomright,0],x:0,y:1,o:1}:{p:[1,t.righttop],x:-1,y:0,o:1};case 9:return 1&n?{p:[1,t.righttop],x:-1,y:0,o:1}:{p:[t.bottomleft,0],x:0,y:1,o:0};case 10:return 1&n?{p:[0,t.leftbottom],x:1,y:0,o:0}:{p:[1,t.righttop],x:-1,y:0,o:1};case 11:return n&to?{p:[1,t.righttop],x:-1,y:0,o:1}:{p:[0,t.lefttop],x:1,y:0,o:1};case 12:return 4&n?{p:[t.bottomright,0],x:0,y:1,o:1}:{p:[1,t.rightbottom],x:-1,y:0,o:0};case 13:return 1&n?{p:[1,t.rightbottom],x:-1,y:0,o:0}:{p:[t.bottomleft,0],x:0,y:1,o:0};case 14:return 1&n?{p:[0,t.leftbottom],x:1,y:0,o:0}:{p:[1,t.rightbottom],x:-1,y:0,o:0};case 15:return n&to?{p:[1,t.rightbottom],x:-1,y:0,o:0}:{p:[0,t.lefttop],x:1,y:0,o:1};case 16:return 4&n?{p:[t.bottomright,0],x:0,y:1,o:1}:{p:[0,t.leftbottom],x:1,y:0,o:0};case 17:return n&to?{p:[t.bottomright,0],x:0,y:1,o:1}:{p:[0,t.lefttop],x:1,y:0,o:1};case 18:return 1&n?{p:[0,t.leftbottom],x:1,y:0,o:0}:{p:[t.bottomleft,0],x:0,y:1,o:0};case 19:return n&to?{p:[t.bottomleft,0],x:0,y:1,o:0}:{p:[0,t.lefttop],x:1,y:0,o:1};case 20:return n&to?{p:[t.topleft,1],x:0,y:-1,o:0}:{p:[0,t.leftbottom],x:1,y:0,o:0};case 21:return n&eo?{p:[0,t.leftbottom],x:1,y:0,o:0}:{p:[t.topright,1],x:0,y:-1,o:1};case 22:return n&to?{p:[t.topleft,1],x:0,y:-1,o:0}:{p:[0,t.lefttop],x:1,y:0,o:1};case 23:return n&eo?{p:[0,t.lefttop],x:1,y:0,o:1}:{p:[t.topright,1],x:0,y:-1,o:1};default:console.log("MarchingSquaresJS-isoBands: edge index out of range!"),console.log(t)}}return null}function Wo(t,e,n,r){var i,o,s,a,u,l=t.cval;switch(e){case-1:switch(r){case 0:i=Fo[l],s=ho[l],a=po[l],u=fo[l];break;default:i=Do[l],s=uo[l],a=lo[l],u=co[l]}break;case 1:switch(r){case 0:i=qo[l],s=Io[l],a=No[l],u=So[l];break;default:i=Bo[l],s=Eo[l],a=bo[l],u=wo[l]}break;default:switch(n){case-1:switch(r){case 0:i=zo[l],s=no[l],a=ro[l],u=io[l];break;default:i=jo[l],s=oo[l],a=so[l],u=ao[l]}break;case 1:switch(r){case 0:i=Go[l],s=go[l],a=yo[l],u=vo[l];break;default:i=ko[l],s=_o[l],a=mo[l],u=xo[l]}}}if(o=t.edges.indexOf(i),void 0===t.edges[o])return null;switch(function(t,e){delete t.edges[e];for(var n=e+1;n0){var a=r[e-1],u=is(n,a);!1!==u&&(a[1]=u,n[0]=u),s.push(a[0]),e===o.length-2&&(s.push(n[0]),s.push(n[1]))}2===o.length&&(s.push(n[0]),s.push(n[1]))}var l,c,h,p,f,g,d,y})),h(s,t.properties)}function ss(t,e,n){var r=e[0]-t[0],i=e[1]-t[1],o=n[0]-e[0];return function(t){return(t>0)-(t<0)||+t}(r*(n[1]-e[1])-o*i)}function as(t,e){return e.geometry.coordinates[0].every((function(e){return ye(a(e),t)}))}Uo[1]=Uo[169]=Co,Uo[4]=Uo[166]=Po,Uo[16]=Uo[154]=Mo,Uo[64]=Uo[106]=Lo,Uo[168]=Uo[2]=Oo,Uo[162]=Uo[8]=Ro,Uo[138]=Uo[32]=To,Uo[42]=Uo[128]=Ao,Uo[5]=Uo[165]=function(t){return[[0,0],[0,t.leftbottom],[1,t.rightbottom],[1,0]]},Uo[20]=Uo[150]=function(t){return[[1,0],[t.bottomright,0],[t.topright,1],[1,1]]},Uo[80]=Uo[90]=function(t){return[[1,1],[1,t.righttop],[0,t.lefttop],[0,1]]},Uo[65]=Uo[105]=function(t){return[[t.bottomleft,0],[0,0],[0,1],[t.topleft,1]]},Uo[160]=Uo[10]=function(t){return[[1,t.righttop],[1,t.rightbottom],[0,t.leftbottom],[0,t.lefttop]]},Uo[130]=Uo[40]=function(t){return[[t.topleft,1],[t.topright,1],[t.bottomright,0],[t.bottomleft,0]]},Uo[85]=function(){return[[0,0],[0,1],[1,1],[1,0]]},Uo[101]=Uo[69]=function(t){return[[1,t.rightbottom],[1,0],[0,0],[0,1],[t.topleft,1]]},Uo[149]=Uo[21]=function(t){return[[t.topright,1],[1,1],[1,0],[0,0],[0,t.leftbottom]]},Uo[86]=Uo[84]=function(t){return[[1,0],[t.bottomright,0],[0,t.lefttop],[0,1],[1,1]]},Uo[89]=Uo[81]=function(t){return[[1,1],[1,t.righttop],[t.bottomleft,0],[0,0],[0,1]]},Uo[96]=Uo[74]=function(t){return[[1,t.righttop],[1,t.rightbottom],[0,t.lefttop],[0,1],[t.topleft,1]]},Uo[24]=Uo[146]=function(t){return[[1,1],[1,t.righttop],[t.bottomright,0],[t.bottomleft,0],[t.topright,1]]},Uo[6]=Uo[164]=function(t){return[[1,t.rightbottom],[1,0],[t.bottomright,0],[0,t.leftbottom],[0,t.lefttop]]},Uo[129]=Uo[41]=function(t){return[[t.topright,1],[t.bottomleft,0],[0,0],[0,t.leftbottom],[t.topleft,1]]},Uo[66]=Uo[104]=function(t){return[[t.bottomright,0],[t.bottomleft,0],[0,t.lefttop],[0,1],[t.topleft,1]]},Uo[144]=Uo[26]=function(t){return[[1,1],[1,t.righttop],[0,t.leftbottom],[0,t.lefttop],[t.topright,1]]},Uo[36]=Uo[134]=function(t){return[[1,t.rightbottom],[1,0],[t.bottomright,0],[t.topleft,1],[t.topright,1]]},Uo[9]=Uo[161]=function(t){return[[1,t.righttop],[1,t.rightbottom],[t.bottomleft,0],[0,0],[0,t.leftbottom]]},Uo[37]=Uo[133]=function(t){return[[1,t.rightbottom],[1,0],[0,0],[0,t.leftbottom],[t.topleft,1],[t.topright,1]]},Uo[148]=Uo[22]=function(t){return[[1,1],[1,0],[t.bottomright,0],[0,t.leftbottom],[0,t.lefttop],[t.topright,1]]},Uo[82]=Uo[88]=function(t){return[[1,1],[1,t.righttop],[t.bottomright,0],[t.bottomleft,0],[0,t.lefttop],[0,1]]},Uo[73]=Uo[97]=function(t){return[[1,t.righttop],[1,t.rightbottom],[t.bottomleft,0],[0,0],[0,1],[t.topleft,1]]},Uo[145]=Uo[25]=function(t){return[[1,1],[1,t.righttop],[t.bottomleft,0],[0,0],[0,t.leftbottom],[t.topright,1]]},Uo[70]=Uo[100]=function(t){return[[1,t.rightbottom],[1,0],[t.bottomright,0],[0,t.lefttop],[0,1],[t.topleft,1]]},Uo[34]=function(t){return[Ao(t),Ro(t)]},Uo[35]=function(t){return[[1,t.righttop],[1,t.rightbottom],[t.bottomright,0],[t.bottomleft,0],[0,t.leftbottom],[0,t.lefttop],[t.topleft,1],[t.topright,1]]},Uo[136]=function(t){return[To(t),Oo(t)]},Uo[153]=function(t){return[Mo(t),Co(t)]},Uo[102]=function(t){return[Po(t),Lo(t)]},Uo[155]=function(t){return[[1,1],[1,t.righttop],[t.bottomleft,0],[0,0],[0,t.leftbottom],[t.topright,1]]},Uo[103]=function(t){return[[1,t.rightbottom],[1,0],[t.bottomright,0],[0,t.lefttop],[0,1],[t.topleft,1]]},Uo[152]=function(t){return[Mo(t),Oo(t)]},Uo[156]=function(t){return[[1,1],[1,t.righttop],[t.bottomright,0],[t.bottomleft,0],[0,t.leftbottom],[0,t.lefttop],[t.topright,1]]},Uo[137]=function(t){return[To(t),Co(t)]},Uo[139]=function(t){return[[1,t.righttop],[1,t.rightbottom],[t.bottomleft,0],[0,0],[0,t.leftbottom],[t.topleft,1],[t.topright,1]]},Uo[98]=function(t){return[Ro(t),Lo(t)]},Uo[99]=function(t){return[[1,t.righttop],[1,t.rightbottom],[t.bottomright,0],[t.bottomleft,0],[0,t.lefttop],[0,1],[t.topleft,1]]},Uo[38]=function(t){return[Po(t),Ao(t)]},Uo[39]=function(t){return[[1,t.rightbottom],[1,0],[t.bottomright,0],[0,t.leftbottom],[0,t.lefttop],[t.topleft,1],[t.topright,1]]};var us=function(){function t(e){this.id=t.buildId(e),this.coordinates=e,this.innerEdges=[],this.outerEdges=[],this.outerEdgesSorted=!1}return t.buildId=function(t){return t.join(",")},t.prototype.removeInnerEdge=function(t){this.innerEdges=this.innerEdges.filter((function(e){return e.from.id!==t.from.id}))},t.prototype.removeOuterEdge=function(t){this.outerEdges=this.outerEdges.filter((function(e){return e.to.id!==t.to.id}))},t.prototype.addOuterEdge=function(t){this.outerEdges.push(t),this.outerEdgesSorted=!1},t.prototype.sortOuterEdges=function(){var t=this;this.outerEdgesSorted||(this.outerEdges.sort((function(e,n){var r=e.to,i=n.to;if(r.coordinates[0]-t.coordinates[0]>=0&&i.coordinates[0]-t.coordinates[0]<0)return 1;if(r.coordinates[0]-t.coordinates[0]<0&&i.coordinates[0]-t.coordinates[0]>=0)return-1;if(r.coordinates[0]-t.coordinates[0]==0&&i.coordinates[0]-t.coordinates[0]==0)return r.coordinates[1]-t.coordinates[1]>=0||i.coordinates[1]-t.coordinates[1]>=0?r.coordinates[1]-i.coordinates[1]:i.coordinates[1]-r.coordinates[1];var o=ss(t.coordinates,r.coordinates,i.coordinates);return o<0?1:o>0?-1:Math.pow(r.coordinates[0]-t.coordinates[0],2)+Math.pow(r.coordinates[1]-t.coordinates[1],2)-(Math.pow(i.coordinates[0]-t.coordinates[0],2)+Math.pow(i.coordinates[1]-t.coordinates[1],2))})),this.outerEdgesSorted=!0)},t.prototype.getOuterEdges=function(){return this.sortOuterEdges(),this.outerEdges},t.prototype.getOuterEdge=function(t){return this.sortOuterEdges(),this.outerEdges[t]},t.prototype.addInnerEdge=function(t){this.innerEdges.push(t)},t}(),ls=function(){function t(t,e){this.from=t,this.to=e,this.next=void 0,this.label=void 0,this.symetric=void 0,this.ring=void 0,this.from.addOuterEdge(this),this.to.addInnerEdge(this)}return t.prototype.getSymetric=function(){return this.symetric||(this.symetric=new t(this.to,this.from),this.symetric.symetric=this),this.symetric},t.prototype.deleteEdge=function(){this.from.removeOuterEdge(this),this.to.removeInnerEdge(this)},t.prototype.isEqual=function(t){return this.from.id===t.from.id&&this.to.id===t.to.id},t.prototype.toString=function(){return"Edge { "+this.from.id+" -> "+this.to.id+" }"},t.prototype.toLineString=function(){return h([this.from.coordinates,this.to.coordinates])},t.prototype.compareTo=function(t){return ss(t.from.coordinates,t.to.coordinates,this.to.coordinates)},t}(),cs=function(){function t(){this.edges=[],this.polygon=void 0,this.envelope=void 0}return t.prototype.push=function(t){this.edges.push(t),this.polygon=this.envelope=void 0},t.prototype.get=function(t){return this.edges[t]},Object.defineProperty(t.prototype,"length",{get:function(){return this.edges.length},enumerable:!0,configurable:!0}),t.prototype.forEach=function(t){this.edges.forEach(t)},t.prototype.map=function(t){return this.edges.map(t)},t.prototype.some=function(t){return this.edges.some(t)},t.prototype.isValid=function(){return!0},t.prototype.isHole=function(){var t=this,e=this.edges.reduce((function(e,n,r){return n.from.coordinates[1]>t.edges[e].from.coordinates[1]&&(e=r),e}),0),n=(0===e?this.length:e)-1,r=(e+1)%this.length,i=ss(this.edges[n].from.coordinates,this.edges[e].from.coordinates,this.edges[r].from.coordinates);return 0===i?this.edges[n].from.coordinates[0]>this.edges[r].from.coordinates[0]:i>0},t.prototype.toMultiPoint=function(){return d(this.edges.map((function(t){return t.from.coordinates})))},t.prototype.toPolygon=function(){if(this.polygon)return this.polygon;var t=this.edges.map((function(t){return t.from.coordinates}));return t.push(this.edges[0].from.coordinates),this.polygon=l([t])},t.prototype.getEnvelope=function(){return this.envelope?this.envelope:this.envelope=dn(this.toPolygon())},t.findEdgeRingContaining=function(t,e){var n,r,i=t.getEnvelope();return e.forEach((function(e){var o,s,u,l,c,h,p=e.getEnvelope();if((r&&(n=r.getEnvelope()),s=i,u=(o=p).geometry.coordinates[0].map((function(t){return t[0]})),l=o.geometry.coordinates[0].map((function(t){return t[1]})),c=s.geometry.coordinates[0].map((function(t){return t[0]})),h=s.geometry.coordinates[0].map((function(t){return t[1]})),Math.max.apply(null,u)!==Math.max.apply(null,c)||Math.max.apply(null,l)!==Math.max.apply(null,h)||Math.min.apply(null,u)!==Math.min.apply(null,c)||Math.min.apply(null,l)!==Math.min.apply(null,h))&&as(p,i)){for(var f=t.map((function(t){return t.from.coordinates})),g=void 0,d=function(t){e.some((function(e){return n=t,r=e.from.coordinates,n[0]===r[0]&&n[1]===r[1];var n,r}))||(g=t)},y=0,v=f;y=0;--o){var s=i[o],a=s.symetric,u=void 0,l=void 0;s.label===e&&(u=s),a.label===e&&(l=a),u&&l&&(l&&(r=l),u&&(r&&(r.next=u,r=void 0),n||(n=u)))}r&&(r.next=n)},t.prototype._findLabeledEdgeRings=function(){var t=[],e=0;return this.edges.forEach((function(n){if(!(n.label>=0)){t.push(n);var r=n;do{r.label=e,r=r.next}while(!n.isEqual(r));e++}})),t},t.prototype.getEdgeRings=function(){var t=this;this._computeNextCWEdges(),this.edges.forEach((function(t){t.label=void 0})),this._findLabeledEdgeRings().forEach((function(e){t._findIntersectionNodes(e).forEach((function(n){t._computeNextCCWEdges(n,e.label)}))}));var e=[];return this.edges.forEach((function(n){n.ring||e.push(t._findEdgeRing(n))})),e},t.prototype._findIntersectionNodes=function(t){var e=[],n=t,r=function(){var r=0;n.from.getOuterEdges().forEach((function(e){e.label===t.label&&++r})),r>1&&e.push(n.from),n=n.next};do{r()}while(!t.isEqual(n));return e},t.prototype._findEdgeRing=function(t){var e=t,n=new cs;do{n.push(e),e.ring=n,e=e.next}while(!t.isEqual(e));return n},t.prototype.removeNode=function(t){var e=this;t.getOuterEdges().forEach((function(t){return e.removeEdge(t)})),t.innerEdges.forEach((function(t){return e.removeEdge(t)})),delete this.nodes[t.id]},t.prototype.removeEdge=function(t){this.edges=this.edges.filter((function(e){return!e.isEqual(t)})),t.deleteEdge()},t}();function ps(t,e){var n=!0;return z(t,(function(t){z(e,(function(e){if(!1===n)return!1;n=function(t,e){switch(t.type){case"Point":switch(e.type){case"Point":return n=t.coordinates,r=e.coordinates,!(n[0]===r[0]&&n[1]===r[1]);case"LineString":return!fs(e,t);case"Polygon":return!ye(t,e)}break;case"LineString":switch(e.type){case"Point":return!fs(t,e);case"LineString":return!function(t,e){if(Or(t,e).features.length>0)return!0;return!1}(t,e);case"Polygon":return!gs(e,t)}break;case"Polygon":switch(e.type){case"Point":return!ye(e,t);case"LineString":return!gs(t,e);case"Polygon":return!function(t,e){for(var n=0,r=t.coordinates[0];n0)return!0;return!1}(e,t)}}var n,r;return!1}(t.geometry,e.geometry)}))})),n}function fs(t,e){for(var n=0;n0}function ds(t,e,n){var r=n[0]-t[0],i=n[1]-t[1],o=e[0]-t[0],s=e[1]-t[1];return 0==r*s-i*o&&(Math.abs(o)>=Math.abs(s)?o>0?t[0]<=n[0]&&n[0]<=e[0]:e[0]<=n[0]&&n[0]<=t[0]:s>0?t[1]<=n[1]&&n[1]<=e[1]:e[1]<=n[1]&&n[1]<=t[1])}function ys(t,e){return!(t[0]>e[0])&&(!(t[2]e[1])&&!(t[3]0}function Es(t,e){for(var n=!1,r=!1,i=t.coordinates.length,o=0;o=Math.abs(a)?s>0?t[0]<=n[0]&&n[0]<=e[0]:e[0]<=n[0]&&n[0]<=t[0]:a>0?t[1]<=n[1]&&n[1]<=e[1]:e[1]<=n[1]&&n[1]<=t[1]:Math.abs(s)>=Math.abs(a)?s>0?t[0]0?t[1]=0&&(n=[].concat(t.slice(r,t.length),t.slice(1,r+1))),n},ws.prototype.comparePath=function(t,e){var n=this;return t.every((function(t,e){return n.compareCoord(t,this[e])}),e)},ws.prototype.comparePolygon=function(t,e){if(this.compareLine(t.coordinates[0],e.coordinates[0],1,!0)){var n=t.coordinates.slice(1,t.coordinates.length),r=e.coordinates.slice(1,e.coordinates.length),i=this;return n.every((function(t){return this.some((function(e){return i.compareLine(t,e,1,!0)}))}),r)}return!1},ws.prototype.compareFeature=function(t,e){return!(t.id!==e.id||!this.objectComparator(t.properties,e.properties)||!this.compareBBox(t,e))&&this.compare(t.geometry,e.geometry)},ws.prototype.compareBBox=function(t,e){return!!(!t.bbox&&!e.bbox||t.bbox&&e.bbox&&this.compareCoord(t.bbox,e.bbox))},ws.prototype.removePseudo=function(t){return t};var Cs=ws;function Ps(t,e){var n=!1;return z(t,(function(t){z(e,(function(e){if(!0===n)return!0;n=!ps(t.geometry,e.geometry)}))})),n}var Ms=Bt((function(t){function e(t,e,n,r){this.dataset=[],this.epsilon=1,this.minPts=2,this.distance=this._euclideanDistance,this.clusters=[],this.noise=[],this._visited=[],this._assigned=[],this._datasetLength=0,this._init(t,e,n,r)}e.prototype.run=function(t,e,n,r){this._init(t,e,n,r);for(var i=0;i=this.minPts&&(e=this._mergeArrays(e,i))}1!==this._assigned[r]&&this._addToCluster(r,t)}},e.prototype._addToCluster=function(t,e){this.clusters[e].push(t),this._assigned[t]=1},e.prototype._regionQuery=function(t){for(var e=[],n=0;n0){for(u=0;u=0);return t},e.prototype.assign=function(){for(var t,e=!1,n=this.dataset.length,r=0;ri&&(n=r):e=this.minPts)return n}},n.prototype._regionQuery=function(t,e){e=e||this.epsilon;for(var n=[],r=0,i=this.dataset.length;r0;r.length0;){var a=t[Math.floor(Math.random()*o)],u=s?a.join("_"):""+a;n[u]||(n[u]=!0,r.push(a))}if(r.length0,s=t[Math.floor(Math.random()*i)];o&&s.join("_");for(r.push(s);r.length0,f=[];if(n)i="kmrand"==n?Gs(t,e):"kmpp"==n?qs(t,e):n;else for(var g={};i.length0;){var u=s.pop();if(u===n)return Vs(u);u.closed=!0;for(var l=t.neighbors(u),c=0,h=l.length;c0)){if(o/=p,p<0){if(o0){if(o>h)return;o>c&&(c=o)}if(o=r-u,p||!(o<0)){if(o/=p,p<0){if(o>h)return;o>c&&(c=o)}else if(p>0){if(o0)){if(o/=f,f<0){if(o0){if(o>h)return;o>c&&(c=o)}if(o=i-l,f||!(o<0)){if(o/=f,f<0){if(o>h)return;o>c&&(c=o)}else if(f>0){if(o0||h<1)||(c>0&&(t[0]=[u+c*p,l+c*f]),h<1&&(t[1]=[u+h*p,l+h*f]),!0)}}}}}function ua(t,e,n,r,i){var o=t[1];if(o)return!0;var s,a,u=t[0],l=t.left,c=t.right,h=l[0],p=l[1],f=c[0],g=c[1],d=(h+f)/2,y=(p+g)/2;if(g===p){if(d=r)return;if(h>f){if(u){if(u[1]>=i)return}else u=[d,n];o=[d,i]}else{if(u){if(u[1]1)if(h>f){if(u){if(u[1]>=i)return}else u=[(n-a)/s,n];o=[(i-a)/s,i]}else{if(u){if(u[1]=r)return}else u=[e,s*e+a];o=[r,s*r+a]}else{if(u){if(u[0]0&&(this.content[0]=e,this.bubbleUp(0)),t},remove:function(t){var e=this.content.indexOf(t),n=this.content.pop();e!==this.content.length-1&&(this.content[e]=n,this.scoreFunction(n)0;){var n=(t+1>>1)-1,r=this.content[n];if(!(this.scoreFunction(e)=-La)){var f=u*u+l*l,g=c*c+h*h,d=(h*f-l*g)/p,y=(u*g-c*f)/p,v=fa.pop()||new ga;v.arc=t,v.site=i,v.x=d+s,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var _=null,m=Ca._;m;)if(v.yMa)a=a.L;else{if(!((i=o-Ia(a,s))>Ma)){r>-Ma?(e=a.P,n=a):i>-Ma?(e=a,n=a.N):e=n=a;break}if(!a.R){e=a;break}a=a.R}!function(t){Sa[t.index]={site:t,halfedges:[]}}(t);var u=ma(t);if(Na.insert(e,u),e||n){if(e===n)return ya(e),n=ma(e.site),Na.insert(u,n),u.edge=n.edge=ia(e.site,u.site),da(e),void da(n);if(n){ya(e),ya(n);var l=e.site,c=l[0],h=l[1],p=t[0]-c,f=t[1]-h,g=n.site,d=g[0]-c,y=g[1]-h,v=2*(p*y-f*d),_=p*p+f*f,m=d*d+y*y,x=[(y*_-f*m)/v+c,(p*m-d*_)/v+h];sa(n.edge,l,g,x),u.edge=ia(l,t,null,x),n.edge=ia(t,g,null,x),da(e),da(n)}else u.edge=ia(e.site,u.site)}}function wa(t,e){var n=t.site,r=n[0],i=n[1],o=i-e;if(!o)return r;var s=t.P;if(!s)return-1/0;var a=(n=s.site)[0],u=n[1],l=u-e;if(!l)return a;var c=a-r,h=1/o-1/l,p=c/l;return h?(-p+Math.sqrt(p*p-2*h*(c*c/(-2*l)-u+l/2+i-o/2)))/h+r:(r+a)/2}function Ia(t,e){var n=t.N;if(n)return wa(n,e);var r=t.site;return r[1]===e?r[0]:1/0}var Na,Sa,Ca,Pa,Ma=1e-6,La=1e-12;function Oa(t,e){return e[1]-t[1]||e[0]-t[0]}function Ra(t,e){var n,r,i,o=t.sort(Oa).pop();for(Pa=[],Sa=new Array(t.length),Na=new $s,Ca=new $s;;)if(i=pa,o&&(!i||o[1]Ma||Math.abs(i[0][1]-i[1][1])>Ma)||delete Pa[o]}(s,a,u,l),function(t,e,n,r){var i,o,s,a,u,l,c,h,p,f,g,d,y=Sa.length,v=!0;for(i=0;iMa||Math.abs(d-p)>Ma)&&(u.splice(a,0,Pa.push(oa(s,f,Math.abs(g-t)Ma?[t,Math.abs(h-t)Ma?[Math.abs(p-r)Ma?[n,Math.abs(h-n)Ma?[Math.abs(p-e)=-270&&(d=-d),g<-180&&g>=-360&&(y=-y),"degrees"===o){var v=d*Math.cos(h)+y*Math.sin(h),_=y*Math.cos(h)-d*Math.sin(h);d=v,y=_}p.push([d+c[0],y+c[1]])}return p.push(p[0]),"degrees"===o?l([p],u):$o(l([p],u),s,{pivot:a})}function Da(t){var e=t*Math.PI/180;return Math.tan(e)}function Fa(t,e){void 0===e&&(e={});var n=0,r=0,i=0;return q(t,(function(t,o,s){var a=e.weight?null==s?void 0:s[e.weight]:void 0;if(!C(a=null==a?1:a))throw new Error("weight value must be a number for feature index "+o);(a=Number(a))>0&&R(t,(function(t){n+=t[0]*a,r+=t[1]*a,i+=a}))})),a([n/i,r/i],e.properties,e)}function ka(t,e,n,r,i){var o=r.tolerance||.001,s=0,u=0,l=0,c=0;if(F(n,(function(e){var n,r=null===(n=e.properties)||void 0===n?void 0:n.weight,i=null==r?1:r;if(!C(i=Number(i)))throw new Error("weight value must be a number");if(i>0){c+=1;var o=i*me(e,t);0===o&&(o=1);var a=i/o;s+=e.geometry.coordinates[0]*a,u+=e.geometry.coordinates[1]*a,l+=a}})),c<1)throw new Error("no features to measure");var h=s/l,p=u/l;return 1===c||0===i||Math.abs(h-e[0])n&&(n=u,r=o,e.push([]));var l=o-r,c=t.coordinates[u][l+1],h=i[0],p=i[1],f=c[0],g=c[1];e[u].push([.75*h+.25*f,.75*p+.25*g]),e[u].push([.25*h+.75*f,.25*p+.75*g])}),!0),e.forEach((function(t){t.push(t[0])}))}function Ba(t,e){var n=0,r=0,i=0;R(t,(function(o,s,a,u,l){u>i&&(i=u,r=s,e.push([[]])),l>n&&(n=l,r=s,e[u].push([]));var c=s-r,h=t.coordinates[u][l][c+1],p=o[0],f=o[1],g=h[0],d=h[1];e[u][l].push([.75*p+.25*g,.75*f+.25*d]),e[u][l].push([.25*p+.75*g,.25*f+.75*d])}),!0),e.forEach((function(t){t.forEach((function(t){t.push(t[0])}))}))}function za(t,e,n){void 0===n&&(n=2);var r=K(t),i=K(e),o=r[0]-i[0],s=r[1]-i[1];return 1===n?Math.abs(o)+Math.abs(s):Math.pow(Math.pow(o,n)+Math.pow(s,n),1/n)}function ja(t,e){var n=(e=e||{}).threshold||1e4,r=e.p||2,i=e.binary||!1,o=e.alpha||-1,s=e.standardization||!1,a=[];F(t,(function(t){a.push(En(t))}));for(var u=[],l=0;l0?1:0}(t[0]))*e,n*Math.log(Math.tan(.25*Math.PI+.5*t[1]*e))];return i[0]>r&&(i[0]=r),i[0]<-r&&(i[0]=-r),i[1]>r&&(i[1]=r),i[1]<-r&&(i[1]=-r),i}function Wa(t){var e=180/Math.PI,n=6378137;return[t[0]*e/n,(.5*Math.PI-2*Math.atan(Math.exp(-t[1]/n)))*e]}Ra.prototype={constructor:Ra,polygons:function(){var t=this.edges;return this.cells.map((function(e){var n=e.halfedges.map((function(n){return ca(e,t[n])}));return n.data=e.site.data,n}))},triangles:function(){var t=[],e=this.edges;return this.cells.forEach((function(n,r){if(o=(i=n.halfedges).length)for(var i,o,s,a,u,l,c=n.site,h=-1,p=e[i[o-1]],f=p.left===c?p.right:p.left;++h=a)return null;var u=t-i.site[0],l=e-i.site[1],c=u*u+l*l;do{i=o.cells[r=s],s=null,i.halfedges.forEach((function(n){var r=o.edges[n],a=r.left;if(a!==i.site&&a||(a=r.right)){var u=t-a[0],l=e-a[1],h=u*u+l*l;h0?t+n[e-1]:t})),o.forEach((function(t){t=2*t*Math.PI/o[o.length-1];var n=Math.random();i.push([n*(e.max_radial_length||10)*Math.sin(t),n*(e.max_radial_length||10)*Math.cos(t)])})),i[i.length-1]=i[0],i=i.map((r=Ka(e.bbox),function(t){return[t[0]+r[0],t[1]+r[1]]})),n.push(l([i]))},i=0;i + * @license MIT + * @preserve + */function fu(t,e){return t>e?1:t0))break;if(null===e.right)break;if(n(t,e.right.key)>0){a=e.right;if(e.right=a.left,a.left=e,null===(e=a).right)break}i.right=e,i=e,e=e.right}}return i.right=e.left,o.left=e.right,e.left=r.right,e.right=r.left,e}function du(t,e,n,r){var i=new pu(t,e);if(null===n)return i.left=i.right=null,i;var o=r(t,(n=gu(t,n,r)).key);return o<0?(i.left=n.left,i.right=n,n.left=null):o>=0&&(i.right=n.right,i.left=n,n.right=null),i}function yu(t,e,n){var r=null,i=null;if(e){var o=n((e=gu(t,e,n)).key,t);0===o?(r=e.left,i=e.right):o<0?(i=e.right,e.right=null,r=e):(r=e.left,e.left=null,i=e)}return{left:r,right:i}}function vu(t,e,n,r,i){if(t){r(e+(n?"└── ":"├── ")+i(t)+"\n");var o=e+(n?" ":"│ ");t.left&&vu(t.left,o,!1,r,i),t.right&&vu(t.right,o,!0,r,i)}}var _u=function(){function t(t){void 0===t&&(t=fu),this._root=null,this._size=0,this._comparator=t}return t.prototype.insert=function(t,e){return this._size++,this._root=du(t,e,this._root,this._comparator)},t.prototype.add=function(t,e){var n=new pu(t,e);null===this._root&&(n.left=n.right=null,this._size++,this._root=n);var r=this._comparator,i=gu(t,this._root,r),o=r(t,i.key);return 0===o?this._root=i:(o<0?(n.left=i.left,n.right=i,i.left=null):o>0&&(n.right=i.right,n.left=i,i.right=null),this._size++,this._root=n),this._root},t.prototype.remove=function(t){this._root=this._remove(t,this._root,this._comparator)},t.prototype._remove=function(t,e,n){var r;return null===e?null:0===n(t,(e=gu(t,e,n)).key)?(null===e.left?r=e.right:(r=gu(t,e.left,n)).right=e.right,this._size--,r):e},t.prototype.pop=function(){var t=this._root;if(t){for(;t.left;)t=t.left;return this._root=gu(t.key,this._root,this._comparator),this._root=this._remove(t.key,this._root,this._comparator),{key:t.key,data:t.data}}return null},t.prototype.findStatic=function(t){for(var e=this._root,n=this._comparator;e;){var r=n(t,e.key);if(0===r)return e;e=r<0?e.left:e.right}return null},t.prototype.find=function(t){return this._root&&(this._root=gu(t,this._root,this._comparator),0!==this._comparator(t,this._root.key))?null:this._root},t.prototype.contains=function(t){for(var e=this._root,n=this._comparator;e;){var r=n(t,e.key);if(0===r)return!0;e=r<0?e.left:e.right}return!1},t.prototype.forEach=function(t,e){for(var n=this._root,r=[],i=!1;!i;)null!==n?(r.push(n),n=n.left):0!==r.length?(n=r.pop(),t.call(e,n),n=n.right):i=!0;return this},t.prototype.range=function(t,e,n,r){for(var i=[],o=this._comparator,s=this._root;0!==i.length||s;)if(s)i.push(s),s=s.left;else{if(o((s=i.pop()).key,e)>0)break;if(o(s.key,t)>=0&&n.call(r,s))return this;s=s.right}return this},t.prototype.keys=function(){var t=[];return this.forEach((function(e){var n=e.key;return t.push(n)})),t},t.prototype.values=function(){var t=[];return this.forEach((function(e){var n=e.data;return t.push(n)})),t},t.prototype.min=function(){return this._root?this.minNode(this._root).key:null},t.prototype.max=function(){return this._root?this.maxNode(this._root).key:null},t.prototype.minNode=function(t){if(void 0===t&&(t=this._root),t)for(;t.left;)t=t.left;return t},t.prototype.maxNode=function(t){if(void 0===t&&(t=this._root),t)for(;t.right;)t=t.right;return t},t.prototype.at=function(t){for(var e=this._root,n=!1,r=0,i=[];!n;)if(e)i.push(e),e=e.left;else if(i.length>0){if(e=i.pop(),r===t)return e;r++,e=e.right}else n=!0;return null},t.prototype.next=function(t){var e=this._root,n=null;if(t.right){for(n=t.right;n.left;)n=n.left;return n}for(var r=this._comparator;e;){var i=r(t.key,e.key);if(0===i)break;i<0?(n=e,e=e.left):e=e.right}return n},t.prototype.prev=function(t){var e=this._root,n=null;if(null!==t.left){for(n=t.left;n.right;)n=n.right;return n}for(var r=this._comparator;e;){var i=r(t.key,e.key);if(0===i)break;i<0?e=e.left:(n=e,e=e.right)}return n},t.prototype.clear=function(){return this._root=null,this._size=0,this},t.prototype.toList=function(){return function(t){var e=t,n=[],r=!1,i=new pu(null,null),o=i;for(;!r;)e?(n.push(e),e=e.left):n.length>0?e=(e=o=o.next=n.pop()).right:r=!0;return o.next=null,i.next}(this._root)},t.prototype.load=function(t,e,n){void 0===e&&(e=[]),void 0===n&&(n=!1);var r=t.length,i=this._comparator;if(n&&Eu(t,e,0,r-1,i),null===this._root)this._root=mu(t,e,0,r),this._size=r;else{var o=function(t,e,n){var r=new pu(null,null),i=r,o=t,s=e;for(;null!==o&&null!==s;)n(o.key,s.key)<0?(i.next=o,o=o.next):(i.next=s,s=s.next),i=i.next;null!==o?i.next=o:null!==s&&(i.next=s);return r.next}(this.toList(),function(t,e){for(var n=new pu(null,null),r=n,i=0;i0){var o=n+Math.floor(i/2),s=t[o],a=e[o],u=new pu(s,a);return u.left=mu(t,e,n,o),u.right=mu(t,e,o+1,r),u}return null}function xu(t,e,n){var r=n-e;if(r>0){var i=e+Math.floor(r/2),o=xu(t,e,i),s=t.head;return s.left=o,t.head=t.head.next,s.right=xu(t,i+1,n),s}return null}function Eu(t,e,n,r,i){if(!(n>=r)){for(var o=t[n+r>>1],s=n-1,a=r+1;;){do{s++}while(i(t[s],o)<0);do{a--}while(i(t[a],o)>0);if(s>=a)break;var u=t[s];t[s]=t[a],t[a]=u,u=e[s],e[s]=e[a],e[a]=u}Eu(t,e,n,a,i),Eu(t,e,a+1,r,i)}}function bu(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function wu(t,e){for(var n=0;ne.x?1:t.ye.y?1:0}}]),Iu(t,[{key:"link",value:function(t){if(t.point===this.point)throw new Error("Tried to link already linked events");for(var e=t.point.events,n=0,r=e.length;n=0&&u>=0?sl?-1:0:o<0&&u<0?sl?1:0:uo?1:0}}}]),t}(),zu=0,ju=function(){function t(e,n,r,i){bu(this,t),this.id=++zu,this.leftSE=e,e.segment=this,e.otherSE=n,this.rightSE=n,n.segment=this,n.otherSE=e,this.rings=r,this.windings=i}return Iu(t,null,[{key:"compare",value:function(t,e){var n=t.leftSE.point.x,r=e.leftSE.point.x,i=t.rightSE.point.x,o=e.rightSE.point.x;if(os&&a>u)return-1;var c=t.comparePoint(e.leftSE.point);if(c<0)return 1;if(c>0)return-1;var h=e.comparePoint(t.rightSE.point);return 0!==h?h:-1}if(n>r){if(sa&&s>l)return 1;var p=e.comparePoint(t.leftSE.point);if(0!==p)return p;var f=t.comparePoint(e.rightSE.point);return f<0?1:f>0?-1:1}if(sa)return 1;if(io){var d=t.comparePoint(e.rightSE.point);if(d<0)return 1;if(d>0)return-1}if(i!==o){var y=u-s,v=i-n,_=l-a,m=o-r;if(y>v&&_m)return-1}return i>o?1:il?1:t.ide.id?1:0}}]),Iu(t,[{key:"replaceRightSE",value:function(t){this.rightSE=t,this.rightSE.segment=this,this.rightSE.otherSE=this.leftSE,this.leftSE.otherSE=this.rightSE}},{key:"bbox",value:function(){var t=this.leftSE.point.y,e=this.rightSE.point.y;return{ll:{x:this.leftSE.point.x,y:te?t:e}}}},{key:"vector",value:function(){return{x:this.rightSE.point.x-this.leftSE.point.x,y:this.rightSE.point.y-this.leftSE.point.y}}},{key:"isAnEndpoint",value:function(t){return t.x===this.leftSE.point.x&&t.y===this.leftSE.point.y||t.x===this.rightSE.point.x&&t.y===this.rightSE.point.y}},{key:"comparePoint",value:function(t){if(this.isAnEndpoint(t))return 0;var e=this.leftSE.point,n=this.rightSE.point,r=this.vector();if(e.x===n.x)return t.x===e.x?0:t.x0&&a.swapEvents(),Bu.comparePoints(this.leftSE.point,this.rightSE.point)>0&&this.swapEvents(),r&&(i.checkForConsuming(),o.checkForConsuming()),n}},{key:"swapEvents",value:function(){var t=this.rightSE;this.rightSE=this.leftSE,this.leftSE=t,this.leftSE.isLeft=!0,this.rightSE.isLeft=!1;for(var e=0,n=this.windings.length;e0){var o=n;n=r,r=o}if(n.prev===r){var s=n;n=r,r=s}for(var a=0,u=r.rings.length;a0))throw new Error("Tried to create degenerate segment at [".concat(e.x,", ").concat(e.y,"]"));i=n,o=e,s=-1}return new t(new Bu(i,!0),new Bu(o,!1),[r],[s])}}]),t}(),Uu=function(){function t(e,n,r){if(bu(this,t),!Array.isArray(e)||0===e.length)throw new Error("Input geometry is not a valid Polygon or MultiPolygon");if(this.poly=n,this.isExterior=r,this.segments=[],"number"!=typeof e[0][0]||"number"!=typeof e[0][1])throw new Error("Input geometry is not a valid Polygon or MultiPolygon");var i=Ru.round(e[0][0],e[0][1]);this.bbox={ll:{x:i.x,y:i.y},ur:{x:i.x,y:i.y}};for(var o=i,s=1,a=e.length;sthis.bbox.ur.x&&(this.bbox.ur.x=u.x),u.y>this.bbox.ur.y&&(this.bbox.ur.y=u.y),o=u)}i.x===o.x&&i.y===o.y||this.segments.push(ju.fromRing(o,i,this))}return Iu(t,[{key:"getSweepEvents",value:function(){for(var t=[],e=0,n=this.segments.length;ethis.bbox.ur.x&&(this.bbox.ur.x=o.bbox.ur.x),o.bbox.ur.y>this.bbox.ur.y&&(this.bbox.ur.y=o.bbox.ur.y),this.interiorRings.push(o)}this.multiPoly=n}return Iu(t,[{key:"getSweepEvents",value:function(){for(var t=this.exteriorRing.getSweepEvents(),e=0,n=this.interiorRings.length;ethis.bbox.ur.x&&(this.bbox.ur.x=o.bbox.ur.x),o.bbox.ur.y>this.bbox.ur.y&&(this.bbox.ur.y=o.bbox.ur.y),this.polys.push(o)}this.isSubject=n}return Iu(t,[{key:"getSweepEvents",value:function(){for(var t=[],e=0,n=this.polys.length;e0&&(t=r)}for(var i=t.segment.prevInResult(),o=i?i.prevInResult():null;;){if(!i)return null;if(!o)return i.ringOut;if(o.ringOut!==i.ringOut)return o.ringOut.enclosingRing()!==i.ringOut?i.ringOut:i.ringOut.enclosingRing();i=o.prevInResult(),o=i?i.prevInResult():null}}}]),t}(),Hu=function(){function t(e){bu(this,t),this.exteriorRing=e,e.poly=this,this.interiorRings=[]}return Iu(t,[{key:"addInterior",value:function(t){this.interiorRings.push(t),t.poly=this}},{key:"getGeom",value:function(){var t=[this.exteriorRing.getGeom()];if(null===t[0])return null;for(var e=0,n=this.interiorRings.length;e1&&void 0!==arguments[1]?arguments[1]:ju.compare;bu(this,t),this.queue=e,this.tree=new _u(n),this.segments=[]}return Iu(t,[{key:"process",value:function(t){var e=t.segment,n=[];if(t.consumedBy)return t.isLeft?this.queue.remove(t.otherSE):this.tree.remove(e),n;var r=t.isLeft?this.tree.insert(e):this.tree.find(e);if(!r)throw new Error("Unable to find segment #".concat(e.id," ")+"[".concat(e.leftSE.point.x,", ").concat(e.leftSE.point.y,"] -> ")+"[".concat(e.rightSE.point.x,", ").concat(e.rightSE.point.y,"] ")+"in SweepLine tree. Please submit a bug report.");for(var i=r,o=r,s=void 0,a=void 0;void 0===s;)null===(i=this.tree.prev(i))?s=null:void 0===i.key.consumedBy&&(s=i.key);for(;void 0===a;)null===(o=this.tree.next(o))?a=null:void 0===o.key.consumedBy&&(a=o.key);if(t.isLeft){var u=null;if(s){var l=s.getIntersection(e);if(null!==l&&(e.isAnEndpoint(l)||(u=l),!s.isAnEndpoint(l)))for(var c=this._splitSafely(s,l),h=0,p=c.length;h0?(this.tree.remove(e),n.push(t)):(this.segments.push(e),e.prev=s)}else{if(s&&a){var b=s.getIntersection(a);if(null!==b){if(!s.isAnEndpoint(b))for(var w=this._splitSafely(s,b),I=0,N=w.length;IZu)throw new Error("Infinite loop when putting segment endpoints in a priority queue (queue size too big). Please file a bug report.");for(var m=new Ju(f),x=f.size,E=f.pop();E;){var b=E.key;if(f.size===x){var w=b.segment;throw new Error("Unable to pop() ".concat(b.isLeft?"left":"right"," SweepEvent ")+"[".concat(b.point.x,", ").concat(b.point.y,"] from segment #").concat(w.id," ")+"[".concat(w.leftSE.point.x,", ").concat(w.leftSE.point.y,"] -> ")+"[".concat(w.rightSE.point.x,", ").concat(w.rightSE.point.y,"] from queue. ")+"Please file a bug report.")}if(f.size>Zu)throw new Error("Infinite loop when passing sweep line over endpoints (queue size too big). Please file a bug report.");if(m.segments.length>Ku)throw new Error("Infinite loop when passing sweep line over endpoints (too many sweep line segments). Please file a bug report.");for(var I=m.process(b),N=0,S=I.length;N1?e-1:0),r=1;r1?e-1:0),r=1;r1?e-1:0),r=1;r1?e-1:0),r=1;re.x?1:this.ye.y?1:0},ul.prototype.clone=function(){},ul.prototype.copy=function(){return new ul(this)},ul.prototype.toString=function(){return"("+this.x+", "+this.y+", "+this.z+")"},ul.prototype.distance3D=function(t){var e=this.x-t.x,n=this.y-t.y,r=this.z-t.z;return Math.sqrt(e*e+n*n+r*r)},ul.prototype.distance=function(t){var e=this.x-t.x,n=this.y-t.y;return Math.sqrt(e*e+n*n)},ul.prototype.hashCode=function(){var t=17;return t=37*(t=37*t+ul.hashCode(this.x))+ul.hashCode(this.y)},ul.prototype.setCoordinate=function(t){this.x=t.x,this.y=t.y,this.z=t.z},ul.prototype.interfaces_=function(){return[il,ol,al]},ul.prototype.getClass=function(){return ul},ul.hashCode=function(){if(1===arguments.length){var t=arguments[0],e=nl.doubleToLongBits(t);return Math.trunc((e^e)>>>32)}},ll.DimensionalComparator.get=function(){return cl},ll.serialVersionUID.get=function(){return 0x5cbf2c235c7e5800},ll.NULL_ORDINATE.get=function(){return nl.NaN},ll.X.get=function(){return 0},ll.Y.get=function(){return 1},ll.Z.get=function(){return 2},Object.defineProperties(ul,ll);var cl=function(t){if(this._dimensionsToTest=2,0===arguments.length);else if(1===arguments.length){var e=arguments[0];if(2!==e&&3!==e)throw new el("only 2 or 3 dimensions may be specified");this._dimensionsToTest=e}};cl.prototype.compare=function(t,e){var n=t,r=e,i=cl.compare(n.x,r.x);if(0!==i)return i;var o=cl.compare(n.y,r.y);return 0!==o?o:this._dimensionsToTest<=2?0:cl.compare(n.z,r.z)},cl.prototype.interfaces_=function(){return[sl]},cl.prototype.getClass=function(){return cl},cl.compare=function(t,e){return te?1:nl.isNaN(t)?nl.isNaN(e)?0:-1:nl.isNaN(e)?1:0};var hl=function(){};hl.prototype.create=function(){},hl.prototype.interfaces_=function(){return[]},hl.prototype.getClass=function(){return hl};var pl=function(){},fl={INTERIOR:{configurable:!0},BOUNDARY:{configurable:!0},EXTERIOR:{configurable:!0},NONE:{configurable:!0}};pl.prototype.interfaces_=function(){return[]},pl.prototype.getClass=function(){return pl},pl.toLocationSymbol=function(t){switch(t){case pl.EXTERIOR:return"e";case pl.BOUNDARY:return"b";case pl.INTERIOR:return"i";case pl.NONE:return"-"}throw new el("Unknown location value: "+t)},fl.INTERIOR.get=function(){return 0},fl.BOUNDARY.get=function(){return 1},fl.EXTERIOR.get=function(){return 2},fl.NONE.get=function(){return-1},Object.defineProperties(pl,fl);var gl=function(t,e){return t.interfaces_&&t.interfaces_().indexOf(e)>-1},dl=function(){},yl={LOG_10:{configurable:!0}};dl.prototype.interfaces_=function(){return[]},dl.prototype.getClass=function(){return dl},dl.log10=function(t){var e=Math.log(t);return nl.isInfinite(e)||nl.isNaN(e)?e:e/dl.LOG_10},dl.min=function(t,e,n,r){var i=t;return en?n:t}if(Number.isInteger(arguments[2])&&Number.isInteger(arguments[0])&&Number.isInteger(arguments[1])){var r=arguments[0],i=arguments[1],o=arguments[2];return ro?o:r}},dl.wrap=function(t,e){return t<0?e- -t%e:t%e},dl.max=function(){if(3===arguments.length){var t=arguments[0],e=arguments[1],n=arguments[2],r=t;return e>r&&(r=e),n>r&&(r=n),r}if(4===arguments.length){var i=arguments[0],o=arguments[1],s=arguments[2],a=arguments[3],u=i;return o>u&&(u=o),s>u&&(u=s),a>u&&(u=a),u}},dl.average=function(t,e){return(t+e)/2},yl.LOG_10.get=function(){return Math.log(10)},Object.defineProperties(dl,yl);var vl=function(t){this.str=t};vl.prototype.append=function(t){this.str+=t},vl.prototype.setCharAt=function(t,e){this.str=this.str.substr(0,t)+e+this.str.substr(t+1)},vl.prototype.toString=function(t){return this.str};var _l=function(t){this.value=t};_l.prototype.intValue=function(){return this.value},_l.prototype.compareTo=function(t){return this.valuet?1:0},_l.isNaN=function(t){return Number.isNaN(t)};var ml=function(){};ml.isWhitespace=function(t){return t<=32&&t>=0||127===t},ml.toUpperCase=function(t){return t.toUpperCase()};var xl=function t(){if(this._hi=0,this._lo=0,0===arguments.length)this.init(0);else if(1===arguments.length){if("number"==typeof arguments[0]){var e=arguments[0];this.init(e)}else if(arguments[0]instanceof t){var n=arguments[0];this.init(n)}else if("string"==typeof arguments[0]){var r=arguments[0];t.call(this,t.parse(r))}}else if(2===arguments.length){var i=arguments[0],o=arguments[1];this.init(i,o)}},El={PI:{configurable:!0},TWO_PI:{configurable:!0},PI_2:{configurable:!0},E:{configurable:!0},NaN:{configurable:!0},EPS:{configurable:!0},SPLIT:{configurable:!0},MAX_PRINT_DIGITS:{configurable:!0},TEN:{configurable:!0},ONE:{configurable:!0},SCI_NOT_EXPONENT_CHAR:{configurable:!0},SCI_NOT_ZERO:{configurable:!0}};xl.prototype.le=function(t){return(this._hi9?(c=!0,h="9"):h="0"+l,s.append(h),n=n.subtract(xl.valueOf(l)).multiply(xl.TEN),c&&n.selfAdd(xl.TEN);var p=!0,f=xl.magnitude(n._hi);if(f<0&&Math.abs(f)>=a-u&&(p=!1),!p)break}return e[0]=r,s.toString()},xl.prototype.sqr=function(){return this.multiply(this)},xl.prototype.doubleValue=function(){return this._hi+this._lo},xl.prototype.subtract=function(){if(arguments[0]instanceof xl){var t=arguments[0];return this.add(t.negate())}if("number"==typeof arguments[0]){var e=arguments[0];return this.add(-e)}},xl.prototype.equals=function(){if(1===arguments.length){var t=arguments[0];return this._hi===t._hi&&this._lo===t._lo}},xl.prototype.isZero=function(){return 0===this._hi&&0===this._lo},xl.prototype.selfSubtract=function(){if(arguments[0]instanceof xl){var t=arguments[0];return this.isNaN()?this:this.selfAdd(-t._hi,-t._lo)}if("number"==typeof arguments[0]){var e=arguments[0];return this.isNaN()?this:this.selfAdd(-e,0)}},xl.prototype.getSpecialNumberString=function(){return this.isZero()?"0.0":this.isNaN()?"NaN ":null},xl.prototype.min=function(t){return this.le(t)?this:t},xl.prototype.selfDivide=function(){if(1===arguments.length){if(arguments[0]instanceof xl){var t=arguments[0];return this.selfDivide(t._hi,t._lo)}if("number"==typeof arguments[0]){var e=arguments[0];return this.selfDivide(e,0)}}else if(2===arguments.length){var n=arguments[0],r=arguments[1],i=null,o=null,s=null,a=null,u=null,l=null,c=null,h=null;return u=this._hi/n,h=(i=(l=xl.SPLIT*u)-(i=l-u))*(s=(h=xl.SPLIT*n)-(s=h-n))-(c=u*n)+i*(a=n-s)+(o=u-i)*s+o*a,h=u+(l=(this._hi-c-h+this._lo-u*r)/n),this._hi=h,this._lo=u-h+l,this}},xl.prototype.dump=function(){return"DD<"+this._hi+", "+this._lo+">"},xl.prototype.divide=function(){if(arguments[0]instanceof xl){var t=arguments[0],e=null,n=null,r=null,i=null,o=null,s=null,a=null,u=null;n=(o=this._hi/t._hi)-(e=(s=xl.SPLIT*o)-(e=s-o)),u=e*(r=(u=xl.SPLIT*t._hi)-(r=u-t._hi))-(a=o*t._hi)+e*(i=t._hi-r)+n*r+n*i;var l=u=o+(s=(this._hi-a-u+this._lo-o*t._lo)/t._hi),c=o-u+s;return new xl(l,c)}if("number"==typeof arguments[0]){var h=arguments[0];return nl.isNaN(h)?xl.createNaN():xl.copy(this).selfDivide(h,0)}},xl.prototype.ge=function(t){return(this._hi>t._hi||this._hi===t._hi)&&this._lo>=t._lo},xl.prototype.pow=function(t){if(0===t)return xl.valueOf(1);var e=new xl(this),n=xl.valueOf(1),r=Math.abs(t);if(r>1)for(;r>0;)r%2==1&&n.selfMultiply(e),(r/=2)>0&&(e=e.sqr());else n=e;return t<0?n.reciprocal():n},xl.prototype.ceil=function(){if(this.isNaN())return xl.NaN;var t=Math.ceil(this._hi),e=0;return t===this._hi&&(e=Math.ceil(this._lo)),new xl(t,e)},xl.prototype.compareTo=function(t){var e=t;return this._hie._hi?1:this._loe._lo?1:0},xl.prototype.rint=function(){return this.isNaN()?this:this.add(.5).floor()},xl.prototype.setValue=function(){if(arguments[0]instanceof xl){var t=arguments[0];return this.init(t),this}if("number"==typeof arguments[0]){var e=arguments[0];return this.init(e),this}},xl.prototype.max=function(t){return this.ge(t)?this:t},xl.prototype.sqrt=function(){if(this.isZero())return xl.valueOf(0);if(this.isNegative())return xl.NaN;var t=1/Math.sqrt(this._hi),e=this._hi*t,n=xl.valueOf(e),r=this.subtract(n.sqr())._hi*(.5*t);return n.add(r)},xl.prototype.selfAdd=function(){if(1===arguments.length){if(arguments[0]instanceof xl){var t=arguments[0];return this.selfAdd(t._hi,t._lo)}if("number"==typeof arguments[0]){var e=arguments[0],n=null,r=null,i=null,o=null,s=null,a=null;return o=(i=this._hi+e)-(s=i-this._hi),r=(a=(o=e-s+(this._hi-o))+this._lo)+(i-(n=i+a)),this._hi=n+r,this._lo=r+(n-this._hi),this}}else if(2===arguments.length){var u=arguments[0],l=arguments[1],c=null,h=null,p=null,f=null,g=null,d=null,y=null;f=this._hi+u,h=this._lo+l,g=f-(d=f-this._hi),p=h-(y=h-this._lo);var v=(c=f+(d=(g=u-d+(this._hi-g))+h))+(d=(p=l-y+(this._lo-p))+(d+(f-c))),_=d+(c-v);return this._hi=v,this._lo=_,this}},xl.prototype.selfMultiply=function(){if(1===arguments.length){if(arguments[0]instanceof xl){var t=arguments[0];return this.selfMultiply(t._hi,t._lo)}if("number"==typeof arguments[0]){var e=arguments[0];return this.selfMultiply(e,0)}}else if(2===arguments.length){var n=arguments[0],r=arguments[1],i=null,o=null,s=null,a=null,u=null,l=null;i=(u=xl.SPLIT*this._hi)-this._hi,l=xl.SPLIT*n,i=u-i,o=this._hi-i,s=l-n;var c=(u=this._hi*n)+(l=i*(s=l-s)-u+i*(a=n-s)+o*s+o*a+(this._hi*r+this._lo*n)),h=l+(i=u-c);return this._hi=c,this._lo=h,this}},xl.prototype.selfSqr=function(){return this.selfMultiply(this)},xl.prototype.floor=function(){if(this.isNaN())return xl.NaN;var t=Math.floor(this._hi),e=0;return t===this._hi&&(e=Math.floor(this._lo)),new xl(t,e)},xl.prototype.negate=function(){return this.isNaN()?this:new xl(-this._hi,-this._lo)},xl.prototype.clone=function(){},xl.prototype.multiply=function(){if(arguments[0]instanceof xl){var t=arguments[0];return t.isNaN()?xl.createNaN():xl.copy(this).selfMultiply(t)}if("number"==typeof arguments[0]){var e=arguments[0];return nl.isNaN(e)?xl.createNaN():xl.copy(this).selfMultiply(e,0)}},xl.prototype.isNaN=function(){return nl.isNaN(this._hi)},xl.prototype.intValue=function(){return Math.trunc(this._hi)},xl.prototype.toString=function(){var t=xl.magnitude(this._hi);return t>=-3&&t<=20?this.toStandardNotation():this.toSciNotation()},xl.prototype.toStandardNotation=function(){var t=this.getSpecialNumberString();if(null!==t)return t;var e=new Array(1).fill(null),n=this.extractSignificantDigits(!0,e),r=e[0]+1,i=n;if("."===n.charAt(0))i="0"+n;else if(r<0)i="0."+xl.stringOfChar("0",-r)+n;else if(-1===n.indexOf(".")){var o=r-n.length;i=n+xl.stringOfChar("0",o)+".0"}return this.isNegative()?"-"+i:i},xl.prototype.reciprocal=function(){var t,e,n,r,i=null,o=null,s=null,a=null;t=(n=1/this._hi)-(i=(s=xl.SPLIT*n)-(i=s-n)),o=(a=xl.SPLIT*this._hi)-this._hi;var u=n+(s=(1-(r=n*this._hi)-(a=i*(o=a-o)-r+i*(e=this._hi-o)+t*o+t*e)-n*this._lo)/this._hi);return new xl(u,n-u+s)},xl.prototype.toSciNotation=function(){if(this.isZero())return xl.SCI_NOT_ZERO;var t=this.getSpecialNumberString();if(null!==t)return t;var e=new Array(1).fill(null),n=this.extractSignificantDigits(!1,e),r=xl.SCI_NOT_EXPONENT_CHAR+e[0];if("0"===n.charAt(0))throw new Error("Found leading zero: "+n);var i="";n.length>1&&(i=n.substring(1));var o=n.charAt(0)+"."+i;return this.isNegative()?"-"+o+r:o+r},xl.prototype.abs=function(){return this.isNaN()?xl.NaN:this.isNegative()?this.negate():new xl(this)},xl.prototype.isPositive=function(){return(this._hi>0||0===this._hi)&&this._lo>0},xl.prototype.lt=function(t){return(this._hit._hi||this._hi===t._hi)&&this._lo>t._lo},xl.prototype.isNegative=function(){return(this._hi<0||0===this._hi)&&this._lo<0},xl.prototype.trunc=function(){return this.isNaN()?xl.NaN:this.isPositive()?this.floor():this.ceil()},xl.prototype.signum=function(){return this._hi>0?1:this._hi<0?-1:this._lo>0?1:this._lo<0?-1:0},xl.prototype.interfaces_=function(){return[al,il,ol]},xl.prototype.getClass=function(){return xl},xl.sqr=function(t){return xl.valueOf(t).selfMultiply(t)},xl.valueOf=function(){if("string"==typeof arguments[0]){var t=arguments[0];return xl.parse(t)}if("number"==typeof arguments[0]){var e=arguments[0];return new xl(e)}},xl.sqrt=function(t){return xl.valueOf(t).sqrt()},xl.parse=function(t){for(var e=0,n=t.length;ml.isWhitespace(t.charAt(e));)e++;var r=!1;if(e=n);){var l=t.charAt(e);if(e++,ml.isDigit(l)){var c=l-"0";o.selfMultiply(xl.TEN),o.selfAdd(c),s++}else{if("."!==l){if("e"===l||"E"===l){var h=t.substring(e);try{u=_l.parseInt(h)}catch(e){throw e instanceof Error?new Error("Invalid exponent "+h+" in string "+t):e}break}throw new Error("Unexpected character '"+l+"' at position "+e+" in string "+t)}a=s}}var p=o,f=s-a-u;if(0===f)p=o;else if(f>0){var g=xl.TEN.pow(f);p=o.divide(g)}else if(f<0){var d=xl.TEN.pow(-f);p=o.multiply(d)}return r?p.negate():p},xl.createNaN=function(){return new xl(nl.NaN,nl.NaN)},xl.copy=function(t){return new xl(t)},xl.magnitude=function(t){var e=Math.abs(t),n=Math.log(e)/Math.log(10),r=Math.trunc(Math.floor(n));return 10*Math.pow(10,r)<=e&&(r+=1),r},xl.stringOfChar=function(t,e){for(var n=new vl,r=0;r0){if(o<=0)return bl.signum(s);r=i+o}else{if(!(i<0))return bl.signum(s);if(o>=0)return bl.signum(s);r=-i-o}var a=bl.DP_SAFE_EPSILON*r;return s>=a||-s>=a?bl.signum(s):2},bl.signum=function(t){return t>0?1:t<0?-1:0},wl.DP_SAFE_EPSILON.get=function(){return 1e-15},Object.defineProperties(bl,wl);var Il=function(){},Nl={X:{configurable:!0},Y:{configurable:!0},Z:{configurable:!0},M:{configurable:!0}};Nl.X.get=function(){return 0},Nl.Y.get=function(){return 1},Nl.Z.get=function(){return 2},Nl.M.get=function(){return 3},Il.prototype.setOrdinate=function(t,e,n){},Il.prototype.size=function(){},Il.prototype.getOrdinate=function(t,e){},Il.prototype.getCoordinate=function(){},Il.prototype.getCoordinateCopy=function(t){},Il.prototype.getDimension=function(){},Il.prototype.getX=function(t){},Il.prototype.clone=function(){},Il.prototype.expandEnvelope=function(t){},Il.prototype.copy=function(){},Il.prototype.getY=function(t){},Il.prototype.toCoordinateArray=function(){},Il.prototype.interfaces_=function(){return[ol]},Il.prototype.getClass=function(){return Il},Object.defineProperties(Il,Nl);var Sl=function(){},Cl=function(t){function e(){t.call(this,"Projective point not representable on the Cartesian plane.")}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(Sl),Pl=function(){};Pl.arraycopy=function(t,e,n,r,i){for(var o=0,s=e;st._minx?this._minx:t._minx,n=this._miny>t._miny?this._miny:t._miny,r=this._maxx=this._minx&&e.getMaxX()<=this._maxx&&e.getMinY()>=this._miny&&e.getMaxY()<=this._maxy)}}else if(2===arguments.length){var n=arguments[0],r=arguments[1];return!this.isNull()&&(n>=this._minx&&n<=this._maxx&&r>=this._miny&&r<=this._maxy)}},Ll.prototype.intersects=function(){if(1===arguments.length){if(arguments[0]instanceof Ll){var t=arguments[0];return!this.isNull()&&!t.isNull()&&!(t._minx>this._maxx||t._maxxthis._maxy||t._maxythis._maxx||nthis._maxy||rthis._maxx&&(this._maxx=e._maxx),e._minythis._maxy&&(this._maxy=e._maxy))}}else if(2===arguments.length){var n=arguments[0],r=arguments[1];this.isNull()?(this._minx=n,this._maxx=n,this._miny=r,this._maxy=r):(nthis._maxx&&(this._maxx=n),rthis._maxy&&(this._maxy=r))}},Ll.prototype.minExtent=function(){if(this.isNull())return 0;var t=this.getWidth(),e=this.getHeight();return te._minx?1:this._minye._miny?1:this._maxxe._maxx?1:this._maxye._maxy?1:0},Ll.prototype.translate=function(t,e){if(this.isNull())return null;this.init(this.getMinX()+t,this.getMaxX()+t,this.getMinY()+e,this.getMaxY()+e)},Ll.prototype.toString=function(){return"Env["+this._minx+" : "+this._maxx+", "+this._miny+" : "+this._maxy+"]"},Ll.prototype.setToNull=function(){this._minx=0,this._maxx=-1,this._miny=0,this._maxy=-1},Ll.prototype.getHeight=function(){return this.isNull()?0:this._maxy-this._miny},Ll.prototype.maxExtent=function(){if(this.isNull())return 0;var t=this.getWidth(),e=this.getHeight();return t>e?t:e},Ll.prototype.expandBy=function(){if(1===arguments.length){var t=arguments[0];this.expandBy(t,t)}else if(2===arguments.length){var e=arguments[0],n=arguments[1];if(this.isNull())return null;this._minx-=e,this._maxx+=e,this._miny-=n,this._maxy+=n,(this._minx>this._maxx||this._miny>this._maxy)&&this.setToNull()}},Ll.prototype.contains=function(){if(1===arguments.length){if(arguments[0]instanceof Ll){var t=arguments[0];return this.covers(t)}if(arguments[0]instanceof ul){var e=arguments[0];return this.covers(e)}}else if(2===arguments.length){var n=arguments[0],r=arguments[1];return this.covers(n,r)}},Ll.prototype.centre=function(){return this.isNull()?null:new ul((this.getMinX()+this.getMaxX())/2,(this.getMinY()+this.getMaxY())/2)},Ll.prototype.init=function(){if(0===arguments.length)this.setToNull();else if(1===arguments.length){if(arguments[0]instanceof ul){var t=arguments[0];this.init(t.x,t.x,t.y,t.y)}else if(arguments[0]instanceof Ll){var e=arguments[0];this._minx=e._minx,this._maxx=e._maxx,this._miny=e._miny,this._maxy=e._maxy}}else if(2===arguments.length){var n=arguments[0],r=arguments[1];this.init(n.x,r.x,n.y,r.y)}else if(4===arguments.length){var i=arguments[0],o=arguments[1],s=arguments[2],a=arguments[3];it._maxx&&(e=this._minx-t._maxx);var n=0;return this._maxyt._maxy&&(n=this._miny-t._maxy),0===e?n:0===n?e:Math.sqrt(e*e+n*n)},Ll.prototype.hashCode=function(){var t=17;return t=37*(t=37*(t=37*(t=37*t+ul.hashCode(this._minx))+ul.hashCode(this._maxx))+ul.hashCode(this._miny))+ul.hashCode(this._maxy)},Ll.prototype.interfaces_=function(){return[il,al]},Ll.prototype.getClass=function(){return Ll},Ll.intersects=function(){if(3===arguments.length){var t=arguments[0],e=arguments[1],n=arguments[2];return n.x>=(t.xe.x?t.x:e.x)&&n.y>=(t.ye.y?t.y:e.y)}if(4===arguments.length){var r=arguments[0],i=arguments[1],o=arguments[2],s=arguments[3],a=Math.min(o.x,s.x),u=Math.max(o.x,s.x),l=Math.min(r.x,i.x),c=Math.max(r.x,i.x);return!(l>u)&&(!(cu)&&!(cn?(this._intLineIndex[t][0]=0,this._intLineIndex[t][1]=1):(this._intLineIndex[t][0]=1,this._intLineIndex[t][1]=0)}},Bl.prototype.isProper=function(){return this.hasIntersection()&&this._isProper},Bl.prototype.setPrecisionModel=function(t){this._precisionModel=t},Bl.prototype.isInteriorIntersection=function(){var t=this;if(0===arguments.length)return!!this.isInteriorIntersection(0)||!!this.isInteriorIntersection(1);if(1===arguments.length){for(var e=arguments[0],n=0;ni?r:i;else{var s=Math.abs(t.x-e.x),a=Math.abs(t.y-e.y);0!==(o=r>i?s:a)||t.equals(e)||(o=Math.max(s,a))}return ql.isTrue(!(0===o&&!t.equals(e)),"Bad distance calculation"),o},Bl.nonRobustComputeEdgeDistance=function(t,e,n){var r=t.x-e.x,i=t.y-e.y,o=Math.sqrt(r*r+i*i);return ql.isTrue(!(0===o&&!t.equals(e)),"Invalid distance calculation"),o},zl.DONT_INTERSECT.get=function(){return 0},zl.DO_INTERSECT.get=function(){return 1},zl.COLLINEAR.get=function(){return 2},zl.NO_INTERSECTION.get=function(){return 0},zl.POINT_INTERSECTION.get=function(){return 1},zl.COLLINEAR_INTERSECTION.get=function(){return 2},Object.defineProperties(Bl,zl);var jl=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.isInSegmentEnvelopes=function(t){var e=new Ll(this._inputLines[0][0],this._inputLines[0][1]),n=new Ll(this._inputLines[1][0],this._inputLines[1][1]);return e.contains(t)&&n.contains(t)},e.prototype.computeIntersection=function(){if(3!==arguments.length)return t.prototype.computeIntersection.apply(this,arguments);var e=arguments[0],n=arguments[1],r=arguments[2];if(this._isProper=!1,Ll.intersects(n,r,e)&&0===Xl.orientationIndex(n,r,e)&&0===Xl.orientationIndex(r,n,e))return this._isProper=!0,(e.equals(n)||e.equals(r))&&(this._isProper=!1),this._result=t.POINT_INTERSECTION,null;this._result=t.NO_INTERSECTION},e.prototype.normalizeToMinimum=function(t,e,n,r,i){i.x=this.smallestInAbsValue(t.x,e.x,n.x,r.x),i.y=this.smallestInAbsValue(t.y,e.y,n.y,r.y),t.x-=i.x,t.y-=i.y,e.x-=i.x,e.y-=i.y,n.x-=i.x,n.y-=i.y,r.x-=i.x,r.y-=i.y},e.prototype.safeHCoordinateIntersection=function(t,n,r,i){var o=null;try{o=Ml.intersection(t,n,r,i)}catch(s){if(!(s instanceof Cl))throw s;o=e.nearestEndpoint(t,n,r,i)}return o},e.prototype.intersection=function(t,n,r,i){var o=this.intersectionWithNormalization(t,n,r,i);return this.isInSegmentEnvelopes(o)||(o=new ul(e.nearestEndpoint(t,n,r,i))),null!==this._precisionModel&&this._precisionModel.makePrecise(o),o},e.prototype.smallestInAbsValue=function(t,e,n,r){var i=t,o=Math.abs(i);return Math.abs(e)1e-4&&Pl.out.println("Distance = "+i.distance(o))},e.prototype.intersectionWithNormalization=function(t,e,n,r){var i=new ul(t),o=new ul(e),s=new ul(n),a=new ul(r),u=new ul;this.normalizeToEnvCentre(i,o,s,a,u);var l=this.safeHCoordinateIntersection(i,o,s,a);return l.x+=u.x,l.y+=u.y,l},e.prototype.computeCollinearIntersection=function(e,n,r,i){var o=Ll.intersects(e,n,r),s=Ll.intersects(e,n,i),a=Ll.intersects(r,i,e),u=Ll.intersects(r,i,n);return o&&s?(this._intPt[0]=r,this._intPt[1]=i,t.COLLINEAR_INTERSECTION):a&&u?(this._intPt[0]=e,this._intPt[1]=n,t.COLLINEAR_INTERSECTION):o&&a?(this._intPt[0]=r,this._intPt[1]=e,!r.equals(e)||s||u?t.COLLINEAR_INTERSECTION:t.POINT_INTERSECTION):o&&u?(this._intPt[0]=r,this._intPt[1]=n,!r.equals(n)||s||a?t.COLLINEAR_INTERSECTION:t.POINT_INTERSECTION):s&&a?(this._intPt[0]=i,this._intPt[1]=e,!i.equals(e)||o||u?t.COLLINEAR_INTERSECTION:t.POINT_INTERSECTION):s&&u?(this._intPt[0]=i,this._intPt[1]=n,!i.equals(n)||o||a?t.COLLINEAR_INTERSECTION:t.POINT_INTERSECTION):t.NO_INTERSECTION},e.prototype.normalizeToEnvCentre=function(t,e,n,r,i){var o=t.xe.x?t.x:e.x,u=t.y>e.y?t.y:e.y,l=n.xr.x?n.x:r.x,p=n.y>r.y?n.y:r.y,f=((o>l?o:l)+(ac?s:c)+(u0&&s>0||o<0&&s<0)return t.NO_INTERSECTION;var a=Xl.orientationIndex(r,i,e),u=Xl.orientationIndex(r,i,n);return a>0&&u>0||a<0&&u<0?t.NO_INTERSECTION:0===o&&0===s&&0===a&&0===u?this.computeCollinearIntersection(e,n,r,i):(0===o||0===s||0===a||0===u?(this._isProper=!1,e.equals2D(r)||e.equals2D(i)?this._intPt[0]=e:n.equals2D(r)||n.equals2D(i)?this._intPt[0]=n:0===o?this._intPt[0]=new ul(r):0===s?this._intPt[0]=new ul(i):0===a?this._intPt[0]=new ul(e):0===u&&(this._intPt[0]=new ul(n))):(this._isProper=!0,this._intPt[0]=this.intersection(e,n,r,i)),t.POINT_INTERSECTION)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e.nearestEndpoint=function(t,e,n,r){var i=t,o=Xl.distancePointLine(t,n,r),s=Xl.distancePointLine(e,n,r);return s0?n>0?-i:i:n>0?i:-i;if(0===e||0===n)return r>0?t>0?i:-i:t>0?-i:i;if(e>0?r>0?e<=r||(i=-i,o=t,t=n,n=o,o=e,e=r,r=o):e<=-r?(i=-i,n=-n,r=-r):(o=t,t=-n,n=o,o=e,e=-r,r=o):r>0?-e<=r?(i=-i,t=-t,e=-e):(o=-t,t=n,n=o,o=-e,e=r,r=o):e>=r?(t=-t,e=-e,n=-n,r=-r):(i=-i,o=-t,t=-n,n=o,o=-e,e=-r,r=o),t>0){if(!(n>0))return i;if(!(t<=n))return i}else{if(n>0)return-i;if(!(t>=n))return-i;i=-i,t=-t,n=-n}for(;;){if((r-=(s=Math.floor(n/t))*e)<0)return-i;if(r>e)return i;if(t>(n-=s*t)+n){if(er+r)return-i;n=t-n,r=e-r,i=-i}if(0===r)return 0===n?0:-i;if(0===n)return i;if((e-=(s=Math.floor(t/n))*r)<0)return i;if(e>r)return-i;if(n>(t-=s*n)+t){if(re+e)return i;t=n-t,e=r-e,i=-i}if(0===e)return 0===t?0:i;if(0===t)return-i}};var Vl=function(){this._p=null,this._crossingCount=0,this._isPointOnSegment=!1;var t=arguments[0];this._p=t};Vl.prototype.countSegment=function(t,e){if(t.xr&&(n=e.x,r=t.x),this._p.x>=n&&this._p.x<=r&&(this._isPointOnSegment=!0),null}if(t.y>this._p.y&&e.y<=this._p.y||e.y>this._p.y&&t.y<=this._p.y){var i=t.x-this._p.x,o=t.y-this._p.y,s=e.x-this._p.x,a=e.y-this._p.y,u=Ul.signOfDet2x2(i,o,s,a);if(0===u)return this._isPointOnSegment=!0,null;a0&&this._crossingCount++}},Vl.prototype.isPointInPolygon=function(){return this.getLocation()!==pl.EXTERIOR},Vl.prototype.getLocation=function(){return this._isPointOnSegment?pl.BOUNDARY:this._crossingCount%2==1?pl.INTERIOR:pl.EXTERIOR},Vl.prototype.isOnSegment=function(){return this._isPointOnSegment},Vl.prototype.interfaces_=function(){return[]},Vl.prototype.getClass=function(){return Vl},Vl.locatePointInRing=function(){if(arguments[0]instanceof ul&&gl(arguments[1],Il)){for(var t=arguments[0],e=arguments[1],n=new Vl(t),r=new ul,i=new ul,o=1;o1||a<0||a>1)&&(i=!0)}}else i=!0;return i?dl.min(Xl.distancePointLine(t,n,r),Xl.distancePointLine(e,n,r),Xl.distancePointLine(n,t,e),Xl.distancePointLine(r,t,e)):0},Xl.isPointInRing=function(t,e){return Xl.locatePointInRing(t,e)!==pl.EXTERIOR},Xl.computeLength=function(t){var e=t.size();if(e<=1)return 0;var n=0,r=new ul;t.getCoordinate(0,r);for(var i=r.x,o=r.y,s=1;sn.y&&(n=o,r=i)}var s=r;do{(s-=1)<0&&(s=e)}while(t[s].equals2D(n)&&s!==r);var a=r;do{a=(a+1)%e}while(t[a].equals2D(n)&&a!==r);var u=t[s],l=t[a];if(u.equals2D(n)||l.equals2D(n)||u.equals2D(l))return!1;var c=Xl.computeOrientation(u,n,l),h=!1;return h=0===c?u.x>l.x:c>0,h},Xl.locatePointInRing=function(t,e){return Vl.locatePointInRing(t,e)},Xl.distancePointLinePerpendicular=function(t,e,n){var r=(n.x-e.x)*(n.x-e.x)+(n.y-e.y)*(n.y-e.y),i=((e.y-t.y)*(n.x-e.x)-(e.x-t.x)*(n.y-e.y))/r;return Math.abs(i)*Math.sqrt(r)},Xl.computeOrientation=function(t,e,n){return Xl.orientationIndex(t,e,n)},Xl.distancePointLine=function(){if(2===arguments.length){var t=arguments[0],e=arguments[1];if(0===e.length)throw new el("Line array must contain at least one vertex");for(var n=t.distance(e[0]),r=0;r=1)return o.distance(a);var c=((s.y-o.y)*(a.x-s.x)-(s.x-o.x)*(a.y-s.y))/u;return Math.abs(c)*Math.sqrt(u)}},Xl.isOnLine=function(t,e){for(var n=new jl,r=1;r0},ec.prototype.interfaces_=function(){return[Ql]},ec.prototype.getClass=function(){return ec};var nc=function(){};nc.prototype.isInBoundary=function(t){return t>1},nc.prototype.interfaces_=function(){return[Ql]},nc.prototype.getClass=function(){return nc};var rc=function(){};rc.prototype.isInBoundary=function(t){return 1===t},rc.prototype.interfaces_=function(){return[Ql]},rc.prototype.getClass=function(){return rc};var ic=function(){};function oc(t){this.message=t||""}ic.prototype.add=function(){},ic.prototype.addAll=function(){},ic.prototype.isEmpty=function(){},ic.prototype.iterator=function(){},ic.prototype.size=function(){},ic.prototype.toArray=function(){},ic.prototype.remove=function(){},oc.prototype=new Error,oc.prototype.name="IndexOutOfBoundsException";var sc=function(){};sc.prototype.hasNext=function(){},sc.prototype.next=function(){},sc.prototype.remove=function(){};var ac=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.get=function(){},e.prototype.set=function(){},e.prototype.isEmpty=function(){},e}(ic);function uc(t){this.message=t||""}uc.prototype=new Error,uc.prototype.name="NoSuchElementException";var lc=function(t){function e(){t.call(this),this.array_=[],arguments[0]instanceof ic&&this.addAll(arguments[0])}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.ensureCapacity=function(){},e.prototype.interfaces_=function(){return[t,ic]},e.prototype.add=function(t){return 1===arguments.length?this.array_.push(t):this.array_.splice(arguments[0],arguments[1]),!0},e.prototype.clear=function(){this.array_=[]},e.prototype.addAll=function(t){for(var e=t.iterator();e.hasNext();)this.add(e.next());return!0},e.prototype.set=function(t,e){var n=this.array_[t];return this.array_[t]=e,n},e.prototype.iterator=function(){return new cc(this)},e.prototype.get=function(t){if(t<0||t>=this.size())throw new oc;return this.array_[t]},e.prototype.isEmpty=function(){return 0===this.array_.length},e.prototype.size=function(){return this.array_.length},e.prototype.toArray=function(){for(var t=[],e=0,n=this.array_.length;e=1){var a=this.get(this.size()-1);if(a.equals2D(o))return null}t.prototype.add.call(this,o)}else if(arguments[0]instanceof Object&&"boolean"==typeof arguments[1]){var u=arguments[0],l=arguments[1];return this.add(u,l),!0}}else if(3===arguments.length){if("boolean"==typeof arguments[2]&&arguments[0]instanceof Array&&"boolean"==typeof arguments[1]){var c=arguments[0],h=arguments[1],p=arguments[2];if(p)for(var f=0;f=0;g--)e.add(c[g],h);return!0}if("boolean"==typeof arguments[2]&&Number.isInteger(arguments[0])&&arguments[1]instanceof ul){var d=arguments[0],y=arguments[1],v=arguments[2];if(!v){var _=this.size();if(_>0){if(d>0){var m=this.get(d-1);if(m.equals2D(y))return null}if(d<_){var x=this.get(d);if(x.equals2D(y))return null}}}t.prototype.add.call(this,d,y)}}else if(4===arguments.length){var E=arguments[0],b=arguments[1],w=arguments[2],I=arguments[3],N=1;w>I&&(N=-1);for(var S=w;S!==I;S+=N)e.add(E[S],b);return!0}},e.prototype.closeRing=function(){this.size()>0&&this.add(new ul(this.get(0)),!1)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},Object.defineProperties(e,n),e}(lc),pc=function(){},fc={ForwardComparator:{configurable:!0},BidirectionalComparator:{configurable:!0},coordArrayType:{configurable:!0}};fc.ForwardComparator.get=function(){return gc},fc.BidirectionalComparator.get=function(){return dc},fc.coordArrayType.get=function(){return new Array(0).fill(null)},pc.prototype.interfaces_=function(){return[]},pc.prototype.getClass=function(){return pc},pc.isRing=function(t){return!(t.length<4)&&!!t[0].equals2D(t[t.length-1])},pc.ptNotInList=function(t,e){for(var n=0;n=t?e:[]},pc.indexOf=function(t,e){for(var n=0;n0)&&(e=t[n]);return e},pc.extract=function(t,e,n){e=dl.clamp(e,0,t.length);var r=(n=dl.clamp(n,-1,t.length))-e+1;n<0&&(r=0),e>=t.length&&(r=0),nr.length)return 1;if(0===n.length)return 0;var i=pc.compare(n,r);return pc.isEqualReversed(n,r)?0:i},dc.prototype.OLDcompare=function(t,e){var n=t,r=e;if(n.lengthr.length)return 1;if(0===n.length)return 0;for(var i=pc.increasingDirection(n),o=pc.increasingDirection(r),s=i>0?0:n.length-1,a=o>0?0:n.length-1,u=0;u0))return e.value;e=e.right}}return null},Cc.prototype.put=function(t,e){if(null===this.root_)return this.root_={key:t,value:e,left:null,right:null,parent:null,color:0,getValue:function(){return this.value},getKey:function(){return this.key}},this.size_=1,null;var n,r,i=this.root_;do{if(n=i,(r=t.compareTo(i.key))<0)i=i.left;else{if(!(r>0)){var o=i.value;return i.value=e,o}i=i.right}}while(null!==i);var s={key:t,left:null,right:null,value:e,parent:n,color:0,getValue:function(){return this.value},getKey:function(){return this.key}};return r<0?n.left=s:n.right=s,this.fixAfterInsertion(s),this.size_++,null},Cc.prototype.fixAfterInsertion=function(t){var e=this;for(t.color=1;null!=t&&t!==this.root_&&1===t.parent.color;)if(wc(t)===Nc(wc(wc(t)))){var n=Sc(wc(wc(t)));1===bc(n)?(Ic(wc(t),0),Ic(n,0),Ic(wc(wc(t)),1),t=wc(wc(t))):(t===Sc(wc(t))&&(t=wc(t),e.rotateLeft(t)),Ic(wc(t),0),Ic(wc(wc(t)),1),e.rotateRight(wc(wc(t))))}else{var r=Nc(wc(wc(t)));1===bc(r)?(Ic(wc(t),0),Ic(r,0),Ic(wc(wc(t)),1),t=wc(wc(t))):(t===Nc(wc(t))&&(t=wc(t),e.rotateRight(t)),Ic(wc(t),0),Ic(wc(wc(t)),1),e.rotateLeft(wc(wc(t))))}this.root_.color=0},Cc.prototype.values=function(){var t=new lc,e=this.getFirstEntry();if(null!==e)for(t.add(e.value);null!==(e=Cc.successor(e));)t.add(e.value);return t},Cc.prototype.entrySet=function(){var t=new xc,e=this.getFirstEntry();if(null!==e)for(t.add(e);null!==(e=Cc.successor(e));)t.add(e);return t},Cc.prototype.rotateLeft=function(t){if(null!=t){var e=t.right;t.right=e.left,null!=e.left&&(e.left.parent=t),e.parent=t.parent,null===t.parent?this.root_=e:t.parent.left===t?t.parent.left=e:t.parent.right=e,e.left=t,t.parent=e}},Cc.prototype.rotateRight=function(t){if(null!=t){var e=t.left;t.left=e.right,null!=e.right&&(e.right.parent=t),e.parent=t.parent,null===t.parent?this.root_=e:t.parent.right===t?t.parent.right=e:t.parent.left=e,e.right=t,t.parent=e}},Cc.prototype.getFirstEntry=function(){var t=this.root_;if(null!=t)for(;null!=t.left;)t=t.left;return t},Cc.successor=function(t){if(null===t)return null;if(null!==t.right){for(var e=t.right;null!==e.left;)e=e.left;return e}for(var n=t.parent,r=t;null!==n&&r===n.right;)r=n,n=n.parent;return n},Cc.prototype.size=function(){return this.size_};var Pc=function(){};function Mc(){}function Lc(){this.array_=[],arguments[0]instanceof ic&&this.addAll(arguments[0])}Pc.prototype.interfaces_=function(){return[]},Pc.prototype.getClass=function(){return Pc},Mc.prototype=new mc,Lc.prototype=new Mc,Lc.prototype.contains=function(t){for(var e=0,n=this.array_.length;e=0;){var s=i.substring(0,o);r.add(s),o=(i=i.substring(o+n)).indexOf(e)}i.length>0&&r.add(i);for(var a=new Array(r.size()).fill(null),u=0;u0)for(var o=i;o0&&r.append(" ");for(var o=0;o0&&r.append(","),r.append(Hc.toString(t.getOrdinate(i,o)))}return r.append(")"),r.toString()}},Jc.ensureValidRing=function(t,e){var n=e.size();return 0===n?e:n<=3?Jc.createClosedRing(t,e,4):e.getOrdinate(0,Il.X)===e.getOrdinate(n-1,Il.X)&&e.getOrdinate(0,Il.Y)===e.getOrdinate(n-1,Il.Y)?e:Jc.createClosedRing(t,e,n+1)},Jc.createClosedRing=function(t,e,n){var r=t.create(n,e.getDimension()),i=e.size();Jc.copy(e,0,r,0,i);for(var o=i;o0&&Jc.reverse(t._points),null}},e.prototype.getCoordinate=function(){return this.isEmpty()?null:this._points.getCoordinate(0)},e.prototype.getBoundaryDimension=function(){return this.isClosed()?Tc.FALSE:0},e.prototype.isClosed=function(){return!this.isEmpty()&&this.getCoordinateN(0).equals2D(this.getCoordinateN(this.getNumPoints()-1))},e.prototype.getEndPoint=function(){return this.isEmpty()?null:this.getPointN(this.getNumPoints()-1)},e.prototype.getDimension=function(){return 1},e.prototype.getLength=function(){return Xl.computeLength(this._points)},e.prototype.getNumPoints=function(){return this._points.size()},e.prototype.reverse=function(){var t=this._points.copy();return Jc.reverse(t),this.getFactory().createLineString(t)},e.prototype.compareToSameClass=function(){var t=this;if(1===arguments.length){for(var e=arguments[0],n=e,r=0,i=0;r= 2)");this._points=t},e.prototype.isCoordinate=function(t){for(var e=0;e=1&&this.getCoordinateSequence().size()= 4)")},e.prototype.getGeometryType=function(){return"LinearRing"},e.prototype.copy=function(){return new e(this._points.copy(),this._factory)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},n.MINIMUM_VALID_SIZE.get=function(){return 4},n.serialVersionUID.get=function(){return-0x3b229e262367a600},Object.defineProperties(e,n),e}(Zc),rh=function(t){function e(){t.apply(this,arguments)}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={serialVersionUID:{configurable:!0}};return e.prototype.getSortIndex=function(){return Wl.SORTINDEX_MULTIPOLYGON},e.prototype.equalsExact=function(){if(2===arguments.length){var e=arguments[0],n=arguments[1];return!!this.isEquivalentClass(e)&&t.prototype.equalsExact.call(this,e,n)}return t.prototype.equalsExact.apply(this,arguments)},e.prototype.getBoundaryDimension=function(){return 1},e.prototype.getDimension=function(){return 2},e.prototype.reverse=function(){for(var t=this._geometries.length,e=new Array(t).fill(null),n=0;n0?e.createPoint(n[0]):e.createPoint():t},ah.prototype.interfaces_=function(){return[ih.GeometryEditorOperation]},ah.prototype.getClass=function(){return ah};var uh=function(){};uh.prototype.edit=function(t,e){return t instanceof nh?e.createLinearRing(this.edit(t.getCoordinateSequence(),t)):t instanceof Zc?e.createLineString(this.edit(t.getCoordinateSequence(),t)):t instanceof Qc?e.createPoint(this.edit(t.getCoordinateSequence(),t)):t},uh.prototype.interfaces_=function(){return[ih.GeometryEditorOperation]},uh.prototype.getClass=function(){return uh};var lh=function(){var t=this;if(this._dimension=3,this._coordinates=null,1===arguments.length){if(arguments[0]instanceof Array)this._coordinates=arguments[0],this._dimension=3;else if(Number.isInteger(arguments[0])){var e=arguments[0];this._coordinates=new Array(e).fill(null);for(var n=0;n0){var t=new vl(17*this._coordinates.length);t.append("("),t.append(this._coordinates[0]);for(var e=1;e3&&(r=3),r<2?new lh(n):new lh(n,r)}},hh.prototype.interfaces_=function(){return[hl,al]},hh.prototype.getClass=function(){return hh},hh.instance=function(){return hh.instanceObject},ph.serialVersionUID.get=function(){return-0x38e49fa6cf6f2e00},ph.instanceObject.get=function(){return new hh},Object.defineProperties(hh,ph);var fh=function(t){function e(){t.call(this),this.map_=new Map}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.get=function(t){return this.map_.get(t)||null},e.prototype.put=function(t,e){return this.map_.set(t,e),e},e.prototype.values=function(){for(var t=new lc,e=this.map_.values(),n=e.next();!n.done;)t.add(n.value),n=e.next();return t},e.prototype.entrySet=function(){var t=new xc;return this.map_.entries().forEach((function(e){return t.add(e)})),t},e.prototype.size=function(){return this.map_.size()},e}(yc),gh=function t(){if(this._modelType=null,this._scale=null,0===arguments.length)this._modelType=t.FLOATING;else if(1===arguments.length)if(arguments[0]instanceof yh){var e=arguments[0];this._modelType=e,e===t.FIXED&&this.setScale(1)}else if("number"==typeof arguments[0]){var n=arguments[0];this._modelType=t.FIXED,this.setScale(n)}else if(arguments[0]instanceof t){var r=arguments[0];this._modelType=r._modelType,this._scale=r._scale}},dh={serialVersionUID:{configurable:!0},maximumPreciseValue:{configurable:!0}};gh.prototype.equals=function(t){if(!(t instanceof gh))return!1;var e=t;return this._modelType===e._modelType&&this._scale===e._scale},gh.prototype.compareTo=function(t){var e=t,n=this.getMaximumSignificantDigits(),r=e.getMaximumSignificantDigits();return new _l(n).compareTo(new _l(r))},gh.prototype.getScale=function(){return this._scale},gh.prototype.isFloating=function(){return this._modelType===gh.FLOATING||this._modelType===gh.FLOATING_SINGLE},gh.prototype.getType=function(){return this._modelType},gh.prototype.toString=function(){var t="UNKNOWN";return this._modelType===gh.FLOATING?t="Floating":this._modelType===gh.FLOATING_SINGLE?t="Floating-Single":this._modelType===gh.FIXED&&(t="Fixed (Scale="+this.getScale()+")"),t},gh.prototype.makePrecise=function(){if("number"==typeof arguments[0]){var t=arguments[0];if(nl.isNaN(t))return t;if(this._modelType===gh.FLOATING_SINGLE){return t}return this._modelType===gh.FIXED?Math.round(t*this._scale)/this._scale:t}if(arguments[0]instanceof ul){var e=arguments[0];if(this._modelType===gh.FLOATING)return null;e.x=this.makePrecise(e.x),e.y=this.makePrecise(e.y)}},gh.prototype.getMaximumSignificantDigits=function(){var t=16;return this._modelType===gh.FLOATING?t=16:this._modelType===gh.FLOATING_SINGLE?t=6:this._modelType===gh.FIXED&&(t=1+Math.trunc(Math.ceil(Math.log(this.getScale())/Math.log(10)))),t},gh.prototype.setScale=function(t){this._scale=Math.abs(t)},gh.prototype.interfaces_=function(){return[al,il]},gh.prototype.getClass=function(){return gh},gh.mostPrecise=function(t,e){return t.compareTo(e)>=0?t:e},dh.serialVersionUID.get=function(){return 0x6bee6404e9a25c00},dh.maximumPreciseValue.get=function(){return 9007199254740992},Object.defineProperties(gh,dh);var yh=function t(e){this._name=e||null,t.nameToTypeMap.put(e,this)},vh={serialVersionUID:{configurable:!0},nameToTypeMap:{configurable:!0}};yh.prototype.readResolve=function(){return yh.nameToTypeMap.get(this._name)},yh.prototype.toString=function(){return this._name},yh.prototype.interfaces_=function(){return[al]},yh.prototype.getClass=function(){return yh},vh.serialVersionUID.get=function(){return-552860263173159e4},vh.nameToTypeMap.get=function(){return new fh},Object.defineProperties(yh,vh),gh.Type=yh,gh.FIXED=new yh("FIXED"),gh.FLOATING=new yh("FLOATING"),gh.FLOATING_SINGLE=new yh("FLOATING SINGLE");var _h=function t(){this._precisionModel=new gh,this._SRID=0,this._coordinateSequenceFactory=t.getDefaultCoordinateSequenceFactory(),0===arguments.length||(1===arguments.length?gl(arguments[0],hl)?this._coordinateSequenceFactory=arguments[0]:arguments[0]instanceof gh&&(this._precisionModel=arguments[0]):2===arguments.length?(this._precisionModel=arguments[0],this._SRID=arguments[1]):3===arguments.length&&(this._precisionModel=arguments[0],this._SRID=arguments[1],this._coordinateSequenceFactory=arguments[2]))},mh={serialVersionUID:{configurable:!0}};_h.prototype.toGeometry=function(t){return t.isNull()?this.createPoint(null):t.getMinX()===t.getMaxX()&&t.getMinY()===t.getMaxY()?this.createPoint(new ul(t.getMinX(),t.getMinY())):t.getMinX()===t.getMaxX()||t.getMinY()===t.getMaxY()?this.createLineString([new ul(t.getMinX(),t.getMinY()),new ul(t.getMaxX(),t.getMaxY())]):this.createPolygon(this.createLinearRing([new ul(t.getMinX(),t.getMinY()),new ul(t.getMinX(),t.getMaxY()),new ul(t.getMaxX(),t.getMaxY()),new ul(t.getMaxX(),t.getMinY()),new ul(t.getMinX(),t.getMinY())]),null)},_h.prototype.createLineString=function(t){return t?t instanceof Array?new Zc(this.getCoordinateSequenceFactory().create(t),this):gl(t,Il)?new Zc(t,this):void 0:new Zc(this.getCoordinateSequenceFactory().create([]),this)},_h.prototype.createMultiLineString=function(){if(0===arguments.length)return new Gc(null,this);if(1===arguments.length){var t=arguments[0];return new Gc(t,this)}},_h.prototype.buildGeometry=function(t){for(var e=null,n=!1,r=!1,i=t.iterator();i.hasNext();){var o=i.next(),s=o.getClass();null===e&&(e=s),s!==e&&(n=!0),o.isGeometryCollectionOrDerived()&&(r=!0)}if(null===e)return this.createGeometryCollection();if(n||r)return this.createGeometryCollection(_h.toGeometryArray(t));var a=t.iterator().next();if(t.size()>1){if(a instanceof th)return this.createMultiPolygon(_h.toPolygonArray(t));if(a instanceof Zc)return this.createMultiLineString(_h.toLineStringArray(t));if(a instanceof Qc)return this.createMultiPoint(_h.toPointArray(t));ql.shouldNeverReachHere("Unhandled class: "+a.getClass().getName())}return a},_h.prototype.createMultiPointFromCoords=function(t){return this.createMultiPoint(null!==t?this.getCoordinateSequenceFactory().create(t):null)},_h.prototype.createPoint=function(){if(0===arguments.length)return this.createPoint(this.getCoordinateSequenceFactory().create([]));if(1===arguments.length){if(arguments[0]instanceof ul){var t=arguments[0];return this.createPoint(null!==t?this.getCoordinateSequenceFactory().create([t]):null)}if(gl(arguments[0],Il)){var e=arguments[0];return new Qc(e,this)}}},_h.prototype.getCoordinateSequenceFactory=function(){return this._coordinateSequenceFactory},_h.prototype.createPolygon=function(){if(0===arguments.length)return new th(null,null,this);if(1===arguments.length){if(gl(arguments[0],Il)){var t=arguments[0];return this.createPolygon(this.createLinearRing(t))}if(arguments[0]instanceof Array){var e=arguments[0];return this.createPolygon(this.createLinearRing(e))}if(arguments[0]instanceof nh){var n=arguments[0];return this.createPolygon(n,null)}}else if(2===arguments.length){var r=arguments[0],i=arguments[1];return new th(r,i,this)}},_h.prototype.getSRID=function(){return this._SRID},_h.prototype.createGeometryCollection=function(){if(0===arguments.length)return new kc(null,this);if(1===arguments.length){var t=arguments[0];return new kc(t,this)}},_h.prototype.createGeometry=function(t){return new ih(this).edit(t,{edit:function(){if(2===arguments.length){var t=arguments[0];return this._coordinateSequenceFactory.create(t)}}})},_h.prototype.getPrecisionModel=function(){return this._precisionModel},_h.prototype.createLinearRing=function(){if(0===arguments.length)return this.createLinearRing(this.getCoordinateSequenceFactory().create([]));if(1===arguments.length){if(arguments[0]instanceof Array){var t=arguments[0];return this.createLinearRing(null!==t?this.getCoordinateSequenceFactory().create(t):null)}if(gl(arguments[0],Il)){var e=arguments[0];return new nh(e,this)}}},_h.prototype.createMultiPolygon=function(){if(0===arguments.length)return new rh(null,this);if(1===arguments.length){var t=arguments[0];return new rh(t,this)}},_h.prototype.createMultiPoint=function(){var t=this;if(0===arguments.length)return new eh(null,this);if(1===arguments.length){if(arguments[0]instanceof Array){var e=arguments[0];return new eh(e,this)}if(arguments[0]instanceof Array){var n=arguments[0];return this.createMultiPoint(null!==n?this.getCoordinateSequenceFactory().create(n):null)}if(gl(arguments[0],Il)){var r=arguments[0];if(null===r)return this.createMultiPoint(new Array(0).fill(null));for(var i=new Array(r.size()).fill(null),o=0;o=this.size())throw new Error;return this.array_[t]},Mh.prototype.push=function(t){return this.array_.push(t),t},Mh.prototype.pop=function(t){if(0===this.array_.length)throw new Ph;return this.array_.pop()},Mh.prototype.peek=function(){if(0===this.array_.length)throw new Ph;return this.array_[this.array_.length-1]},Mh.prototype.empty=function(){return 0===this.array_.length},Mh.prototype.isEmpty=function(){return this.empty()},Mh.prototype.search=function(t){return this.array_.indexOf(t)},Mh.prototype.size=function(){return this.array_.length},Mh.prototype.toArray=function(){for(var t=[],e=0,n=this.array_.length;e0&&this._minIndexthis._minCoord.y&&n.y>this._minCoord.y&&r===Xl.CLOCKWISE)&&(i=!0),i&&(this._minIndex=this._minIndex-1)},Lh.prototype.getRightmostSideOfSegment=function(t,e){var n=t.getEdge().getCoordinates();if(e<0||e+1>=n.length)return-1;if(n[e].y===n[e+1].y)return-1;var r=Sh.LEFT;return n[e].ye._minCoord.x)&&(e._minDe=t,e._minIndex=r,e._minCoord=n[r])},Lh.prototype.findRightmostEdgeAtNode=function(){var t=this._minDe.getNode().getEdges();this._minDe=t.getRightmostEdge(),this._minDe.isForward()||(this._minDe=this._minDe.getSym(),this._minIndex=this._minDe.getEdge().getCoordinates().length-1)},Lh.prototype.findEdge=function(t){for(var e=t.iterator();e.hasNext();){var n=e.next();n.isForward()&&this.checkForRightmostCoordinate(n)}ql.isTrue(0!==this._minIndex||this._minCoord.equals(this._minDe.getCoordinate()),"inconsistency in rightmost processing"),0===this._minIndex?this.findRightmostEdgeAtNode():this.findRightmostEdgeAtVertex(),this._orientedDe=this._minDe,this.getRightmostSide(this._minDe,this._minIndex)===Sh.LEFT&&(this._orientedDe=this._minDe.getSym())},Lh.prototype.interfaces_=function(){return[]},Lh.prototype.getClass=function(){return Lh};var Oh=function(t){function e(n,r){t.call(this,e.msgWithCoord(n,r)),this.pt=r?new ul(r):null,this.name="TopologyException"}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.getCoordinate=function(){return this.pt},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e.msgWithCoord=function(t,e){return e?t:t+" [ "+e+" ]"},e}(kl),Rh=function(){this.array_=[]};Rh.prototype.addLast=function(t){this.array_.push(t)},Rh.prototype.removeFirst=function(){return this.array_.shift()},Rh.prototype.isEmpty=function(){return 0===this.array_.length};var Th=function(){this._finder=null,this._dirEdgeList=new lc,this._nodes=new lc,this._rightMostCoord=null,this._env=null,this._finder=new Lh};Th.prototype.clearVisitedEdges=function(){for(var t=this._dirEdgeList.iterator();t.hasNext();){t.next().setVisited(!1)}},Th.prototype.getRightmostCoordinate=function(){return this._rightMostCoord},Th.prototype.computeNodeDepth=function(t){for(var e=null,n=t.getEdges().iterator();n.hasNext();){var r=n.next();if(r.isVisited()||r.getSym().isVisited()){e=r;break}}if(null===e)throw new Oh("unable to find edge to compute depths at "+t.getCoordinate());t.getEdges().computeDepths(e);for(var i=t.getEdges().iterator();i.hasNext();){var o=i.next();o.setVisited(!0),this.copySymDepths(o)}},Th.prototype.computeDepth=function(t){this.clearVisitedEdges();var e=this._finder.getEdge();e.setEdgeDepths(Sh.RIGHT,t),this.copySymDepths(e),this.computeDepths(e)},Th.prototype.create=function(t){this.addReachable(t),this._finder.findEdge(this._dirEdgeList),this._rightMostCoord=this._finder.getCoordinate()},Th.prototype.findResultEdges=function(){for(var t=this._dirEdgeList.iterator();t.hasNext();){var e=t.next();e.getDepth(Sh.RIGHT)>=1&&e.getDepth(Sh.LEFT)<=0&&!e.isInteriorAreaEdge()&&e.setInResult(!0)}},Th.prototype.computeDepths=function(t){var e=new xc,n=new Rh,r=t.getNode();for(n.addLast(r),e.add(r),t.setVisited(!0);!n.isEmpty();){var i=n.removeFirst();e.add(i),this.computeNodeDepth(i);for(var o=i.getEdges().iterator();o.hasNext();){var s=o.next().getSym();if(!s.isVisited()){var a=s.getNode();e.contains(a)||(n.addLast(a),e.add(a))}}}},Th.prototype.compareTo=function(t){var e=t;return this._rightMostCoord.xe._rightMostCoord.x?1:0},Th.prototype.getEnvelope=function(){if(null===this._env){for(var t=new Ll,e=this._dirEdgeList.iterator();e.hasNext();)for(var n=e.next().getEdge().getCoordinates(),r=0;rthis.location.length){var e=new Array(3).fill(null);e[Sh.ON]=this.location[Sh.ON],e[Sh.LEFT]=pl.NONE,e[Sh.RIGHT]=pl.NONE,this.location=e}for(var n=0;n1&&t.append(pl.toLocationSymbol(this.location[Sh.LEFT])),t.append(pl.toLocationSymbol(this.location[Sh.ON])),this.location.length>1&&t.append(pl.toLocationSymbol(this.location[Sh.RIGHT])),t.toString()},Ah.prototype.setLocations=function(t,e,n){this.location[Sh.ON]=t,this.location[Sh.LEFT]=e,this.location[Sh.RIGHT]=n},Ah.prototype.get=function(t){return t1},Ah.prototype.isAnyNull=function(){for(var t=0;tt._maxNodeDegree&&(t._maxNodeDegree=n),e=t.getNext(e)}while(e!==this._startDe);this._maxNodeDegree*=2},Fh.prototype.addPoints=function(t,e,n){var r=t.getCoordinates();if(e){var i=1;n&&(i=0);for(var o=i;o=0;a--)this._pts.add(r[a])}},Fh.prototype.isHole=function(){return this._isHole},Fh.prototype.setInResult=function(){var t=this._startDe;do{t.getEdge().setInResult(!0),t=t.getNext()}while(t!==this._startDe)},Fh.prototype.containsPoint=function(t){var e=this.getLinearRing();if(!e.getEnvelopeInternal().contains(t))return!1;if(!Xl.isPointInRing(t,e.getCoordinates()))return!1;for(var n=this._holes.iterator();n.hasNext();){if(n.next().containsPoint(t))return!1}return!0},Fh.prototype.addHole=function(t){this._holes.add(t)},Fh.prototype.isShell=function(){return null===this._shell},Fh.prototype.getLabel=function(){return this._label},Fh.prototype.getEdges=function(){return this._edges},Fh.prototype.getMaxNodeDegree=function(){return this._maxNodeDegree<0&&this.computeMaxNodeDegree(),this._maxNodeDegree},Fh.prototype.getShell=function(){return this._shell},Fh.prototype.mergeLabel=function(){if(1===arguments.length){var t=arguments[0];this.mergeLabel(t,0),this.mergeLabel(t,1)}else if(2===arguments.length){var e=arguments[0],n=arguments[1],r=e.getLocation(n,Sh.RIGHT);if(r===pl.NONE)return null;if(this._label.getLocation(n)===pl.NONE)return this._label.setLocation(n,r),null}},Fh.prototype.setShell=function(t){this._shell=t,null!==t&&t.addHole(this)},Fh.prototype.toPolygon=function(t){for(var e=new Array(this._holes.size()).fill(null),n=0;n=2,"found partial label"),this.computeIM(t)},qh.prototype.isInResult=function(){return this._isInResult},qh.prototype.isVisited=function(){return this._isVisited},qh.prototype.interfaces_=function(){return[]},qh.prototype.getClass=function(){return qh};var Bh=function(t){function e(){t.call(this),this._coord=null,this._edges=null;var e=arguments[0],n=arguments[1];this._coord=e,this._edges=n,this._label=new Dh(0,pl.NONE)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.isIncidentEdgeInResult=function(){for(var t=this.getEdges().getEdges().iterator();t.hasNext();){if(t.next().getEdge().isInResult())return!0}return!1},e.prototype.isIsolated=function(){return 1===this._label.getGeometryCount()},e.prototype.getCoordinate=function(){return this._coord},e.prototype.print=function(t){t.println("node "+this._coord+" lbl: "+this._label)},e.prototype.computeIM=function(t){},e.prototype.computeMergedLocation=function(t,e){var n=pl.NONE;if(n=this._label.getLocation(e),!t.isNull(e)){var r=t.getLocation(e);n!==pl.BOUNDARY&&(n=r)}return n},e.prototype.setLabel=function(){if(2!==arguments.length)return t.prototype.setLabel.apply(this,arguments);var e=arguments[0],n=arguments[1];null===this._label?this._label=new Dh(e,n):this._label.setLocation(e,n)},e.prototype.getEdges=function(){return this._edges},e.prototype.mergeLabel=function(){var t=this;if(arguments[0]instanceof e){var n=arguments[0];this.mergeLabel(n._label)}else if(arguments[0]instanceof Dh)for(var r=arguments[0],i=0;i<2;i++){var o=t.computeMergedLocation(r,i),s=t._label.getLocation(i);s===pl.NONE&&t._label.setLocation(i,o)}},e.prototype.add=function(t){this._edges.insert(t),t.setNode(this)},e.prototype.setLabelBoundary=function(t){if(null===this._label)return null;var e=pl.NONE;null!==this._label&&(e=this._label.getLocation(t));var n=null;switch(e){case pl.BOUNDARY:n=pl.INTERIOR;break;case pl.INTERIOR:default:n=pl.BOUNDARY}this._label.setLocation(t,n)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(qh),zh=function(){this.nodeMap=new Cc,this.nodeFact=null;var t=arguments[0];this.nodeFact=t};zh.prototype.find=function(t){return this.nodeMap.get(t)},zh.prototype.addNode=function(){if(arguments[0]instanceof ul){var t=arguments[0],e=this.nodeMap.get(t);return null===e&&(e=this.nodeFact.createNode(t),this.nodeMap.put(t,e)),e}if(arguments[0]instanceof Bh){var n=arguments[0],r=this.nodeMap.get(n.getCoordinate());return null===r?(this.nodeMap.put(n.getCoordinate(),n),n):(r.mergeLabel(n),r)}},zh.prototype.print=function(t){for(var e=this.iterator();e.hasNext();){e.next().print(t)}},zh.prototype.iterator=function(){return this.nodeMap.values().iterator()},zh.prototype.values=function(){return this.nodeMap.values()},zh.prototype.getBoundaryNodes=function(t){for(var e=new lc,n=this.iterator();n.hasNext();){var r=n.next();r.getLabel().getLocation(t)===pl.BOUNDARY&&e.add(r)}return e},zh.prototype.add=function(t){var e=t.getCoordinate();this.addNode(e).add(t)},zh.prototype.interfaces_=function(){return[]},zh.prototype.getClass=function(){return zh};var jh=function(){},Uh={NE:{configurable:!0},NW:{configurable:!0},SW:{configurable:!0},SE:{configurable:!0}};jh.prototype.interfaces_=function(){return[]},jh.prototype.getClass=function(){return jh},jh.isNorthern=function(t){return t===jh.NE||t===jh.NW},jh.isOpposite=function(t,e){return t!==e&&2===(t-e+4)%4},jh.commonHalfPlane=function(t,e){if(t===e)return t;if(2===(t-e+4)%4)return-1;var n=te?t:e)?3:n},jh.isInHalfPlane=function(t,e){return e===jh.SE?t===jh.SE||t===jh.SW:t===e||t===e+1},jh.quadrant=function(){if("number"==typeof arguments[0]&&"number"==typeof arguments[1]){var t=arguments[0],e=arguments[1];if(0===t&&0===e)throw new el("Cannot compute the quadrant for point ( "+t+", "+e+" )");return t>=0?e>=0?jh.NE:jh.SE:e>=0?jh.NW:jh.SW}if(arguments[0]instanceof ul&&arguments[1]instanceof ul){var n=arguments[0],r=arguments[1];if(r.x===n.x&&r.y===n.y)throw new el("Cannot compute the quadrant for two identical points "+n);return r.x>=n.x?r.y>=n.y?jh.NE:jh.SE:r.y>=n.y?jh.NW:jh.SW}},Uh.NE.get=function(){return 0},Uh.NW.get=function(){return 1},Uh.SW.get=function(){return 2},Uh.SE.get=function(){return 3},Object.defineProperties(jh,Uh);var Vh=function(){if(this._edge=null,this._label=null,this._node=null,this._p0=null,this._p1=null,this._dx=null,this._dy=null,this._quadrant=null,1===arguments.length){var t=arguments[0];this._edge=t}else if(3===arguments.length){var e=arguments[0],n=arguments[1],r=arguments[2],i=null;this._edge=e,this.init(n,r),this._label=i}else if(4===arguments.length){var o=arguments[0],s=arguments[1],a=arguments[2],u=arguments[3];this._edge=o,this.init(s,a),this._label=u}};Vh.prototype.compareDirection=function(t){return this._dx===t._dx&&this._dy===t._dy?0:this._quadrant>t._quadrant?1:this._quadrant2){o.linkDirectedEdgesForMinimalEdgeRings();var s=o.buildMinimalRings(),a=this.findShell(s);null!==a?(this.placePolygonHoles(a,s),e.add(a)):n.addAll(s)}else r.add(o)}return r},Wh.prototype.containsPoint=function(t){for(var e=this._shellList.iterator();e.hasNext();){if(e.next().containsPoint(t))return!0}return!1},Wh.prototype.buildMaximalEdgeRings=function(t){for(var e=new lc,n=t.iterator();n.hasNext();){var r=n.next();if(r.isInResult()&&r.getLabel().isArea()&&null===r.getEdgeRing()){var i=new Gh(r,this._geometryFactory);e.add(i),i.setInResult()}}return e},Wh.prototype.placePolygonHoles=function(t,e){for(var n=e.iterator();n.hasNext();){var r=n.next();r.isHole()&&r.setShell(t)}},Wh.prototype.getPolygons=function(){return this.computePolygons(this._shellList)},Wh.prototype.findEdgeRingContaining=function(t,e){for(var n=t.getLinearRing(),r=n.getEnvelopeInternal(),i=n.getCoordinateN(0),o=null,s=null,a=e.iterator();a.hasNext();){var u=a.next(),l=u.getLinearRing(),c=l.getEnvelopeInternal();null!==o&&(s=o.getLinearRing().getEnvelopeInternal());var h=!1;c.contains(r)&&Xl.isPointInRing(i,l.getCoordinates())&&(h=!0),h&&(null===o||s.contains(c))&&(o=u)}return o},Wh.prototype.findShell=function(t){for(var e=0,n=null,r=t.iterator();r.hasNext();){var i=r.next();i.isHole()||(n=i,e++)}return ql.isTrue(e<=1,"found two shells in MinimalEdgeRing list"),n},Wh.prototype.add=function(){if(1===arguments.length){var t=arguments[0];this.add(t.getEdgeEnds(),t.getNodes())}else if(2===arguments.length){var e=arguments[0],n=arguments[1];Hh.linkResultDirectedEdges(n);var r=this.buildMaximalEdgeRings(e),i=new lc,o=this.buildMinimalEdgeRings(r,this._shellList,i);this.sortShellsAndHoles(o,this._shellList,i),this.placeFreeHoles(this._shellList,i)}},Wh.prototype.interfaces_=function(){return[]},Wh.prototype.getClass=function(){return Wh};var Jh=function(){};Jh.prototype.getBounds=function(){},Jh.prototype.interfaces_=function(){return[]},Jh.prototype.getClass=function(){return Jh};var Zh=function(){this._bounds=null,this._item=null;var t=arguments[0],e=arguments[1];this._bounds=t,this._item=e};Zh.prototype.getItem=function(){return this._item},Zh.prototype.getBounds=function(){return this._bounds},Zh.prototype.interfaces_=function(){return[Jh,al]},Zh.prototype.getClass=function(){return Zh};var Kh=function(){this._size=null,this._items=null,this._size=0,this._items=new lc,this._items.add(null)};Kh.prototype.poll=function(){if(this.isEmpty())return null;var t=this._items.get(1);return this._items.set(1,this._items.get(this._size)),this._size-=1,this.reorder(1),t},Kh.prototype.size=function(){return this._size},Kh.prototype.reorder=function(t){for(var e=this,n=null,r=this._items.get(t);2*t<=this._size&&((n=2*t)!==e._size&&e._items.get(n+1).compareTo(e._items.get(n))<0&&n++,e._items.get(n).compareTo(r)<0);t=n)e._items.set(t,e._items.get(n));this._items.set(t,r)},Kh.prototype.clear=function(){this._size=0,this._items.clear()},Kh.prototype.isEmpty=function(){return 0===this._size},Kh.prototype.add=function(t){this._items.add(null),this._size+=1;var e=this._size;for(this._items.set(0,t);t.compareTo(this._items.get(Math.trunc(e/2)))<0;e/=2)this._items.set(e,this._items.get(Math.trunc(e/2)));this._items.set(e,t)},Kh.prototype.interfaces_=function(){return[]},Kh.prototype.getClass=function(){return Kh};var Qh=function(){};Qh.prototype.visitItem=function(t){},Qh.prototype.interfaces_=function(){return[]},Qh.prototype.getClass=function(){return Qh};var $h=function(){};$h.prototype.insert=function(t,e){},$h.prototype.remove=function(t,e){},$h.prototype.query=function(){},$h.prototype.interfaces_=function(){return[]},$h.prototype.getClass=function(){return $h};var tp=function(){if(this._childBoundables=new lc,this._bounds=null,this._level=null,0===arguments.length);else if(1===arguments.length){var t=arguments[0];this._level=t}},ep={serialVersionUID:{configurable:!0}};tp.prototype.getLevel=function(){return this._level},tp.prototype.size=function(){return this._childBoundables.size()},tp.prototype.getChildBoundables=function(){return this._childBoundables},tp.prototype.addChildBoundable=function(t){ql.isTrue(null===this._bounds),this._childBoundables.add(t)},tp.prototype.isEmpty=function(){return this._childBoundables.isEmpty()},tp.prototype.getBounds=function(){return null===this._bounds&&(this._bounds=this.computeBounds()),this._bounds},tp.prototype.interfaces_=function(){return[Jh,al]},tp.prototype.getClass=function(){return tp},ep.serialVersionUID.get=function(){return 0x5a1e55ec41369800},Object.defineProperties(tp,ep);var np=function(){};np.reverseOrder=function(){return{compare:function(t,e){return e.compareTo(t)}}},np.min=function(t){return np.sort(t),t.get(0)},np.sort=function(t,e){var n=t.toArray();e?Rc.sort(n,e):Rc.sort(n);for(var r=t.iterator(),i=0,o=n.length;irp.area(this._boundable2)?(this.expand(this._boundable1,this._boundable2,t,e),null):(this.expand(this._boundable2,this._boundable1,t,e),null);if(n)return this.expand(this._boundable1,this._boundable2,t,e),null;if(r)return this.expand(this._boundable2,this._boundable1,t,e),null;throw new el("neither boundable is composite")},rp.prototype.isLeaves=function(){return!(rp.isComposite(this._boundable1)||rp.isComposite(this._boundable2))},rp.prototype.compareTo=function(t){var e=t;return this._distancee._distance?1:0},rp.prototype.expand=function(t,e,n,r){for(var i=t.getChildBoundables().iterator();i.hasNext();){var o=i.next(),s=new rp(o,e,this._itemDistance);s.getDistance()1,"Node capacity must be greater than 1"),this._nodeCapacity=n}},op={IntersectsOp:{configurable:!0},serialVersionUID:{configurable:!0},DEFAULT_NODE_CAPACITY:{configurable:!0}};ip.prototype.getNodeCapacity=function(){return this._nodeCapacity},ip.prototype.lastNode=function(t){return t.get(t.size()-1)},ip.prototype.size=function(){var t=this;if(0===arguments.length)return this.isEmpty()?0:(this.build(),this.size(this._root));if(1===arguments.length){for(var e=arguments[0],n=0,r=e.getChildBoundables().iterator();r.hasNext();){var i=r.next();i instanceof tp?n+=t.size(i):i instanceof Zh&&(n+=1)}return n}},ip.prototype.removeItem=function(t,e){for(var n=null,r=t.getChildBoundables().iterator();r.hasNext();){var i=r.next();i instanceof Zh&&i.getItem()===e&&(n=i)}return null!==n&&(t.getChildBoundables().remove(n),!0)},ip.prototype.itemsTree=function(){var t=this;if(0===arguments.length){this.build();var e=this.itemsTree(this._root);return null===e?new lc:e}if(1===arguments.length){for(var n=arguments[0],r=new lc,i=n.getChildBoundables().iterator();i.hasNext();){var o=i.next();if(o instanceof tp){var s=t.itemsTree(o);null!==s&&r.add(s)}else o instanceof Zh?r.add(o.getItem()):ql.shouldNeverReachHere()}return r.size()<=0?null:r}},ip.prototype.insert=function(t,e){ql.isTrue(!this._built,"Cannot insert items into an STR packed R-tree after it has been built."),this._itemBoundables.add(new Zh(t,e))},ip.prototype.boundablesAtLevel=function(){var t=this;if(1===arguments.length){var e=arguments[0],n=new lc;return this.boundablesAtLevel(e,this._root,n),n}if(3===arguments.length){var r=arguments[0],i=arguments[1],o=arguments[2];if(ql.isTrue(r>-2),i.getLevel()===r)return o.add(i),null;for(var s=i.getChildBoundables().iterator();s.hasNext();){var a=s.next();a instanceof tp?t.boundablesAtLevel(r,a,o):(ql.isTrue(a instanceof Zh),-1===r&&o.add(a))}return null}},ip.prototype.query=function(){var t=this;if(1===arguments.length){var e=arguments[0];this.build();var n=new lc;return this.isEmpty()||this.getIntersectsOp().intersects(this._root.getBounds(),e)&&this.query(e,this._root,n),n}if(2===arguments.length){var r=arguments[0],i=arguments[1];if(this.build(),this.isEmpty())return null;this.getIntersectsOp().intersects(this._root.getBounds(),r)&&this.query(r,this._root,i)}else if(3===arguments.length)if(gl(arguments[2],Qh)&&arguments[0]instanceof Object&&arguments[1]instanceof tp)for(var o=arguments[0],s=arguments[1],a=arguments[2],u=s.getChildBoundables(),l=0;ln&&(n=o)}}return n+1}},ip.prototype.createParentBoundables=function(t,e){var n=this;ql.isTrue(!t.isEmpty());var r=new lc;r.add(this.createNode(e));var i=new lc(t);np.sort(i,this.getComparator());for(var o=i.iterator();o.hasNext();){var s=o.next();n.lastNode(r).getChildBoundables().size()===n.getNodeCapacity()&&r.add(n.createNode(e)),n.lastNode(r).addChildBoundable(s)}return r},ip.prototype.isEmpty=function(){return this._built?this._root.isEmpty():this._itemBoundables.isEmpty()},ip.prototype.interfaces_=function(){return[al]},ip.prototype.getClass=function(){return ip},ip.compareDoubles=function(t,e){return t>e?1:t0);for(var n=new lc,r=0;r0;){var p=h.poll(),f=p.getDistance();if(f>=l)break;p.isLeaves()?(l=f,c=p):p.expandToQueue(h,l)}return[c.getBoundable(0).getItem(),c.getBoundable(1).getItem()]}}else if(3===arguments.length){var g=arguments[0],d=arguments[1],y=arguments[2],v=new Zh(g,d),_=new rp(this.getRoot(),v,y);return this.nearestNeighbour(_)[0]}},e.prototype.interfaces_=function(){return[$h,al]},e.prototype.getClass=function(){return e},e.centreX=function(t){return e.avg(t.getMinX(),t.getMaxX())},e.avg=function(t,e){return(t+e)/2},e.centreY=function(t){return e.avg(t.getMinY(),t.getMaxY())},n.STRtreeNode.get=function(){return lp},n.serialVersionUID.get=function(){return 0x39920f7d5f261e0},n.xComparator.get=function(){return{interfaces_:function(){return[sl]},compare:function(n,r){return t.compareDoubles(e.centreX(n.getBounds()),e.centreX(r.getBounds()))}}},n.yComparator.get=function(){return{interfaces_:function(){return[sl]},compare:function(n,r){return t.compareDoubles(e.centreY(n.getBounds()),e.centreY(r.getBounds()))}}},n.intersectsOp.get=function(){return{interfaces_:function(){return[t.IntersectsOp]},intersects:function(t,e){return t.intersects(e)}}},n.DEFAULT_NODE_CAPACITY.get=function(){return 10},Object.defineProperties(e,n),e}(ip),lp=function(t){function e(){var e=arguments[0];t.call(this,e)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.computeBounds=function(){for(var t=null,e=this.getChildBoundables().iterator();e.hasNext();){var n=e.next();null===t?t=new Ll(n.getBounds()):t.expandToInclude(n.getBounds())}return t},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(tp),cp=function(){};cp.prototype.interfaces_=function(){return[]},cp.prototype.getClass=function(){return cp},cp.relativeSign=function(t,e){return te?1:0},cp.compare=function(t,e,n){if(e.equals2D(n))return 0;var r=cp.relativeSign(e.x,n.x),i=cp.relativeSign(e.y,n.y);switch(t){case 0:return cp.compareValue(r,i);case 1:return cp.compareValue(i,r);case 2:return cp.compareValue(i,-r);case 3:return cp.compareValue(-r,i);case 4:return cp.compareValue(-r,-i);case 5:return cp.compareValue(-i,-r);case 6:return cp.compareValue(-i,r);case 7:return cp.compareValue(r,-i)}return ql.shouldNeverReachHere("invalid octant value"),0},cp.compareValue=function(t,e){return t<0?-1:t>0?1:e<0?-1:e>0?1:0};var hp=function(){this._segString=null,this.coord=null,this.segmentIndex=null,this._segmentOctant=null,this._isInterior=null;var t=arguments[0],e=arguments[1],n=arguments[2],r=arguments[3];this._segString=t,this.coord=new ul(e),this.segmentIndex=n,this._segmentOctant=r,this._isInterior=!e.equals2D(t.getCoordinate(n))};hp.prototype.getCoordinate=function(){return this.coord},hp.prototype.print=function(t){t.print(this.coord),t.print(" seg # = "+this.segmentIndex)},hp.prototype.compareTo=function(t){var e=t;return this.segmentIndexe.segmentIndex?1:this.coord.equals2D(e.coord)?0:cp.compare(this._segmentOctant,this.coord,e.coord)},hp.prototype.isEndPoint=function(t){return 0===this.segmentIndex&&!this._isInterior||this.segmentIndex===t},hp.prototype.isInterior=function(){return this._isInterior},hp.prototype.interfaces_=function(){return[il]},hp.prototype.getClass=function(){return hp};var pp=function(){this._nodeMap=new Cc,this._edge=null;var t=arguments[0];this._edge=t};pp.prototype.getSplitCoordinates=function(){var t=new hc;this.addEndpoints();for(var e=this.iterator(),n=e.next();e.hasNext();){var r=e.next();this.addEdgeCoordinates(n,r,t),n=r}return t.toCoordinateArray()},pp.prototype.addCollapsedNodes=function(){var t=new lc;this.findCollapsesFromInsertedNodes(t),this.findCollapsesFromExistingVertices(t);for(var e=t.iterator();e.hasNext();){var n=e.next().intValue();this.add(this._edge.getCoordinate(n),n)}},pp.prototype.print=function(t){t.println("Intersections:");for(var e=this.iterator();e.hasNext();){e.next().print(t)}},pp.prototype.findCollapsesFromExistingVertices=function(t){for(var e=0;e=0?e>=0?n>=r?0:1:n>=r?7:6:e>=0?n>=r?3:2:n>=r?4:5}if(arguments[0]instanceof ul&&arguments[1]instanceof ul){var i=arguments[0],o=arguments[1],s=o.x-i.x,a=o.y-i.y;if(0===s&&0===a)throw new el("Cannot compute the octant for two identical points "+i);return fp.octant(s,a)}};var gp=function(){};gp.prototype.getCoordinates=function(){},gp.prototype.size=function(){},gp.prototype.getCoordinate=function(t){},gp.prototype.isClosed=function(){},gp.prototype.setData=function(t){},gp.prototype.getData=function(){},gp.prototype.interfaces_=function(){return[]},gp.prototype.getClass=function(){return gp};var dp=function(){};dp.prototype.addIntersection=function(t,e){},dp.prototype.interfaces_=function(){return[gp]},dp.prototype.getClass=function(){return dp};var yp=function(){this._nodeList=new pp(this),this._pts=null,this._data=null;var t=arguments[0],e=arguments[1];this._pts=t,this._data=e};yp.prototype.getCoordinates=function(){return this._pts},yp.prototype.size=function(){return this._pts.length},yp.prototype.getCoordinate=function(t){return this._pts[t]},yp.prototype.isClosed=function(){return this._pts[0].equals(this._pts[this._pts.length-1])},yp.prototype.getSegmentOctant=function(t){return t===this._pts.length-1?-1:this.safeOctant(this.getCoordinate(t),this.getCoordinate(t+1))},yp.prototype.setData=function(t){this._data=t},yp.prototype.safeOctant=function(t,e){return t.equals2D(e)?0:fp.octant(t,e)},yp.prototype.getData=function(){return this._data},yp.prototype.addIntersection=function(){if(2===arguments.length){var t=arguments[0],e=arguments[1];this.addIntersectionNode(t,e)}else if(4===arguments.length){var n=arguments[0],r=arguments[1],i=arguments[3],o=new ul(n.getIntersection(i));this.addIntersection(o,r)}},yp.prototype.toString=function(){return Fl.toLineString(new lh(this._pts))},yp.prototype.getNodeList=function(){return this._nodeList},yp.prototype.addIntersectionNode=function(t,e){var n=e,r=n+1;if(r=0&&n>=0||e<=0&&n<=0?Math.max(e,n):0}if(arguments[0]instanceof ul){var r=arguments[0];return Xl.orientationIndex(this.p0,this.p1,r)}},vp.prototype.toGeometry=function(t){return t.createLineString([this.p0,this.p1])},vp.prototype.isVertical=function(){return this.p0.x===this.p1.x},vp.prototype.equals=function(t){if(!(t instanceof vp))return!1;var e=t;return this.p0.equals(e.p0)&&this.p1.equals(e.p1)},vp.prototype.intersection=function(t){var e=new jl;return e.computeIntersection(this.p0,this.p1,t.p0,t.p1),e.hasIntersection()?e.getIntersection(0):null},vp.prototype.project=function(){if(arguments[0]instanceof ul){var t=arguments[0];if(t.equals(this.p0)||t.equals(this.p1))return new ul(t);var e=this.projectionFactor(t),n=new ul;return n.x=this.p0.x+e*(this.p1.x-this.p0.x),n.y=this.p0.y+e*(this.p1.y-this.p0.y),n}if(arguments[0]instanceof vp){var r=arguments[0],i=this.projectionFactor(r.p0),o=this.projectionFactor(r.p1);if(i>=1&&o>=1)return null;if(i<=0&&o<=0)return null;var s=this.project(r.p0);i<0&&(s=this.p0),i>1&&(s=this.p1);var a=this.project(r.p1);return o<0&&(a=this.p0),o>1&&(a=this.p1),new vp(s,a)}},vp.prototype.normalize=function(){this.p1.compareTo(this.p0)<0&&this.reverse()},vp.prototype.angle=function(){return Math.atan2(this.p1.y-this.p0.y,this.p1.x-this.p0.x)},vp.prototype.getCoordinate=function(t){return 0===t?this.p0:this.p1},vp.prototype.distancePerpendicular=function(t){return Xl.distancePointLinePerpendicular(t,this.p0,this.p1)},vp.prototype.minY=function(){return Math.min(this.p0.y,this.p1.y)},vp.prototype.midPoint=function(){return vp.midPoint(this.p0,this.p1)},vp.prototype.projectionFactor=function(t){if(t.equals(this.p0))return 0;if(t.equals(this.p1))return 1;var e=this.p1.x-this.p0.x,n=this.p1.y-this.p0.y,r=e*e+n*n;return r<=0?nl.NaN:((t.x-this.p0.x)*e+(t.y-this.p0.y)*n)/r},vp.prototype.closestPoints=function(t){var e=this.intersection(t);if(null!==e)return[e,e];var n=new Array(2).fill(null),r=nl.MAX_VALUE,i=null,o=this.closestPoint(t.p0);r=o.distance(t.p0),n[0]=o,n[1]=t.p0;var s=this.closestPoint(t.p1);(i=s.distance(t.p1))0&&e<1?this.project(t):this.p0.distance(t)1||nl.isNaN(e))&&(e=1),e},vp.prototype.toString=function(){return"LINESTRING( "+this.p0.x+" "+this.p0.y+", "+this.p1.x+" "+this.p1.y+")"},vp.prototype.isHorizontal=function(){return this.p0.y===this.p1.y},vp.prototype.distance=function(){if(arguments[0]instanceof vp){var t=arguments[0];return Xl.distanceLineLine(this.p0,this.p1,t.p0,t.p1)}if(arguments[0]instanceof ul){var e=arguments[0];return Xl.distancePointLine(e,this.p0,this.p1)}},vp.prototype.pointAlong=function(t){var e=new ul;return e.x=this.p0.x+t*(this.p1.x-this.p0.x),e.y=this.p0.y+t*(this.p1.y-this.p0.y),e},vp.prototype.hashCode=function(){var t=nl.doubleToLongBits(this.p0.x);t^=31*nl.doubleToLongBits(this.p0.y);var e=Math.trunc(t)^Math.trunc(t>>32),n=nl.doubleToLongBits(this.p1.x);return n^=31*nl.doubleToLongBits(this.p1.y),e^(Math.trunc(n)^Math.trunc(n>>32))},vp.prototype.interfaces_=function(){return[il,al]},vp.prototype.getClass=function(){return vp},vp.midPoint=function(t,e){return new ul((t.x+e.x)/2,(t.y+e.y)/2)},_p.serialVersionUID.get=function(){return 0x2d2172135f411c00},Object.defineProperties(vp,_p);var mp=function(){this.tempEnv1=new Ll,this.tempEnv2=new Ll,this._overlapSeg1=new vp,this._overlapSeg2=new vp};mp.prototype.overlap=function(){if(2===arguments.length);else if(4===arguments.length){var t=arguments[0],e=arguments[1],n=arguments[2],r=arguments[3];t.getLineSegment(e,this._overlapSeg1),n.getLineSegment(r,this._overlapSeg2),this.overlap(this._overlapSeg1,this._overlapSeg2)}},mp.prototype.interfaces_=function(){return[]},mp.prototype.getClass=function(){return mp};var xp=function(){this._pts=null,this._start=null,this._end=null,this._env=null,this._context=null,this._id=null;var t=arguments[0],e=arguments[1],n=arguments[2],r=arguments[3];this._pts=t,this._start=e,this._end=n,this._context=r};xp.prototype.getLineSegment=function(t,e){e.p0=this._pts[t],e.p1=this._pts[t+1]},xp.prototype.computeSelect=function(t,e,n,r){var i=this._pts[e],o=this._pts[n];if(r.tempEnv1.init(i,o),n-e==1)return r.select(this,e),null;if(!t.intersects(r.tempEnv1))return null;var s=Math.trunc((e+n)/2);e=t.length-1)return t.length-1;for(var r=jh.quadrant(t[n],t[n+1]),i=e+1;ir.getId()&&(r.computeOverlaps(o,e),t._nOverlaps++),t._segInt.isDone())return null}},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},n.SegmentOverlapAction.get=function(){return Np},Object.defineProperties(e,n),e}(wp),Np=function(t){function e(){t.call(this),this._si=null;var e=arguments[0];this._si=e}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.overlap=function(){if(4!==arguments.length)return t.prototype.overlap.apply(this,arguments);var e=arguments[0],n=arguments[1],r=arguments[2],i=arguments[3],o=e.getContext(),s=r.getContext();this._si.processIntersections(o,n,s,i)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(mp),Sp=function t(){if(this._quadrantSegments=t.DEFAULT_QUADRANT_SEGMENTS,this._endCapStyle=t.CAP_ROUND,this._joinStyle=t.JOIN_ROUND,this._mitreLimit=t.DEFAULT_MITRE_LIMIT,this._isSingleSided=!1,this._simplifyFactor=t.DEFAULT_SIMPLIFY_FACTOR,0===arguments.length);else if(1===arguments.length){var e=arguments[0];this.setQuadrantSegments(e)}else if(2===arguments.length){var n=arguments[0],r=arguments[1];this.setQuadrantSegments(n),this.setEndCapStyle(r)}else if(4===arguments.length){var i=arguments[0],o=arguments[1],s=arguments[2],a=arguments[3];this.setQuadrantSegments(i),this.setEndCapStyle(o),this.setJoinStyle(s),this.setMitreLimit(a)}},Cp={CAP_ROUND:{configurable:!0},CAP_FLAT:{configurable:!0},CAP_SQUARE:{configurable:!0},JOIN_ROUND:{configurable:!0},JOIN_MITRE:{configurable:!0},JOIN_BEVEL:{configurable:!0},DEFAULT_QUADRANT_SEGMENTS:{configurable:!0},DEFAULT_MITRE_LIMIT:{configurable:!0},DEFAULT_SIMPLIFY_FACTOR:{configurable:!0}};Sp.prototype.getEndCapStyle=function(){return this._endCapStyle},Sp.prototype.isSingleSided=function(){return this._isSingleSided},Sp.prototype.setQuadrantSegments=function(t){this._quadrantSegments=t,0===this._quadrantSegments&&(this._joinStyle=Sp.JOIN_BEVEL),this._quadrantSegments<0&&(this._joinStyle=Sp.JOIN_MITRE,this._mitreLimit=Math.abs(this._quadrantSegments)),t<=0&&(this._quadrantSegments=1),this._joinStyle!==Sp.JOIN_ROUND&&(this._quadrantSegments=Sp.DEFAULT_QUADRANT_SEGMENTS)},Sp.prototype.getJoinStyle=function(){return this._joinStyle},Sp.prototype.setJoinStyle=function(t){this._joinStyle=t},Sp.prototype.setSimplifyFactor=function(t){this._simplifyFactor=t<0?0:t},Sp.prototype.getSimplifyFactor=function(){return this._simplifyFactor},Sp.prototype.getQuadrantSegments=function(){return this._quadrantSegments},Sp.prototype.setEndCapStyle=function(t){this._endCapStyle=t},Sp.prototype.getMitreLimit=function(){return this._mitreLimit},Sp.prototype.setMitreLimit=function(t){this._mitreLimit=t},Sp.prototype.setSingleSided=function(t){this._isSingleSided=t},Sp.prototype.interfaces_=function(){return[]},Sp.prototype.getClass=function(){return Sp},Sp.bufferDistanceError=function(t){var e=Math.PI/2/t;return 1-Math.cos(e/2)},Cp.CAP_ROUND.get=function(){return 1},Cp.CAP_FLAT.get=function(){return 2},Cp.CAP_SQUARE.get=function(){return 3},Cp.JOIN_ROUND.get=function(){return 1},Cp.JOIN_MITRE.get=function(){return 2},Cp.JOIN_BEVEL.get=function(){return 3},Cp.DEFAULT_QUADRANT_SEGMENTS.get=function(){return 8},Cp.DEFAULT_MITRE_LIMIT.get=function(){return 5},Cp.DEFAULT_SIMPLIFY_FACTOR.get=function(){return.01},Object.defineProperties(Sp,Cp);var Pp=function(t){this._distanceTol=null,this._isDeleted=null,this._angleOrientation=Xl.COUNTERCLOCKWISE,this._inputLine=t||null},Mp={INIT:{configurable:!0},DELETE:{configurable:!0},KEEP:{configurable:!0},NUM_PTS_TO_CHECK:{configurable:!0}};Pp.prototype.isDeletable=function(t,e,n,r){var i=this._inputLine[t],o=this._inputLine[e],s=this._inputLine[n];return!!this.isConcave(i,o,s)&&(!!this.isShallow(i,o,s,r)&&this.isShallowSampled(i,o,t,n,r))},Pp.prototype.deleteShallowConcavities=function(){for(var t=this,e=1,n=this.findNextNonDeletedIndex(e),r=this.findNextNonDeletedIndex(n),i=!1;r=0;r--)this.addPt(t[r])},Lp.prototype.isRedundant=function(t){if(this._ptList.size()<1)return!1;var e=this._ptList.get(this._ptList.size()-1);return t.distance(e)Math.PI;)t-=Rp.PI_TIMES_2;for(;t<=-Math.PI;)t+=Rp.PI_TIMES_2;return t},Rp.angle=function(){if(1===arguments.length){var t=arguments[0];return Math.atan2(t.y,t.x)}if(2===arguments.length){var e=arguments[0],n=arguments[1],r=n.x-e.x,i=n.y-e.y;return Math.atan2(i,r)}},Rp.isAcute=function(t,e,n){var r=t.x-e.x,i=t.y-e.y;return r*(n.x-e.x)+i*(n.y-e.y)>0},Rp.isObtuse=function(t,e,n){var r=t.x-e.x,i=t.y-e.y;return r*(n.x-e.x)+i*(n.y-e.y)<0},Rp.interiorAngle=function(t,e,n){var r=Rp.angle(e,t),i=Rp.angle(e,n);return Math.abs(i-r)},Rp.normalizePositive=function(t){if(t<0){for(;t<0;)t+=Rp.PI_TIMES_2;t>=Rp.PI_TIMES_2&&(t=0)}else{for(;t>=Rp.PI_TIMES_2;)t-=Rp.PI_TIMES_2;t<0&&(t=0)}return t},Rp.angleBetween=function(t,e,n){var r=Rp.angle(e,t),i=Rp.angle(e,n);return Rp.diff(r,i)},Rp.diff=function(t,e){var n=null;return(n=tMath.PI&&(n=2*Math.PI-n),n},Rp.toRadians=function(t){return t*Math.PI/180},Rp.getTurn=function(t,e){var n=Math.sin(e-t);return n>0?Rp.COUNTERCLOCKWISE:n<0?Rp.CLOCKWISE:Rp.NONE},Rp.angleBetweenOriented=function(t,e,n){var r=Rp.angle(e,t),i=Rp.angle(e,n)-r;return i<=-Math.PI?i+Rp.PI_TIMES_2:i>Math.PI?i-Rp.PI_TIMES_2:i},Tp.PI_TIMES_2.get=function(){return 2*Math.PI},Tp.PI_OVER_2.get=function(){return Math.PI/2},Tp.PI_OVER_4.get=function(){return Math.PI/4},Tp.COUNTERCLOCKWISE.get=function(){return Xl.COUNTERCLOCKWISE},Tp.CLOCKWISE.get=function(){return Xl.CLOCKWISE},Tp.NONE.get=function(){return Xl.COLLINEAR},Object.defineProperties(Rp,Tp);var Ap=function t(){this._maxCurveSegmentError=0,this._filletAngleQuantum=null,this._closingSegLengthFactor=1,this._segList=null,this._distance=0,this._precisionModel=null,this._bufParams=null,this._li=null,this._s0=null,this._s1=null,this._s2=null,this._seg0=new vp,this._seg1=new vp,this._offset0=new vp,this._offset1=new vp,this._side=0,this._hasNarrowConcaveAngle=!1;var e=arguments[0],n=arguments[1],r=arguments[2];this._precisionModel=e,this._bufParams=n,this._li=new jl,this._filletAngleQuantum=Math.PI/2/n.getQuadrantSegments(),n.getQuadrantSegments()>=8&&n.getJoinStyle()===Sp.JOIN_ROUND&&(this._closingSegLengthFactor=t.MAX_CLOSING_SEG_LEN_FACTOR),this.init(r)},Dp={OFFSET_SEGMENT_SEPARATION_FACTOR:{configurable:!0},INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR:{configurable:!0},CURVE_VERTEX_SNAP_DISTANCE_FACTOR:{configurable:!0},MAX_CLOSING_SEG_LEN_FACTOR:{configurable:!0}};Ap.prototype.addNextSegment=function(t,e){if(this._s0=this._s1,this._s1=this._s2,this._s2=t,this._seg0.setCoordinates(this._s0,this._s1),this.computeOffsetSegment(this._seg0,this._side,this._distance,this._offset0),this._seg1.setCoordinates(this._s1,this._s2),this.computeOffsetSegment(this._seg1,this._side,this._distance,this._offset1),this._s1.equals(this._s2))return null;var n=Xl.computeOrientation(this._s0,this._s1,this._s2),r=n===Xl.CLOCKWISE&&this._side===Sh.LEFT||n===Xl.COUNTERCLOCKWISE&&this._side===Sh.RIGHT;0===n?this.addCollinear(e):r?this.addOutsideTurn(n,e):this.addInsideTurn(n,e)},Ap.prototype.addLineEndCap=function(t,e){var n=new vp(t,e),r=new vp;this.computeOffsetSegment(n,Sh.LEFT,this._distance,r);var i=new vp;this.computeOffsetSegment(n,Sh.RIGHT,this._distance,i);var o=e.x-t.x,s=e.y-t.y,a=Math.atan2(s,o);switch(this._bufParams.getEndCapStyle()){case Sp.CAP_ROUND:this._segList.addPt(r.p1),this.addFilletArc(e,a+Math.PI/2,a-Math.PI/2,Xl.CLOCKWISE,this._distance),this._segList.addPt(i.p1);break;case Sp.CAP_FLAT:this._segList.addPt(r.p1),this._segList.addPt(i.p1);break;case Sp.CAP_SQUARE:var u=new ul;u.x=Math.abs(this._distance)*Math.cos(a),u.y=Math.abs(this._distance)*Math.sin(a);var l=new ul(r.p1.x+u.x,r.p1.y+u.y),c=new ul(i.p1.x+u.x,i.p1.y+u.y);this._segList.addPt(l),this._segList.addPt(c)}},Ap.prototype.getCoordinates=function(){return this._segList.getCoordinates()},Ap.prototype.addMitreJoin=function(t,e,n,r){var i=!0,o=null;try{o=Ml.intersection(e.p0,e.p1,n.p0,n.p1),(r<=0?1:o.distance(t)/Math.abs(r))>this._bufParams.getMitreLimit()&&(i=!1)}catch(t){if(!(t instanceof Cl))throw t;o=new ul(0,0),i=!1}i?this._segList.addPt(o):this.addLimitedMitreJoin(e,n,r,this._bufParams.getMitreLimit())},Ap.prototype.addFilletCorner=function(t,e,n,r,i){var o=e.x-t.x,s=e.y-t.y,a=Math.atan2(s,o),u=n.x-t.x,l=n.y-t.y,c=Math.atan2(l,u);r===Xl.CLOCKWISE?a<=c&&(a+=2*Math.PI):a>=c&&(a-=2*Math.PI),this._segList.addPt(e),this.addFilletArc(t,a,c,r,i),this._segList.addPt(n)},Ap.prototype.addOutsideTurn=function(t,e){if(this._offset0.p1.distance(this._offset1.p0)0){var n=new ul((this._closingSegLengthFactor*this._offset0.p1.x+this._s1.x)/(this._closingSegLengthFactor+1),(this._closingSegLengthFactor*this._offset0.p1.y+this._s1.y)/(this._closingSegLengthFactor+1));this._segList.addPt(n);var r=new ul((this._closingSegLengthFactor*this._offset1.p0.x+this._s1.x)/(this._closingSegLengthFactor+1),(this._closingSegLengthFactor*this._offset1.p0.y+this._s1.y)/(this._closingSegLengthFactor+1));this._segList.addPt(r)}else this._segList.addPt(this._s1);this._segList.addPt(this._offset1.p0)}},Ap.prototype.createCircle=function(t){var e=new ul(t.x+this._distance,t.y);this._segList.addPt(e),this.addFilletArc(t,0,2*Math.PI,-1,this._distance),this._segList.closeRing()},Ap.prototype.addBevelJoin=function(t,e){this._segList.addPt(t.p1),this._segList.addPt(e.p0)},Ap.prototype.init=function(t){this._distance=t,this._maxCurveSegmentError=t*(1-Math.cos(this._filletAngleQuantum/2)),this._segList=new Lp,this._segList.setPrecisionModel(this._precisionModel),this._segList.setMinimumVertexDistance(t*Ap.CURVE_VERTEX_SNAP_DISTANCE_FACTOR)},Ap.prototype.addCollinear=function(t){this._li.computeIntersection(this._s0,this._s1,this._s1,this._s2),this._li.getIntersectionNum()>=2&&(this._bufParams.getJoinStyle()===Sp.JOIN_BEVEL||this._bufParams.getJoinStyle()===Sp.JOIN_MITRE?(t&&this._segList.addPt(this._offset0.p1),this._segList.addPt(this._offset1.p0)):this.addFilletCorner(this._s1,this._offset0.p1,this._offset1.p0,Xl.CLOCKWISE,this._distance))},Ap.prototype.closeRing=function(){this._segList.closeRing()},Ap.prototype.hasNarrowConcaveAngle=function(){return this._hasNarrowConcaveAngle},Ap.prototype.interfaces_=function(){return[]},Ap.prototype.getClass=function(){return Ap},Dp.OFFSET_SEGMENT_SEPARATION_FACTOR.get=function(){return.001},Dp.INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR.get=function(){return.001},Dp.CURVE_VERTEX_SNAP_DISTANCE_FACTOR.get=function(){return 1e-6},Dp.MAX_CLOSING_SEG_LEN_FACTOR.get=function(){return 80},Object.defineProperties(Ap,Dp);var Fp=function(){this._distance=0,this._precisionModel=null,this._bufParams=null;var t=arguments[0],e=arguments[1];this._precisionModel=t,this._bufParams=e};Fp.prototype.getOffsetCurve=function(t,e){if(this._distance=e,0===e)return null;var n=e<0,r=Math.abs(e),i=this.getSegGen(r);t.length<=1?this.computePointCurve(t[0],i):this.computeOffsetCurve(t,n,i);var o=i.getCoordinates();return n&&pc.reverse(o),o},Fp.prototype.computeSingleSidedBufferCurve=function(t,e,n){var r=this.simplifyTolerance(this._distance);if(e){n.addSegments(t,!0);var i=Pp.simplify(t,-r),o=i.length-1;n.initSideSegments(i[o],i[o-1],Sh.LEFT),n.addFirstSegment();for(var s=o-2;s>=0;s--)n.addNextSegment(i[s],!0)}else{n.addSegments(t,!1);var a=Pp.simplify(t,r),u=a.length-1;n.initSideSegments(a[0],a[1],Sh.LEFT),n.addFirstSegment();for(var l=2;l<=u;l++)n.addNextSegment(a[l],!0)}n.addLastSegment(),n.closeRing()},Fp.prototype.computeRingBufferCurve=function(t,e,n){var r=this.simplifyTolerance(this._distance);e===Sh.RIGHT&&(r=-r);var i=Pp.simplify(t,r),o=i.length-1;n.initSideSegments(i[o-1],i[0],e);for(var s=1;s<=o;s++){var a=1!==s;n.addNextSegment(i[s],a)}n.closeRing()},Fp.prototype.computeLineBufferCurve=function(t,e){var n=this.simplifyTolerance(this._distance),r=Pp.simplify(t,n),i=r.length-1;e.initSideSegments(r[0],r[1],Sh.LEFT);for(var o=2;o<=i;o++)e.addNextSegment(r[o],!0);e.addLastSegment(),e.addLineEndCap(r[i-1],r[i]);var s=Pp.simplify(t,-n),a=s.length-1;e.initSideSegments(s[a],s[a-1],Sh.LEFT);for(var u=a-2;u>=0;u--)e.addNextSegment(s[u],!0);e.addLastSegment(),e.addLineEndCap(s[1],s[0]),e.closeRing()},Fp.prototype.computePointCurve=function(t,e){switch(this._bufParams.getEndCapStyle()){case Sp.CAP_ROUND:e.createCircle(t);break;case Sp.CAP_SQUARE:e.createSquare(t)}},Fp.prototype.getLineCurve=function(t,e){if(this._distance=e,e<0&&!this._bufParams.isSingleSided())return null;if(0===e)return null;var n=Math.abs(e),r=this.getSegGen(n);if(t.length<=1)this.computePointCurve(t[0],r);else if(this._bufParams.isSingleSided()){var i=e<0;this.computeSingleSidedBufferCurve(t,i,r)}else this.computeLineBufferCurve(t,r);return r.getCoordinates()},Fp.prototype.getBufferParameters=function(){return this._bufParams},Fp.prototype.simplifyTolerance=function(t){return t*this._bufParams.getSimplifyFactor()},Fp.prototype.getRingCurve=function(t,e,n){if(this._distance=n,t.length<=2)return this.getLineCurve(t,n);if(0===n)return Fp.copyCoordinates(t);var r=this.getSegGen(n);return this.computeRingBufferCurve(t,e,r),r.getCoordinates()},Fp.prototype.computeOffsetCurve=function(t,e,n){var r=this.simplifyTolerance(this._distance);if(e){var i=Pp.simplify(t,-r),o=i.length-1;n.initSideSegments(i[o],i[o-1],Sh.LEFT),n.addFirstSegment();for(var s=o-2;s>=0;s--)n.addNextSegment(i[s],!0)}else{var a=Pp.simplify(t,r),u=a.length-1;n.initSideSegments(a[0],a[1],Sh.LEFT),n.addFirstSegment();for(var l=2;l<=u;l++)n.addNextSegment(a[l],!0)}n.addLastSegment()},Fp.prototype.getSegGen=function(t){return new Ap(this._precisionModel,this._bufParams,t)},Fp.prototype.interfaces_=function(){return[]},Fp.prototype.getClass=function(){return Fp},Fp.copyCoordinates=function(t){for(var e=new Array(t.length).fill(null),n=0;no.getMaxY()||t.findStabbedSegments(e,i.getDirectedEdges(),n)}return n}if(3===arguments.length)if(gl(arguments[2],ac)&&arguments[0]instanceof ul&&arguments[1]instanceof Xh)for(var s=arguments[0],a=arguments[1],u=arguments[2],l=a.getEdge().getCoordinates(),c=0;ct._seg.p1.y&&t._seg.reverse();var h=Math.max(t._seg.p0.x,t._seg.p1.x);if(!(ht._seg.p1.y||Xl.computeOrientation(t._seg.p0,t._seg.p1,s)===Xl.RIGHT)){var p=a.getDepth(Sh.LEFT);t._seg.p0.equals(l[c])||(p=a.getDepth(Sh.RIGHT));var f=new qp(t._seg,p);u.add(f)}}else if(gl(arguments[2],ac)&&arguments[0]instanceof ul&&gl(arguments[1],ac))for(var g=arguments[0],d=arguments[1],y=arguments[2],v=d.iterator();v.hasNext();){var _=v.next();_.isForward()&&t.findStabbedSegments(g,_,y)}},kp.prototype.getDepth=function(t){var e=this.findStabbedSegments(t);return 0===e.size()?0:np.min(e)._leftDepth},kp.prototype.interfaces_=function(){return[]},kp.prototype.getClass=function(){return kp},Gp.DepthSegment.get=function(){return qp},Object.defineProperties(kp,Gp);var qp=function(){this._upwardSeg=null,this._leftDepth=null;var t=arguments[0],e=arguments[1];this._upwardSeg=new vp(t),this._leftDepth=e};qp.prototype.compareTo=function(t){var e=t;if(this._upwardSeg.minX()>=e._upwardSeg.maxX())return 1;if(this._upwardSeg.maxX()<=e._upwardSeg.minX())return-1;var n=this._upwardSeg.orientationIndex(e._upwardSeg);return 0!==n||0!==(n=-1*e._upwardSeg.orientationIndex(this._upwardSeg))?n:this._upwardSeg.compareTo(e._upwardSeg)},qp.prototype.compareX=function(t,e){var n=t.p0.compareTo(e.p0);return 0!==n?n:t.p1.compareTo(e.p1)},qp.prototype.toString=function(){return this._upwardSeg.toString()},qp.prototype.interfaces_=function(){return[il]},qp.prototype.getClass=function(){return qp};var Bp=function(t,e,n){this.p0=t||null,this.p1=e||null,this.p2=n||null};Bp.prototype.area=function(){return Bp.area(this.p0,this.p1,this.p2)},Bp.prototype.signedArea=function(){return Bp.signedArea(this.p0,this.p1,this.p2)},Bp.prototype.interpolateZ=function(t){if(null===t)throw new el("Supplied point is null.");return Bp.interpolateZ(t,this.p0,this.p1,this.p2)},Bp.prototype.longestSideLength=function(){return Bp.longestSideLength(this.p0,this.p1,this.p2)},Bp.prototype.isAcute=function(){return Bp.isAcute(this.p0,this.p1,this.p2)},Bp.prototype.circumcentre=function(){return Bp.circumcentre(this.p0,this.p1,this.p2)},Bp.prototype.area3D=function(){return Bp.area3D(this.p0,this.p1,this.p2)},Bp.prototype.centroid=function(){return Bp.centroid(this.p0,this.p1,this.p2)},Bp.prototype.inCentre=function(){return Bp.inCentre(this.p0,this.p1,this.p2)},Bp.prototype.interfaces_=function(){return[]},Bp.prototype.getClass=function(){return Bp},Bp.area=function(t,e,n){return Math.abs(((n.x-t.x)*(e.y-t.y)-(e.x-t.x)*(n.y-t.y))/2)},Bp.signedArea=function(t,e,n){return((n.x-t.x)*(e.y-t.y)-(e.x-t.x)*(n.y-t.y))/2},Bp.det=function(t,e,n,r){return t*r-e*n},Bp.interpolateZ=function(t,e,n,r){var i=e.x,o=e.y,s=n.x-i,a=r.x-i,u=n.y-o,l=r.y-o,c=s*l-a*u,h=t.x-i,p=t.y-o,f=(l*h-a*p)/c,g=(-u*h+s*p)/c;return e.z+f*(n.z-e.z)+g*(r.z-e.z)},Bp.longestSideLength=function(t,e,n){var r=t.distance(e),i=e.distance(n),o=n.distance(t),s=r;return i>s&&(s=i),o>s&&(s=o),s},Bp.isAcute=function(t,e,n){return!!Rp.isAcute(t,e,n)&&(!!Rp.isAcute(e,n,t)&&!!Rp.isAcute(n,t,e))},Bp.circumcentre=function(t,e,n){var r=n.x,i=n.y,o=t.x-r,s=t.y-i,a=e.x-r,u=e.y-i,l=2*Bp.det(o,s,a,u),c=Bp.det(s,o*o+s*s,u,a*a+u*u),h=Bp.det(o,o*o+s*s,a,a*a+u*u);return new ul(r-c/l,i+h/l)},Bp.perpendicularBisector=function(t,e){var n=e.x-t.x,r=e.y-t.y,i=new Ml(t.x+n/2,t.y+r/2,1),o=new Ml(t.x-r+n/2,t.y+n+r/2,1);return new Ml(i,o)},Bp.angleBisector=function(t,e,n){var r=e.distance(t),i=r/(r+e.distance(n)),o=n.x-t.x,s=n.y-t.y;return new ul(t.x+i*o,t.y+i*s)},Bp.area3D=function(t,e,n){var r=e.x-t.x,i=e.y-t.y,o=e.z-t.z,s=n.x-t.x,a=n.y-t.y,u=n.z-t.z,l=i*u-o*a,c=o*s-r*u,h=r*a-i*s,p=l*l+c*c+h*h,f=Math.sqrt(p)/2;return f},Bp.centroid=function(t,e,n){var r=(t.x+e.x+n.x)/3,i=(t.y+e.y+n.y)/3;return new ul(r,i)},Bp.inCentre=function(t,e,n){var r=e.distance(n),i=t.distance(n),o=t.distance(e),s=r+i+o,a=(r*t.x+i*e.x+o*n.x)/s,u=(r*t.y+i*e.y+o*n.y)/s;return new ul(a,u)};var zp=function(){this._inputGeom=null,this._distance=null,this._curveBuilder=null,this._curveList=new lc;var t=arguments[0],e=arguments[1],n=arguments[2];this._inputGeom=t,this._distance=e,this._curveBuilder=n};zp.prototype.addPoint=function(t){if(this._distance<=0)return null;var e=t.getCoordinates(),n=this._curveBuilder.getLineCurve(e,this._distance);this.addCurve(n,pl.EXTERIOR,pl.INTERIOR)},zp.prototype.addPolygon=function(t){var e=this,n=this._distance,r=Sh.LEFT;this._distance<0&&(n=-this._distance,r=Sh.RIGHT);var i=t.getExteriorRing(),o=pc.removeRepeatedPoints(i.getCoordinates());if(this._distance<0&&this.isErodedCompletely(i,this._distance))return null;if(this._distance<=0&&o.length<3)return null;this.addPolygonRing(o,n,r,pl.EXTERIOR,pl.INTERIOR);for(var s=0;s0&&e.isErodedCompletely(a,-e._distance)||e.addPolygonRing(u,n,Sh.opposite(r),pl.INTERIOR,pl.EXTERIOR)}},zp.prototype.isTriangleErodedCompletely=function(t,e){var n=new Bp(t[0],t[1],t[2]),r=n.inCentre();return Xl.distancePointLine(r,n.p0,n.p1)=nh.MINIMUM_VALID_SIZE&&Xl.isCCW(t)&&(o=i,s=r,n=Sh.opposite(n));var a=this._curveBuilder.getRingCurve(t,n,e);this.addCurve(a,o,s)},zp.prototype.add=function(t){if(t.isEmpty())return null;t instanceof th?this.addPolygon(t):t instanceof Zc?this.addLineString(t):t instanceof Qc?this.addPoint(t):(t instanceof eh||t instanceof Gc||t instanceof rh||t instanceof kc)&&this.addCollection(t)},zp.prototype.isErodedCompletely=function(t,e){var n=t.getCoordinates();if(n.length<4)return e<0;if(4===n.length)return this.isTriangleErodedCompletely(n,e);var r=t.getEnvelopeInternal(),i=Math.min(r.getHeight(),r.getWidth());return e<0&&2*Math.abs(e)>i},zp.prototype.addCollection=function(t){for(var e=0;e=this._max)throw new uc;var t=this._parent.getGeometryN(this._index++);return t instanceof kc?(this._subcollectionIterator=new Up(t),this._subcollectionIterator.next()):t},Up.prototype.remove=function(){throw new Error(this.getClass().getName())},Up.prototype.hasNext=function(){if(this._atStart)return!0;if(null!==this._subcollectionIterator){if(this._subcollectionIterator.hasNext())return!0;this._subcollectionIterator=null}return!(this._index>=this._max)},Up.prototype.interfaces_=function(){return[sc]},Up.prototype.getClass=function(){return Up},Up.isAtomic=function(t){return!(t instanceof kc)};var Vp=function(){this._geom=null;var t=arguments[0];this._geom=t};Vp.prototype.locate=function(t){return Vp.locate(t,this._geom)},Vp.prototype.interfaces_=function(){return[jp]},Vp.prototype.getClass=function(){return Vp},Vp.isPointInRing=function(t,e){return!!e.getEnvelopeInternal().intersects(t)&&Xl.isPointInRing(t,e.getCoordinates())},Vp.containsPointInPolygon=function(t,e){if(e.isEmpty())return!1;var n=e.getExteriorRing();if(!Vp.isPointInRing(t,n))return!1;for(var r=0;r=0;n--){var r=this._edgeList.get(n),i=r.getSym();null===e&&(e=i),null!==t&&i.setNext(t),t=r}e.setNext(t)},e.prototype.computeDepths=function(){var t=this;if(1===arguments.length){var e=arguments[0],n=this.findIndex(e),r=e.getDepth(Sh.LEFT),i=e.getDepth(Sh.RIGHT),o=this.computeDepths(n+1,this._edgeList.size(),r),s=this.computeDepths(0,n,o);if(s!==i)throw new Oh("depth mismatch at "+e.getCoordinate())}else if(3===arguments.length){for(var a=arguments[0],u=arguments[1],l=arguments[2],c=l,h=a;h=0;o--){var s=e._resultAreaEdgeList.get(o),a=s.getSym();switch(null===n&&s.getEdgeRing()===t&&(n=s),i){case e._SCANNING_FOR_INCOMING:if(a.getEdgeRing()!==t)continue;r=a,i=e._LINKING_TO_OUTGOING;break;case e._LINKING_TO_OUTGOING:if(s.getEdgeRing()!==t)continue;r.setNextMin(s),i=e._SCANNING_FOR_INCOMING}}i===this._LINKING_TO_OUTGOING&&(ql.isTrue(null!==n,"found null for first outgoing dirEdge"),ql.isTrue(n.getEdgeRing()===t,"unable to link last incoming dirEdge"),r.setNextMin(n))},e.prototype.getOutgoingDegree=function(){if(0===arguments.length){for(var t=0,e=this.iterator();e.hasNext();){var n=e.next();n.isInResult()&&t++}return t}if(1===arguments.length){for(var r=arguments[0],i=0,o=this.iterator();o.hasNext();){var s=o.next();s.getEdgeRing()===r&&i++}return i}},e.prototype.getLabel=function(){return this._label},e.prototype.findCoveredLineEdges=function(){for(var t=pl.NONE,e=this.iterator();e.hasNext();){var n=e.next(),r=n.getSym();if(!n.isLineEdge()){if(n.isInResult()){t=pl.INTERIOR;break}if(r.isInResult()){t=pl.EXTERIOR;break}}}if(t===pl.NONE)return null;for(var i=t,o=this.iterator();o.hasNext();){var s=o.next(),a=s.getSym();s.isLineEdge()?s.getEdge().setCovered(i===pl.INTERIOR):(s.isInResult()&&(i=pl.EXTERIOR),a.isInResult()&&(i=pl.INTERIOR))}},e.prototype.computeLabelling=function(e){t.prototype.computeLabelling.call(this,e),this._label=new Dh(pl.NONE);for(var n=this.iterator();n.hasNext();)for(var r=n.next().getEdge().getLabel(),i=0;i<2;i++){var o=r.getLocation(i);o!==pl.INTERIOR&&o!==pl.BOUNDARY||this._label.setLocation(i,pl.INTERIOR)}},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(Xp),Hp=function(t){function e(){t.apply(this,arguments)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.createNode=function(t){return new Bh(t,new Yp)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(Yh),Wp=function t(){this._pts=null,this._orientation=null;var e=arguments[0];this._pts=e,this._orientation=t.orientation(e)};Wp.prototype.compareTo=function(t){var e=t;return Wp.compareOriented(this._pts,this._orientation,e._pts,e._orientation)},Wp.prototype.interfaces_=function(){return[il]},Wp.prototype.getClass=function(){return Wp},Wp.orientation=function(t){return 1===pc.increasingDirection(t)},Wp.compareOriented=function(t,e,n,r){for(var i=e?1:-1,o=r?1:-1,s=e?t.length:-1,a=r?n.length:-1,u=e?0:t.length-1,l=r?0:n.length-1;;){var c=t[u].compareTo(n[l]);if(0!==c)return c;var h=(u+=i)===s,p=(l+=o)===a;if(h&&!p)return-1;if(!h&&p)return 1;if(h&&p)return 0}};var Jp=function(){this._edges=new lc,this._ocaMap=new Cc};Jp.prototype.print=function(t){t.print("MULTILINESTRING ( ");for(var e=0;e0&&t.print(","),t.print("(");for(var r=n.getCoordinates(),i=0;i0&&t.print(","),t.print(r[i].x+" "+r[i].y);t.println(")")}t.print(") ")},Jp.prototype.addAll=function(t){for(var e=t.iterator();e.hasNext();)this.add(e.next())},Jp.prototype.findEdgeIndex=function(t){for(var e=0;e0||!e.coord.equals2D(r);i||n--;var o=new Array(n).fill(null),s=0;o[s++]=new ul(t.coord);for(var a=t.segmentIndex+1;a<=e.segmentIndex;a++)o[s++]=this.edge.pts[a];return i&&(o[s]=e.coord),new of(o,new Dh(this.edge._label))},$p.prototype.add=function(t,e,n){var r=new Qp(t,e,n),i=this._nodeMap.get(r);return null!==i?i:(this._nodeMap.put(r,r),r)},$p.prototype.isIntersection=function(t){for(var e=this.iterator();e.hasNext();){if(e.next().coord.equals(t))return!0}return!1},$p.prototype.interfaces_=function(){return[]},$p.prototype.getClass=function(){return $p};var tf=function(){};tf.prototype.getChainStartIndices=function(t){var e=0,n=new lc;n.add(new _l(e));do{var r=this.findChainEnd(t,e);n.add(new _l(r)),e=r}while(en?e:n},ef.prototype.getMinX=function(t){var e=this.pts[this.startIndex[t]].x,n=this.pts[this.startIndex[t+1]].x;return en&&(i=1),t._depth[e][r]=i}}},nf.prototype.getDelta=function(t){return this._depth[t][Sh.RIGHT]-this._depth[t][Sh.LEFT]},nf.prototype.getLocation=function(t,e){return this._depth[t][e]<=0?pl.EXTERIOR:pl.INTERIOR},nf.prototype.toString=function(){return"A: "+this._depth[0][1]+","+this._depth[0][2]+" B: "+this._depth[1][1]+","+this._depth[1][2]},nf.prototype.add=function(){var t=this;if(1===arguments.length)for(var e=arguments[0],n=0;n<2;n++)for(var r=1;r<3;r++){var i=e.getLocation(n,r);i!==pl.EXTERIOR&&i!==pl.INTERIOR||(t.isNull(n,r)?t._depth[n][r]=nf.depthAtLocation(i):t._depth[n][r]+=nf.depthAtLocation(i))}else if(3===arguments.length){var o=arguments[0],s=arguments[1],a=arguments[2];a===pl.INTERIOR&&this._depth[o][s]++}},nf.prototype.interfaces_=function(){return[]},nf.prototype.getClass=function(){return nf},nf.depthAtLocation=function(t){return t===pl.EXTERIOR?0:t===pl.INTERIOR?1:nf.NULL_VALUE},rf.NULL_VALUE.get=function(){return-1},Object.defineProperties(nf,rf);var of=function(t){function e(){if(t.call(this),this.pts=null,this._env=null,this.eiList=new $p(this),this._name=null,this._mce=null,this._isIsolated=!0,this._depth=new nf,this._depthDelta=0,1===arguments.length){var n=arguments[0];e.call(this,n,null)}else if(2===arguments.length){var r=arguments[0],i=arguments[1];this.pts=r,this._label=i}}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.getDepth=function(){return this._depth},e.prototype.getCollapsedEdge=function(){var t=new Array(2).fill(null);return t[0]=this.pts[0],t[1]=this.pts[1],new e(t,Dh.toLineLabel(this._label))},e.prototype.isIsolated=function(){return this._isIsolated},e.prototype.getCoordinates=function(){return this.pts},e.prototype.setIsolated=function(t){this._isIsolated=t},e.prototype.setName=function(t){this._name=t},e.prototype.equals=function(t){if(!(t instanceof e))return!1;var n=t;if(this.pts.length!==n.pts.length)return!1;for(var r=!0,i=!0,o=this.pts.length,s=0;s0?this.pts[0]:null;if(1===arguments.length){var t=arguments[0];return this.pts[t]}},e.prototype.print=function(t){t.print("edge "+this._name+": "),t.print("LINESTRING (");for(var e=0;e0&&t.print(","),t.print(this.pts[e].x+" "+this.pts[e].y);t.print(") "+this._label+" "+this._depthDelta)},e.prototype.computeIM=function(t){e.updateIM(this._label,t)},e.prototype.isCollapsed=function(){return!!this._label.isArea()&&(3===this.pts.length&&!!this.pts[0].equals(this.pts[2]))},e.prototype.isClosed=function(){return this.pts[0].equals(this.pts[this.pts.length-1])},e.prototype.getMaximumSegmentIndex=function(){return this.pts.length-1},e.prototype.getDepthDelta=function(){return this._depthDelta},e.prototype.getNumPoints=function(){return this.pts.length},e.prototype.printReverse=function(t){t.print("edge "+this._name+": ");for(var e=this.pts.length-1;e>=0;e--)t.print(this.pts[e]+" ");t.println("")},e.prototype.getMonotoneChainEdge=function(){return null===this._mce&&(this._mce=new ef(this)),this._mce},e.prototype.getEnvelope=function(){if(null===this._env){this._env=new Ll;for(var t=0;t0&&t.append(","),t.append(this.pts[e].x+" "+this.pts[e].y);return t.append(") "+this._label+" "+this._depthDelta),t.toString()},e.prototype.isPointwiseEqual=function(t){if(this.pts.length!==t.pts.length)return!1;for(var e=0;er||this._maxyo;if(s)return!1;var a=this.intersectsToleranceSquare(t,e);return ql.isTrue(!(s&&a),"Found bad envelope test"),a},cf.prototype.initCorners=function(t){var e=.5;this._minx=t.x-e,this._maxx=t.x+e,this._miny=t.y-e,this._maxy=t.y+e,this._corner[0]=new ul(this._maxx,this._maxy),this._corner[1]=new ul(this._minx,this._maxy),this._corner[2]=new ul(this._minx,this._miny),this._corner[3]=new ul(this._maxx,this._miny)},cf.prototype.intersects=function(t,e){return 1===this._scaleFactor?this.intersectsScaled(t,e):(this.copyScaled(t,this._p0Scaled),this.copyScaled(e,this._p1Scaled),this.intersectsScaled(this._p0Scaled,this._p1Scaled))},cf.prototype.scale=function(t){return Math.round(t*this._scaleFactor)},cf.prototype.getCoordinate=function(){return this._originalPt},cf.prototype.copyScaled=function(t,e){e.x=this.scale(t.x),e.y=this.scale(t.y)},cf.prototype.getSafeEnvelope=function(){if(null===this._safeEnv){var t=cf.SAFE_ENV_EXPANSION_FACTOR/this._scaleFactor;this._safeEnv=new Ll(this._originalPt.x-t,this._originalPt.x+t,this._originalPt.y-t,this._originalPt.y+t)}return this._safeEnv},cf.prototype.intersectsPixelClosure=function(t,e){return this._li.computeIntersection(t,e,this._corner[0],this._corner[1]),!!this._li.hasIntersection()||(this._li.computeIntersection(t,e,this._corner[1],this._corner[2]),!!this._li.hasIntersection()||(this._li.computeIntersection(t,e,this._corner[2],this._corner[3]),!!this._li.hasIntersection()||(this._li.computeIntersection(t,e,this._corner[3],this._corner[0]),!!this._li.hasIntersection())))},cf.prototype.intersectsToleranceSquare=function(t,e){var n=!1,r=!1;return this._li.computeIntersection(t,e,this._corner[0],this._corner[1]),!!this._li.isProper()||(this._li.computeIntersection(t,e,this._corner[1],this._corner[2]),!!this._li.isProper()||(this._li.hasIntersection()&&(n=!0),this._li.computeIntersection(t,e,this._corner[2],this._corner[3]),!!this._li.isProper()||(this._li.hasIntersection()&&(r=!0),this._li.computeIntersection(t,e,this._corner[3],this._corner[0]),!!this._li.isProper()||(!(!n||!r)||(!!t.equals(this._pt)||!!e.equals(this._pt))))))},cf.prototype.addSnappedNode=function(t,e){var n=t.getCoordinate(e),r=t.getCoordinate(e+1);return!!this.intersects(n,r)&&(t.addIntersection(this.getCoordinate(),e),!0)},cf.prototype.interfaces_=function(){return[]},cf.prototype.getClass=function(){return cf},hf.SAFE_ENV_EXPANSION_FACTOR.get=function(){return.75},Object.defineProperties(cf,hf);var pf=function(){this.tempEnv1=new Ll,this.selectedSegment=new vp};pf.prototype.select=function(){if(1===arguments.length);else if(2===arguments.length){var t=arguments[0],e=arguments[1];t.getLineSegment(e,this.selectedSegment),this.select(this.selectedSegment)}},pf.prototype.interfaces_=function(){return[]},pf.prototype.getClass=function(){return pf};var ff=function(){this._index=null;var t=arguments[0];this._index=t},gf={HotPixelSnapAction:{configurable:!0}};ff.prototype.snap=function(){if(1===arguments.length){var t=arguments[0];return this.snap(t,null,-1)}if(3===arguments.length){var e=arguments[0],n=arguments[1],r=arguments[2],i=e.getSafeEnvelope(),o=new df(e,n,r);return this._index.query(i,{interfaces_:function(){return[Qh]},visitItem:function(t){t.select(i,o)}}),o.isNodeAdded()}},ff.prototype.interfaces_=function(){return[]},ff.prototype.getClass=function(){return ff},gf.HotPixelSnapAction.get=function(){return df},Object.defineProperties(ff,gf);var df=function(t){function e(){t.call(this),this._hotPixel=null,this._parentEdge=null,this._hotPixelVertexIndex=null,this._isNodeAdded=!1;var e=arguments[0],n=arguments[1],r=arguments[2];this._hotPixel=e,this._parentEdge=n,this._hotPixelVertexIndex=r}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.isNodeAdded=function(){return this._isNodeAdded},e.prototype.select=function(){if(2!==arguments.length)return t.prototype.select.apply(this,arguments);var e=arguments[0],n=arguments[1],r=e.getContext();if(null!==this._parentEdge&&r===this._parentEdge&&n===this._hotPixelVertexIndex)return null;this._isNodeAdded=this._hotPixel.addSnappedNode(r,n)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(pf),yf=function(){this._li=null,this._interiorIntersections=null;var t=arguments[0];this._li=t,this._interiorIntersections=new lc};yf.prototype.processIntersections=function(t,e,n,r){if(t===n&&e===r)return null;var i=t.getCoordinates()[e],o=t.getCoordinates()[e+1],s=n.getCoordinates()[r],a=n.getCoordinates()[r+1];if(this._li.computeIntersection(i,o,s,a),this._li.hasIntersection()&&this._li.isInteriorIntersection()){for(var u=0;u=0;e--){try{t.bufferReducedPrecision(e)}catch(e){if(!(e instanceof Oh))throw e;t._saveException=e}if(null!==t._resultGeometry)return null}throw this._saveException}if(1===arguments.length){var n=arguments[0],r=_f.precisionScaleFactor(this._argGeom,this._distance,n),i=new gh(r);this.bufferFixedPrecision(i)}},_f.prototype.computeGeometry=function(){if(this.bufferOriginalPrecision(),null!==this._resultGeometry)return null;var t=this._argGeom.getFactory().getPrecisionModel();t.getType()===gh.FIXED?this.bufferFixedPrecision(t):this.bufferReducedPrecision()},_f.prototype.setQuadrantSegments=function(t){this._bufParams.setQuadrantSegments(t)},_f.prototype.bufferOriginalPrecision=function(){try{var t=new sf(this._bufParams);this._resultGeometry=t.buffer(this._argGeom,this._distance)}catch(t){if(!(t instanceof kl))throw t;this._saveException=t}},_f.prototype.getResultGeometry=function(t){return this._distance=t,this.computeGeometry(),this._resultGeometry},_f.prototype.setEndCapStyle=function(t){this._bufParams.setEndCapStyle(t)},_f.prototype.interfaces_=function(){return[]},_f.prototype.getClass=function(){return _f},_f.bufferOp=function(){if(2===arguments.length){var t=arguments[0],e=arguments[1],n=new _f(t),r=n.getResultGeometry(e);return r}if(3===arguments.length){if(Number.isInteger(arguments[2])&&arguments[0]instanceof Wl&&"number"==typeof arguments[1]){var i=arguments[0],o=arguments[1],s=arguments[2],a=new _f(i);a.setQuadrantSegments(s);var u=a.getResultGeometry(o);return u}if(arguments[2]instanceof Sp&&arguments[0]instanceof Wl&&"number"==typeof arguments[1]){var l=arguments[0],c=arguments[1],h=arguments[2],p=new _f(l,h),f=p.getResultGeometry(c);return f}}else if(4===arguments.length){var g=arguments[0],d=arguments[1],y=arguments[2],v=arguments[3],_=new _f(g);_.setQuadrantSegments(y),_.setEndCapStyle(v);var m=_.getResultGeometry(d);return m}},_f.precisionScaleFactor=function(t,e,n){var r=t.getEnvelopeInternal(),i=dl.max(Math.abs(r.getMaxX()),Math.abs(r.getMaxY()),Math.abs(r.getMinX()),Math.abs(r.getMinY()))+2*(e>0?e:0),o=n-Math.trunc(Math.log(i)/Math.log(10)+1);return Math.pow(10,o)},mf.CAP_ROUND.get=function(){return Sp.CAP_ROUND},mf.CAP_BUTT.get=function(){return Sp.CAP_FLAT},mf.CAP_FLAT.get=function(){return Sp.CAP_FLAT},mf.CAP_SQUARE.get=function(){return Sp.CAP_SQUARE},mf.MAX_PRECISION_DIGITS.get=function(){return 12},Object.defineProperties(_f,mf);var xf=function(){this._pt=[new ul,new ul],this._distance=nl.NaN,this._isNull=!0};xf.prototype.getCoordinates=function(){return this._pt},xf.prototype.getCoordinate=function(t){return this._pt[t]},xf.prototype.setMinimum=function(){if(1===arguments.length){var t=arguments[0];this.setMinimum(t._pt[0],t._pt[1])}else if(2===arguments.length){var e=arguments[0],n=arguments[1];if(this._isNull)return this.initialize(e,n),null;var r=e.distance(n);rthis._distance&&this.initialize(e,n,r)}},xf.prototype.interfaces_=function(){return[]},xf.prototype.getClass=function(){return xf};var Ef=function(){};Ef.prototype.interfaces_=function(){return[]},Ef.prototype.getClass=function(){return Ef},Ef.computeDistance=function(){if(arguments[2]instanceof xf&&arguments[0]instanceof Zc&&arguments[1]instanceof ul)for(var t=arguments[0],e=arguments[1],n=arguments[2],r=t.getCoordinates(),i=new vp,o=0;o0||this._isIn?pl.INTERIOR:pl.EXTERIOR)},Pf.prototype.interfaces_=function(){return[]},Pf.prototype.getClass=function(){return Pf};var Mf=function t(){if(this._component=null,this._segIndex=null,this._pt=null,2===arguments.length){var e=arguments[0],n=arguments[1];t.call(this,e,t.INSIDE_AREA,n)}else if(3===arguments.length){var r=arguments[0],i=arguments[1],o=arguments[2];this._component=r,this._segIndex=i,this._pt=o}},Lf={INSIDE_AREA:{configurable:!0}};Mf.prototype.isInsideArea=function(){return this._segIndex===Mf.INSIDE_AREA},Mf.prototype.getCoordinate=function(){return this._pt},Mf.prototype.getGeometryComponent=function(){return this._component},Mf.prototype.getSegmentIndex=function(){return this._segIndex},Mf.prototype.interfaces_=function(){return[]},Mf.prototype.getClass=function(){return Mf},Lf.INSIDE_AREA.get=function(){return-1},Object.defineProperties(Mf,Lf);var Of=function(t){this._pts=t||null};Of.prototype.filter=function(t){t instanceof Qc&&this._pts.add(t)},Of.prototype.interfaces_=function(){return[Dc]},Of.prototype.getClass=function(){return Of},Of.getPoints=function(){if(1===arguments.length){var t=arguments[0];return t instanceof Qc?np.singletonList(t):Of.getPoints(t,new lc)}if(2===arguments.length){var e=arguments[0],n=arguments[1];return e instanceof Qc?n.add(e):e instanceof kc&&e.apply(new Of(n)),n}};var Rf=function(){this._locations=null;var t=arguments[0];this._locations=t};Rf.prototype.filter=function(t){(t instanceof Qc||t instanceof Zc||t instanceof th)&&this._locations.add(new Mf(t,0,t.getCoordinate()))},Rf.prototype.interfaces_=function(){return[Dc]},Rf.prototype.getClass=function(){return Rf},Rf.getLocations=function(t){var e=new lc;return t.apply(new Rf(e)),e};var Tf=function(){if(this._geom=null,this._terminateDistance=0,this._ptLocator=new Pf,this._minDistanceLocation=null,this._minDistance=nl.MAX_VALUE,2===arguments.length){var t=arguments[0],e=arguments[1];this._geom=[t,e],this._terminateDistance=0}else if(3===arguments.length){var n=arguments[0],r=arguments[1],i=arguments[2];this._geom=new Array(2).fill(null),this._geom[0]=n,this._geom[1]=r,this._terminateDistance=i}};Tf.prototype.computeContainmentDistance=function(){var t=this;if(0===arguments.length){var e=new Array(2).fill(null);if(this.computeContainmentDistance(0,e),this._minDistance<=this._terminateDistance)return null;this.computeContainmentDistance(1,e)}else if(2===arguments.length){var n=arguments[0],r=arguments[1],i=1-n,o=Sf.getPolygons(this._geom[n]);if(o.size()>0){var s=Rf.getLocations(this._geom[i]);if(this.computeContainmentDistance(s,o,r),this._minDistance<=this._terminateDistance)return this._minDistanceLocation[i]=r[0],this._minDistanceLocation[n]=r[1],null}}else if(3===arguments.length)if(arguments[2]instanceof Array&&gl(arguments[0],ac)&&gl(arguments[1],ac)){for(var a=arguments[0],u=arguments[1],l=arguments[2],c=0;cthis._minDistance)return null;for(var i=e.getCoordinates(),o=n.getCoordinate(),s=0;sthis._minDistance)return null;for(var f=c.getCoordinates(),g=h.getCoordinates(),d=0;dthis._distance&&this.initialize(e,n,r)}},Af.prototype.interfaces_=function(){return[]},Af.prototype.getClass=function(){return Af};var Df=function(){};Df.prototype.interfaces_=function(){return[]},Df.prototype.getClass=function(){return Df},Df.computeDistance=function(){if(arguments[2]instanceof Af&&arguments[0]instanceof Zc&&arguments[1]instanceof ul)for(var t=arguments[0],e=arguments[1],n=arguments[2],r=new vp,i=t.getCoordinates(),o=0;o1||t<=0)throw new el("Fraction is not in range (0.0 - 1.0]");this._densifyFrac=t},Ff.prototype.compute=function(t,e){this.computeOrientedDistance(t,e,this._ptDist),this.computeOrientedDistance(e,t,this._ptDist)},Ff.prototype.distance=function(){return this.compute(this._g0,this._g1),this._ptDist.getDistance()},Ff.prototype.computeOrientedDistance=function(t,e,n){var r=new Gf(e);if(t.apply(r),n.setMaximum(r.getMaxPointDistance()),this._densifyFrac>0){var i=new qf(e,this._densifyFrac);t.apply(i),n.setMaximum(i.getMaxPointDistance())}},Ff.prototype.orientedDistance=function(){return this.computeOrientedDistance(this._g0,this._g1,this._ptDist),this._ptDist.getDistance()},Ff.prototype.interfaces_=function(){return[]},Ff.prototype.getClass=function(){return Ff},Ff.distance=function(){if(2===arguments.length){var t=arguments[0],e=arguments[1],n=new Ff(t,e);return n.distance()}if(3===arguments.length){var r=arguments[0],i=arguments[1],o=arguments[2],s=new Ff(r,i);return s.setDensifyFraction(o),s.distance()}},kf.MaxPointDistanceFilter.get=function(){return Gf},kf.MaxDensifiedByFractionDistanceFilter.get=function(){return qf},Object.defineProperties(Ff,kf);var Gf=function(){this._maxPtDist=new Af,this._minPtDist=new Af,this._euclideanDist=new Df,this._geom=null;var t=arguments[0];this._geom=t};Gf.prototype.filter=function(t){this._minPtDist.initialize(),Df.computeDistance(this._geom,t,this._minPtDist),this._maxPtDist.setMaximum(this._minPtDist)},Gf.prototype.getMaxPointDistance=function(){return this._maxPtDist},Gf.prototype.interfaces_=function(){return[Kl]},Gf.prototype.getClass=function(){return Gf};var qf=function(){this._maxPtDist=new Af,this._minPtDist=new Af,this._geom=null,this._numSubSegs=0;var t=arguments[0],e=arguments[1];this._geom=t,this._numSubSegs=Math.trunc(Math.round(1/e))};qf.prototype.filter=function(t,e){var n=this;if(0===e)return null;for(var r=t.getCoordinate(e-1),i=t.getCoordinate(e),o=(i.x-r.x)/this._numSubSegs,s=(i.y-r.y)/this._numSubSegs,a=0;an){this._isValid=!1;var i=r.getCoordinates();this._errorLocation=i[1],this._errorIndicator=t.getFactory().createLineString(i),this._errMsg="Distance between buffer curve and input is too large ("+this._maxDistanceFound+" at "+Fl.toLineString(i[0],i[1])+")"}},Bf.prototype.isValid=function(){var t=Math.abs(this._bufDistance),e=Bf.MAX_DISTANCE_DIFF_FRAC*t;return this._minValidDistance=t-e,this._maxValidDistance=t+e,!(!this._input.isEmpty()&&!this._result.isEmpty())||(this._bufDistance>0?this.checkPositiveValid():this.checkNegativeValid(),Bf.VERBOSE&&Pl.out.println("Min Dist= "+this._minDistanceFound+" err= "+(1-this._minDistanceFound/this._bufDistance)+" Max Dist= "+this._maxDistanceFound+" err= "+(this._maxDistanceFound/this._bufDistance-1)),this._isValid)},Bf.prototype.checkNegativeValid=function(){if(!(this._input instanceof th||this._input instanceof rh||this._input instanceof kc))return null;var t=this.getPolygonLines(this._input);if(this.checkMinimumDistance(t,this._result,this._minValidDistance),!this._isValid)return null;this.checkMaximumDistance(t,this._result,this._maxValidDistance)},Bf.prototype.getErrorIndicator=function(){return this._errorIndicator},Bf.prototype.checkMinimumDistance=function(t,e,n){var r=new Tf(t,e,n);if(this._minDistanceFound=r.distance(),this._minDistanceFound0&&t>e&&(this._isValid=!1,this._errorMsg="Area of positive buffer is smaller than input",this._errorIndicator=this._result),this._distance<0&&t=2||this._distance>0?null:(this._result.isEmpty()||(this._isValid=!1,this._errorMsg="Result is non-empty",this._errorIndicator=this._result),void this.report("ExpectedEmpty"))},jf.prototype.report=function(t){if(!jf.VERBOSE)return null;Pl.out.println("Check "+t+": "+(this._isValid?"passed":"FAILED"))},jf.prototype.getErrorMessage=function(){return this._errorMsg},jf.prototype.interfaces_=function(){return[]},jf.prototype.getClass=function(){return jf},jf.isValidMsg=function(t,e,n){var r=new jf(t,e,n);return r.isValid()?null:r.getErrorMessage()},jf.isValid=function(t,e,n){return!!new jf(t,e,n).isValid()},Uf.VERBOSE.get=function(){return!1},Uf.MAX_ENV_DIFF_FRAC.get=function(){return.012},Object.defineProperties(jf,Uf);var Vf=function(){this._pts=null,this._data=null;var t=arguments[0],e=arguments[1];this._pts=t,this._data=e};Vf.prototype.getCoordinates=function(){return this._pts},Vf.prototype.size=function(){return this._pts.length},Vf.prototype.getCoordinate=function(t){return this._pts[t]},Vf.prototype.isClosed=function(){return this._pts[0].equals(this._pts[this._pts.length-1])},Vf.prototype.getSegmentOctant=function(t){return t===this._pts.length-1?-1:fp.octant(this.getCoordinate(t),this.getCoordinate(t+1))},Vf.prototype.setData=function(t){this._data=t},Vf.prototype.getData=function(){return this._data},Vf.prototype.toString=function(){return Fl.toLineString(new lh(this._pts))},Vf.prototype.interfaces_=function(){return[gp]},Vf.prototype.getClass=function(){return Vf};var Xf=function(){this._findAllIntersections=!1,this._isCheckEndSegmentsOnly=!1,this._li=null,this._interiorIntersection=null,this._intSegments=null,this._intersections=new lc,this._intersectionCount=0,this._keepIntersections=!0;var t=arguments[0];this._li=t,this._interiorIntersection=null};Xf.prototype.getInteriorIntersection=function(){return this._interiorIntersection},Xf.prototype.setCheckEndSegmentsOnly=function(t){this._isCheckEndSegmentsOnly=t},Xf.prototype.getIntersectionSegments=function(){return this._intSegments},Xf.prototype.count=function(){return this._intersectionCount},Xf.prototype.getIntersections=function(){return this._intersections},Xf.prototype.setFindAllIntersections=function(t){this._findAllIntersections=t},Xf.prototype.setKeepIntersections=function(t){this._keepIntersections=t},Xf.prototype.processIntersections=function(t,e,n,r){if(!this._findAllIntersections&&this.hasIntersection())return null;if(t===n&&e===r)return null;if(this._isCheckEndSegmentsOnly&&!(this.isEndSegment(t,e)||this.isEndSegment(n,r)))return null;var i=t.getCoordinates()[e],o=t.getCoordinates()[e+1],s=n.getCoordinates()[r],a=n.getCoordinates()[r+1];this._li.computeIntersection(i,o,s,a),this._li.hasIntersection()&&this._li.isInteriorIntersection()&&(this._intSegments=new Array(4).fill(null),this._intSegments[0]=i,this._intSegments[1]=o,this._intSegments[2]=s,this._intSegments[3]=a,this._interiorIntersection=this._li.getIntersection(0),this._keepIntersections&&this._intersections.add(this._interiorIntersection),this._intersectionCount++)},Xf.prototype.isEndSegment=function(t,e){return 0===e||e>=t.size()-2},Xf.prototype.hasIntersection=function(){return null!==this._interiorIntersection},Xf.prototype.isDone=function(){return!this._findAllIntersections&&null!==this._interiorIntersection},Xf.prototype.interfaces_=function(){return[Zp]},Xf.prototype.getClass=function(){return Xf},Xf.createAllIntersectionsFinder=function(t){var e=new Xf(t);return e.setFindAllIntersections(!0),e},Xf.createAnyIntersectionFinder=function(t){return new Xf(t)},Xf.createIntersectionCounter=function(t){var e=new Xf(t);return e.setFindAllIntersections(!0),e.setKeepIntersections(!1),e};var Yf=function(){this._li=new jl,this._segStrings=null,this._findAllIntersections=!1,this._segInt=null,this._isValid=!0;var t=arguments[0];this._segStrings=t};Yf.prototype.execute=function(){if(null!==this._segInt)return null;this.checkInteriorIntersections()},Yf.prototype.getIntersections=function(){return this._segInt.getIntersections()},Yf.prototype.isValid=function(){return this.execute(),this._isValid},Yf.prototype.setFindAllIntersections=function(t){this._findAllIntersections=t},Yf.prototype.checkInteriorIntersections=function(){this._isValid=!0,this._segInt=new Xf(this._li),this._segInt.setFindAllIntersections(this._findAllIntersections);var t=new Ip;if(t.setSegmentIntersector(this._segInt),t.computeNodes(this._segStrings),this._segInt.hasIntersection())return this._isValid=!1,null},Yf.prototype.checkValid=function(){if(this.execute(),!this._isValid)throw new Oh(this.getErrorMessage(),this._segInt.getInteriorIntersection())},Yf.prototype.getErrorMessage=function(){if(this._isValid)return"no intersections found";var t=this._segInt.getIntersectionSegments();return"found non-noded intersection between "+Fl.toLineString(t[0],t[1])+" and "+Fl.toLineString(t[2],t[3])},Yf.prototype.interfaces_=function(){return[]},Yf.prototype.getClass=function(){return Yf},Yf.computeIntersections=function(t){var e=new Yf(t);return e.setFindAllIntersections(!0),e.isValid(),e.getIntersections()};var Hf=function t(){this._nv=null;var e=arguments[0];this._nv=new Yf(t.toSegmentStrings(e))};Hf.prototype.checkValid=function(){this._nv.checkValid()},Hf.prototype.interfaces_=function(){return[]},Hf.prototype.getClass=function(){return Hf},Hf.toSegmentStrings=function(t){for(var e=new lc,n=t.iterator();n.hasNext();){var r=n.next();e.add(new Vf(r.getCoordinates(),r))}return e},Hf.checkValid=function(t){new Hf(t).checkValid()};var Wf=function(t){this._mapOp=t};Wf.prototype.map=function(t){for(var e=new lc,n=0;n0&&r<4&&!this._preserveType?this._factory.createLineString(n):this._factory.createLinearRing(n)},Kf.prototype.interfaces_=function(){return[]},Kf.prototype.getClass=function(){return Kf};var Qf=function t(){if(this._snapTolerance=0,this._srcPts=null,this._seg=new vp,this._allowSnappingToSourceVertices=!1,this._isClosed=!1,arguments[0]instanceof Zc&&"number"==typeof arguments[1]){var e=arguments[0],n=arguments[1];t.call(this,e.getCoordinates(),n)}else if(arguments[0]instanceof Array&&"number"==typeof arguments[1]){var r=arguments[0],i=arguments[1];this._srcPts=r,this._isClosed=t.isClosed(r),this._snapTolerance=i}};Qf.prototype.snapVertices=function(t,e){for(var n=this._isClosed?t.size()-1:t.size(),r=0;r=0&&t.add(o+1,new ul(i),!1)}},Qf.prototype.findSegmentIndexToSnap=function(t,e){for(var n=this,r=nl.MAX_VALUE,i=-1,o=0;oe&&(e=r)}return e}if(2===arguments.length){var i=arguments[0],o=arguments[1];return Math.min($f.computeOverlaySnapTolerance(i),$f.computeOverlaySnapTolerance(o))}},$f.computeSizeBasedSnapTolerance=function(t){var e=t.getEnvelopeInternal();return Math.min(e.getHeight(),e.getWidth())*$f.SNAP_PRECISION_FACTOR},$f.snapToSelf=function(t,e,n){return new $f(t).snapToSelf(e,n)},tg.SNAP_PRECISION_FACTOR.get=function(){return 1e-9},Object.defineProperties($f,tg);var eg=function(t){function e(e,n,r){t.call(this),this._snapTolerance=e||null,this._snapPts=n||null,this._isSelfSnap=void 0!==r&&r}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.snapLine=function(t,e){var n=new Qf(t,this._snapTolerance);return n.setAllowSnappingToSourceVertices(this._isSelfSnap),n.snapTo(e)},e.prototype.transformCoordinates=function(t,e){var n=t.toCoordinateArray(),r=this.snapLine(n,this._snapPts);return this._factory.getCoordinateSequenceFactory().create(r)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(Kf),ng=function(){this._isFirst=!0,this._commonMantissaBitsCount=53,this._commonBits=0,this._commonSignExp=null};ng.prototype.getCommon=function(){return nl.longBitsToDouble(this._commonBits)},ng.prototype.add=function(t){var e=nl.doubleToLongBits(t);return this._isFirst?(this._commonBits=e,this._commonSignExp=ng.signExpBits(this._commonBits),this._isFirst=!1,null):ng.signExpBits(e)!==this._commonSignExp?(this._commonBits=0,null):(this._commonMantissaBitsCount=ng.numCommonMostSigMantissaBits(this._commonBits,e),void(this._commonBits=ng.zeroLowerBits(this._commonBits,64-(12+this._commonMantissaBitsCount))))},ng.prototype.toString=function(){if(1===arguments.length){var t=arguments[0],e=nl.longBitsToDouble(t),n=nl.toBinaryString(t),r="0000000000000000000000000000000000000000000000000000000000000000"+n,i=r.substring(r.length-64),o=i.substring(0,1)+" "+i.substring(1,12)+"(exp) "+i.substring(12)+" [ "+e+" ]";return o}},ng.prototype.interfaces_=function(){return[]},ng.prototype.getClass=function(){return ng},ng.getBit=function(t,e){return 0!=(t&1<>52},ng.zeroLowerBits=function(t,e){return t&~((1<=0;r--){if(ng.getBit(t,r)!==ng.getBit(e,r))return n;n++}return 52};var rg=function(){this._commonCoord=null,this._ccFilter=new og},ig={CommonCoordinateFilter:{configurable:!0},Translater:{configurable:!0}};rg.prototype.addCommonBits=function(t){var e=new sg(this._commonCoord);t.apply(e),t.geometryChanged()},rg.prototype.removeCommonBits=function(t){if(0===this._commonCoord.x&&0===this._commonCoord.y)return t;var e=new ul(this._commonCoord);e.x=-e.x,e.y=-e.y;var n=new sg(e);return t.apply(n),t.geometryChanged(),t},rg.prototype.getCommonCoordinate=function(){return this._commonCoord},rg.prototype.add=function(t){t.apply(this._ccFilter),this._commonCoord=this._ccFilter.getCommonCoordinate()},rg.prototype.interfaces_=function(){return[]},rg.prototype.getClass=function(){return rg},ig.CommonCoordinateFilter.get=function(){return og},ig.Translater.get=function(){return sg},Object.defineProperties(rg,ig);var og=function(){this._commonBitsX=new ng,this._commonBitsY=new ng};og.prototype.filter=function(t){this._commonBitsX.add(t.x),this._commonBitsY.add(t.y)},og.prototype.getCommonCoordinate=function(){return new ul(this._commonBitsX.getCommon(),this._commonBitsY.getCommon())},og.prototype.interfaces_=function(){return[Kl]},og.prototype.getClass=function(){return og};var sg=function(){this.trans=null;var t=arguments[0];this.trans=t};sg.prototype.filter=function(t,e){var n=t.getOrdinate(e,0)+this.trans.x,r=t.getOrdinate(e,1)+this.trans.y;t.setOrdinate(e,0,n),t.setOrdinate(e,1,r)},sg.prototype.isDone=function(){return!1},sg.prototype.isGeometryChanged=function(){return!0},sg.prototype.interfaces_=function(){return[Fc]},sg.prototype.getClass=function(){return sg};var ag=function(t,e){this._geom=new Array(2).fill(null),this._snapTolerance=null,this._cbr=null,this._geom[0]=t,this._geom[1]=e,this.computeSnapTolerance()};ag.prototype.selfSnap=function(t){return new $f(t).snapTo(t,this._snapTolerance)},ag.prototype.removeCommonBits=function(t){this._cbr=new rg,this._cbr.add(t[0]),this._cbr.add(t[1]);var e=new Array(2).fill(null);return e[0]=this._cbr.removeCommonBits(t[0].copy()),e[1]=this._cbr.removeCommonBits(t[1].copy()),e},ag.prototype.prepareResult=function(t){return this._cbr.addCommonBits(t),t},ag.prototype.getResultGeometry=function(t){var e=this.snap(this._geom),n=Mg.overlayOp(e[0],e[1],t);return this.prepareResult(n)},ag.prototype.checkValid=function(t){t.isValid()||Pl.out.println("Snapped geometry is invalid")},ag.prototype.computeSnapTolerance=function(){this._snapTolerance=$f.computeOverlaySnapTolerance(this._geom[0],this._geom[1])},ag.prototype.snap=function(t){var e=this.removeCommonBits(t);return $f.snap(e[0],e[1],this._snapTolerance)},ag.prototype.interfaces_=function(){return[]},ag.prototype.getClass=function(){return ag},ag.overlayOp=function(t,e,n){return new ag(t,e).getResultGeometry(n)},ag.union=function(t,e){return ag.overlayOp(t,e,Mg.UNION)},ag.intersection=function(t,e){return ag.overlayOp(t,e,Mg.INTERSECTION)},ag.symDifference=function(t,e){return ag.overlayOp(t,e,Mg.SYMDIFFERENCE)},ag.difference=function(t,e){return ag.overlayOp(t,e,Mg.DIFFERENCE)};var ug=function(t,e){this._geom=new Array(2).fill(null),this._geom[0]=t,this._geom[1]=e};ug.prototype.getResultGeometry=function(t){var e=null,n=!1,r=null;try{e=Mg.overlayOp(this._geom[0],this._geom[1],t);n=!0}catch(t){if(!(t instanceof kl))throw t;r=t}if(!n)try{e=ag.overlayOp(this._geom[0],this._geom[1],t)}catch(t){throw t instanceof kl?r:t}return e},ug.prototype.interfaces_=function(){return[]},ug.prototype.getClass=function(){return ug},ug.overlayOp=function(t,e,n){return new ug(t,e).getResultGeometry(n)},ug.union=function(t,e){return ug.overlayOp(t,e,Mg.UNION)},ug.intersection=function(t,e){return ug.overlayOp(t,e,Mg.INTERSECTION)},ug.symDifference=function(t,e){return ug.overlayOp(t,e,Mg.SYMDIFFERENCE)},ug.difference=function(t,e){return ug.overlayOp(t,e,Mg.DIFFERENCE)};var lg=function(){this.mce=null,this.chainIndex=null;var t=arguments[0],e=arguments[1];this.mce=t,this.chainIndex=e};lg.prototype.computeIntersections=function(t,e){this.mce.computeIntersectsForChain(this.chainIndex,t.mce,t.chainIndex,e)},lg.prototype.interfaces_=function(){return[]},lg.prototype.getClass=function(){return lg};var cg=function t(){if(this._label=null,this._xValue=null,this._eventType=null,this._insertEvent=null,this._deleteEventIndex=null,this._obj=null,2===arguments.length){var e=arguments[0],n=arguments[1];this._eventType=t.DELETE,this._xValue=e,this._insertEvent=n}else if(3===arguments.length){var r=arguments[0],i=arguments[1],o=arguments[2];this._eventType=t.INSERT,this._label=r,this._xValue=i,this._obj=o}},hg={INSERT:{configurable:!0},DELETE:{configurable:!0}};cg.prototype.isDelete=function(){return this._eventType===cg.DELETE},cg.prototype.setDeleteEventIndex=function(t){this._deleteEventIndex=t},cg.prototype.getObject=function(){return this._obj},cg.prototype.compareTo=function(t){var e=t;return this._xValuee._xValue?1:this._eventTypee._eventType?1:0},cg.prototype.getInsertEvent=function(){return this._insertEvent},cg.prototype.isInsert=function(){return this._eventType===cg.INSERT},cg.prototype.isSameLabel=function(t){return null!==this._label&&this._label===t._label},cg.prototype.getDeleteEventIndex=function(){return this._deleteEventIndex},cg.prototype.interfaces_=function(){return[il]},cg.prototype.getClass=function(){return cg},hg.INSERT.get=function(){return 1},hg.DELETE.get=function(){return 2},Object.defineProperties(cg,hg);var pg=function(){};pg.prototype.interfaces_=function(){return[]},pg.prototype.getClass=function(){return pg};var fg=function(){this._hasIntersection=!1,this._hasProper=!1,this._hasProperInterior=!1,this._properIntersectionPoint=null,this._li=null,this._includeProper=null,this._recordIsolated=null,this._isSelfIntersection=null,this._numIntersections=0,this.numTests=0,this._bdyNodes=null,this._isDone=!1,this._isDoneWhenProperInt=!1;var t=arguments[0],e=arguments[1],n=arguments[2];this._li=t,this._includeProper=e,this._recordIsolated=n};fg.prototype.isTrivialIntersection=function(t,e,n,r){if(t===n&&1===this._li.getIntersectionNum()){if(fg.isAdjacentSegments(e,r))return!0;if(t.isClosed()){var i=t.getNumPoints()-1;if(0===e&&r===i||0===r&&e===i)return!0}}return!1},fg.prototype.getProperIntersectionPoint=function(){return this._properIntersectionPoint},fg.prototype.setIsDoneIfProperInt=function(t){this._isDoneWhenProperInt=t},fg.prototype.hasProperInteriorIntersection=function(){return this._hasProperInterior},fg.prototype.isBoundaryPointInternal=function(t,e){for(var n=e.iterator();n.hasNext();){var r=n.next().getCoordinate();if(t.isIntersection(r))return!0}return!1},fg.prototype.hasProperIntersection=function(){return this._hasProper},fg.prototype.hasIntersection=function(){return this._hasIntersection},fg.prototype.isDone=function(){return this._isDone},fg.prototype.isBoundaryPoint=function(t,e){return null!==e&&(!!this.isBoundaryPointInternal(t,e[0])||!!this.isBoundaryPointInternal(t,e[1]))},fg.prototype.setBoundaryNodes=function(t,e){this._bdyNodes=new Array(2).fill(null),this._bdyNodes[0]=t,this._bdyNodes[1]=e},fg.prototype.addIntersections=function(t,e,n,r){if(t===n&&e===r)return null;this.numTests++;var i=t.getCoordinates()[e],o=t.getCoordinates()[e+1],s=n.getCoordinates()[r],a=n.getCoordinates()[r+1];this._li.computeIntersection(i,o,s,a),this._li.hasIntersection()&&(this._recordIsolated&&(t.setIsolated(!1),n.setIsolated(!1)),this._numIntersections++,this.isTrivialIntersection(t,e,n,r)||(this._hasIntersection=!0,!this._includeProper&&this._li.isProper()||(t.addIntersections(this._li,e,0),n.addIntersections(this._li,r,1)),this._li.isProper()&&(this._properIntersectionPoint=this._li.getIntersection(0).copy(),this._hasProper=!0,this._isDoneWhenProperInt&&(this._isDone=!0),this.isBoundaryPoint(this._li,this._bdyNodes)||(this._hasProperInterior=!0))))},fg.prototype.interfaces_=function(){return[]},fg.prototype.getClass=function(){return fg},fg.isAdjacentSegments=function(t,e){return 1===Math.abs(t-e)};var gg=function(t){function e(){t.call(this),this.events=new lc,this.nOverlaps=null}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.prepareEvents=function(){np.sort(this.events);for(var t=0;te||this._maxo?1:0},vg.prototype.interfaces_=function(){return[sl]},vg.prototype.getClass=function(){return vg};var _g=function(t){function e(){t.call(this),this._item=null;var e=arguments[0],n=arguments[1],r=arguments[2];this._min=e,this._max=n,this._item=r}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.query=function(t,e,n){if(!this.intersects(t,e))return null;n.visitItem(this._item)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(dg),mg=function(t){function e(){t.call(this),this._node1=null,this._node2=null;var e=arguments[0],n=arguments[1];this._node1=e,this._node2=n,this.buildExtent(this._node1,this._node2)}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.buildExtent=function(t,e){this._min=Math.min(t._min,e._min),this._max=Math.max(t._max,e._max)},e.prototype.query=function(t,e,n){if(!this.intersects(t,e))return null;null!==this._node1&&this._node1.query(t,e,n),null!==this._node2&&this._node2.query(t,e,n)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e}(dg),xg=function(){this._leaves=new lc,this._root=null,this._level=0};xg.prototype.buildTree=function(){np.sort(this._leaves,new dg.NodeComparator);for(var t=this._leaves,e=null,n=new lc;;){if(this.buildLevel(t,n),1===n.size())return n.get(0);e=t,t=n,n=e}},xg.prototype.insert=function(t,e,n){if(null!==this._root)throw new Error("Index cannot be added to once it has been queried");this._leaves.add(new _g(t,e,n))},xg.prototype.query=function(t,e,n){this.init(),this._root.query(t,e,n)},xg.prototype.buildRoot=function(){if(null!==this._root)return null;this._root=this.buildTree()},xg.prototype.printNode=function(t){Pl.out.println(Fl.toLineString(new ul(t._min,this._level),new ul(t._max,this._level)))},xg.prototype.init=function(){if(null!==this._root)return null;this.buildRoot()},xg.prototype.buildLevel=function(t,e){this._level++,e.clear();for(var n=0;n=2,"found LineString with single point"),this.insertBoundaryPoint(this._argIndex,e[0]),this.insertBoundaryPoint(this._argIndex,e[e.length-1])},e.prototype.getInvalidPoint=function(){return this._invalidPoint},e.prototype.getBoundaryPoints=function(){for(var t=this.getBoundaryNodes(),e=new Array(t.size()).fill(null),n=0,r=t.iterator();r.hasNext();){var i=r.next();e[n++]=i.getCoordinate().copy()}return e},e.prototype.getBoundaryNodes=function(){return null===this._boundaryNodes&&(this._boundaryNodes=this._nodes.getBoundaryNodes(this._argIndex)),this._boundaryNodes},e.prototype.addSelfIntersectionNode=function(t,e,n){if(this.isBoundaryNode(t,e))return null;n===pl.BOUNDARY&&this._useBoundaryDeterminationRule?this.insertBoundaryPoint(t,e):this.insertPoint(t,e,n)},e.prototype.addPolygonRing=function(t,e,n){if(t.isEmpty())return null;var r=pc.removeRepeatedPoints(t.getCoordinates());if(r.length<4)return this._hasTooFewPoints=!0,this._invalidPoint=r[0],null;var i=e,o=n;Xl.isCCW(r)&&(i=n,o=e);var s=new of(r,new Dh(this._argIndex,pl.BOUNDARY,i,o));this._lineEdgeMap.put(t,s),this.insertEdge(s),this.insertPoint(this._argIndex,r[0],pl.BOUNDARY)},e.prototype.insertPoint=function(t,e,n){var r=this._nodes.addNode(e),i=r.getLabel();null===i?r._label=new Dh(t,n):i.setLocation(t,n)},e.prototype.createEdgeSetIntersector=function(){return new gg},e.prototype.addSelfIntersectionNodes=function(t){for(var e=this._edges.iterator();e.hasNext();)for(var n=e.next(),r=n.getLabel().getLocation(t),i=n.eiList.iterator();i.hasNext();){var o=i.next();this.addSelfIntersectionNode(t,o.coord,r)}},e.prototype.add=function(){if(1!==arguments.length)return t.prototype.add.apply(this,arguments);var e=arguments[0];if(e.isEmpty())return null;if(e instanceof rh&&(this._useBoundaryDeterminationRule=!1),e instanceof th)this.addPolygon(e);else if(e instanceof Zc)this.addLineString(e);else if(e instanceof Qc)this.addPoint(e);else if(e instanceof eh)this.addCollection(e);else if(e instanceof Gc)this.addCollection(e);else if(e instanceof rh)this.addCollection(e);else{if(!(e instanceof kc))throw new Error(e.getClass().getName());this.addCollection(e)}},e.prototype.addCollection=function(t){for(var e=0;e50?(null===this._areaPtLocator&&(this._areaPtLocator=new bg(this._parentGeom)),this._areaPtLocator.locate(t)):this._ptLocator.locate(t,this._parentGeom)},e.prototype.findEdge=function(){if(1===arguments.length){var e=arguments[0];return this._lineEdgeMap.get(e)}return t.prototype.findEdge.apply(this,arguments)},e.prototype.interfaces_=function(){return[]},e.prototype.getClass=function(){return e},e.determineBoundary=function(t,e){return t.isInBoundary(e)?pl.BOUNDARY:pl.INTERIOR},e}(Hh),Cg=function(){if(this._li=new jl,this._resultPrecisionModel=null,this._arg=null,1===arguments.length){var t=arguments[0];this.setComputationPrecision(t.getPrecisionModel()),this._arg=new Array(1).fill(null),this._arg[0]=new Sg(0,t)}else if(2===arguments.length){var e=arguments[0],n=arguments[1],r=Ql.OGC_SFS_BOUNDARY_RULE;e.getPrecisionModel().compareTo(n.getPrecisionModel())>=0?this.setComputationPrecision(e.getPrecisionModel()):this.setComputationPrecision(n.getPrecisionModel()),this._arg=new Array(2).fill(null),this._arg[0]=new Sg(0,e,r),this._arg[1]=new Sg(1,n,r)}else if(3===arguments.length){var i=arguments[0],o=arguments[1],s=arguments[2];i.getPrecisionModel().compareTo(o.getPrecisionModel())>=0?this.setComputationPrecision(i.getPrecisionModel()):this.setComputationPrecision(o.getPrecisionModel()),this._arg=new Array(2).fill(null),this._arg[0]=new Sg(0,i,s),this._arg[1]=new Sg(1,o,s)}};Cg.prototype.getArgGeometry=function(t){return this._arg[t].getGeometry()},Cg.prototype.setComputationPrecision=function(t){this._resultPrecisionModel=t,this._li.setPrecisionModel(this._resultPrecisionModel)},Cg.prototype.interfaces_=function(){return[]},Cg.prototype.getClass=function(){return Cg};var Pg=function(){};Pg.prototype.interfaces_=function(){return[]},Pg.prototype.getClass=function(){return Pg},Pg.map=function(){if(arguments[0]instanceof Wl&&gl(arguments[1],Pg.MapOp)){for(var t=arguments[0],e=arguments[1],n=new lc,r=0;r=t.size()?null:t.get(e)},Fg.union=function(t){return new Fg(t).union()},kg.STRTREE_NODE_CAPACITY.get=function(){return 4},Object.defineProperties(Fg,kg);var Gg=function(){};function qg(){return new Bg}function Bg(){this.reset()}Gg.prototype.interfaces_=function(){return[]},Gg.prototype.getClass=function(){return Gg},Gg.union=function(t,e){if(t.isEmpty()||e.isEmpty()){if(t.isEmpty()&&e.isEmpty())return Mg.createEmptyResult(Mg.UNION,t,e,t.getFactory());if(t.isEmpty())return e.copy();if(e.isEmpty())return t.copy()}return t.checkNotGeometryCollection(t),t.checkNotGeometryCollection(e),ug.overlayOp(t,e,Mg.UNION)},Bg.prototype={constructor:Bg,reset:function(){this.s=this.t=0},add:function(t){jg(zg,t,this.t),jg(this,zg.s,this.s),this.s?this.t+=zg.t:this.s=zg.t},valueOf:function(){return this.s}};var zg=new Bg;function jg(t,e,n){var r=t.s=e+n,i=r-e,o=r-i;t.t=e-o+(n-i)}var Ug=1e-6,Vg=Math.PI,Xg=Vg/2,Yg=Vg/4,Hg=2*Vg,Wg=180/Vg,Jg=Vg/180,Zg=Math.abs,Kg=Math.atan,Qg=Math.atan2,$g=Math.cos,td=Math.sin,ed=Math.sqrt;function nd(t){return t>1?0:t<-1?Vg:Math.acos(t)}function rd(t){return t>1?Xg:t<-1?-Xg:Math.asin(t)}function id(){}function od(t,e){t&&ad.hasOwnProperty(t.type)&&ad[t.type](t,e)}var sd={Feature:function(t,e){od(t.geometry,e)},FeatureCollection:function(t,e){for(var n=t.features,r=-1,i=n.length;++rVg?t-Hg:t<-Vg?t+Hg:t,e]}function md(t){return function(e,n){return[(e+=t)>Vg?e-Hg:e<-Vg?e+Hg:e,n]}}function xd(t){var e=md(t);return e.invert=md(-t),e}function Ed(t,e){var n=$g(t),r=td(t),i=$g(e),o=td(e);function s(t,e){var s=$g(e),a=$g(t)*s,u=td(t)*s,l=td(e),c=l*n+a*r;return[Qg(u*i-c*o,a*n-l*r),rd(c*i+u*o)]}return s.invert=function(t,e){var s=$g(e),a=$g(t)*s,u=td(t)*s,l=td(e),c=l*i-u*o;return[Qg(u*i+l*o,a*n+c*r),rd(c*n-a*r)]},s}function bd(t,e){(e=hd(e))[0]-=t,yd(e);var n=nd(-e[1]);return((-e[2]<0?-n:n)+Hg-Ug)%Hg}function wd(){var t,e=[];return{point:function(e,n){t.push([e,n])},lineStart:function(){e.push(t=[])},lineEnd:id,rejoin:function(){e.length>1&&e.push(e.pop().concat(e.shift()))},result:function(){var n=e;return e=[],t=null,n}}}function Id(t,e){return Zg(t[0]-e[0])=0;--o)i.point((c=l[o])[0],c[1]);else r(p.x,p.p.x,-1,i);p=p.p}l=(p=p.o).z,f=!f}while(!p.v);i.lineEnd()}}}function Cd(t){if(e=t.length){for(var e,n,r=0,i=t[0];++re?1:t>=e?0:NaN}_d.invert=_d;var Md,Ld;1===(Md=Pd).length&&(Ld=Md,Md=function(t,e){return Pd(Ld(t),e)});function Od(t){for(var e,n,r,i=t.length,o=-1,s=0;++o=0;)for(e=(r=t[i]).length;--e>=0;)n[--s]=r[e];return n}var Rd=1e9,Td=-Rd;function Ad(t,e,n,r){function i(i,o){return t<=i&&i<=n&&e<=o&&o<=r}function o(i,o,a,l){var c=0,h=0;if(null==i||(c=s(i,a))!==(h=s(o,a))||u(i,o)<0^a>0)do{l.point(0===c||3===c?t:n,c>1?r:e)}while((c=(c+a+4)%4)!==h);else l.point(o[0],o[1])}function s(r,i){return Zg(r[0]-t)0?0:3:Zg(r[0]-n)0?2:1:Zg(r[1]-e)0?1:0:i>0?3:2}function a(t,e){return u(t.x,e.x)}function u(t,e){var n=s(t,1),r=s(e,1);return n!==r?n-r:0===n?e[1]-t[1]:1===n?t[0]-e[0]:2===n?t[1]-e[1]:e[0]-t[0]}return function(s){var u,l,c,h,p,f,g,d,y,v,_,m=s,x=wd(),E={point:b,lineStart:function(){E.point=w,l&&l.push(c=[]);v=!0,y=!1,g=d=NaN},lineEnd:function(){u&&(w(h,p),f&&y&&x.rejoin(),u.push(x.result()));E.point=b,y&&m.lineEnd()},polygonStart:function(){m=x,u=[],l=[],_=!0},polygonEnd:function(){var e=function(){for(var e=0,n=0,i=l.length;nr&&(p-o)*(r-s)>(f-s)*(t-o)&&++e:f<=r&&(p-o)*(r-s)<(f-s)*(t-o)&&--e;return e}(),n=_&&e,i=(u=Od(u)).length;(n||i)&&(s.polygonStart(),n&&(s.lineStart(),o(null,null,1,s),s.lineEnd()),i&&Sd(u,a,e,o,s),s.polygonEnd());m=s,u=l=c=null}};function b(t,e){i(t,e)&&m.point(t,e)}function w(o,s){var a=i(o,s);if(l&&c.push([o,s]),v)h=o,p=s,f=a,v=!1,a&&(m.lineStart(),m.point(o,s));else if(a&&y)m.point(o,s);else{var u=[g=Math.max(Td,Math.min(Rd,g)),d=Math.max(Td,Math.min(Rd,d))],x=[o=Math.max(Td,Math.min(Rd,o)),s=Math.max(Td,Math.min(Rd,s))];!function(t,e,n,r,i,o){var s,a=t[0],u=t[1],l=0,c=1,h=e[0]-a,p=e[1]-u;if(s=n-a,h||!(s>0)){if(s/=h,h<0){if(s0){if(s>c)return;s>l&&(l=s)}if(s=i-a,h||!(s<0)){if(s/=h,h<0){if(s>c)return;s>l&&(l=s)}else if(h>0){if(s0)){if(s/=p,p<0){if(s0){if(s>c)return;s>l&&(l=s)}if(s=o-u,p||!(s<0)){if(s/=p,p<0){if(s>c)return;s>l&&(l=s)}else if(p>0){if(s0&&(t[0]=a+l*h,t[1]=u+l*p),c<1&&(e[0]=a+c*h,e[1]=u+c*p),!0}}}}}(u,x,t,e,n,r)?a&&(m.lineStart(),m.point(o,s),_=!1):(y||(m.lineStart(),m.point(u[0],u[1])),m.point(x[0],x[1]),a||m.lineEnd(),_=!1)}g=o,d=s,y=a}return E}}var Dd=qg();qg();function Fd(t){return t}qg(),qg();var kd=1/0,Gd=kd,qd=-kd,Bd=qd,zd={point:function(t,e){tqd&&(qd=t);eBd&&(Bd=e)},lineStart:id,lineEnd:id,polygonStart:id,polygonEnd:id,result:function(){var t=[[kd,Gd],[qd,Bd]];return qd=Bd=-(Gd=kd=1/0),t}};qg();function jd(t,e,n,r){return function(i,o){var s,a,u,l=e(o),c=i.invert(r[0],r[1]),h=wd(),p=e(h),f=!1,g={point:d,lineStart:v,lineEnd:_,polygonStart:function(){g.point=m,g.lineStart=x,g.lineEnd=E,a=[],s=[]},polygonEnd:function(){g.point=d,g.lineStart=v,g.lineEnd=_,a=Od(a);var t=function(t,e){var n=e[0],r=e[1],i=[td(n),-$g(n),0],o=0,s=0;Dd.reset();for(var a=0,u=t.length;a=0?1:-1,I=w*b,N=I>Vg,S=g*x;if(Dd.add(Qg(S*w*td(I),d*E+S*$g(I))),o+=N?b+w*Hg:b,N^p>=n^_>=n){var C=fd(hd(h),hd(v));yd(C);var P=fd(i,C);yd(P);var M=(N^b>=0?-1:1)*rd(P[2]);(r>M||r===M&&(C[0]||C[1]))&&(s+=N^b>=0?1:-1)}}return(o<-1e-6||o0){for(f||(o.polygonStart(),f=!0),o.lineStart(),t=0;t1&&2&i&&l.push(l.pop().concat(l.shift())),a.push(l.filter(Ud))}return g}}function Ud(t){return t.length>1}function Vd(t,e){return((t=t.x)[0]<0?t[1]-Xg-Ug:Xg-t[1])-((e=e.x)[0]<0?e[1]-Xg-Ug:Xg-e[1])}var Xd=jd((function(){return!0}),(function(t){var e,n=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),e=1},point:function(o,s){var a=o>0?Vg:-Vg,u=Zg(o-n);Zg(u-Vg)0?Xg:-Xg),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),e=0):i!==a&&u>=Vg&&(Zg(n-i)Ug?Kg((td(e)*(o=$g(r))*td(n)-td(r)*(i=$g(e))*td(t))/(i*o*s)):(e+r)/2}(n,r,o,s),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),e=0),t.point(n=o,r=s),i=a},lineEnd:function(){t.lineEnd(),n=r=NaN},clean:function(){return 2-e}}}),(function(t,e,n,r){var i;if(null==t)i=n*Xg,r.point(-Vg,i),r.point(0,i),r.point(Vg,i),r.point(Vg,0),r.point(Vg,-i),r.point(0,-i),r.point(-Vg,-i),r.point(-Vg,0),r.point(-Vg,i);else if(Zg(t[0]-e[0])>Ug){var o=t[0]0,i=Zg(n)>Ug;function o(t,e){return $g(t)*$g(e)>n}function s(t,e,r){var i=[1,0,0],o=fd(hd(t),hd(e)),s=pd(o,o),a=o[0],u=s-a*a;if(!u)return!r&&t;var l=n*s/u,c=-n*a/u,h=fd(i,o),p=dd(i,l);gd(p,dd(o,c));var f=h,g=pd(p,f),d=pd(f,f),y=g*g-d*(pd(p,p)-1);if(!(y<0)){var v=ed(y),_=dd(f,(-g-v)/d);if(gd(_,p),_=cd(_),!r)return _;var m,x=t[0],E=e[0],b=t[1],w=e[1];E0^_[1]<(Zg(_[0]-x)Vg^(x<=_[0]&&_[0]<=E)){var S=dd(f,(-g+v)/d);return gd(S,p),[_,cd(S)]}}}function a(e,n){var i=r?t:Vg-t,o=0;return e<-i?o|=1:e>i&&(o|=2),n<-i?o|=4:n>i&&(o|=8),o}return jd(o,(function(t){var e,n,u,l,c;return{lineStart:function(){l=u=!1,c=1},point:function(h,p){var f,g=[h,p],d=o(h,p),y=r?d?0:a(h,p):d?a(h+(h<0?Vg:-Vg),p):0;if(!e&&(l=u=d)&&t.lineStart(),d!==u&&(!(f=s(e,g))||Id(e,f)||Id(g,f))&&(g[0]+=Ug,g[1]+=Ug,d=o(g[0],g[1])),d!==u)c=0,d?(t.lineStart(),f=s(g,e),t.point(f[0],f[1])):(f=s(e,g),t.point(f[0],f[1]),t.lineEnd()),e=f;else if(i&&e&&r^d){var v;y&n||!(v=s(g,e,!0))||(c=0,r?(t.lineStart(),t.point(v[0][0],v[0][1]),t.point(v[1][0],v[1][1]),t.lineEnd()):(t.point(v[1][0],v[1][1]),t.lineEnd(),t.lineStart(),t.point(v[0][0],v[0][1])))}!d||e&&Id(e,g)||t.point(g[0],g[1]),e=g,u=d,n=y},lineEnd:function(){u&&t.lineEnd(),e=null},clean:function(){return c|(l&&u)<<1}}}),(function(n,r,i,o){!function(t,e,n,r,i,o){if(n){var s=$g(e),a=td(e),u=r*n;null==i?(i=e+r*Hg,o=e-u/2):(i=bd(s,i),o=bd(s,o),(r>0?io)&&(i+=r*Hg));for(var l,c=i;r>0?c>o:c4*e&&d--){var x=s+p,E=a+f,b=u+g,w=ed(x*x+E*E+b*b),I=rd(b/=w),N=Zg(Zg(b)-1)e||Zg((v*M+_*L)/m-.5)>.3||s*p+a*f+u*g2?t[2]%360*Jg:0,P()):[y*Wg,v*Wg,_*Wg]},N.precision=function(t){return arguments.length?(I=Kd(C,w=t*t),M()):ed(w)},N.fitExtent=function(t,e){return Jd(N,t,e)},N.fitSize=function(t,e){return function(t,e,n){return Jd(t,[[0,0],e],n)}(N,t,e)},function(){return e=t.apply(this,arguments),N.invert=e.invert&&S,P()}}((function(){return t}))()}var ty=function(t){return function(e,n){var r=$g(e),i=$g(n),o=t(r*i);return[o*i*td(e),o*td(n)]}}((function(t){return(t=nd(t))&&t/td(t)}));function ey(){return $d(ty).scale(79.4188).clipAngle(179.999)}function ny(t,n,r,i){var s=t.properties||{},a="Feature"===t.type?t.geometry:t;if("GeometryCollection"===a.type){var u=[];return q(t,(function(t){var e=ny(t,n,r,i);e&&u.push(e)})),f(u)}var l=function(t){var n=xn(t).geometry.coordinates,r=[-n[0],-n[1]];return ey().rotate(r).scale(e)}(a),c={type:a.type,coordinates:iy(a.coordinates,l)},h=(new Ih).read(c),p=m(x(n,r),"meters"),g=_f.bufferOp(h,p,i);if(!ry((g=(new Nh).write(g)).coordinates))return o({type:g.type,coordinates:oy(g.coordinates,l)},s)}function ry(t){return Array.isArray(t[0])?ry(t[0]):isNaN(t[0])}function iy(t,e){return"object"!=typeof t[0]?e(t):t.map((function(t){return iy(t,e)}))}function oy(t,e){return"object"!=typeof t[0]?e.invert(t):t.map((function(t){return oy(t,e)}))}function sy(t,e,n){void 0===n&&(n={});var r=rt(t),i=rt(e),o=$u.intersection(r.coordinates,i.coordinates);return 0===o.length?null:1===o.length?l(o[0],n.properties):y(o,n.properties)}function ay(t,e,n){void 0===n&&(n={});var r=JSON.stringify(n.properties||{}),i=t[0],o=t[1],s=t[2],a=t[3],u=(o+a)/2,l=(i+s)/2,c=2*e/me([i,u],[s,u],n)*(s-i),h=2*e/me([l,o],[l,a],n)*(a-o),p=c/2,g=2*p,d=Math.sqrt(3)/2*h,y=s-i,v=a-o,_=3/4*g,m=d,x=(y-g)/(g-p/2),E=Math.floor(x),b=(E*_-p/2-y)/2-p/2+_/2,w=Math.floor((v-d)/d),I=(v-w*d)/2,N=w*d-v>d/2;N&&(I-=d/4);for(var S=[],C=[],P=0;P<6;P++){var M=2*Math.PI/6*P;S.push(Math.cos(M)),C.push(Math.sin(M))}for(var L=[],O=0;O<=E;O++)for(var R=0;R<=w;R++){var T=O%2==1;if((0!==R||!T)&&(0!==R||!N)){var A=O*_+i-b,D=R*m+o+I;if(T&&(D-=d/2),!0===n.triangles)ly([A,D],c/2,h/2,JSON.parse(r),S,C).forEach((function(t){n.mask?sy(n.mask,t)&&L.push(t):L.push(t)}));else{var F=uy([A,D],c/2,h/2,JSON.parse(r),S,C);n.mask?sy(n.mask,F)&&L.push(F):L.push(F)}}}return f(L)}function uy(t,e,n,r,i,o){for(var s=[],a=0;a<6;a++){var u=t[0]+e*i[a],c=t[1]+n*o[a];s.push([u,c])}return s.push(s[0].slice()),l([s],r)}function ly(t,e,n,r,i,o){for(var s=[],a=0;a<6;a++){var u=[];u.push(t),u.push([t[0]+e*i[a],t[1]+n*o[a]]),u.push([t[0]+e*i[(a+1)%6],t[1]+n*o[(a+1)%6]]),u.push(t),s.push(l([u],r))}return s}function cy(t){return y(t)}function hy(t){return l(t&&t.geometry.coordinates||[[[180,90],[-180,90],[-180,-90],[180,-90],[180,90]]])}function py(t,e,n){return void 0===n&&(n={}),function(t,e,n,r){void 0===r&&(r={});for(var i=[],o=t[0],s=t[1],a=t[2],u=t[3],c=e/me([o,s],[a,s],r)*(a-o),h=n/me([o,s],[o,u],r)*(u-s),p=a-o,g=u-s,d=Math.floor(p/c),y=Math.floor(g/h),v=(g-y*h)/2,_=o+(p-d*c)/2,m=0;m=i&&o===r.length-1);o++){if(i>=e){var s=e-i;if(s){var u=mn(r[o],r[o-1])-180;return vn(r[o],s,u,n)}return a(r[o])}i+=me(r[o],r[o+1],n)}return a(r[r.length-1])},t.angle=function(t,e,n,r){if(void 0===r&&(r={}),!P(r))throw new Error("options is invalid");if(!t)throw new Error("startPoint is required");if(!e)throw new Error("midPoint is required");if(!n)throw new Error("endPoint is required");var i=t,o=e,s=n,a=b(!0!==r.mercator?mn(i,o):Bi(i,o)),u=b(!0!==r.mercator?mn(s,o):Bi(s,o)),l=Math.abs(a-u);return!0===r.explementary?360-l:l},t.applyFilter=uu,t.area=jr,t.areaFactors=i,t.bbox=Z,t.bboxClip=function(t,e){var n=rt(t),r=n.type,i="Feature"===t.type?t.properties:{},o=n.coordinates;switch(r){case"LineString":case"MultiLineString":var s=[];return"LineString"===r&&(o=[o]),o.forEach((function(t){!function(t,e,n){var r,i,o,s,a,u=t.length,l=Ri(t[0],e),c=[];for(n||(n=[]),r=1;r0)for(var n=0;n0},t.booleanParallel=function(t,e){if(!t)throw new Error("line1 is required");if(!e)throw new Error("line2 is required");if("LineString"!==Us(t,"line1"))throw new Error("line1 must be a LineString");if("LineString"!==Us(e,"line2"))throw new Error("line2 must be a LineString");for(var n=Zn(tn(t)).features,r=Zn(tn(e)).features,i=0;in&&(e.numberOfClusters=n),!0!==e.mutate&&(t=Ie(t));var r=G(t),i=r.slice(0,e.numberOfClusters),o=zs(r,e.numberOfClusters,i),s={};return o.centroids.forEach((function(t,e){s[e]=t})),F(t,(function(t,e){var n=o.idxs[e];t.properties.cluster=n,t.properties.centroid=s[n]})),t},t.collect=function(t,e,n,r){var i=Nt(6),o=e.features.map((function(t){var e;return{minX:t.geometry.coordinates[0],minY:t.geometry.coordinates[1],maxX:t.geometry.coordinates[0],maxY:t.geometry.coordinates[1],property:null===(e=t.properties)||void 0===e?void 0:e[n]}}));return i.load(o),t.features.forEach((function(t){t.properties||(t.properties={});var e=Z(t),n=i.search({minX:e[0],minY:e[1],maxX:e[2],maxY:e[3]}),o=[];n.forEach((function(e){ye([e.minX,e.minY],t)&&o.push(e.property)})),t.properties[r]=o})),t},t.collectionOf=nt,t.combine=function(t){var e={MultiPoint:{coordinates:[],properties:[]},MultiLineString:{coordinates:[],properties:[]},MultiPolygon:{coordinates:[],properties:[]}};return F(t,(function(t){var n,r,i,o;switch(null===(o=t.geometry)||void 0===o?void 0:o.type){case"Point":e.MultiPoint.coordinates.push(t.geometry.coordinates),e.MultiPoint.properties.push(t.properties);break;case"MultiPoint":(n=e.MultiPoint.coordinates).push.apply(n,t.geometry.coordinates),e.MultiPoint.properties.push(t.properties);break;case"LineString":e.MultiLineString.coordinates.push(t.geometry.coordinates),e.MultiLineString.properties.push(t.properties);break;case"MultiLineString":(r=e.MultiLineString.coordinates).push.apply(r,t.geometry.coordinates),e.MultiLineString.properties.push(t.properties);break;case"Polygon":e.MultiPolygon.coordinates.push(t.geometry.coordinates),e.MultiPolygon.properties.push(t.properties);break;case"MultiPolygon":(i=e.MultiPolygon.coordinates).push.apply(i,t.geometry.coordinates),e.MultiPolygon.properties.push(t.properties)}})),f(Object.keys(e).filter((function(t){return e[t].coordinates.length})).sort().map((function(t){return o({type:t,coordinates:e[t].coordinates},{collectedProperties:e[t].properties})})))},t.concave=function(t,e){void 0===e&&(e={});var n=e.maxEdge||1/0,r=xe(function(t){var e=[],n={};return F(t,(function(t){if(t.geometry){var r=t.geometry.coordinates.join("-");Object.prototype.hasOwnProperty.call(n,r)||(e.push(t),n[r]=!0)}})),f(e)}(t));if(r.features=r.features.filter((function(t){var r=t.geometry.coordinates[0][0],i=t.geometry.coordinates[0][1],o=t.geometry.coordinates[0][2],s=me(r,i,e),a=me(i,o,e),u=me(r,o,e);return s<=n&&a<=n&&u<=n})),r.features.length<1)return null;var i=$e(r);return 1===i.coordinates.length&&(i.coordinates=i.coordinates[0],i.type="Polygon"),o(i)},t.containsNumber=$,t.convertArea=S,t.convertDistance=N,t.convertLength=N,t.convex=de,t.coordAll=G,t.coordEach=R,t.coordReduce=T,t.createBins=au,t.degrees2radians=I,t.degreesToRadians=I,t.destination=vn,t.difference=function(t,e){var n=rt(t),r=rt(e),i=t.properties||{},o=$u.difference(n.coordinates,r.coordinates);return 0===o.length?null:1===o.length?l(o[0],i):y(o,i)},t.dissolve=function(t,e){if(!P(e=e||{}))throw new Error("options is invalid");var n=e.propertyName;nt(t,"Polygon","dissolve");var r=[];if(!e.propertyName)return ni(y($u.union.apply(null,t.features.map((function(t){return t.geometry.coordinates})))));var i={};F(t,(function(t){Object.prototype.hasOwnProperty.call(i,t.properties[n])||(i[t.properties[n]]=[]),i[t.properties[n]].push(t)}));for(var o=Object.keys(i),s=0;s0&&(s=l(o).geometry),qa(s,a),o=a.slice(0);n.push(l(o,i));break;case"MultiPolygon":o=[[[]]];for(var c=0;c0&&(s=y(o).geometry),Ba(s,a),o=a.slice(0);n.push(y(o,i));break;default:throw new Error("geometry is invalid, must be Polygon or MultiPolygon")}})),f(n)},t.polygonTangents=function(t,e){var n,r,i,o,s=Q(t),u=Q(e),l=Z(e),c=0,h=null;switch(s[0]>l[0]&&s[0]l[1]&&s[1] is required");if("boolean"!=typeof n)throw new Error(" must be a boolean");if("boolean"!=typeof r)throw new Error(" must be a boolean");!1===r&&(t=Ie(t));var i=[];switch(t.type){case"GeometryCollection":return q(t,(function(t){Hi(t,n)})),t;case"FeatureCollection":return F(t,(function(t){F(Hi(t,n),(function(t){i.push(t)}))})),f(i)}return Hi(t,n)},t.rhumbBearing=Bi,t.rhumbDestination=ji,t.rhumbDistance=Ar,t.round=_,t.sample=function(t,e){if(!t)throw new Error("featurecollection is required");if(null==e)throw new Error("num is required");if("number"!=typeof e)throw new Error("num must be a number");return f(function(t,e){var n,r,i=t.slice(0),o=t.length,s=o-e;for(;o-- >s;)n=i[r=Math.floor((o+1)*Math.random())],i[r]=i[o],i[o]=n;return i.slice(s)}(t.features,e))},t.sector=function(t,e,n,r,i){if(!P(i=i||{}))throw new Error("options is invalid");var o=i.properties;if(!t)throw new Error("center is required");if(null==n)throw new Error("bearing1 is required");if(null==r)throw new Error("bearing2 is required");if(!e)throw new Error("radius is required");if("object"!=typeof i)throw new Error("options must be an object");if(qi(n)===qi(r))return _n(t,e,i);var s=Q(t),a=bi(t,e,n,r,i),u=[[s]];return R(a,(function(t){u[0].push(t)})),u[0].push(s),l(u,o)},t.segmentEach=U,t.segmentReduce=V,t.shortestPath=function(t,e,n){if(!P(n=n||{}))throw new Error("options is invalid");var r=n.resolution,i=n.minDistance,s=n.obstacles||f([]);if(!t)throw new Error("start is required");if(!e)throw new Error("end is required");if(r&&!C(r)||r<=0)throw new Error("options.resolution must be a number, greater than 0");if(i)throw new Error("options.minDistance is not yet implemented");var u=K(t),l=K(e);switch(t=a(u),e=a(l),it(s)){case"FeatureCollection":if(0===s.features.length)return h([u,l]);break;case"Polygon":s=f([o(rt(s))]);break;default:throw new Error("invalid obstacles")}var c=s;c.features.push(t),c.features.push(e);var p=Z(ts(gn(Z(c)),1.15));r||(r=me([p[0],p[1]],[p[2],p[1]],n)/100),c.features.pop(),c.features.pop();for(var g=p[0],d=p[1],y=p[2],v=p[3],_=r/me([g,d],[y,d],n)*(y-g),m=r/me([g,d],[g,v],n)*(v-d),x=y-g,E=v-d,b=Math.floor(x/_),w=Math.floor(E/m),I=(x-b*_)/2,N=[],S=[],M=[],L=[],O=1/0,R=1/0,T=v-(E-w*m)/2,A=0;T>=d;){for(var D=[],F=[],k=g+I,G=0;k<=y;){var q=a([k,T]),B=Js(q,s);D.push(B?0:1),F.push(k+"|"+T);var z=me(q,t);!B&&z + + +{% endblock %} + +{% block content %} +

          Appliquer la configuration d'un utilisateur sur les sélectionnés

          + +
          + Attention : Cette action va écraser la configuration existante + (rôles, vues, thématiques, contrats, équipes) des utilisateurs ci-dessous avec celle de + l'utilisateur source choisi. L'utilisateur lui-même ne changera pas. +
          + +

          Utilisateurs cibles ({{ queryset.count }}) :

          +
            + {% for obj in queryset %} +
          • {{ obj.user.username }} {% if obj.user.get_full_name %}({{ obj.user.get_full_name }}){% endif %}
          • + {% endfor %} +
          + +
          + {% csrf_token %} + {# Re-post the selected IDs so the queryset is reconstructed on submit #} + {% for obj in queryset %} + + {% endfor %} + + + +
          + +
          + + + {% if form.source.errors %} +
            {% for e in form.source.errors %}
          • {{ e }}
          • {% endfor %}
          + {% endif %} +

          {{ form.source.help_text }}

          +
          +
          + +
          + + Annuler +
          + + +{% endblock %} diff --git a/streetup/common/templates/admin/common/assign_company.html b/streetup/common/templates/admin/common/assign_company.html new file mode 100644 index 0000000..4427030 --- /dev/null +++ b/streetup/common/templates/admin/common/assign_company.html @@ -0,0 +1,105 @@ +{% extends "admin/base_site.html" %} +{% load static %} + +{% block extrahead %} + {{ block.super }} + + + +{% endblock %} + +{% block content %} +

          Assigner une société aux utilisateurs sélectionnés

          + +

          Utilisateurs cibles ({{ queryset.count }}) :

          +
            + {% for obj in queryset %} +
          • {{ obj.user.username }}{% if obj.user.get_full_name %} ({{ obj.user.get_full_name }}){% endif %} + {% if obj.company %} — actuellement : {{ obj.company }}{% endif %} +
          • + {% endfor %} +
          + +
          + {% csrf_token %} + {% for obj in queryset %} + + {% endfor %} + + + +
          + +
          + + + {% if form.company.errors %} +
            {% for e in form.company.errors %}
          • {{ e }}
          • {% endfor %}
          + {% endif %} +

          {{ form.company.help_text }}

          +
          +
          + +
          + + Annuler +
          + + +{% endblock %} diff --git a/streetup/common/templates/admin/common/duplicate_userconfig.html b/streetup/common/templates/admin/common/duplicate_userconfig.html new file mode 100644 index 0000000..48f5533 --- /dev/null +++ b/streetup/common/templates/admin/common/duplicate_userconfig.html @@ -0,0 +1,69 @@ +{% extends "admin/base_site.html" %} +{% load static %} + +{% block extrahead %} + {{ block.super }} + + + + +{% endblock %} + +{% block content %} +

          Dupliquer la configuration de {{ orig_config.user.username }}

          +
          {% csrf_token %} +
          + {{ form.user.label_tag }} +
          + +
          + + Ajouter + +
          +
          + + Annuler +
          + +{% endblock %} diff --git a/streetup/common/templates/admin/common/requestlog/change_list.html b/streetup/common/templates/admin/common/requestlog/change_list.html new file mode 100644 index 0000000..a3caa9a --- /dev/null +++ b/streetup/common/templates/admin/common/requestlog/change_list.html @@ -0,0 +1,64 @@ +{% extends "admin/change_list.html" %} +{% load i18n format_number %} + +{% block object-tools-items %} +{{ block.super }} +
        • + +
        • +{% endblock %} + +{% block extrahead %} +{{ block.super }} + + +{% endblock %} diff --git a/streetup/common/templates/admin/common/status_permission_matrix.html b/streetup/common/templates/admin/common/status_permission_matrix.html new file mode 100644 index 0000000..e5ccb4f --- /dev/null +++ b/streetup/common/templates/admin/common/status_permission_matrix.html @@ -0,0 +1,105 @@ +{% load i18n %} +{{ inline_admin_formset.formset.management_form }} +{{ inline_admin_formset.formset.non_form_errors }} + +{% if inline_admin_formset.formset.forms %} +
          +

          {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}

          + + {% for inline_admin_form in inline_admin_formset %} + {{ inline_admin_form.form.id }} + {% for hidden in inline_admin_form.form.hidden_fields %}{{ hidden }}{% endfor %} + {% endfor %} + +
          + + + + + {% for inline_admin_form in inline_admin_formset %} + {% if inline_admin_form.form.instance.pk %} + + {% endif %} + {% endfor %} + + + + + + {% for inline_admin_form in inline_admin_formset %} + {% if inline_admin_form.form.instance.pk %} + + {% endif %} + {% endfor %} + + + + {% for inline_admin_form in inline_admin_formset %} + {% if inline_admin_form.form.instance.pk %} + + {% endif %} + {% endfor %} + + + + {% for inline_admin_form in inline_admin_formset %} + {% if inline_admin_form.form.instance.pk %} + + {% endif %} + {% endfor %} + + +
          + {% trans "Permission" %} + +
          + {{ inline_admin_form.form.instance.get_status_display }} +
          +
          + {% trans "Voir" %} + +
          + {{ inline_admin_form.form.can_view }} + {% if inline_admin_form.form.can_view.errors %} + {{ inline_admin_form.form.can_view.errors }} + {% endif %} +
          +
          + {% trans "Modifier" %} + +
          + {{ inline_admin_form.form.can_edit }} + {% if inline_admin_form.form.can_edit.errors %} + {{ inline_admin_form.form.can_edit.errors }} + {% endif %} +
          +
          + {% trans "Changer → vers" %} + +
          + {{ inline_admin_form.form.can_change_status_to }} + {% if inline_admin_form.form.can_change_status_to.errors %} + {{ inline_admin_form.form.can_change_status_to.errors }} + {% endif %} +
          +
          +
          + + {% for inline_admin_form in inline_admin_formset %} + {% if inline_admin_form.form.non_field_errors %} +

          {{ inline_admin_form.form.non_field_errors }}

          + {% endif %} + {% endfor %} + +
          + +{% else %} +{# No forms: object not saved yet #} +
          +

          {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}

          +

          + {% trans "Enregistrez d'abord pour configurer les permissions par statut." %} +

          +
          +{% endif %} diff --git a/streetup/common/templates/common/account_info.html b/streetup/common/templates/common/account_info.html new file mode 100644 index 0000000..63aedc1 --- /dev/null +++ b/streetup/common/templates/common/account_info.html @@ -0,0 +1,203 @@ + + + + {% load i18n %} + {% load static %} + + + {% translate "Déconnexion" %} + + + + + + +
          +
          +

          StreetUp

          + +

          {% translate "Vous êtes connecté en tant que :" %}

          +
          {{ user.get_username }}
          + +
          + + + + +
          + +
          + + + + +
          + +
          + {% csrf_token %} + + +
          + + {% if origin_next %} + + {% endif %} +
          +
          + + + + + + + + diff --git a/streetup/common/templates/common/components/document_viewer.html b/streetup/common/templates/common/components/document_viewer.html new file mode 100644 index 0000000..59fedfd --- /dev/null +++ b/streetup/common/templates/common/components/document_viewer.html @@ -0,0 +1,434 @@ +{% load i18n document_viewer_tags short_name %} +{% comment %} + Reusable document thumbnail grid + carousel viewer. + Rendered by: common/templatetags/document_viewer_tags.py → render_document_viewer + + Context variables: + docs – list of doc dicts, sorted by (tag.order, tag.name, doc.title) + modal_id – HTML id for the viewer modal + carousel_id – derived id for Bootstrap carousel + thumbs_id – derived id for bottom strip + download_id – derived id for download button + fullpage_id – derived id for open-in-new-tab button + allow_delete – show delete-overlay buttons (edit-mode) + show_tags – show tag badge on thumbnails and in carousel caption + show_versions – show version number + history button on thumbnails + location_id – context-object PK emitted as data-location-id on history buttons +{% endcomment %} + +{% if docs %} + +{# ═══════════════════════════ Thumbnail Grid ═══════════════════════════ #} + + + +{# ═══════════════════════════ Viewer Modal ════════════════════════════ #} +{# /modal #} + +{# ═══════════════════════════ Inline JS ══════════════════════════════ #} + + +{% else %} + +{% endif %} diff --git a/streetup/common/templates/common/components/help_panel.html b/streetup/common/templates/common/components/help_panel.html new file mode 100644 index 0000000..c6e5358 --- /dev/null +++ b/streetup/common/templates/common/components/help_panel.html @@ -0,0 +1,118 @@ +{% load i18n %} +{% load roles %} + + +
          + + + diff --git a/streetup/common/templatetags/__init__.py b/streetup/common/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/common/templatetags/document_viewer_tags.py b/streetup/common/templatetags/document_viewer_tags.py new file mode 100644 index 0000000..7c1f7d4 --- /dev/null +++ b/streetup/common/templatetags/document_viewer_tags.py @@ -0,0 +1,290 @@ +""" +document_viewer_tags.py +----------------------- +Reusable template tag for rendering a document thumbnail grid + carousel viewer modal. + +Usage: + {% load document_viewer_tags %} + {% render_document_viewer attachments modal_id="myModalId" allow_delete=True %} + + {# With tags and versioning (managed document dicts) #} + {% render_document_viewer doc_infos modal_id="myModalId" + show_tags=True show_versions=True location_id=object.pk %} + +Parameters +---------- +attachments : iterable of DocumentAttachment *or* iterable of managed-doc dicts + Two accepted input formats: + 1. Queryset / list of ``DocumentAttachment`` objects (legacy format). + 2. List of dicts ``{document: ManagedDocument, tag: DocumentTag, version: DocumentVersion}`` + as returned by ``DocumentManagementService.get_documents_for_asset()``. +modal_id : str + HTML id used for the modal element. Must be unique on the page. + Derived IDs: ``Carousel``, ``Thumbs``, ``Download``. +allow_delete : bool + When True, renders delete-overlay buttons on each thumbnail using the + ``data-edit-mode-only="true"`` pattern so they are toggled by the + asset CMS edit-mode JS. Buttons carry the class ``document-delete-btn`` + and ``data-document-id`` matching ManagedDocument.pk. +show_tags : bool + When True, renders a tag badge on each thumbnail card and in the carousel caption. +show_versions : bool + When True, renders the version number and a history button (``bi-clock-history``) + on each thumbnail card. The history button targets ``#documentHistoryModal`` + (Bootstrap modal expected to exist in the host page) and carries the following + data attributes consumed by the page's JS handler: + data-tag-slug, data-location-id, data-use-managed, data-unique-params +location_id : int | str | None + The PK of the context object (intersection, structure, …). Emitted as + ``data-location-id`` on every history button so the page's JS can build + the correct history API URL. +""" + +import json +import os + +from django import template + +from common.private_files.helpers import build_private_url + +register = template.Library() + +# Extensions treated as images (can be embedded via ) +IMAGE_EXTENSIONS = { + ".jpg", ".jpeg", ".png", ".gif", ".svg", + ".bmp", ".webp", ".tiff", ".tif", +} + +# Bootstrap Icons class → file extension +_FILE_ICONS = { + ".pdf": "bi bi-file-earmark-pdf text-danger", + ".doc": "bi bi-file-earmark-word text-primary", + ".docx": "bi bi-file-earmark-word text-primary", + ".xls": "bi bi-file-earmark-excel text-success", + ".xlsx": "bi bi-file-earmark-excel text-success", + ".txt": "bi bi-file-earmark-text text-secondary", + ".csv": "bi bi-file-earmark-spreadsheet text-secondary", + ".zip": "bi bi-file-earmark-zip text-warning", + ".rar": "bi bi-file-earmark-zip text-warning", + ".png": "bi bi-file-earmark-image text-info", + ".jpg": "bi bi-file-earmark-image text-info", + ".jpeg": "bi bi-file-earmark-image text-info", + ".gif": "bi bi-file-earmark-image text-info", + ".svg": "bi bi-file-earmark-image text-info", + ".webp": "bi bi-file-earmark-image text-info", + ".dwg": "bi bi-file-earmark-easel text-secondary", + ".dxf": "bi bi-file-earmark-easel text-secondary", +} + + +@register.filter +def file_icon_class(ext): + """Return a Bootstrap Icons CSS class string for a file extension. + + Accepts either a bare extension (".pdf") or a full filename ("report.pdf"). + + Example:: + + {{ ".pdf"|file_icon_class }} + {# → 'bi bi-file-earmark-pdf text-danger' #} + {{ document.filename|file_icon_class }} + {# → 'bi bi-file-earmark-excel text-success' #} + """ + if not ext: + return "bi bi-file-earmark text-secondary" + ext = ext.strip() + if not ext.startswith("."): + _, _, tail = ext.rpartition(".") + ext = f".{tail}" if tail else ext + return _FILE_ICONS.get(ext.lower(), "bi bi-file-earmark text-secondary") + + +def _private_url(instance, field_name): + """Build a signed private-storage URL for *field_name* on *instance*.""" + if not instance: + return "" + field = getattr(instance, field_name, None) + if not field or not getattr(field, "name", ""): + return "" + return build_private_url( + instance._meta.app_label, + instance._meta.object_name, + instance.pk, + field_name, + ) + + +@register.inclusion_tag("common/components/document_viewer.html") +def render_document_viewer( + attachments, + modal_id="docViewerModal", + allow_delete=False, + delete_always_visible=False, + allow_copy=False, + show_tags=False, + show_versions=False, + location_id=None, +): + """Render a thumbnail grid + carousel viewer modal. + + Accepts two input formats: + 1. Queryset / list of ``DocumentAttachment`` objects (legacy format). + 2. List of dicts ``{document, tag, version}`` as returned by + ``DocumentManagementService.get_documents_for_asset()``. + """ + docs = [] + for i, item in enumerate(attachments): + if isinstance(item, dict): + # ── Managed-doc dict format ── + doc = item.get("document") + version = item.get("version") or (doc.current_version if doc else None) + tag = item.get("tag") + doc_tags = [tag] if tag else [] + attached_at = getattr(doc, "updated_at", None) + attached_by = getattr(version, "uploaded_by", None) if version else None + elif hasattr(item, "file") and not hasattr(item, "document"): + # ── Direct legacy Document subclass (InterventionDocument, ITSLocationDocument, …) ── + doc = item # the item IS the file-bearing object + version = item # file/thumbnail live on the item itself + doc_tags = [] + tag = None + attached_at = getattr(item, "upload_date", None) + attached_by = getattr(item, "uploaded_by", None) + else: + # ── Legacy DocumentAttachment format ── + doc = getattr(item, "document", None) + version = doc.current_version if doc else None + doc_tags = list(doc.tags.all()) if doc else [] + tag = doc_tags[0] if doc_tags else None + attached_at = getattr(item, "attached_at", None) + attached_by = getattr(item, "attached_by", None) + + if not doc: + continue + if not version: + continue + if not getattr(version.file, "name", ""): + continue + + filename = ( + getattr(version, "filename", None) + or os.path.basename(getattr(version.file, "name", "")) + or "" + ) + ext = os.path.splitext(filename)[1].lower() if filename else "" + is_pdf = ext == ".pdf" + is_image = ext in IMAGE_EXTENSIONS + + # ── Text / tabular previews (server-side, embedded in the carousel slide) ── + text_preview = "" + text_truncated = False + tabular_rows = [] + tabular_truncated = False + + if ext in (".txt", ".log"): + try: + with version.file.open("rb") as f: + raw = f.read(50_001) + text_truncated = len(raw) > 50_000 + text_preview = raw[:50_000].decode("utf-8-sig", errors="replace") + except Exception: + pass + elif ext == ".csv": + try: + import csv as _csv + import io as _io + with version.file.open("rb") as f: + raw = f.read() + try: + text_data = raw.decode("utf-8-sig") + except UnicodeDecodeError: + text_data = raw.decode("latin-1") + try: + dialect = _csv.Sniffer().sniff(text_data[:4096]) + except _csv.Error: + dialect = _csv.excel + reader = _csv.reader(_io.StringIO(text_data), dialect=dialect) + rows_tmp = [row for _, row in zip(range(201), reader)] + tabular_truncated = len(rows_tmp) > 200 + tabular_rows = rows_tmp[:200] + except Exception: + pass + elif ext in (".xlsx", ".xls"): + try: + import openpyxl + from io import BytesIO + with version.file.open("rb") as f: + raw = f.read() + wb = openpyxl.load_workbook(BytesIO(raw), read_only=True, data_only=True) + ws = wb.active + rows_tmp = [] + for i_row, row in enumerate(ws.iter_rows(values_only=True)): + if i_row >= 201: + break + rows_tmp.append([str(c) if c is not None else "" for c in row]) + wb.close() + tabular_truncated = len(rows_tmp) > 200 + tabular_rows = rows_tmp[:200] + except Exception: + pass + + unique_params = getattr(doc, "unique_params", None) or {} + + # doc_id: for legacy docs, use the item's own pk so delete/copy JS can match + doc_id = doc.pk + + # legacy_delete_url: populated by the view by setting item._delete_url + legacy_delete_url = getattr(item, "_delete_url", "") + + docs.append({ + "index": i, + "doc_id": doc_id, + "title": getattr(doc, "title", None) or filename, + "description": getattr(doc, "description", None) or "", + "filename": filename, + "file_url": _private_url(version, "file"), + "thumbnail_url": _private_url(version, "thumbnail") if getattr(version, "thumbnail", None) else "", + "is_pdf": is_pdf, + "is_image": is_image, + "ext": ext, + "text_preview": text_preview, + "text_truncated": text_truncated, + "tabular_rows": tabular_rows, + "tabular_truncated": tabular_truncated, + "attached_at": attached_at, + "attached_by": attached_by, + "legacy_delete_url": legacy_delete_url, + # ── Tag info ── + "tag_name": tag.name if tag else "", + "tag_slug": tag.slug if tag else "", + "tag_order": tag.order if tag else 9999, + "tags_list": [{"name": t.name, "slug": t.slug} for t in doc_tags], + # ── Version info ── + "version_number": getattr(version, "version_number", None), + # JSON-encoded unique_params; safe to place in data-* attributes + # (Django auto-escaping of " → " is transparent to getAttribute()) + "unique_params_json": json.dumps(unique_params), + "unique_params": unique_params, + }) + + # ── Sort docs by tag display order, then tag name, then document title ── + docs.sort(key=lambda d: (d["tag_order"], d["tag_name"].lower(), d["title"].lower())) + + # Re-assign sequential indices after sorting so carousel slides stay in sync + for i, d in enumerate(docs): + d["index"] = i + + return { + "docs": docs, + "modal_id": modal_id, + "carousel_id": f"{modal_id}Carousel", + "thumbs_id": f"{modal_id}Thumbs", + "download_id": f"{modal_id}Download", + "fullpage_id": f"{modal_id}Fullpage", + "allow_delete": allow_delete, + "delete_always_visible": delete_always_visible, + "allow_copy": allow_copy, + "show_tags": show_tags, + "show_versions": show_versions, + "location_id": location_id, + } diff --git a/streetup/common/templatetags/format_number.py b/streetup/common/templatetags/format_number.py new file mode 100644 index 0000000..4166f79 --- /dev/null +++ b/streetup/common/templatetags/format_number.py @@ -0,0 +1,57 @@ +from django import template +from decimal import Decimal + +register = template.Library() + + +@register.filter(name='format_quantity') +def format_quantity(value): + """ + Formate un nombre en enlevant les décimales si elles sont égales à 0. + Exemple: 10.00 -> 10, 10.50 -> 10.5, 10.55 -> 10.55 + """ + if value is None or value == '': + return '' + + try: + num = Decimal(str(value)) + # Si le nombre est un entier (pas de partie décimale) + if num % 1 == 0: + return str(int(num)) + # Sinon, formater avec jusqu'à 2 décimales, en enlevant les zéros inutiles + formatted = f"{num:.2f}".rstrip('0').rstrip('.') + return formatted + except (ValueError, TypeError, Exception): + return value + + +@register.filter(name='format_price') +def format_price(value): + """ + Formate un prix avec espace insécable avant le symbole €. + Exemple: 10.00 -> 10 €, 10.50 -> 10.5 € + """ + if value is None or value == '': + return '' + + try: + num = Decimal(str(value)) + # Si le nombre est un entier (pas de partie décimale) + if num % 1 == 0: + return f"{int(num)} €" + # Sinon, formater avec jusqu'à 2 décimales, en enlevant les zéros inutiles + formatted = f"{num:.2f}".rstrip('0').rstrip('.') + return f"{formatted} €" + except (ValueError, TypeError, Exception): + return value + + +@register.filter(name='get_item') +def get_item(dictionary, key): + """ + Permet d'accéder à une valeur d'un dictionnaire dans un template. + Usage: {{ my_dict|get_item:key }} + """ + if dictionary is None: + return None + return dictionary.get(key, 0) diff --git a/streetup/common/templatetags/roles.py b/streetup/common/templatetags/roles.py new file mode 100644 index 0000000..e98d145 --- /dev/null +++ b/streetup/common/templatetags/roles.py @@ -0,0 +1,41 @@ +from django import template + +register = template.Library() + +def _parse_roles(value): + # accepte "manager, admin ; tech" + parts = str(value).replace(";", ",").split(",") + return [p.strip() for p in parts if p.strip()] + +@register.filter +def has_any_role(user_config, roles_csv): + if not user_config or isinstance(user_config, str): + return False + names = _parse_roles(roles_csv) + return user_config.roles.filter(name__in=names).exists() + +@register.filter +def has_all_roles(user_config, roles_csv): + if not user_config or isinstance(user_config, str): + return False + names = list({*_parse_roles(roles_csv)}) + found = (user_config.roles + .filter(name__in=names) + .values_list("name", flat=True) + .distinct()) + return set(names).issubset(set(found)) + + +@register.filter +def can_view_teams(user_config): + """Renvoie True si l'utilisateur peut consulter au moins une équipe.""" + if not user_config or isinstance(user_config, str): + return False + user_roles = set(user_config.roles.values_list("name", flat=True)) + if "admin" in user_roles: + return True + from contracts.models import UserCompanyTeamAccess + return UserCompanyTeamAccess.objects.filter( + user_config=user_config, + can_view=True, + ).exists() diff --git a/streetup/common/templatetags/settings_tags.py b/streetup/common/templatetags/settings_tags.py new file mode 100644 index 0000000..ae5b439 --- /dev/null +++ b/streetup/common/templatetags/settings_tags.py @@ -0,0 +1,8 @@ +from django import template +from django.conf import settings + +register = template.Library() + +@register.simple_tag +def get_setting(name, default=""): + return getattr(settings, name, default) diff --git a/streetup/common/templatetags/short_name.py b/streetup/common/templatetags/short_name.py new file mode 100644 index 0000000..2ab58ef --- /dev/null +++ b/streetup/common/templatetags/short_name.py @@ -0,0 +1,53 @@ +from django import template +import os + +register = template.Library() + +@register.filter +def short_name(user): + """ + Retourne 'Prénom N.' à partir d'un objet User. + """ + if not user: + return "" + first = getattr(user, "first_name", "") or "" + last = getattr(user, "last_name", "") or "" + initial = (last[0].upper() + ".") if last else "" + return f"{first} {initial}".strip() + + +@register.filter +def user_company(user): + """ + Retourne le nom de la société de l'utilisateur. + """ + if not user: + return "" + if hasattr(user, 'config') and user.config and user.config.company: + return user.config.company.name + try: + from contracts.models import CompanyMember + member = CompanyMember.objects.filter(user=user).prefetch_related('teams__company').first() + if member: + for team in member.teams.all(): + if team.company: + return team.company.name + except Exception: + pass + return "" + + +@register.filter +def filename(value): + """ + Extrait le nom de fichier à partir d'un chemin complet. + Fonctionne avec les objets FileField ou les chaînes de caractères. + """ + if not value: + return "" + # Si c'est un objet FileField, récupérer le nom + if hasattr(value, 'name'): + value = value.name + # Extraire le nom de base du chemin + return os.path.basename(str(value)) + diff --git a/streetup/common/tests.py b/streetup/common/tests.py new file mode 100644 index 0000000..8fad7e0 --- /dev/null +++ b/streetup/common/tests.py @@ -0,0 +1,252 @@ +from django.contrib.auth.models import User +from django.contrib.messages import get_messages +from django.test import TestCase +from django.urls import reverse +from django.conf import settings +from django.utils.translation import gettext as _ + +from common.models import UserConfig + + +class SwitchAccountViewTests(TestCase): + def setUp(self): + self.user_1 = User.objects.create_user(username="alpha", password="password123") + self.user_2 = User.objects.create_user(username="beta", password="password123") + self.user_3 = User.objects.create_user(username="gamma", password="password123") + self.user_4 = User.objects.create_user(username="delta", password="password123") + + self.config_1 = UserConfig.objects.create(user=self.user_1) + self.config_2 = UserConfig.objects.create(user=self.user_2) + self.config_3 = UserConfig.objects.create(user=self.user_3) + self.config_4 = UserConfig.objects.create(user=self.user_4) + + # Link the first two users together + self.config_1.linked_configs.add(self.config_2) + + def test_switch_to_linked_account(self): + self.client.force_login(self.user_1) + + response = self.client.post( + reverse("common:switch_account"), + {"target_user_id": self.user_2.pk, "origin_next": "/interventions/"}, + ) + + self.assertRedirects(response, "/interventions/", target_status_code=302) + self.assertEqual(int(self.client.session["_auth_user_id"]), self.user_2.pk) + + def test_switch_to_unlinked_account_is_prevented(self): + self.client.force_login(self.user_1) + + response = self.client.post( + reverse("common:switch_account"), + {"target_user_id": self.user_3.pk}, + ) + + messages = list(get_messages(response.wsgi_request)) + self.assertTrue(any(_("Vous ne pouvez pas accéder à ce compte.") in message.message for message in messages)) + self.assertRedirects(response, reverse("home:index")) + self.assertEqual(int(self.client.session["_auth_user_id"]), self.user_1.pk) + + def test_chained_switch_uses_origin_permissions(self): + self.config_1.linked_configs.add(self.config_3) + self.config_4.linked_configs.add(self.config_2) + + self.client.force_login(self.user_1) + + response = self.client.post( + reverse("common:switch_account"), + {"target_user_id": self.user_2.pk}, + ) + + self.assertRedirects(response, reverse("home:index")) + self.assertEqual(int(self.client.session["_auth_user_id"]), self.user_2.pk) + + response = self.client.post( + reverse("common:switch_account"), + {"target_user_id": self.user_3.pk}, + ) + + self.assertRedirects(response, reverse("home:index")) + self.assertEqual(int(self.client.session["_auth_user_id"]), self.user_3.pk) + + response = self.client.post( + reverse("common:switch_account"), + {"target_user_id": self.user_4.pk}, + ) + + messages = list(get_messages(response.wsgi_request)) + self.assertTrue(any(_("Vous ne pouvez pas accéder à ce compte.") in message.message for message in messages)) + self.assertRedirects(response, reverse("home:index")) + self.assertEqual(int(self.client.session["_auth_user_id"]), self.user_3.pk) + + +class LanguageSwitchingTests(TestCase): + """Tests for language switching functionality in mobile/user settings.""" + + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="password123") + UserConfig.objects.create(user=self.user) + + def test_account_info_page_loads(self): + """Test that the account info page loads successfully.""" + self.client.force_login(self.user) + response = self.client.get(reverse("common:account_info")) + self.assertEqual(response.status_code, 200) + + def test_account_info_contains_language_selector(self): + """Test that the account info page contains language selection elements.""" + self.client.force_login(self.user) + response = self.client.get(reverse("common:account_info")) + + # Check for language modal and buttons + self.assertContains(response, "languageModal") + self.assertContains(response, "Français") + self.assertContains(response, "Nederlands") + self.assertContains(response, "English") + + def test_set_language_to_french(self): + """Test switching to French language.""" + self.client.force_login(self.user) + + response = self.client.post( + reverse("set_language"), + {"language": "fr", "next": reverse("common:account_info")}, + follow=True + ) + + self.assertEqual(response.status_code, 200) + # Check that the language cookie is set + self.assertIn(settings.LANGUAGE_COOKIE_NAME, self.client.cookies) + self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "fr") + + def test_set_language_to_dutch(self): + """Test switching to Dutch language.""" + self.client.force_login(self.user) + + response = self.client.post( + reverse("set_language"), + {"language": "nl", "next": reverse("common:account_info")}, + follow=True + ) + + self.assertEqual(response.status_code, 200) + self.assertIn(settings.LANGUAGE_COOKIE_NAME, self.client.cookies) + self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "nl") + + def test_set_language_to_english(self): + """Test switching to English language.""" + self.client.force_login(self.user) + + response = self.client.post( + reverse("set_language"), + {"language": "en", "next": reverse("common:account_info")}, + follow=True + ) + + self.assertEqual(response.status_code, 200) + self.assertIn(settings.LANGUAGE_COOKIE_NAME, self.client.cookies) + self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "en") + + def test_invalid_language_code(self): + """Test that invalid language codes are handled gracefully.""" + self.client.force_login(self.user) + + response = self.client.post( + reverse("set_language"), + {"language": "invalid", "next": reverse("common:account_info")}, + follow=True + ) + + # Django's set_language view should handle invalid languages gracefully + self.assertEqual(response.status_code, 200) + + +class HelpPanelTests(TestCase): + def setUp(self): + from common.models import Role + self.user = User.objects.create_user(username="helpuser", password="password123") + self.user_config = UserConfig.objects.create(user=self.user) + + # Superuser + self.superuser = User.objects.create_superuser(username="helpsuper", password="password123") + self.superuser_config = UserConfig.objects.create(user=self.superuser) + + def test_can_approve_any_intervention_property(self): + from common.models import Role + # By default, regular user cannot approve + self.assertFalse(self.user_config.can_approve_any_intervention) + + # Superuser can approve + self.assertTrue(self.superuser_config.can_approve_any_intervention) + + # Admin role can approve + admin_role = Role.objects.get_or_create(name='admin')[0] + self.user_config.roles.add(admin_role) + self.assertTrue(self.user_config.can_approve_any_intervention) + + # Remove admin role + self.user_config.roles.remove(admin_role) + self.assertFalse(self.user_config.can_approve_any_intervention) + + def test_get_help_document_prise_en_main(self): + self.client.force_login(self.user) + response = self.client.get(reverse("common:get_help_document", args=["prise_en_main"])) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIn("html", data) + # Check that it rendered markdown headers into HTML + self.assertIn("/", views.get_help_document, name="get_help_document"), +] \ No newline at end of file diff --git a/streetup/common/utils.py b/streetup/common/utils.py new file mode 100644 index 0000000..3de35eb --- /dev/null +++ b/streetup/common/utils.py @@ -0,0 +1,119 @@ +from django.http import HttpResponseForbidden, HttpResponseRedirect, JsonResponse +from django.urls import reverse +from django.shortcuts import get_object_or_404 +from django.db.models import Func, CharField + +from urllib.parse import urlencode +import time +import functools +import unicodedata + +from common.models import Thematic, UserConfig, UserThematics + + +def check_thematic_access(thematic_code, edit_permission_required=False): + """Decorator to verify user access to a specific thematic (passed as a parameter).""" + + def decorator(view_func): + @functools.wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + user_config = UserConfig.objects.filter(user=request.user).first() # Get user access configuration + thematic = get_object_or_404(Thematic, code=thematic_code) # Get thematic from the given parameter + + # Check if the user has a configuration + if not user_config: + return HttpResponseForbidden("Access denied: You do not have access to this thematic.") + + # Check if the user has access to the thematic + if edit_permission_required: + if not UserThematics.objects.filter( + user_config=user_config, + thematic=thematic, + can_edit_assets=True + ).exists(): + return HttpResponseForbidden("Access denied: You do not have edit permissions for this thematic.") + else: + # Si l'utilisateur a un rôle 'admin' ou 'operator', il peut voir tous les assets + if ( + (not user_config.roles.filter(name__in=['admin','operator']).exists()) + and + (not UserThematics.objects.filter( + user_config=user_config, + thematic=thematic, + can_view_assets=True + ).exists()) + ): + return HttpResponseForbidden("Access denied: You do not have view permissions for this thematic.") + + # Execute the view if everything is OK + return view_func(request, *args, **kwargs) + + return _wrapped_view + + return decorator + + +def redirect_with_variant(viewname, variant=None, url_kwargs=None, extra_query_params=None): + """ + Redirige vers une vue avec des paramètres d'URL et le paramètre ?v=... si fourni. + + :param viewname: nom de la vue (ex: 'controls:controls_detail') + :param variant: 'mobile' ou 'desktop' (ajouté comme ?v=mobile dans l'URL) + :param url_kwargs: dictionnaire des arguments de reverse (ex: {'control_id': 3}) + :param extra_query_params: dictionnaire de paramètres supplémentaires à ajouter dans la query string + """ + if url_kwargs is None: + url_kwargs = {} + if extra_query_params is None: + extra_query_params = {} + + if variant: + extra_query_params['v'] = variant + + base_url = reverse(viewname, kwargs=url_kwargs) + + if extra_query_params: + query_string = urlencode(extra_query_params) + full_url = f"{base_url}?{query_string}" + else: + full_url = base_url + + return HttpResponseRedirect(full_url) + + + +def measure_time(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.time() + response = func(*args, **kwargs) + elapsed = time.time() - start + print(f"{func.__name__} exécutée en {elapsed:.4f} secondes") + return response + return wrapper + + + +# Appelle la fonction PostgreSQL unaccent(...) +class Unaccent(Func): + function = 'public.unaccent_immutable' + output_field = CharField() + +def strip_accents_lower(s: str) -> str: + if not s: + return '' + s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') + return s.lower() + + + +def get_short_name(user): + """ + Retourne 'Prénom N.' à partir d'un objet User. + """ + if not user: + return "" + first = getattr(user, "first_name", "") or "" + last = getattr(user, "last_name", "") or "" + initial = (last[0].upper() + ".") if last else "" + return f"{first} {initial}".strip() \ No newline at end of file diff --git a/streetup/common/views.py b/streetup/common/views.py new file mode 100644 index 0000000..af48eaa --- /dev/null +++ b/streetup/common/views.py @@ -0,0 +1,584 @@ +from django.http import JsonResponse, FileResponse, Http404 +from django.shortcuts import render, redirect +from django.views.decorators.http import require_GET, require_POST +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.contrib import messages +from django.contrib.gis.db.models.functions import Distance +from django.contrib.gis.geos import Point +from django.contrib.auth import login as auth_login +from django.contrib.auth.decorators import login_not_required +from django.contrib.auth.forms import PasswordChangeForm +from django.contrib.auth import update_session_auth_hash +from django.utils.http import url_has_allowed_host_and_scheme +from django.utils.translation import gettext as _ +from django.conf import settings +from django.urls import reverse + +import re +import os +from urllib.parse import urlencode + +from common.context_processors import ( + SESSION_ALLOWED_IDS_KEY, + SESSION_ORIGIN_ID_KEY, + compute_initial_allowed_ids, +) + +from common.models import AdrStreet, AdrPoint, UserConfig + +from assets.models import AssetCategory + +@login_not_required +@require_GET +def autocomplete_street(request): + query = request.GET.get('q', '').strip() + if len(query) < 2: + return JsonResponse([], safe=False) + + # Regex patterns to detect number and street in different positions + patterns = [ + ("number_first", r'^(\d+\w*)[\s,]+(.+)$'), # ex: "12A rue de la Loi" + ("street_first", r'^(.+)[\s,]+(\d+\w*)$'), # ex: "rue de la Loi 12A" +] + + number_part = None + street_part = None + + for mode, pat in patterns: + match = re.match(pat, query) + if match: + g1, g2 = match.groups() + if mode == "number_first": + number_part, street_part = g1.strip(), g2.strip() + else: + street_part, number_part = g1.strip(), g2.strip() + break + + if number_part and street_part: + # Recherche par AdrPoint avec numéro ET nom de rue + points = ( + AdrPoint.objects + .filter(number__iexact=number_part) + .filter( + Q(street_name__name_fr__icontains=street_part) | + Q(street_name__name_nl__icontains=street_part) + ) + .select_related('street_name', 'municipality')[:10] + ) + + data = [{ + "id": point.fid, + "label": f"{point.number} {point.street_name.name_fr} / {point.street_name.name_nl}" + + (f" ({point.municipality.name})" if point.municipality else ""), + "lon": point.lon, + "lat": point.lat + } for point in points] + + else: + # Recherche standard par rue uniquement + streets = ( + AdrStreet.objects + .filter(Q(name_fr__icontains=query) | Q(name_nl__icontains=query)) + .select_related('municipality')[:10] + ) + + data = [{ + "id": street.fid, + "label": f"{street.name_fr} / {street.name_nl}" + + (f" ({street.municipality.name})" if street.municipality else ""), + "lon": street.lon, + "lat": street.lat + } for street in streets] + + return JsonResponse(data, safe=False) + + +@login_not_required +@require_GET +def get_nearest_address(request): + try: + lon = float(request.GET.get("lon")) + lat = float(request.GET.get("lat")) + except (TypeError, ValueError): + return JsonResponse({"error": _("Coordonnées invalides")}, status=400) + + input_point = Point(lon, lat, srid=4326) + input_point.transform(3812) + + # Étape 1 : Trouver la rue la plus proche + nearest_street = ( + AdrStreet.objects + .filter(geom__isnull=False) + .annotate(distance=Distance("geom", input_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by("distance") + .first() + ) + + if not nearest_street: + return JsonResponse({"error": _("Aucune rue trouvée")}, status=404) + + # Étape 2 : Rechercher un point dans un rayon de 50 m pour cette rue + nearest_point = ( + AdrPoint.objects + .filter(street_name=nearest_street, geom__isnull=False) + .annotate(distance=Distance("geom", input_point)) + .filter(distance__lte=50) # filtre à 50 mètres + .order_by("distance") + .first() + ) + + # Étape 3 : Si un point est trouvé dans les 50 m, on le retourne + if nearest_point: + return JsonResponse({ + "id": nearest_point.id, + "label": f"{nearest_point.number} {nearest_point.street_name.name_fr} / {nearest_point.street_name.name_nl}" + + (f" ({nearest_point.municipality.name})" if nearest_point.municipality else "") + }) + + # Sinon : on retourne uniquement le nom de la rue + return JsonResponse({ + "id": nearest_street.id, + "label": f"{nearest_street.name_fr} / {nearest_street.name_nl}" + + (f" ({nearest_street.municipality.name})" if nearest_street.municipality else ""), + }) + + + +@login_not_required +@require_GET +def get_regional_roads_geojson(request): + file_path = os.path.join(settings.BASE_DIR, 'common', 'static', 'geojson', 'regional_roads_simplified_4326.geojson') + + if not os.path.exists(file_path): + raise Http404("GeoJSON file not found") + + return FileResponse(open(file_path, 'rb'), content_type='application/json') + + + +def get_categories_for_thematics(request): + ids_string = request.GET.get('thematic_ids', '') + if ids_string: + try: + thematic_ids = [int(x) for x in ids_string.split(',') if x.strip().isdigit()] + except ValueError: + return JsonResponse({"results": []}) + else: + thematic_ids = [] + + categories = AssetCategory.objects.filter(thematic__id__in=thematic_ids).distinct() + + results = [ + {"id": cat.id, "text": cat.get_name(), "parent_id": cat.parent_id} for cat in categories + ] + return JsonResponse({"results": results}) + + + + +def _safe_origin_next(request): + # 1) prendre 'next' si présent à l’arrivée + origin_next = request.GET.get("origin_next") or request.GET.get("next") + # 2) sinon, si c’est un POST (ex: retour form), reprendre la valeur transmise + origin_next = origin_next or request.POST.get("origin_next") + # 3) safe-guard (évite open redirect) + if origin_next and not url_has_allowed_host_and_scheme( + url=origin_next, + allowed_hosts={request.get_host()}, + require_https=request.is_secure(), + ): + origin_next = None + return origin_next + +@login_required +def account_info(request): + origin_next = _safe_origin_next(request) # on capture une fois, puis on la véhicule + + open_password_modal = False + if request.method == "POST" and "password_change" in request.POST: + form = PasswordChangeForm(request.user, request.POST) + if form.is_valid(): + user = form.save() + update_session_auth_hash(request, user) + messages.success(request, _("Mot de passe mis à jour avec succès.")) + # On redirige vers account_info en conservant origin_next + flag + params = {"password_changed": "1"} + if origin_next: + params["origin_next"] = origin_next + return redirect(f"{request.path}?{urlencode(params)}") + else: + open_password_modal = True + else: + form = PasswordChangeForm(request.user) + + if request.GET.get("password_changed") == "1": + open_password_modal = True + + return render(request, "common/account_info.html", { + "form": form, + "open_password_modal": open_password_modal, + "origin_next": origin_next, # <- clé : toujours renvoyer au template + }) + + +@login_required +@require_POST +def switch_account(request): + origin_next = _safe_origin_next(request) + target_user_id = request.POST.get("target_user_id") + + try: + current_config: UserConfig = request.user.config + except UserConfig.DoesNotExist: + messages.error(request, _("Aucun compte lié n'est disponible.")) + return redirect(origin_next or reverse("home:index")) + + if not target_user_id: + messages.error(request, _("Aucun compte cible n'a été sélectionné.")) + return redirect(origin_next or reverse("home:index")) + + if str(request.user.pk) == str(target_user_id): + messages.info(request, _("Ce compte est déjà actif.")) + return redirect(origin_next or reverse("home:index")) + + session = request.session + + allowed_ids = session.get(SESSION_ALLOWED_IDS_KEY) + origin_user_id = session.get(SESSION_ORIGIN_ID_KEY) + + if allowed_ids is None: + allowed_ids = compute_initial_allowed_ids(current_config) + + if origin_user_id is None: + origin_user_id = request.user.pk + + allowed_ids = list(dict.fromkeys(int(pk) for pk in allowed_ids)) + + if request.user.pk not in allowed_ids: + allowed_ids.append(request.user.pk) + + try: + target_user_pk = int(target_user_id) + except (TypeError, ValueError): + messages.error(request, _("Vous ne pouvez pas accéder à ce compte.")) + return redirect(origin_next or reverse("home:index")) + + if target_user_pk not in allowed_ids: + messages.error(request, _("Vous ne pouvez pas accéder à ce compte.")) + return redirect(origin_next or reverse("home:index")) + + session[SESSION_ALLOWED_IDS_KEY] = allowed_ids + session[SESSION_ORIGIN_ID_KEY] = origin_user_id + + try: + target_config = ( + UserConfig.objects.select_related("user").get(user__pk=target_user_pk) + ) + except UserConfig.DoesNotExist: + messages.error(request, _("Vous ne pouvez pas accéder à ce compte.")) + return redirect(origin_next or reverse("home:index")) + + target_user = target_config.user + target_user.backend = "django.contrib.auth.backends.ModelBackend" + auth_login(request, target_user) + messages.success(request, _("Changement de compte effectué.")) + + session[SESSION_ALLOWED_IDS_KEY] = allowed_ids + session[SESSION_ORIGIN_ID_KEY] = origin_user_id + + return redirect(origin_next or reverse("home:index")) + + +@login_required +@require_POST +def save_intervention_list_layout(request): + """API endpoint to save the user's preferred layout for the interventions list.""" + import json + + try: + data = json.loads(request.body) + layout = data.get('layout') + except (json.JSONDecodeError, KeyError): + return JsonResponse({'error': _('Données invalides')}, status=400) + + valid_layouts = ['both', 'table_only', 'map_only'] + if layout not in valid_layouts: + return JsonResponse({'error': _('Layout invalide')}, status=400) + + try: + user_config = request.user.config + user_config.intervention_list_layout = layout + user_config.save(update_fields=['intervention_list_layout']) + return JsonResponse({'success': True, 'layout': layout}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_POST +def save_assets_list_layout(request): + """API endpoint to save the user's preferred layout for the assets list.""" + import json + + try: + data = json.loads(request.body) + layout = data.get('layout') + except (json.JSONDecodeError, KeyError): + return JsonResponse({'error': _('Données invalides')}, status=400) + + valid_layouts = ['both', 'table_only', 'map_only'] + if layout not in valid_layouts: + return JsonResponse({'error': _('Layout invalide')}, status=400) + + try: + user_config = request.user.config + user_config.assets_list_layout = layout + user_config.save(update_fields=['assets_list_layout']) + return JsonResponse({'success': True, 'layout': layout}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_POST +def save_intervention_list_preferences(request): + """API endpoint to save the user's preferred filters and sort for the interventions list.""" + import json + + try: + data = json.loads(request.body) + filters = data.get('filters', {}) + sort = data.get('sort', {}) + columns = data.get('columns', None) + except (json.JSONDecodeError, KeyError): + return JsonResponse({'error': _('Données invalides')}, status=400) + + try: + user_config = request.user.config + + # Validate filters structure (should be a dict with filter names as keys) + if not isinstance(filters, dict): + return JsonResponse({'error': _('Format de filtres invalide')}, status=400) + + # Validate sort structure (should have sortName and sortOrder) + if not isinstance(sort, dict): + return JsonResponse({'error': _('Format de tri invalide')}, status=400) + + update_fields = ['intervention_list_filters', 'intervention_list_sort'] + user_config.intervention_list_filters = filters + user_config.intervention_list_sort = sort + + if columns is not None: + if not isinstance(columns, list): + return JsonResponse({'error': _('Format de colonnes invalide')}, status=400) + user_config.intervention_list_columns = columns + update_fields.append('intervention_list_columns') + + user_config.save(update_fields=update_fields) + + return JsonResponse({'success': True, 'filters': filters, 'sort': sort}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_POST +def save_observation_list_preferences(request): + """API endpoint to save the user's preferred filters and sort for the observations list.""" + import json + + try: + data = json.loads(request.body) + filters = data.get('filters', {}) + sort = data.get('sort', {}) + except (json.JSONDecodeError, KeyError): + return JsonResponse({'error': _('Données invalides')}, status=400) + + try: + user_config = request.user.config + + if not isinstance(filters, dict): + return JsonResponse({'error': _('Format de filtres invalide')}, status=400) + + if not isinstance(sort, dict): + return JsonResponse({'error': _('Format de tri invalide')}, status=400) + + user_config.observation_list_filters = filters + user_config.observation_list_sort = sort + user_config.save(update_fields=['observation_list_filters', 'observation_list_sort']) + + return JsonResponse({'success': True, 'filters': filters, 'sort': sort}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_POST +def save_assets_index_preferences(request): + """API endpoint to save the user's preferences for the unified assets index page.""" + import json + + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({'error': _('Données invalides')}, status=400) + + thematics = data.get('thematics', []) + categories = data.get('categories', []) + models = data.get('models', []) + conditional_filters = data.get('conditional_filters', {}) + visible_columns = data.get('visible_columns') + type_filter = data.get('type_filter', 'all') + status_filters = data.get('status_filters', []) + map_center = data.get('map_center') + map_zoom = data.get('map_zoom') + + if not isinstance(thematics, list): + return JsonResponse({'error': _('Format de thématiques invalide')}, status=400) + if not isinstance(categories, list): + return JsonResponse({'error': _('Format de catégories invalide')}, status=400) + if not isinstance(models, list): + return JsonResponse({'error': _('Format de modèles invalide')}, status=400) + if not isinstance(conditional_filters, dict): + return JsonResponse({'error': _('Format de filtres thématiques invalide')}, status=400) + if visible_columns is not None and not isinstance(visible_columns, list): + return JsonResponse({'error': _('Format de colonnes visibles invalide')}, status=400) + if not isinstance(type_filter, str): + return JsonResponse({'error': _('Format de filtre type invalide')}, status=400) + if not isinstance(status_filters, list): + return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400) + + try: + user_config = request.user.config + existing_prefs = user_config.assets_index_preferences or {} + existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else [] + if not isinstance(existing_visible_columns, list): + existing_visible_columns = [] + + user_config.assets_index_preferences = { + 'thematics': thematics, + 'categories': categories, + 'models': models, + 'conditional_filters': conditional_filters, + 'visible_columns': visible_columns if visible_columns is not None else existing_visible_columns, + 'type_filter': type_filter, + 'status_filters': status_filters, + 'map_center': map_center, + 'map_zoom': map_zoom, + } + user_config.save(update_fields=['assets_index_preferences']) + return JsonResponse({'success': True}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_POST +def save_calendar_preferences(request): + """API endpoint to save the user's preferred filters, view_mode, zoom and backlog states for the calendar.""" + import json + + try: + data = json.loads(request.body) + preferences = data.get('preferences', {}) + except (json.JSONDecodeError, KeyError): + return JsonResponse({'error': _('Données invalides')}, status=400) + + try: + user_config = request.user.config + + if not isinstance(preferences, dict): + return JsonResponse({'error': _('Format de préférences invalide')}, status=400) + + user_config.calendar_preferences = preferences + user_config.save(update_fields=['calendar_preferences']) + + return JsonResponse({'success': True, 'preferences': preferences}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_POST +def save_project_list_preferences(request): + """API endpoint to save the user's preferred filters and sort for the projects list.""" + import json + + try: + data = json.loads(request.body) + filters = data.get('filters', {}) + sort = data.get('sort', {}) + except (json.JSONDecodeError, KeyError): + return JsonResponse({'error': _('Données invalides')}, status=400) + + try: + user_config = request.user.config + + if not isinstance(filters, dict): + return JsonResponse({'error': _('Format de filtres invalide')}, status=400) + + if not isinstance(sort, dict): + return JsonResponse({'error': _('Format de tri invalide')}, status=400) + + user_config.project_list_filters = filters + user_config.project_list_sort = sort + user_config.save(update_fields=['project_list_filters', 'project_list_sort']) + + return JsonResponse({'success': True, 'filters': filters, 'sort': sort}) + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + +@login_required +@require_GET +def get_help_document(request, doc_name): + """View to securely load and render Markdown help documents based on user roles and permissions.""" + import markdown + from django.utils import translation + from django.http import HttpResponseForbidden + + ALLOWED_DOCS = ['prise_en_main', 'assets', 'approuver_interventions', 'gerer_projets', 'statuts_intervention', 'creer_controle'] + if doc_name not in ALLOWED_DOCS: + return JsonResponse({'error': _('Document non trouvé')}, status=404) + + try: + user_config = request.user.config + except UserConfig.DoesNotExist: + return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404) + + # Permission check for specific guides + if doc_name == 'approuver_interventions': + if not user_config.can_approve_any_intervention: + return HttpResponseForbidden(_('Accès interdit')) + elif doc_name in ['gerer_projets', 'creer_controle']: + has_project_role = user_config.roles.filter(name__in=["admin", "manager", "controller", "external_manager", "top_manager"]).exists() + if not (has_project_role or request.user.is_superuser): + return HttpResponseForbidden(_('Accès interdit')) + + # Determine language directory + lang = translation.get_language() + if lang and lang.startswith('nl'): + lang_dir = 'nl' + else: + lang_dir = 'fr' + + # Resolve file path + file_path = settings.BASE_DIR / 'common' / 'help_docs' / lang_dir / f'{doc_name}.md' + if not os.path.exists(file_path): + return JsonResponse({'error': _('Document non trouvé')}, status=404) + + try: + with open(file_path, 'r', encoding='utf-8') as f: + md_content = f.read() + except Exception: + return JsonResponse({'error': _('Erreur lors de la lecture du document')}, status=500) + + # Convert Markdown to HTML + html_content = markdown.markdown(md_content, extensions=['extra', 'nl2br']) + + return JsonResponse({'html': html_content}) + + + diff --git a/streetup/contracts/__init__.py b/streetup/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/contracts/admin.py b/streetup/contracts/admin.py new file mode 100644 index 0000000..0a2d644 --- /dev/null +++ b/streetup/contracts/admin.py @@ -0,0 +1,360 @@ +import io + +from django import forms +from django.contrib import admin, messages +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.urls import path, reverse + +from .models import ( + Company, + Contract, + ContractPost, + ContractOrder, + ContractOrderDocument, + CompanyTeam, + CompanyMember, + CompanyMemberHistory, + ContractOrderQuote, + ContractOrderQuoteItem, + ContractOrderProgressState, + ContractOrderProgressStateItem, + MaintainType, + ClaimDeclarationAttachment, + UserCompanyTeamAccess, + DirectOrderPost, +) + +# --------------------------------------------------------------------------- +# Formulaire d'import de postes +# --------------------------------------------------------------------------- + +class ImportContractPostsForm(forms.Form): + contract = forms.ModelChoiceField( + queryset=None, # initialisé dans __init__ + label="Contrat", + empty_label="-- Sélectionner un contrat --", + ) + excel_file = forms.FileField( + label="Fichier Excel (.xlsx / .xlsm)", + help_text="Fichier inventaire des postes (format SPRB-GOB Fonteinen).", + ) + update_existing = forms.BooleanField( + required=False, + label="Mettre à jour les postes existants", + help_text=( + "Si coché, les postes déjà présents (même numéro de poste) seront" + " mis à jour avec les valeurs du fichier Excel." + " Sinon, ils sont ignorés." + ), + ) + dry_run = forms.BooleanField( + required=False, + label="Mode simulation (dry-run)", + help_text="Affiche le résultat sans écrire quoi que ce soit en base de données.", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + from .models import Contract + self.fields['contract'].queryset = Contract.objects.order_by('contract_number') + + +# --------------------------------------------------------------------------- +# Admins +# --------------------------------------------------------------------------- + +def _get_pause_reason_inline(): + from interventions.models import ContractPauseReasonAssignment + + class ContractPauseReasonAssignmentInline(admin.TabularInline): + model = ContractPauseReasonAssignment + extra = 0 + fields = ('pause_reason', 'assigned_member') + verbose_name = "Réassignation automatique sur pause" + verbose_name_plural = "Réassignations automatiques sur pause" + + return ContractPauseReasonAssignmentInline + + +def _get_symptom_autoclose_inline(): + from interventions.models import SymptomContractAutoClose + + class SymptomAutoCloseInline(admin.TabularInline): + model = SymptomContractAutoClose + extra = 0 + fields = ('symptom', 'delay_days') + verbose_name = "Règle de clôture automatique" + verbose_name_plural = "Règles de clôture automatique (par symptôme)" + + return SymptomAutoCloseInline + + +@admin.register(Company) +class CompanyAdmin(admin.ModelAdmin): + list_display = ('name', 'contact_email', 'phone_number') + +@admin.register(Contract) +class ContractAdmin(admin.ModelAdmin): + list_display = ('contract_number', 'description', 'company', 'start_date', 'end_date', 'is_active', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'thematics_list') + search_fields = ('contract_number', 'company__name', 'description', 'contact_phone') + list_filter = ('is_active', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'send_daily_summary') + filter_horizontal = ('thematics',) + + def get_inlines(self, request, obj=None): + return [_get_pause_reason_inline(), _get_symptom_autoclose_inline()] + list(super().get_inlines(request, obj)) + + fieldsets = ( + (None, { + 'fields': ('company', 'contract_number', 'description', 'start_date', 'end_date', 'is_active', 'committed_amount', 'internal_link', 'executive_officer', 'contact_phone', 'signatory_name', 'signatory_role', 'thematics'), + }), + ('Notifications email', { + 'fields': ('send_email_notifications', 'notification_emails', 'cc_notification_emails', 'send_daily_summary', 'daily_summary_hour', 'email_only_if_urgent'), + 'description': ( + "Si \"Envoi groupé journalier\" est activé, les fiches d'intervention ne sont PAS envoyées " + "individuellement. Elles sont regroupées et envoyées une fois par jour via la commande " + "\"send_daily_intervention_summary\" (à planifier via les tâches planifiées)." + ), + }), + ('Options de workflow', { + 'fields': ('needs_approval', 'needs_checking', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'warn_if_no_prestation_on_processed'), + }), + ) + + def thematics_list(self, obj): + return ", ".join([thematic.name for thematic in obj.thematics.all()]) + thematics_list.short_description = 'Thématiques' + +@admin.register(ContractPost) +class ContractPostAdmin(admin.ModelAdmin): + list_display = ('contract', 'order_number', 'code', 'description_fr', 'unit_price', 'post_type', 'is_coefficient_multipliable') + list_filter = ('contract',) + search_fields = ('code', 'description_fr', 'contract__contract_number') + change_list_template = 'admin/contracts/contractpost/change_list.html' + + def get_urls(self): + urls = super().get_urls() + custom = [ + path( + 'import-posts/', + self.admin_site.admin_view(self.import_posts_view), + name='contracts_contractpost_import_posts', + ), + ] + return custom + urls + + def import_posts_view(self, request): + from .utils_import_posts import import_contract_posts + + context = { + **self.admin_site.each_context(request), + 'title': 'Importer / mettre à jour des postes (Excel)', + 'form': ImportContractPostsForm(), + 'opts': self.model._meta, + 'stats': None, + 'log_lines': None, + 'dry_run': False, + } + + if request.method == 'POST': + form = ImportContractPostsForm(request.POST, request.FILES) + context['form'] = form + + if form.is_valid(): + contract = form.cleaned_data['contract'] + excel_file = form.cleaned_data['excel_file'] + update_existing = form.cleaned_data['update_existing'] + dry_run = form.cleaned_data['dry_run'] + + log_lines = [] + + # openpyxl accepte un objet bytes-like ; on wrappe le fichier + file_bytes = io.BytesIO(excel_file.read()) + + stats = import_contract_posts( + contract=contract, + excel_source=file_bytes, + dry_run=dry_run, + update_existing=update_existing, + log=log_lines.append, + ) + + context['stats'] = stats + context['log_lines'] = log_lines + context['dry_run'] = dry_run + + if stats['errors']: + messages.error(request, f"{stats['errors']} erreur(s) lors de l'import.") + elif not dry_run: + messages.success( + request, + f"{stats['created']} poste(s) créé(s), " + f"{stats['updated']} mis à jour pour le contrat " + f"« {contract.contract_number} »." + ) + else: + messages.info( + request, + f"[DRY-RUN] {stats['created']} poste(s) seraient créés, " + f"{stats['updated']} mis à jour." + ) + + return render(request, 'admin/contracts/contractpost/import_posts.html', context) + +@admin.register(ContractOrder) +class ContractOrderAdmin(admin.ModelAdmin): + list_display = ('contract', 'order_code', 'description', 'order_date') + search_fields = ('order_code', 'contract__contract_number') + list_filter = ('contract',) + +@admin.register(MaintainType) +class MaintainTypeAdmin(admin.ModelAdmin): + list_display = ('code', 'name_fr', 'name_nl') + search_fields = ('code', 'name_fr', 'name_nl') + ordering = ('code',) + + +class CompanyTeamMemberInline(admin.TabularInline): + model = CompanyMember.teams.through + extra = 0 + verbose_name = "Membre" + verbose_name_plural = "Membres" + + +@admin.register(CompanyTeam) +class CompanyTeamAdmin(admin.ModelAdmin): + list_display = ('name', 'company', 'thematics_list', 'members_count', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'visibility_start_hour', 'restrict_to_assigned_interventions') + search_fields = ('name', 'company__name') + list_filter = ('company', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions') + filter_horizontal = ('thematics',) + inlines = [CompanyTeamMemberInline] + + def thematics_list(self, obj): + return ", ".join([thematic.name for thematic in obj.thematics.all()]) + thematics_list.short_description = 'Thématiques' + + def members_count(self, obj): + return obj.members.count() + members_count.short_description = 'Nb membres' + + +@admin.register(CompanyMember) +class CompanyMemberAdmin(admin.ModelAdmin): + list_display = ('name', 'user', 'display_teams', 'visible_maintain_types_list') + search_fields = ('name', 'user__username', 'teams__name') + list_filter = ('teams', 'visible_maintain_types') + filter_horizontal = ('teams', 'visible_maintain_types') + + def display_teams(self, obj): + return ", ".join([t.name for t in obj.teams.all()]) or "—" + display_teams.short_description = 'Équipes' + + def visible_maintain_types_list(self, obj): + types = obj.visible_maintain_types.all() + if not types: + return "Tous" + return ", ".join([t.name_fr for t in types]) + visible_maintain_types_list.short_description = 'Types visibles' + + +class ContractOrderQuoteItemInline(admin.TabularInline): + model = ContractOrderQuoteItem + extra = 0 + autocomplete_fields = ('contract_post',) + + def get_queryset(self, request): + return super().get_queryset(request).select_related('contract_post__contract') + + +@admin.register(ContractOrderQuote) +class ContractOrderQuoteAdmin(admin.ModelAdmin): + list_display = ('order', 'created_by', 'created_at') + search_fields = ('order__order_code', 'order__contract__contract_number') + autocomplete_fields = ('order', 'created_by') + inlines = [ContractOrderQuoteItemInline] + + +class ContractOrderProgressStateItemInline(admin.TabularInline): + model = ContractOrderProgressStateItem + extra = 0 + readonly_fields = ('contract_post', 'quantity') + + +@admin.register(ContractOrderProgressState) +class ContractOrderProgressStateAdmin(admin.ModelAdmin): + list_display = ('order', 'created_at', 'created_by') + search_fields = ('order__order_code', 'order__contract__contract_number') + list_filter = ('created_at',) + readonly_fields = ('created_at', 'created_by', 'order') + inlines = [ContractOrderProgressStateItemInline] + + +class ClaimDeclarationAttachmentInline(admin.TabularInline): + model = ClaimDeclarationAttachment + extra = 0 + readonly_fields = ('uploaded_at', 'uploaded_by') + + +@admin.register(ContractOrderDocument) +class ContractOrderDocumentAdmin(admin.ModelAdmin): + list_display = ('title', 'order', 'document_type', 'uploaded_at') + search_fields = ('title', 'order__order_code') + list_filter = ('document_type',) + inlines = [ClaimDeclarationAttachmentInline] + + +@admin.register(ClaimDeclarationAttachment) +class ClaimDeclarationAttachmentAdmin(admin.ModelAdmin): + list_display = ('title', 'claim_document', 'uploaded_at', 'uploaded_by') + search_fields = ('title', 'claim_document__title', 'claim_document__order__order_code') + list_filter = ('uploaded_at',) + readonly_fields = ('uploaded_at',) + + +# --------------------------------------------------------------------------- +# Accès utilisateur aux équipes +# --------------------------------------------------------------------------- + +@admin.register(UserCompanyTeamAccess) +class UserCompanyTeamAccessAdmin(admin.ModelAdmin): + list_display = ('user_config_user', 'team', 'can_view', 'can_edit') + list_filter = ('can_view', 'can_edit', 'team__company') + search_fields = ('user_config__user__username', 'user_config__user__first_name', 'user_config__user__last_name', 'team__name') + autocomplete_fields = ('team',) + + def user_config_user(self, obj): + return obj.user_config.user.username + user_config_user.short_description = 'Utilisateur' + user_config_user.admin_order_field = 'user_config__user__username' + + +# --------------------------------------------------------------------------- +# Historique des appartenances aux équipes +# --------------------------------------------------------------------------- + +@admin.register(CompanyMemberHistory) +class CompanyMemberHistoryAdmin(admin.ModelAdmin): + list_display = ('member', 'team', 'join_date', 'leave_date', 'changed_by') + list_filter = ('team__company', 'team') + search_fields = ('member__name', 'team__name') + readonly_fields = ('member', 'team', 'join_date', 'leave_date', 'changed_by') + date_hierarchy = 'join_date' + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +# --------------------------------------------------------------------------- +# Commandes directes +# --------------------------------------------------------------------------- + +@admin.register(DirectOrderPost) +class DirectOrderPostAdmin(admin.ModelAdmin): + list_display = ('order', 'contract_post', 'quantity', 'control_status', 'created_by', 'created_at') + list_filter = ('control_status', 'order__contract') + search_fields = ('order__order_code', 'contract_post__code') + readonly_fields = ('created_at', 'created_by', 'validated_at', 'validated_by') + autocomplete_fields = ('contract_post',) diff --git a/streetup/contracts/apps.py b/streetup/contracts/apps.py new file mode 100644 index 0000000..0c16154 --- /dev/null +++ b/streetup/contracts/apps.py @@ -0,0 +1,27 @@ +from django.apps import AppConfig + + +class ContractsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'contracts' + + def ready(self): + # Importer les signaux pour qu'ils soient enregistrés + import contracts.signals + + from common.private_files.registry import register + from .permissions import ( + guard_contract_order_file, + guard_order_document_file, + guard_claim_attachment_file, + guard_order_attachment_file, + ) + # ContractOrder.order_file + register("contracts", "ContractOrder", "order_file", guard_contract_order_file) + # ContractOrderDocument.document + register("contracts", "ContractOrderDocument", "document", guard_order_document_file) + # ClaimDeclarationAttachment.document + register("contracts", "ClaimDeclarationAttachment", "document", guard_claim_attachment_file) + # ContractOrderAttachment (photos / PDF génériques) + register("contracts", "ContractOrderAttachment", "file", guard_order_attachment_file) + register("contracts", "ContractOrderAttachment", "thumbnail", guard_order_attachment_file) diff --git a/streetup/contracts/forms.py b/streetup/contracts/forms.py new file mode 100644 index 0000000..44d8ebd --- /dev/null +++ b/streetup/contracts/forms.py @@ -0,0 +1,233 @@ +from django import forms +from django.forms import DateInput, formset_factory +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from decimal import Decimal + +from .models import ( + ContractOrder, + CONTRACT_ORDER_STATUS_CHOICES, + ClaimDeclarationAttachment, + ContractOrderAttachment, +) + +from .permissions import get_allowed_contracts_for_user + +class ContractOrderForm(forms.ModelForm): + def __init__(self, *args, user=None, project_id=None, **kwargs): + super().__init__(*args, **kwargs) + + # Limiter les contrats visibles par l'utilisateur + if user is not None: + self.fields['contract'].queryset = get_allowed_contracts_for_user(user) + + # Si des interventions existent pour cet ordre → on fige le contrat (UI) + if self.instance and self.instance.pk: + has_interventions = self.instance.interventions.exists() # via related_name="interventions" + if has_interventions: + self.fields['contract'].disabled = True + self.fields['contract'].help_text = _("Ce contrat ne peut plus être modifié car des interventions sont déjà liées.") + + + # Ajout de la classe Bootstrap à tous les champs + for field_name, field in self.fields.items(): + widget = field.widget + + base_class = '' + if isinstance(widget, forms.Select): + base_class = 'form-select' + else: + base_class = 'form-control' + + # Marquer les champs optionnels avec une classe supplémentaire + if not field.required: + widget.attrs['class'] = f"{base_class} optional-field" + else: + widget.attrs['class'] = base_class + + + # Remettre le champ vide si la valeur est 0 + if self.initial.get('total_amount') in [0, 0.0] or self.instance.total_amount in [0, 0.0]: + self.initial['total_amount'] = '' + + if self.instance and self.instance.pk: + self.fields['order_date'].initial = self.instance.order_date.strftime('%Y-%m-%d') + self.fields['delivery_date'].initial = self.instance.delivery_date.strftime('%Y-%m-%d') + + + + def clean(self): + cleaned = super().clean() + # Sécurité serveur : si des interventions existent, interdire toute modif du contrat + if self.instance and self.instance.pk and self.instance.interventions.exists(): + if 'contract' in self.changed_data: + self.add_error( + 'contract', + _("Le contrat ne peut plus être modifié car des interventions sont déjà liées à cette commande.") + ) + raise ValidationError(_("Modification du contrat interdite.")) + return cleaned + + + order_date = forms.DateField( + required=True, + widget=DateInput( + attrs={'type': 'date', 'class': 'form-control'}, + format='%Y-%m-%d' # Format ISO compatible avec HTML5 + ), + label=_("Date de la commande") + ) + delivery_date = forms.DateField( + required=True, + widget=DateInput( + attrs={'type': 'date', 'class': 'form-control'}, + format='%Y-%m-%d' + ), + label=_("Date de livraison") + ) + order_status = forms.ChoiceField( + choices=CONTRACT_ORDER_STATUS_CHOICES, + widget=forms.Select(attrs={'class': 'form-select'}), + label=_("Statut de la commande") + ) + order_file = forms.FileField( + required=False, + widget=forms.ClearableFileInput(attrs={'class': 'form-control'}), + label=_("Fichier de commande") + ) + total_amount = forms.DecimalField( + required=False, + label=_("Montant total (€)"), + widget=forms.NumberInput(attrs={ + 'step': '0.01', + 'placeholder': 'ex. 1000.00', + }) + ) + + class Meta: + model = ContractOrder + fields = [ + 'contract', + 'order_code', + 'description', + 'order_date', + 'delivery_date', + 'total_amount', + 'order_status', + 'order_file' + ] + widgets = { + 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), + } + labels = { + 'contract': _("Contrat"), + 'order_code': _("Référence de commande"), + 'description': _("Description"), + } + + +class ContractOrderQuoteQuantityForm(forms.Form): + contract_post_id = forms.IntegerField( + min_value=1, + widget=forms.HiddenInput(), + ) + quantity = forms.DecimalField( + label=_("Quantité"), + decimal_places=2, + max_digits=10, + min_value=Decimal('0'), + required=False, + widget=forms.NumberInput( + attrs={ + 'class': 'form-control form-control-sm text-end quote-quantity-input', + 'step': '1', + 'min': '0', + 'inputmode': 'decimal', + 'style': 'text-align: center;', + } + ), + ) + + def __init__(self, *args, allowed_post_ids=None, disabled=False, **kwargs): + self.allowed_post_ids = allowed_post_ids + self._is_disabled = disabled + super().__init__(*args, **kwargs) + + if self._is_disabled: + self.fields['quantity'].disabled = True + css_class = self.fields['quantity'].widget.attrs.get('class', '') + self.fields['quantity'].widget.attrs['class'] = f"{css_class} bg-light".strip() + + + def clean_contract_post_id(self): + value = self.cleaned_data.get('contract_post_id') + if not value: + raise ValidationError(_("Poste invalide.")) + if self.allowed_post_ids is not None and value not in self.allowed_post_ids: + raise ValidationError(_("Poste invalide.")) + return value + + def clean_quantity(self): + quantity = self.cleaned_data.get('quantity') + if quantity in (None, ''): + return Decimal('0') + return quantity + + +ContractOrderQuoteQuantityFormSet = formset_factory( + ContractOrderQuoteQuantityForm, + extra=0, +) + + +class ClaimDeclarationAttachmentForm(forms.ModelForm): + """Formulaire pour ajouter une pièce jointe à une déclaration de créance""" + + class Meta: + model = ClaimDeclarationAttachment + fields = ['title', 'document'] + widgets = { + 'title': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': _('Ex: Facture fournisseur, Bon de livraison...'), + }), + 'document': forms.ClearableFileInput(attrs={ + 'class': 'form-control', + }), + } + labels = { + 'title': _("Titre du justificatif"), + 'document': _("Fichier"), + } + + +class MultipleFileInput(forms.ClearableFileInput): + allow_multiple_selected = True + + +class MultipleFileField(forms.FileField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", MultipleFileInput()) + super().__init__(*args, **kwargs) + + def clean(self, data, initial=None): + single_file_clean = super().clean + if isinstance(data, (list, tuple)): + result = [single_file_clean(d, initial) for d in data] + else: + result = [single_file_clean(data, initial)] + return [item for item in result if item] + + +class ContractOrderAttachmentForm(forms.ModelForm): + """Formulaire pour ajouter des documents (photos, PDF, …) à une commande.""" + files = MultipleFileField(required=True, label=_("Fichiers")) + + class Meta: + model = ContractOrderAttachment + fields = ['files'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['files'].widget.attrs.update({'class': 'form-control'}) \ No newline at end of file diff --git a/streetup/contracts/locale/en/LC_MESSAGES/django.mo b/streetup/contracts/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..1947875 Binary files /dev/null and b/streetup/contracts/locale/en/LC_MESSAGES/django.mo differ diff --git a/streetup/contracts/locale/en/LC_MESSAGES/django.po b/streetup/contracts/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..311f343 --- /dev/null +++ b/streetup/contracts/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,1943 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-06 10:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: contracts/forms.py:30 +msgid "" +"Ce contrat ne peut plus être modifié car des interventions sont déjà liées." +msgstr "" +"This contract can no longer be modified because interventions are already " +"linked." + +#: contracts/forms.py:67 +msgid "" +"Le contrat ne peut plus être modifié car des interventions sont déjà liées à " +"cette commande." +msgstr "" +"The contract can no longer be modified because interventions are already " +"linked to this order." + +#: contracts/forms.py:69 +msgid "Modification du contrat interdite." +msgstr "Contract modification prohibited." + +#: contracts/forms.py:79 +msgid "Date de la commande" +msgstr "Order date" + +#: contracts/forms.py:87 +#: contracts/templates/contracts/contracts_orders_detail.html:106 +msgid "Date de livraison" +msgstr "Delivery date" + +#: contracts/forms.py:92 +msgid "Statut de la commande" +msgstr "Order status" + +#: contracts/forms.py:97 +#: contracts/templates/contracts/contracts_orders_detail.html:202 +msgid "Fichier de commande" +msgstr "Order file" + +#: contracts/forms.py:101 +msgid "Montant total (€)" +msgstr "Total amount (€)" + +#: contracts/forms.py:124 +#: contracts/templates/contracts/contracts_detail.html:22 +#: contracts/templates/contracts/contracts_order_quote_form.html:37 +msgid "Contrat" +msgstr "Contract" + +#: contracts/forms.py:125 +msgid "Référence de commande" +msgstr "Order reference" + +#: contracts/forms.py:126 +#: contracts/templates/contracts/contracts_detail.html:25 +#: contracts/templates/contracts/contracts_index.html:198 +#: contracts/templates/contracts/contracts_index.html:241 +#: contracts/templates/contracts/contracts_order_quote_form.html:38 +#: contracts/templates/contracts/contracts_order_quote_form.html:71 +#: contracts/templates/contracts/contracts_orders_detail.html:102 +#: contracts/templates/contracts/contracts_orders_detail.html:497 +#: contracts/templates/contracts/contracts_orders_detail.html:585 +#: contracts/views_pdf_reports.py:580 contracts/views_pdf_reports.py:718 +#: contracts/views_pdf_reports.py:946 contracts/views_pdf_reports.py:1139 +msgid "Description" +msgstr "Description" + +#: contracts/forms.py:136 contracts/models.py:950 +#: contracts/templates/contracts/contracts_order_quote_form.html:74 +#: contracts/templates/contracts/contracts_orders_detail.html:588 +#: contracts/templates/contracts/contracts_orders_detail.html:709 +#: contracts/views_pdf_reports.py:949 contracts/views_pdf_reports.py:1142 +msgid "Quantité" +msgstr "Quantity" + +#: contracts/forms.py:166 contracts/forms.py:168 +msgid "Poste invalide." +msgstr "Invalid position." + +#: contracts/forms.py:193 +msgid "Ex: Facture fournisseur, Bon de livraison..." +msgstr "E.g. Supplier invoice, Delivery note..." + +#: contracts/forms.py:200 +msgid "Titre du justificatif" +msgstr "Justificative title" + +#: contracts/forms.py:201 contracts/models.py:862 contracts/models.py:1034 +#: contracts/templates/contracts/contracts_index.html:201 +msgid "Fichier" +msgstr "File" + +#: contracts/forms.py:225 +msgid "Fichiers" +msgstr "Files" + +#: contracts/models.py:39 +msgid "Maintenance corrective" +msgstr "Corrective maintenance" + +#: contracts/models.py:40 +msgid "Maintenance préventive" +msgstr "Preventive maintenance" + +#: contracts/models.py:41 +msgid "Maintenance améliorative" +msgstr "Improvement maintenance" + +#: contracts/models.py:47 +msgid "Code du type de maintenance (corrective, preventive, ameliorative)" +msgstr "Maintenance type code (corrective, preventive, improvement)" + +#: contracts/models.py:48 +msgid "Nom (FR)" +msgstr "Name (FR)" + +#: contracts/models.py:49 +msgid "Nom (NL)" +msgstr "Name (NL)" + +#: contracts/models.py:64 +msgid "Type de maintenance" +msgstr "Maintenance type" + +#: contracts/models.py:65 +msgid "Types de maintenance" +msgstr "Maintenance types" + +#: contracts/models.py:78 +msgid "Catégories d'assets gérées" +msgstr "" + +#: contracts/models.py:79 +msgid "Catégories d'assets dont cette équipe est responsable." +msgstr "" + +#: contracts/models.py:83 +msgid "Gestionnaire d'assets" +msgstr "" + +#: contracts/models.py:84 +msgid "" +"Indique si cette équipe est gestionnaire des catégories d'assets associées." +msgstr "" + +#: contracts/models.py:88 +#: contracts/templates/contracts/contracts_detail.html:26 +#: contracts/templates/contracts/contracts_index.html:242 +msgid "Prestataire" +msgstr "Contractor" + +#: contracts/models.py:89 +msgid "Indique si cette équipe est un prestataire extérieur." +msgstr "" + +#: contracts/models.py:93 +msgid "Peut générer le rapport journalier" +msgstr "Can generate the daily report" + +#: contracts/models.py:94 +msgid "" +"Indique si les membres de cette équipe peuvent générer un rapport quotidien " +"des interventions." +msgstr "" +"Indicates if the members of this team can generate a daily report of " +"interventions." + +#: contracts/models.py:98 +msgid "Photos obligatoires" +msgstr "" + +#: contracts/models.py:99 +msgid "" +"Si activé, au moins une photo doit être ajoutée à l'intervention avant de " +"pouvoir la terminer." +msgstr "" + +#: contracts/models.py:103 +msgid "Restreindre la visibilité aux interventions du jour" +msgstr "" + +#: contracts/models.py:104 +msgid "" +"Si activé, les interventions ne sont visibles qu'à partir d'une certaine " +"heure, et uniquement pour le jour même." +msgstr "" + +#: contracts/models.py:109 +msgid "Heure de début de visibilité" +msgstr "" + +#: contracts/models.py:110 +msgid "" +"Heure locale à partir de laquelle les interventions du jour deviennent " +"visibles (ex: 06:00:00)." +msgstr "" + +#: contracts/models.py:114 +#, fuzzy +#| msgid "Nombre d'interventions par personne" +msgid "Restreindre aux interventions assignées" +msgstr "Number of interventions per person" + +#: contracts/models.py:115 +msgid "" +"Si activé, les membres de cette équipe ne voient que les interventions " +"assignées à l'équipe ou directement à un membre." +msgstr "" + +#: contracts/models.py:122 +msgid "Équipe de société/organisme" +msgstr "Contractor team" + +#: contracts/models.py:123 +msgid "Équipes de sociétés/organismes" +msgstr "Contractor teams" + +#: contracts/models.py:135 +#, fuzzy +#| msgid "Équipe" +msgid "Équipes" +msgstr "Team" + +#: contracts/models.py:136 +msgid "Équipes auxquelles ce membre appartient." +msgstr "" + +#: contracts/models.py:142 +msgid "Types de maintenance visibles" +msgstr "Maintenance types" + +#: contracts/models.py:143 +msgid "" +"Types de maintenance visibles par défaut pour cet utilisateur. Si vide, tous " +"les types sont visibles. Les interventions assignées à l'utilisateur restent " +"toujours visibles." +msgstr "" +"Default maintenance types visible for this user. If empty, all types are " +"visible. Interventions assigned to the user remain always visible." + +#: contracts/models.py:178 +#, fuzzy +#| msgid "Nombre" +msgid "Membre" +msgstr "Number" + +#: contracts/models.py:186 contracts/models.py:222 +#: contracts/templates/contracts/teams_index.html:349 +msgid "Équipe" +msgstr "Team" + +#: contracts/models.py:188 +#, fuzzy +#| msgid "Date de création" +msgid "Date d'entrée" +msgstr "Creation date" + +#: contracts/models.py:189 +#, fuzzy +#| msgid "Date de création" +msgid "Date de sortie" +msgstr "Creation date" + +#: contracts/models.py:196 +#, fuzzy +#| msgid "Commandé par" +msgid "Modifié par" +msgstr "Ordered by" + +#: contracts/models.py:205 +msgid "Historique d'équipe d'un membre" +msgstr "" + +#: contracts/models.py:206 +msgid "Historiques d'équipe des membres" +msgstr "" + +#: contracts/models.py:216 +msgid "Configuration utilisateur" +msgstr "" + +#: contracts/models.py:226 +msgid "Peut consulter" +msgstr "Can consult" + +#: contracts/models.py:227 +msgid "L'utilisateur peut voir cette équipe et ses membres." +msgstr "" + +#: contracts/models.py:231 +msgid "Peut éditer" +msgstr "Can edit" + +#: contracts/models.py:232 +msgid "" +"L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer " +"des membres entre équipes." +msgstr "" + +#: contracts/models.py:240 +msgid "Accès utilisateur à une équipe" +msgstr "" + +#: contracts/models.py:241 +msgid "Accès utilisateurs aux équipes" +msgstr "" + +#: contracts/models.py:259 +msgid "Emails en copie (CC)" +msgstr "CC emails" + +#: contracts/models.py:260 +msgid "" +"Adresses email à ajouter en copie des notifications (séparées par des " +"virgules ou points-virgules)" +msgstr "" +"Email addresses to add in copy of notifications (separated by commas or " +"semicolons)" + +#: contracts/models.py:264 +msgid "Envoyer des notifications liées aux interventions par email." +msgstr "Send intervention-related notifications by email." + +#: contracts/models.py:270 +msgid "Nom du signataire (prestataire)" +msgstr "Signatory name (contractor)" + +#: contracts/models.py:271 +msgid "Nom de la personne qui signe les documents côté prestataire." +msgstr "Name of the person who signs documents on the contractor side." + +#: contracts/models.py:277 +msgid "Rôle du signataire (prestataire)" +msgstr "Signatory role (contractor)" + +#: contracts/models.py:278 +msgid "Rôle/fonction de la personne qui signe les documents côté prestataire." +msgstr "" +"Role/function of the person who signs documents on the contractor side." + +#: contracts/models.py:284 +msgid "Téléphone de contact" +msgstr "Contact phone" + +#: contracts/models.py:285 +msgid "" +"Numéro de téléphone du technicien de garde ou contact principal pour les " +"urgences." +msgstr "" +"Phone number of the on-call technician or primary contact for emergencies." + +#: contracts/models.py:291 +msgid "Thématiques" +msgstr "Themes" + +#: contracts/models.py:292 +msgid "Thématiques associées à ce contrat." +msgstr "Themes associated with this contract." + +#: contracts/models.py:296 +msgid "Actif" +msgstr "Active" + +#: contracts/models.py:297 +msgid "Indique si le contrat est actif." +msgstr "Indicates if the contract is active." + +#: contracts/models.py:301 +msgid "Nécessite une approbation" +msgstr "" + +#: contracts/models.py:302 +msgid "" +"Indique si les interventions liées à ce contrat nécessitent une approbation " +"avant d'être transmises pour traitement." +msgstr "" + +#: contracts/models.py:306 +msgid "Nécessite une vérification" +msgstr "" + +#: contracts/models.py:307 +msgid "" +"Indique si les interventions liées à ce contrat nécessitent une vérification " +"avant d'être transmises pour traitement." +msgstr "" + +#: contracts/models.py:311 +msgid "Utilise les occupations" +msgstr "" + +#: contracts/models.py:312 +msgid "" +"Indique si les interventions liées à ce contrat utilisent la gestion des " +"occupations (temps passé par membre)." +msgstr "" + +#: contracts/models.py:316 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Utilise les gestionnaires d'intervention" +msgstr "Cannot freeze an empty quote." + +#: contracts/models.py:317 +msgid "" +"Indique si les interventions liées à ce contrat utilisent un gestionnaire " +"d'intervention." +msgstr "" + +#: contracts/models.py:321 +msgid "Occupations obligatoires" +msgstr "" + +#: contracts/models.py:322 +msgid "" +"Si activé, au moins une occupation doit être enregistrée avant de terminer " +"une intervention corrective liée à ce contrat." +msgstr "" + +#: contracts/models.py:326 +msgid "Avertir si aucune prestation au passage à 'Traité'" +msgstr "" + +#: contracts/models.py:327 +msgid "" +"Si activé, un avertissement est affiché lorsque l'utilisateur tente de " +"passer une intervention au statut 'Traité' sans avoir encodé de prestation." +msgstr "" + +#: contracts/models.py:331 +#, fuzzy +#| msgid "Rapport journalier" +msgid "Envoi groupé journalier" +msgstr "Daily report" + +#: contracts/models.py:332 +msgid "" +"Si activé, les fiches d'intervention du jour sont regroupées dans un seul " +"email envoyé en fin de journée, plutôt qu'un email par intervention." +msgstr "" + +#: contracts/models.py:336 +msgid "Heure d'envoi du résumé journalier" +msgstr "" + +#: contracts/models.py:337 +msgid "" +"Heure (0-23) à laquelle le résumé journalier des interventions est envoyé " +"(heure locale)." +msgstr "" + +#: contracts/models.py:341 +msgid "Emails uniquement si priorité urgente" +msgstr "" + +#: contracts/models.py:342 +msgid "" +"Si activé, les notifications email ne sont envoyées que pour les " +"interventions de priorité 'Urgent'." +msgstr "" + +#: contracts/models.py:436 +#: contracts/templates/contracts/contracts_index.html:139 +msgid "En préparation" +msgstr "In preparation" + +#: contracts/models.py:437 +#: contracts/templates/contracts/contracts_index.html:144 +msgid "Envoyé" +msgstr "Sent" + +#: contracts/models.py:438 +#: contracts/templates/contracts/contracts_index.html:149 +msgid "Terminé" +msgstr "Completed" + +#: contracts/models.py:439 +#: contracts/templates/contracts/contracts_index.html:154 +msgid "Comptabilisé" +msgstr "Accounted" + +#: contracts/models.py:440 +#: contracts/templates/contracts/contracts_index.html:159 +msgid "Payé" +msgstr "Paid" + +#: contracts/models.py:441 +#: contracts/templates/contracts/contracts_index.html:164 +msgid "Annulé" +msgstr "Cancelled" + +#: contracts/models.py:460 +msgid "Projets" +msgstr "Projects" + +#: contracts/models.py:826 +msgid "État d'avancement associé à ce document, le cas échéant" +msgstr "Progress status associated with this document, if applicable" + +#: contracts/models.py:852 +#: contracts/templates/contracts/claim_declaration_attachments.html:47 +#: contracts/views_pdf_reports.py:432 +msgid "Déclaration de créance" +msgstr "Debt declaration" + +#: contracts/models.py:856 +#: contracts/templates/contracts/claim_declaration_attachments.html:52 +#: contracts/templates/contracts/contracts_detail.html:116 +#: contracts/templates/contracts/contracts_orders_detail.html:768 +#: contracts/templates/contracts/teams_index.html:741 +msgid "Titre" +msgstr "Title" + +#: contracts/models.py:857 +msgid "Titre ou description du justificatif" +msgstr "Title or description of the justificative" + +#: contracts/models.py:864 contracts/models.py:1045 +msgid "Date d'ajout" +msgstr "Added on" + +#: contracts/models.py:871 contracts/models.py:1052 +msgid "Ajouté par" +msgstr "Added by" + +#: contracts/models.py:878 +msgid "Pièce jointe de déclaration de créance" +msgstr "Debt declaration attachment" + +#: contracts/models.py:879 +msgid "Pièces jointes de déclarations de créance" +msgstr "Debt declaration attachments" + +#: contracts/models.py:886 +msgid "Numéro d'ordre de l'état d'avancement pour cette commande" +msgstr "Progress status order number for this order" + +#: contracts/models.py:905 +msgid "Quantité consommée à cette date" +msgstr "Quantity consumed on this date" + +#: contracts/models.py:926 +#, fuzzy +#| msgid "Date de livraison" +msgid "En attente de validation" +msgstr "Delivery date" + +#: contracts/models.py:927 +#: contracts/templates/contracts/contracts_orders_detail.html:616 +#: contracts/templates/contracts/contracts_orders_detail.html:926 +msgid "Approuvé" +msgstr "" + +#: contracts/models.py:928 +#: contracts/templates/contracts/contracts_orders_detail.html:621 +#: contracts/templates/contracts/contracts_orders_detail.html:927 +msgid "Refusé" +msgstr "" + +#: contracts/models.py:929 +#: contracts/templates/contracts/contracts_orders_detail.html:623 +#: contracts/templates/contracts/contracts_orders_detail.html:929 +msgid "Facturé" +msgstr "Invoiced" + +#: contracts/models.py:939 contracts/models.py:1029 +#: contracts/templates/contracts/claim_declaration_attachments.html:53 +#: contracts/templates/contracts/contracts_orders_detail.html:86 +msgid "Commande" +msgstr "Order" + +#: contracts/models.py:945 +#: contracts/templates/contracts/contracts_orders_detail.html:699 +msgid "Poste de marché" +msgstr "" + +#: contracts/models.py:952 +#: contracts/templates/contracts/contracts_orders_detail.html:591 +#: contracts/templates/contracts/contracts_orders_detail.html:713 +msgid "Commentaire" +msgstr "Comment" + +#: contracts/models.py:957 +msgid "Statut de validation" +msgstr "Validation status" + +#: contracts/models.py:959 +#: contracts/templates/contracts/claim_declaration_attachments.html:54 +msgid "Date de création" +msgstr "Creation date" + +#: contracts/models.py:966 +msgid "Créé par" +msgstr "Created by" + +#: contracts/models.py:968 +#, fuzzy +#| msgid "Date de livraison" +msgid "Date de validation" +msgstr "Delivery date" + +#: contracts/models.py:975 +msgid "Validé par" +msgstr "Validated by" + +#: contracts/models.py:981 +#, fuzzy +#| msgid "Quantité prévue" +msgid "Quantité déjà facturée" +msgstr "Planned quantity" + +#: contracts/models.py:982 +#, fuzzy +#| msgid "Générer la déclaration de créance" +msgid "Quantité cumulée sur les déclarations de créance précédentes." +msgstr "Generate the debt declaration" + +#: contracts/models.py:1017 +#, fuzzy +#| msgid "Consommation des postes" +msgid "Commande directe – poste" +msgstr "Position consumption" + +#: contracts/models.py:1018 +#, fuzzy +#| msgid "Consommation des postes" +msgid "Commandes directes – postes" +msgstr "Position consumption" + +#: contracts/models.py:1043 +msgid "Vignette" +msgstr "Thumbnail" + +#: contracts/models.py:1064 +msgid "Pièce jointe de commande" +msgstr "Order attachment" + +#: contracts/models.py:1065 +msgid "Pièces jointes de commande" +msgstr "Order attachments" + +#: contracts/permissions.py:98 +msgid "Paramètre 'contract_id' manquant." +msgstr "Missing 'contract_id' parameter." + +#: contracts/permissions.py:103 +msgid "Accès au contrat refusé." +msgstr "Contract access denied." + +#: contracts/permissions.py:132 +msgid "Paramètre 'order_id' manquant." +msgstr "Missing 'order_id' parameter." + +#: contracts/permissions.py:137 contracts/views.py:976 +msgid "Accès à la commande refusé." +msgstr "Order access denied." + +#: contracts/templates/admin/contracts/contractpost/change_list.html:7 +msgid "Importer / mettre à jour des postes (Excel)" +msgstr "" + +#: contracts/templates/admin/contracts/contractpost/import_posts.html:8 +msgid "Accueil" +msgstr "" + +#: contracts/templates/contracts/claim_declaration_attachments.html:27 +msgid "Contrats" +msgstr "Contracts" + +#: contracts/templates/contracts/claim_declaration_attachments.html:29 +#: contracts/templates/contracts/contracts_orders_detail.html:474 +msgid "Justificatifs" +msgstr "Supporting documents" + +#: contracts/templates/contracts/claim_declaration_attachments.html:33 +msgid "Justificatifs de la déclaration de créance" +msgstr "Debt declaration supporting documents" + +#: contracts/templates/contracts/claim_declaration_attachments.html:58 +msgid "Voir le document" +msgstr "View document" + +#: contracts/templates/contracts/claim_declaration_attachments.html:68 +msgid "Ajouter un justificatif" +msgstr "Add a supporting document" + +#: contracts/templates/contracts/claim_declaration_attachments.html:90 +#: contracts/templates/contracts/contracts_orders_detail.html:313 +#: contracts/templates/contracts/teams_index.html:324 +msgid "Ajouter" +msgstr "Add" + +#: contracts/templates/contracts/claim_declaration_attachments.html:101 +msgid "Justificatifs attachés" +msgstr "Attached supporting documents" + +#: contracts/templates/contracts/claim_declaration_attachments.html:117 +#: contracts/templates/contracts/contracts_orders_detail.html:466 +msgid "Ajouté le" +msgstr "Added on" + +#: contracts/templates/contracts/claim_declaration_attachments.html:119 +msgid "par" +msgstr "by" + +#: contracts/templates/contracts/claim_declaration_attachments.html:125 +#: contracts/templates/contracts/contracts_orders_detail.html:332 +#: contracts/templates/contracts/contracts_orders_detail.html:333 +msgid "Télécharger" +msgstr "Download" + +#: contracts/templates/contracts/claim_declaration_attachments.html:128 +msgid "Êtes-vous sûr de vouloir supprimer ce justificatif ?" +msgstr "Are you sure you want to delete this supporting document?" + +#: contracts/templates/contracts/claim_declaration_attachments.html:130 +#: contracts/templates/contracts/contracts_orders_detail.html:238 +#: contracts/templates/contracts/contracts_orders_detail.html:290 +#: contracts/templates/contracts/contracts_orders_detail.html:642 +#: contracts/templates/contracts/contracts_orders_detail.html:743 +#: contracts/templates/contracts/contracts_orders_detail.html:949 +msgid "Supprimer" +msgstr "Delete" + +#: contracts/templates/contracts/claim_declaration_attachments.html:141 +msgid "" +"Aucun justificatif n'a encore été ajouté à cette déclaration de créance." +msgstr "No supporting document has been added to this debt declaration yet." + +#: contracts/templates/contracts/claim_declaration_attachments.html:149 +#: contracts/templates/contracts/contracts_order_quote_form.html:31 +#: contracts/templates/contracts/contracts_order_quote_form.html:154 +msgid "Retour à la commande" +msgstr "Back to order" + +#: contracts/templates/contracts/contracts_detail.html:19 +msgid "Détail du marché / contrat" +msgstr "Contract/market detail" + +#: contracts/templates/contracts/contracts_detail.html:28 +#: contracts/templates/contracts/contracts_detail.html:117 +#: contracts/templates/contracts/contracts_index.html:243 +#: contracts/templates/contracts/contracts_orders_detail.html:769 +msgid "Début" +msgstr "Start" + +#: contracts/templates/contracts/contracts_detail.html:29 +#: contracts/templates/contracts/contracts_index.html:244 +msgid "Fin" +msgstr "End" + +#: contracts/templates/contracts/contracts_detail.html:31 +msgid "Fonctionnaire dirigeant" +msgstr "Managing official" + +#: contracts/templates/contracts/contracts_detail.html:34 +msgid "Notifications email" +msgstr "Email notifications" + +#: contracts/templates/contracts/contracts_detail.html:36 +msgid "Oui" +msgstr "Yes" + +#: contracts/templates/contracts/contracts_detail.html:38 +msgid "Non" +msgstr "No" + +#: contracts/templates/contracts/contracts_detail.html:45 +msgid "Montant engagé" +msgstr "Committed amount" + +#: contracts/templates/contracts/contracts_detail.html:49 +msgid "Montant \"presté" +msgstr "Amount \"worked\"" + +#: contracts/templates/contracts/contracts_detail.html:67 +#: contracts/templates/contracts/contracts_orders_detail.html:135 +msgid "Montant validé" +msgstr "Validated amount" + +#: contracts/templates/contracts/contracts_detail.html:87 +msgid "Dossier interne" +msgstr "Internal file" + +#: contracts/templates/contracts/contracts_detail.html:96 +#: contracts/templates/contracts/contracts_orders_detail.html:212 +msgid "Retour à la liste" +msgstr "Back to list" + +#: contracts/templates/contracts/contracts_detail.html:104 +#: contracts/templates/contracts/contracts_index.html:120 +msgid "Commandes" +msgstr "Orders" + +#: contracts/templates/contracts/contracts_detail.html:115 +#: contracts/templates/contracts/contracts_order_quote_form.html:70 +#: contracts/templates/contracts/contracts_orders_detail.html:496 +#: contracts/templates/contracts/contracts_orders_detail.html:584 +#: contracts/templates/contracts/contracts_orders_detail.html:767 +#: contracts/templates/contracts/teams_index.html:740 +#: contracts/views_pdf_reports.py:945 contracts/views_pdf_reports.py:1138 +msgid "Code" +msgstr "Code" + +#: contracts/templates/contracts/contracts_detail.html:118 +#: contracts/templates/contracts/contracts_index.html:200 +#: contracts/templates/contracts/contracts_orders_detail.html:592 +#: contracts/templates/contracts/contracts_orders_detail.html:770 +#: contracts/templates/contracts/teams_index.html:743 +msgid "Statut" +msgstr "Status" + +#: contracts/templates/contracts/contracts_index.html:114 +msgid "Nouvelle commande" +msgstr "New order" + +#: contracts/templates/contracts/contracts_index.html:123 +msgid "Marchés/Contrats" +msgstr "Markets/Contracts" + +#: contracts/templates/contracts/contracts_index.html:134 +msgid "Statuts" +msgstr "Statuses" + +#: contracts/templates/contracts/contracts_index.html:171 +msgid "Commandes à partir du" +msgstr "Orders from" + +#: contracts/templates/contracts/contracts_index.html:175 +msgid "jusqu'au" +msgstr "until" + +#: contracts/templates/contracts/contracts_index.html:181 +msgid "Filtrer" +msgstr "Filter" + +#: contracts/templates/contracts/contracts_index.html:182 +msgid "Réinitialiser" +msgstr "Reset" + +#: contracts/templates/contracts/contracts_index.html:197 +msgid "Numéro de commande" +msgstr "Order number" + +#: contracts/templates/contracts/contracts_index.html:199 +msgid "Date" +msgstr "Date" + +#: contracts/templates/contracts/contracts_index.html:202 +msgid "Marché" +msgstr "Contract" + +#: contracts/templates/contracts/contracts_index.html:240 +msgid "Numéro" +msgstr "Number" + +#: contracts/templates/contracts/contracts_index.html:289 +msgid "Aucune commande trouvée" +msgstr "No order found" + +#: contracts/templates/contracts/contracts_index.html:294 +msgid "Aucun marché trouvé" +msgstr "No market found" + +#: contracts/templates/contracts/contracts_order_form.html:22 +msgid "Modifier la commande" +msgstr "Edit order" + +#: contracts/templates/contracts/contracts_order_form.html:24 +msgid "Ajouter une nouvelle commande" +msgstr "Add a new order" + +#: contracts/templates/contracts/contracts_order_form.html:35 +msgid "Mettre à jour" +msgstr "Update" + +#: contracts/templates/contracts/contracts_order_form.html:35 +msgid "Créer la commande" +msgstr "Create order" + +#: contracts/templates/contracts/contracts_order_quote_form.html:29 +#, python-format +msgid "Devis pour la commande %(order_code)s" +msgstr "Quote for order %(order_code)s" + +#: contracts/templates/contracts/contracts_order_quote_form.html:38 +#: contracts/templates/contracts/contracts_orders_detail.html:112 +msgid "Non renseigné" +msgstr "Not specified" + +#: contracts/templates/contracts/contracts_order_quote_form.html:39 +msgid "Montant total du devis" +msgstr "Total quote amount" + +#: contracts/templates/contracts/contracts_order_quote_form.html:54 +#, python-format +msgid "Ce devis est figé depuis le %(finalized_at)s." +msgstr "This quote has been frozen since %(finalized_at)s." + +#: contracts/templates/contracts/contracts_order_quote_form.html:56 +#: contracts/views.py:460 +msgid "Ce devis est figé et ne peut plus être modifié." +msgstr "This quote is frozen and can no longer be modified." + +#: contracts/templates/contracts/contracts_order_quote_form.html:69 +msgid "N° de poste" +msgstr "Position no." + +#: contracts/templates/contracts/contracts_order_quote_form.html:72 +#: contracts/templates/contracts/contracts_orders_detail.html:586 +#: contracts/views_pdf_reports.py:947 contracts/views_pdf_reports.py:1140 +msgid "Unité" +msgstr "Unit" + +#: contracts/templates/contracts/contracts_order_quote_form.html:73 +#: contracts/templates/contracts/contracts_orders_detail.html:510 +#: contracts/views_pdf_reports.py:1141 +msgid "Prix unitaire" +msgstr "Unit price" + +#: contracts/templates/contracts/contracts_order_quote_form.html:75 +#: contracts/templates/contracts/contracts_orders_detail.html:590 +#: contracts/views_pdf_reports.py:1143 +msgid "Montant" +msgstr "Amount" + +#: contracts/templates/contracts/contracts_order_quote_form.html:81 +#: contracts/templates/contracts/contracts_order_quote_form.html:87 +#: contracts/templates/contracts/contracts_order_quote_form.html:93 +msgid "Filtrer…" +msgstr "Filter..." + +#: contracts/templates/contracts/contracts_order_quote_form.html:103 +msgid "Quantité > 0" +msgstr "Quantity > 0" + +#: contracts/templates/contracts/contracts_order_quote_form.html:140 +msgid "Aucun poste disponible pour ce contrat." +msgstr "No position available for this contract." + +#: contracts/templates/contracts/contracts_order_quote_form.html:145 +msgid "Aucun poste ne correspond aux filtres." +msgstr "No position matches the filters." + +#: contracts/templates/contracts/contracts_order_quote_form.html:159 +msgid "Enregistrer le devis" +msgstr "Save quote" + +#: contracts/templates/contracts/contracts_order_quote_form.html:163 +msgid "Figer le devis" +msgstr "Freeze quote" + +#: contracts/templates/contracts/contracts_orders_detail.html:64 +msgid "Détail de la commande" +msgstr "Order detail" + +#: contracts/templates/contracts/contracts_orders_detail.html:99 +msgid "Contrat lié" +msgstr "Linked contract" + +#: contracts/templates/contracts/contracts_orders_detail.html:105 +msgid "Date de commande" +msgstr "Order date" + +#: contracts/templates/contracts/contracts_orders_detail.html:111 +msgid "Montant prévisionnel" +msgstr "Estimated amount" + +#: contracts/templates/contracts/contracts_orders_detail.html:117 +msgid "Montant \"presté\"" +msgstr "Amount \"worked\"" + +#: contracts/templates/contracts/contracts_orders_detail.html:156 +msgid "Devis" +msgstr "Quote" + +#: contracts/templates/contracts/contracts_orders_detail.html:158 +msgid "Montant total" +msgstr "Total amount" + +#: contracts/templates/contracts/contracts_orders_detail.html:160 +msgid "Devis figé" +msgstr "Frozen quote" + +#: contracts/templates/contracts/contracts_orders_detail.html:167 +#, python-format +msgid "" +"\n" +" %(item_count)s poste dans le devis.\n" +" " +msgid_plural "" +"\n" +" %(item_count)s postes dans le devis.\n" +" " +msgstr[0] "" +"\n" +" %(item_count)s position in the quote.\n" +" " +msgstr[1] "" +"\n" +" %(item_count)s positions in the quote.\n" +" " + +#: contracts/templates/contracts/contracts_orders_detail.html:174 +msgid "Aucun devis n'est associé à cette commande pour le moment." +msgstr "No quote is associated with this order at the moment." + +#: contracts/templates/contracts/contracts_orders_detail.html:181 +msgid "Voir le devis" +msgstr "View quote" + +#: contracts/templates/contracts/contracts_orders_detail.html:183 +msgid "Gérer le devis" +msgstr "Manage quote" + +#: contracts/templates/contracts/contracts_orders_detail.html:189 +msgid "Exporter en PDF" +msgstr "Export to PDF" + +#: contracts/templates/contracts/contracts_orders_detail.html:198 +msgid "Commandé par" +msgstr "Ordered by" + +#: contracts/templates/contracts/contracts_orders_detail.html:205 +msgid "Télécharger le fichier" +msgstr "Download file" + +#: contracts/templates/contracts/contracts_orders_detail.html:219 +#: contracts/templates/contracts/contracts_orders_detail.html:329 +msgid "Documents" +msgstr "Documents" + +#: contracts/templates/contracts/contracts_orders_detail.html:221 +#: contracts/templates/contracts/contracts_orders_detail.html:301 +msgid "Ajouter des documents" +msgstr "Add documents" + +#: contracts/templates/contracts/contracts_orders_detail.html:270 +msgid "Aucun document associé à cette commande." +msgstr "No document associated with this order." + +#: contracts/templates/contracts/contracts_orders_detail.html:281 +msgid "Supprimer le document" +msgstr "Delete document" + +#: contracts/templates/contracts/contracts_orders_detail.html:285 +msgid "Voulez-vous supprimer ce document ?" +msgstr "Do you want to delete this document?" + +#: contracts/templates/contracts/contracts_orders_detail.html:289 +#: contracts/templates/contracts/contracts_orders_detail.html:719 +#: contracts/templates/contracts/contracts_orders_detail.html:742 +#: contracts/templates/contracts/teams_index.html:322 +#: contracts/templates/contracts/teams_index.html:363 +msgid "Annuler" +msgstr "Cancel" + +#: contracts/templates/contracts/contracts_orders_detail.html:374 +msgid "Précédent" +msgstr "Previous" + +#: contracts/templates/contracts/contracts_orders_detail.html:379 +msgid "Suivant" +msgstr "Next" + +#: contracts/templates/contracts/contracts_orders_detail.html:416 +msgid "Documents financiers" +msgstr "Financial documents" + +#: contracts/templates/contracts/contracts_orders_detail.html:421 +msgid "Générer un draft de l'état de travaux" +msgstr "Generate a work statement draft" + +#: contracts/templates/contracts/contracts_orders_detail.html:424 +msgid "Générer un draft de la déclaration de créance" +msgstr "Generate a debt declaration draft" + +#: contracts/templates/contracts/contracts_orders_detail.html:430 +msgid "Générer la déclaration de créance" +msgstr "Generate the debt declaration" + +#: contracts/templates/contracts/contracts_orders_detail.html:434 +msgid "" +"Cette opération va générer un état de travaux, une déclaration de créance et " +"faire passer les interventions validées au statut \"comptabilisé\"." +msgstr "" +"This operation will generate a work statement, a debt declaration and change " +"validated interventions to the \"accounted\" status." + +#: contracts/templates/contracts/contracts_orders_detail.html:444 +#, python-format +msgid "État d'avancement %(num)s" +msgstr "Progress report %(num)s" + +#: contracts/templates/contracts/contracts_orders_detail.html:448 +msgid "Autres documents" +msgstr "Other documents" + +#: contracts/templates/contracts/contracts_orders_detail.html:465 +msgid "Type" +msgstr "Type" + +#: contracts/templates/contracts/contracts_orders_detail.html:468 +msgid "justificatif(s)" +msgstr "supporting document(s)" + +#: contracts/templates/contracts/contracts_orders_detail.html:473 +msgid "Gérer les justificatifs" +msgstr "Manage supporting documents" + +#: contracts/templates/contracts/contracts_orders_detail.html:487 +msgid "Consommation des postes" +msgstr "Position consumption" + +#: contracts/templates/contracts/contracts_orders_detail.html:495 +msgid "Poste" +msgstr "Position" + +#: contracts/templates/contracts/contracts_orders_detail.html:499 +msgid "Quantité prévue" +msgstr "Planned quantity" + +#: contracts/templates/contracts/contracts_orders_detail.html:502 +msgid "État d avancement du" +msgstr "Progress status of" + +#: contracts/templates/contracts/contracts_orders_detail.html:503 +msgid "État" +msgstr "Status" + +#: contracts/templates/contracts/contracts_orders_detail.html:506 +msgid "Quantité consommée" +msgstr "Consumed quantity" + +#: contracts/templates/contracts/contracts_orders_detail.html:508 +msgid "Quantité restante" +msgstr "Remaining quantity" + +#: contracts/templates/contracts/contracts_orders_detail.html:511 +msgid "Montant consommé" +msgstr "Consumed amount" + +#: contracts/templates/contracts/contracts_orders_detail.html:550 +msgid "Aucune consommation enregistrée pour cette commande." +msgstr "No consumption recorded for this order." + +#: contracts/templates/contracts/contracts_orders_detail.html:557 +msgid "Projets associés" +msgstr "Associated projects" + +#: contracts/templates/contracts/contracts_orders_detail.html:567 +#: contracts/views_pdf_reports.py:713 +#, fuzzy +#| msgid "Commandes" +msgid "Commandes directes" +msgstr "Orders" + +#: contracts/templates/contracts/contracts_orders_detail.html:569 +#: contracts/templates/contracts/contracts_orders_detail.html:693 +#: contracts/templates/contracts/contracts_orders_detail.html:1110 +#, fuzzy +#| msgid "Ajouter un justificatif" +msgid "Ajouter un poste direct" +msgstr "Add a supporting document" + +#: contracts/templates/contracts/contracts_orders_detail.html:577 +msgid "" +"Les commandes directes permettent d'enregistrer des postes de marché " +"consommés sans passer par une intervention. Les postes validés sont inclus " +"dans les totaux, les états d'avancement et les déclarations de créance." +msgstr "" + +#: contracts/templates/contracts/contracts_orders_detail.html:583 +msgid "N°" +msgstr "N°" + +#: contracts/templates/contracts/contracts_orders_detail.html:587 +#, fuzzy +#| msgid "Prix unitaire" +msgid "Prix unit." +msgstr "Unit price" + +#: contracts/templates/contracts/contracts_orders_detail.html:589 +#, fuzzy +#| msgid "Facturé" +msgid "Qté facturée" +msgstr "Invoiced" + +#: contracts/templates/contracts/contracts_orders_detail.html:593 +msgid "Actions" +msgstr "Actions" + +#: contracts/templates/contracts/contracts_orders_detail.html:625 +#: contracts/templates/contracts/contracts_orders_detail.html:928 +msgid "En attente" +msgstr "" + +#: contracts/templates/contracts/contracts_orders_detail.html:635 +#: contracts/templates/contracts/contracts_orders_detail.html:944 +msgid "Modifier" +msgstr "Edit" + +#: contracts/templates/contracts/contracts_orders_detail.html:652 +#: contracts/templates/contracts/contracts_orders_detail.html:956 +msgid "Valider" +msgstr "Validate" + +#: contracts/templates/contracts/contracts_orders_detail.html:660 +#: contracts/templates/contracts/contracts_orders_detail.html:961 +msgid "Refuser" +msgstr "" + +#: contracts/templates/contracts/contracts_orders_detail.html:668 +#: contracts/templates/contracts/contracts_orders_detail.html:966 +msgid "Remettre en attente" +msgstr "" + +#: contracts/templates/contracts/contracts_orders_detail.html:678 +#: contracts/templates/contracts/contracts_orders_detail.html:1213 +#, fuzzy +#| msgid "Aucune commande trouvée" +msgid "Aucune commande directe enregistrée." +msgstr "No order found" + +#: contracts/templates/contracts/contracts_orders_detail.html:700 +msgid "Rechercher par code, numéro ou description…" +msgstr "" + +#: contracts/templates/contracts/contracts_orders_detail.html:721 +#: contracts/templates/contracts/teams_index.html:365 +msgid "Enregistrer" +msgstr "Save" + +#: contracts/templates/contracts/contracts_orders_detail.html:735 +#, fuzzy +#| msgid "Supprimer le document" +msgid "Supprimer le poste direct" +msgstr "Delete document" + +#: contracts/templates/contracts/contracts_orders_detail.html:739 +#, fuzzy +#| msgid "Voulez-vous supprimer ce document ?" +msgid "Voulez-vous supprimer ce poste direct ?" +msgstr "Do you want to delete this document?" + +#: contracts/templates/contracts/contracts_orders_detail.html:755 +msgid "Ajouter une intervention" +msgstr "Add an intervention" + +#: contracts/templates/contracts/contracts_orders_detail.html:1107 +#, fuzzy +#| msgid "Modifier la commande" +msgid "Modifier le poste direct" +msgstr "Edit order" + +#: contracts/templates/contracts/contracts_orders_detail.html:1143 +msgid "Veuillez sélectionner un poste de marché." +msgstr "" + +#: contracts/templates/contracts/contracts_orders_detail.html:1173 +#: contracts/templates/contracts/contracts_orders_detail.html:1178 +#: contracts/templates/contracts/teams_index.html:436 +#: contracts/templates/contracts/teams_index.html:440 +msgid "Une erreur est survenue." +msgstr "An error occurred." + +#: contracts/templates/contracts/teams_index.html:130 +#, fuzzy +#| msgid "Consommation des postes" +msgid "Gestion des équipes" +msgstr "Position consumption" + +#: contracts/templates/contracts/teams_index.html:137 +msgid "Aucune équipe accessible." +msgstr "" + +#: contracts/templates/contracts/teams_index.html:150 +#: contracts/templates/contracts/teams_index.html:157 +#, fuzzy +#| msgid "Non renseigné" +msgid "Non assignés" +msgstr "Not specified" + +#: contracts/templates/contracts/teams_index.html:152 +#, fuzzy +#| msgid "Aucun projet" +msgid "Aucune équipe" +msgstr "No project" + +#: contracts/templates/contracts/teams_index.html:156 +#: contracts/templates/contracts/teams_index.html:578 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Ajouter un membre non assigné" +msgstr "Add an intervention" + +#: contracts/templates/contracts/teams_index.html:180 +#: contracts/templates/contracts/teams_index.html:353 +msgid "Assigner à une équipe" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:185 +#: contracts/templates/contracts/teams_index.html:258 +#, fuzzy +#| msgid "Supprimer le document" +msgid "Supprimer ce membre" +msgstr "Delete document" + +#: contracts/templates/contracts/teams_index.html:195 +#: contracts/templates/contracts/teams_index.html:268 +msgid "Aucun membre" +msgstr "No member" + +#: contracts/templates/contracts/teams_index.html:202 +#: contracts/templates/contracts/teams_index.html:276 +msgid "membre(s)" +msgstr "member(s)" + +#: contracts/templates/contracts/teams_index.html:219 +#, fuzzy +#| msgid "Nombre d'interventions par personne" +msgid "Voir les interventions assignées" +msgstr "Number of interventions per person" + +#: contracts/templates/contracts/teams_index.html:227 +#: contracts/templates/contracts/teams_index.html:298 +#: contracts/templates/contracts/teams_index.html:577 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Ajouter un membre" +msgstr "Add an intervention" + +#: contracts/templates/contracts/teams_index.html:253 +msgid "Déplacer vers une autre équipe" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:305 +#: contracts/templates/contracts/teams_index.html:345 +msgid "Nom affiché" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:306 +msgid "Ex : Jean Dupont" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:309 +msgid "Compte utilisateur" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:311 +msgid "Sélectionner un utilisateur" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:337 +#, fuzzy +#| msgid "Modifier la commande" +msgid "Modifier le membre" +msgstr "Edit order" + +#: contracts/templates/contracts/teams_index.html:351 +msgid "Ne pas modifier l'équipe" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:352 +msgid "Désassigner (aucune équipe)" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:380 +msgid "Interventions" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:387 +#: contracts/templates/contracts/teams_index.html:687 +#: contracts/templates/contracts/teams_index.html:763 +msgid "Chargement..." +msgstr "" + +#: contracts/templates/contracts/teams_index.html:402 +#: contracts/templates/contracts/teams_index.html:403 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Résumé intervention" +msgstr "Add an intervention" + +#: contracts/templates/contracts/teams_index.html:410 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Retour aux interventions" +msgstr "Add an intervention" + +#: contracts/templates/contracts/teams_index.html:412 +msgid "Fermer" +msgstr "Close" + +#: contracts/templates/contracts/teams_index.html:551 +#: contracts/templates/contracts/teams_index.html:633 +#, fuzzy +#| msgid "Le contrat est requis" +msgid "Le nom du membre est requis." +msgstr "Contract is required" + +#: contracts/templates/contracts/teams_index.html:634 +#, fuzzy +#| msgid "Le contrat est requis" +msgid "L\\'utilisateur est requis." +msgstr "Contract is required" + +#: contracts/templates/contracts/teams_index.html:651 +#, fuzzy +#| msgid "Supprimer le document" +msgid "Supprimer le membre" +msgstr "Delete document" + +#: contracts/templates/contracts/teams_index.html:651 +msgid "Cette action est irréversible." +msgstr "This action is irreversible." + +#: contracts/templates/contracts/teams_index.html:700 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de charger les interventions." +msgstr "Cannot freeze an empty quote." + +#: contracts/templates/contracts/teams_index.html:706 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "intervention(s)" +msgstr "Add an intervention" + +#: contracts/templates/contracts/teams_index.html:709 +#, fuzzy +#| msgid "Aucun document associé à cette commande." +msgid "Aucune intervention assignée à cette équipe." +msgstr "No document associated with this order." + +#: contracts/templates/contracts/teams_index.html:721 +#, fuzzy +#| msgid "Voir le document" +msgid "Voir le résumé" +msgstr "View document" + +#: contracts/templates/contracts/teams_index.html:742 +msgid "Membre assigné" +msgstr "" + +#: contracts/templates/contracts/teams_index.html:779 +msgid "Résumé non disponible." +msgstr "" + +#: contracts/views.py:269 contracts/views.py:873 contracts/views.py:917 +#: contracts/views.py:942 +msgid "Vous n'avez pas accès à cette commande." +msgstr "You do not have access to this order." + +#: contracts/views.py:275 +msgid "" +"Vous n'avez pas de thématique d'intervention autorisée pour cette commande." +msgstr "You do not have any authorized intervention theme for this order." + +#: contracts/views.py:347 +msgid "Vous n'avez pas le droit d'ajouter une commande pour ce contrat." +msgstr "You do not have permission to add an order for this contract." + +#: contracts/views.py:388 +msgid "" +"Impossible de changer le contrat: des interventions sont déjà liées à cette " +"commande." +msgstr "" +"Cannot change the contract: interventions are already linked to this order." + +#: contracts/views.py:397 +msgid "Vous n'avez pas le droit de modifier une commande pour ce contrat." +msgstr "You do not have permission to modify an order for this contract." + +#: contracts/views.py:416 +msgid "Vous n'avez pas le droit de modifier le devis de cette commande." +msgstr "You do not have permission to modify this order's quote." + +#: contracts/views.py:469 contracts/views.py:474 contracts/views.py:480 +msgid "Les données reçues sont invalides." +msgstr "The received data is invalid." + +#: contracts/views.py:523 +msgid "Le formulaire contient des erreurs." +msgstr "The form contains errors." + +#: contracts/views.py:552 +msgid "Impossible de figer un devis vide." +msgstr "Cannot freeze an empty quote." + +#: contracts/views.py:555 +msgid "Le devis a été figé." +msgstr "The quote has been frozen." + +#: contracts/views.py:558 +msgid "Le devis a été enregistré." +msgstr "The quote has been saved." + +#: contracts/views.py:702 +msgid "Accès refusé au document de la commande." +msgstr "Access denied to order document." + +#: contracts/views.py:707 +msgid "Fichier introuvable" +msgstr "File not found" + +#: contracts/views.py:732 +msgid "Le contrat est requis" +msgstr "Contract is required" + +#: contracts/views.py:735 +msgid "La référence de commande est requise" +msgstr "Order reference is required" + +#: contracts/views.py:738 +msgid "La date de commande est requise" +msgstr "Order date is required" + +#: contracts/views.py:741 +msgid "La date de livraison est requise" +msgstr "Delivery date is required" + +#: contracts/views.py:745 +msgid "Vous n'avez pas le droit d'ajouter une commande pour ce contrat" +msgstr "You do not have permission to add an order for this contract" + +#: contracts/views.py:752 +msgid "Une commande avec cette référence existe déjà pour ce contrat" +msgstr "An order with this reference already exists for this contract" + +#: contracts/views.py:775 +msgid "Données JSON invalides" +msgstr "Invalid JSON data" + +#: contracts/views.py:834 +msgid "Vous n'avez pas accès à ce contrat" +msgstr "You do not have access to this contract" + +#: contracts/views.py:882 +msgid "Le justificatif a été ajouté avec succès." +msgstr "The supporting document has been added successfully." + +#: contracts/views.py:927 +#, python-format +msgid "Le justificatif « %(title)s » a été supprimé." +msgstr "The supporting document « %(title)s » has been deleted." + +#: contracts/views.py:947 +msgid "" +"Seuls les managers et administrateurs peuvent changer le statut d'une " +"commande." +msgstr "Only managers and administrators can change an order status." + +#: contracts/views.py:956 contracts/views.py:1226 +msgid "Statut invalide." +msgstr "Invalid status." + +#: contracts/views.py:964 +msgid "Le statut de la commande a été mis à jour avec succès." +msgstr "The order status has been updated successfully." + +#: contracts/views.py:979 +msgid "Vous n'avez pas la permission d'ajouter des documents à cette commande." +msgstr "You do not have permission to add documents to this order." + +#: contracts/views.py:991 +msgid "Document(s) ajouté(s) avec succès." +msgstr "Document(s) added successfully." + +#: contracts/views.py:993 +msgid "Erreur lors de l'ajout des documents." +msgstr "Error while adding documents." + +#: contracts/views.py:1005 contracts/views.py:1043 contracts/views.py:1074 +#: contracts/views.py:1142 contracts/views.py:1192 contracts/views.py:1212 +msgid "Accès refusé." +msgstr "Access denied." + +#: contracts/views.py:1008 +msgid "Vous n'avez pas la permission de supprimer ce document." +msgstr "You do not have permission to delete this document." + +#: contracts/views.py:1076 contracts/views.py:1144 contracts/views.py:1194 +#: contracts/views.py:1214 +msgid "Permission insuffisante." +msgstr "" + +#: contracts/views.py:1081 contracts/views.py:1153 contracts/views.py:1221 +#, fuzzy +#| msgid "Données JSON invalides" +msgid "Données JSON invalides." +msgstr "Invalid JSON data" + +#: contracts/views.py:1088 +#, fuzzy +#| msgid "La date de commande est requise" +msgid "Le poste de marché est requis." +msgstr "Order date is required" + +#: contracts/views.py:1095 +msgid "La quantité doit être un nombre positif." +msgstr "" + +#: contracts/views.py:1102 +msgid "" +"Un poste direct existe déjà pour ce poste dans cette commande. Modifiez-le " +"plutôt." +msgstr "" + +#: contracts/views.py:1148 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de modifier un poste déjà validé." +msgstr "Cannot freeze an empty quote." + +#: contracts/views.py:1163 +msgid "La quantité doit être un nombre positif ou nul." +msgstr "" + +#: contracts/views.py:1198 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de supprimer un poste déjà validé." +msgstr "Cannot freeze an empty quote." + +#: contracts/views.py:1200 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de supprimer un poste déjà facturé." +msgstr "Cannot freeze an empty quote." + +#: contracts/views_daily_report.py:227 +msgid "Rapport journalier des interventions" +msgstr "Daily intervention report" + +#: contracts/views_daily_report.py:248 contracts/views_daily_report.py:393 +msgid "Synthèse par thématique" +msgstr "Summary by theme" + +#: contracts/views_daily_report.py:251 contracts/views_daily_report.py:403 +msgid "Thématique" +msgstr "Theme" + +#: contracts/views_daily_report.py:302 contracts/views_daily_report.py:344 +#: contracts/views_daily_report.py:433 contracts/views_daily_report.py:481 +msgid "TOTAL" +msgstr "TOTAL" + +#: contracts/views_daily_report.py:314 +msgid "Nombre d'interventions par personne" +msgstr "Number of interventions per person" + +#: contracts/views_daily_report.py:317 contracts/views_daily_report.py:459 +msgid "Personne" +msgstr "Person" + +#: contracts/views_daily_report.py:317 contracts/views_daily_report.py:459 +msgid "Nombre" +msgstr "Number" + +#: contracts/views_daily_report.py:397 contracts/views_daily_report.py:454 +msgid "Rapport journalier" +msgstr "Daily report" + +#: contracts/views_daily_report.py:451 +msgid "Par personne" +msgstr "By person" + +#: contracts/views_daily_report.py:518 +msgid "Aucune de vos équipes n'est autorisée à générer ce rapport." +msgstr "None of your teams is authorized to generate this report." + +#: contracts/views_pdf_reports.py:301 contracts/views_pdf_reports.py:481 +msgid "Service public régional de Bruxelles" +msgstr "Brussels Regional Public Service" + +#: contracts/views_pdf_reports.py:302 contracts/views_pdf_reports.py:482 +msgid "Cahier des charges " +msgstr "Specifications " + +#: contracts/views_pdf_reports.py:307 contracts/views_pdf_reports.py:487 +msgid "Bruxelles: " +msgstr "Brussels: " + +#: contracts/views_pdf_reports.py:310 +msgid "DECLARATION DE CREANCE" +msgstr "DEBT DECLARATION" + +#: contracts/views_pdf_reports.py:314 +msgid "Concerne:" +msgstr "Concerning:" + +#: contracts/views_pdf_reports.py:325 +#, python-format +msgid "" +"Je soussigné %(signatory)s, %(role)s, de la firme %(company)s, déclare que " +"le Service public régional de Bruxelles – Place Saint-Lazare 2 à 1035 " +"Bruxelles, est redevable de la somme de :" +msgstr "" +"I, the undersigned %(signatory)s, %(role)s, of the company %(company)s, " +"declare that the Brussels Regional Public Service – Place Saint-Lazare 2, " +"1035 Brussels, owes the sum of:" + +#: contracts/views_pdf_reports.py:345 contracts/views_pdf_reports.py:788 +#: contracts/views_pdf_reports.py:1295 +msgid "Total général TVA non comprise :" +msgstr "Total excl. VAT:" + +#: contracts/views_pdf_reports.py:350 contracts/views_pdf_reports.py:793 +#: contracts/views_pdf_reports.py:1307 +msgid "TVA 21 % :" +msgstr "VAT 21%:" + +#: contracts/views_pdf_reports.py:370 +msgid "euro" +msgid_plural "euros" +msgstr[0] "euro" +msgstr[1] "euros" + +#: contracts/views_pdf_reports.py:374 +msgid "centime" +msgid_plural "centimes" +msgstr[0] "cent" +msgstr[1] "cents" + +#: contracts/views_pdf_reports.py:375 +msgid "et" +msgstr "and" + +#: contracts/views_pdf_reports.py:380 +#, python-format +msgid "Montant total, TVA incluse : %(amount)s" +msgstr "Total amount, VAT included: %(amount)s" + +#: contracts/views_pdf_reports.py:399 +#, python-format +msgid "Declaration_Creance_%(slug_contract_number)s_%(slug_order_code)s.pdf" +msgstr "Debt_Declaration_%(slug_contract_number)s_%(slug_order_code)s.pdf" + +#: contracts/views_pdf_reports.py:427 contracts/views_pdf_reports.py:827 +msgid "Declaration_Creance" +msgstr "Debt_Declaration" + +#: contracts/views_pdf_reports.py:490 +msgid "ETAT DE TRAVAUX" +msgstr "WORK STATEMENT" + +#: contracts/views_pdf_reports.py:558 +#, python-format +msgid "Intervention %(code)s du %(date)s" +msgstr "Intervention %(code)s of %(date)s" + +#: contracts/views_pdf_reports.py:569 +msgid "périodes" +msgstr "periods" + +#: contracts/views_pdf_reports.py:579 contracts/views_pdf_reports.py:717 +msgid "Code poste" +msgstr "Position code" + +#: contracts/views_pdf_reports.py:581 contracts/views_pdf_reports.py:719 +#: contracts/views_pdf_reports.py:948 +msgid "PU" +msgstr "UP" + +#: contracts/views_pdf_reports.py:582 contracts/views_pdf_reports.py:720 +msgid "Qté" +msgstr "Qty" + +#: contracts/views_pdf_reports.py:583 contracts/views_pdf_reports.py:721 +#: contracts/views_pdf_reports.py:950 +msgid "Total" +msgstr "Total" + +#: contracts/views_pdf_reports.py:681 +msgid "Aucun poste validé pour cette intervention" +msgstr "No position validated for this intervention" + +#: contracts/views_pdf_reports.py:687 contracts/views_pdf_reports.py:743 +msgid "Total HTVA :" +msgstr "Total excl. VAT:" + +#: contracts/views_pdf_reports.py:798 contracts/views_pdf_reports.py:1318 +msgid "Total général TVAC :" +msgstr "Grand total incl. VAT:" + +#: contracts/views_pdf_reports.py:853 +msgid "Etat_de_travaux" +msgstr "Work_statement" + +#: contracts/views_pdf_reports.py:856 +msgid "État de travaux" +msgstr "Work statement" + +#: contracts/views_pdf_reports.py:914 +#, python-format +msgid "Devis de la commande %(order)s" +msgstr "Quote for order %(order)s" + +#: contracts/views_pdf_reports.py:917 +#, python-format +msgid "Contrat %(contract)s – Commande %(order)s" +msgstr "Contract %(contract)s – Order %(order)s" + +#: contracts/views_pdf_reports.py:927 +msgid "Postes du devis" +msgstr "Quote positions" + +#: contracts/views_pdf_reports.py:944 contracts/views_pdf_reports.py:1137 +msgid "N° poste" +msgstr "Position no." + +#: contracts/views_pdf_reports.py:1033 +#, python-format +msgid "Total HTVA : %(amount)s" +msgstr "Total excl. VAT: %(amount)s" + +#: contracts/views_pdf_reports.py:1037 +#, python-format +msgid "TVA 21%% : %(amount)s" +msgstr "VAT 21%%: %(amount)s" + +#: contracts/views_pdf_reports.py:1041 +#, python-format +msgid "Total TVAC : %(amount)s" +msgstr "Total incl. VAT: %(amount)s" + +#: contracts/views_pdf_reports.py:1059 +msgid "Aucun devis n'est disponible pour cette commande." +msgstr "No quote is available for this order." + +#: contracts/views_pdf_reports.py:1065 +#, python-format +msgid "Devis_%(contract)s_%(order)s.pdf" +msgstr "Quote_%(contract)s_%(order)s.pdf" + +#: contracts/views_pdf_reports.py:1084 +msgid "Postes" +msgstr "Positions" + +#: contracts/views_pdf_reports.py:1103 +msgid "Marché :" +msgstr "Contract:" + +#: contracts/views_pdf_reports.py:1109 contracts/views_pdf_reports.py:1121 +msgid "Description :" +msgstr "Description:" + +#: contracts/views_pdf_reports.py:1115 +msgid "Commande :" +msgstr "Order:" + +#: contracts/views_pdf_reports.py:1128 +msgid "Date :" +msgstr "Date:" + +#: contracts/views_pdf_reports.py:1144 +msgid "SAJ" +msgstr "SAJ" + +#: contracts/views_pdf_reports.py:1239 +msgid "Total général des fournitures et travaux revisables hors TVA :" +msgstr "Total excl. VAT for revisable supplies and works:" + +#: contracts/views_pdf_reports.py:1250 +msgid "Coefficient de révision :" +msgstr "Revision coefficient:" + +#: contracts/views_pdf_reports.py:1265 +msgid "" +"Total général des fournitures et travaux non revisables hors TVA (SAJ) :" +msgstr "Total excl. VAT for non-revisable supplies and works (SAJ):" + +#: contracts/views_pdf_reports.py:1278 +#, python-format +msgid "" +"Total général des %(type)s non revisables hors TVA (SAJ) avec coefficient " +"%(coef)s :" +msgstr "" +"Total excl. VAT for non-revisable %(type)s (SAJ) with coefficient %(coef)s:" + +#: contracts/views_pdf_reports.py:1345 +msgid "Detail_postes" +msgstr "Position_details" + +#: contracts/views_pdf_reports.py:1349 +msgid "Détail des postes" +msgstr "Position details" + +#: contracts/views_pdf_reports.py:1372 +msgid "" +"Vous n'avez pas la permission de générer des documents pour cette commande." +msgstr "You do not have permission to generate documents for this order." + +#: contracts/views_pdf_reports.py:1484 +msgid "Déclaration de créance créée et sauvegardée." +msgstr "Debt declaration created and saved." + +#~| msgid "Nouvelle commande" +#~ msgid "Nouvelle équipe" +#~ msgstr "New team" + +#, fuzzy +#~| msgid "Supprimer le document" +#~ msgid "Supprimer l'équipe" +#~ msgstr "Delete document" + +#~ msgid "Créer" +#~ msgstr "Create" + +#, fuzzy +#~| msgid "Le contrat est requis" +#~ msgid "La société est requise." +#~ msgstr "Contract is required" + +#~ msgid "Types de maintenance autorisés" +#~ msgstr "Allowed maintenance types" + +#~ msgid "" +#~ "Types de maintenance que cette équipe peut voir. Si vide, tous les types " +#~ "sont autorisés." +#~ msgstr "" +#~ "Maintenance types that this team can see. If empty, all types are allowed." + +#~ msgid "Total général HTVA :" +#~ msgstr "Grand total excl. VAT:" diff --git a/streetup/contracts/locale/en/LC_MESSAGES/djangojs.mo b/streetup/contracts/locale/en/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..484f21d Binary files /dev/null and b/streetup/contracts/locale/en/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/contracts/locale/en/LC_MESSAGES/djangojs.po b/streetup/contracts/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..1ad6f93 --- /dev/null +++ b/streetup/contracts/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: contracts/static/contracts/timeline.js:115 +msgid "Êtes-vous sûr de vouloir annuler cette commande ?" +msgstr "Are you sure you want to cancel this order?" + +#: contracts/static/contracts/timeline.js:136 +msgid "Erreur lors de la mise à jour du statut" +msgstr "Error updating status" diff --git a/streetup/contracts/locale/fr/LC_MESSAGES/django.mo b/streetup/contracts/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..394235f Binary files /dev/null and b/streetup/contracts/locale/fr/LC_MESSAGES/django.mo differ diff --git a/streetup/contracts/locale/fr/LC_MESSAGES/django.po b/streetup/contracts/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..f5c8c0e --- /dev/null +++ b/streetup/contracts/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,1949 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: streetup/contracts/forms.py:30 +msgid "" +"Ce contrat ne peut plus être modifié car des interventions sont déjà liées." +msgstr "" +"Ce contrat ne peut plus être modifié car des interventions sont déjà liées." + +#: streetup/contracts/forms.py:67 +msgid "" +"Le contrat ne peut plus être modifié car des interventions sont déjà liées à " +"cette commande." +msgstr "" +"Le contrat ne peut plus être modifié car des interventions sont déjà liées à " +"cette commande." + +#: streetup/contracts/forms.py:69 +msgid "Modification du contrat interdite." +msgstr "Modification du contrat interdite." + +#: streetup/contracts/forms.py:79 +msgid "Date de la commande" +msgstr "Date de la commande" + +#: streetup/contracts/forms.py:87 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:106 +msgid "Date de livraison" +msgstr "Date de livraison" + +#: streetup/contracts/forms.py:92 +msgid "Statut de la commande" +msgstr "Statut de la commande" + +#: streetup/contracts/forms.py:97 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:202 +msgid "Fichier de commande" +msgstr "Fichier de commande" + +#: streetup/contracts/forms.py:101 +msgid "Montant total (€)" +msgstr "Montant total (€)" + +#: streetup/contracts/forms.py:124 +#: streetup/contracts/templates/contracts/contracts_detail.html:22 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:37 +msgid "Contrat" +msgstr "Contrat" + +#: streetup/contracts/forms.py:125 +msgid "Référence de commande" +msgstr "Référence de commande" + +#: streetup/contracts/forms.py:126 +#: streetup/contracts/templates/contracts/contracts_detail.html:25 +#: streetup/contracts/templates/contracts/contracts_index.html:198 +#: streetup/contracts/templates/contracts/contracts_index.html:241 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:71 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:102 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:497 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:585 +#: streetup/contracts/views_pdf_reports.py:580 +#: streetup/contracts/views_pdf_reports.py:718 +#: streetup/contracts/views_pdf_reports.py:946 +#: streetup/contracts/views_pdf_reports.py:1139 +msgid "Description" +msgstr "Description" + +#: streetup/contracts/forms.py:136 streetup/contracts/models.py:950 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:74 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:588 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:709 +#: streetup/contracts/views_pdf_reports.py:949 +#: streetup/contracts/views_pdf_reports.py:1142 +msgid "Quantité" +msgstr "Quantité" + +#: streetup/contracts/forms.py:166 streetup/contracts/forms.py:168 +msgid "Poste invalide." +msgstr "Poste invalide." + +#: streetup/contracts/forms.py:193 +msgid "Ex: Facture fournisseur, Bon de livraison..." +msgstr "Ex: Facture fournisseur, Bon de livraison..." + +#: streetup/contracts/forms.py:200 +msgid "Titre du justificatif" +msgstr "Titre du justificatif" + +#: streetup/contracts/forms.py:201 streetup/contracts/models.py:862 +#: streetup/contracts/models.py:1034 +#: streetup/contracts/templates/contracts/contracts_index.html:201 +msgid "Fichier" +msgstr "Fichier" + +#: streetup/contracts/forms.py:225 +msgid "Fichiers" +msgstr "Fichiers" + +#: streetup/contracts/models.py:39 +msgid "Maintenance corrective" +msgstr "Maintenance corrective" + +#: streetup/contracts/models.py:40 +msgid "Maintenance préventive" +msgstr "Maintenance préventive" + +#: streetup/contracts/models.py:41 +msgid "Maintenance améliorative" +msgstr "Maintenance améliorative" + +#: streetup/contracts/models.py:47 +msgid "Code du type de maintenance (corrective, preventive, ameliorative)" +msgstr "Code du type de maintenance (corrective, preventive, ameliorative)" + +#: streetup/contracts/models.py:48 +msgid "Nom (FR)" +msgstr "Nom (FR)" + +#: streetup/contracts/models.py:49 +msgid "Nom (NL)" +msgstr "Nom (NL)" + +#: streetup/contracts/models.py:64 +msgid "Type de maintenance" +msgstr "Type de maintenance" + +#: streetup/contracts/models.py:65 +msgid "Types de maintenance" +msgstr "Types de maintenance" + +#: streetup/contracts/models.py:78 +msgid "Catégories d'assets gérées" +msgstr "Catégories d'assets gérées" + +#: streetup/contracts/models.py:79 +msgid "Catégories d'assets dont cette équipe est responsable." +msgstr "Catégories d'assets dont cette équipe est responsable." + +#: streetup/contracts/models.py:83 +msgid "Gestionnaire d'assets" +msgstr "Gestionnaire d'assets" + +#: streetup/contracts/models.py:84 +msgid "" +"Indique si cette équipe est gestionnaire des catégories d'assets associées." +msgstr "" +"Indique si cette équipe est gestionnaire des catégories d'assets associées." + +#: streetup/contracts/models.py:88 +#: streetup/contracts/templates/contracts/contracts_detail.html:26 +#: streetup/contracts/templates/contracts/contracts_index.html:242 +msgid "Prestataire" +msgstr "Prestataire" + +#: streetup/contracts/models.py:89 +msgid "Indique si cette équipe est un prestataire extérieur." +msgstr "Indique si cette équipe est un prestataire extérieur." + +#: streetup/contracts/models.py:93 +msgid "Peut générer le rapport journalier" +msgstr "Peut générer le rapport journalier" + +#: streetup/contracts/models.py:94 +msgid "" +"Indique si les membres de cette équipe peuvent générer un rapport quotidien " +"des interventions." +msgstr "" +"Indique si les membres de cette équipe peuvent générer un rapport quotidien " +"des interventions." + +#: streetup/contracts/models.py:98 +msgid "Photos obligatoires" +msgstr "Photos obligatoires" + +#: streetup/contracts/models.py:99 +msgid "" +"Si activé, au moins une photo doit être ajoutée à l'intervention avant de " +"pouvoir la terminer." +msgstr "" +"Si activé, au moins une photo doit être ajoutée à l'intervention avant de " +"pouvoir la terminer." + +#: streetup/contracts/models.py:103 +msgid "Restreindre la visibilité aux interventions du jour" +msgstr "" + +#: streetup/contracts/models.py:104 +msgid "" +"Si activé, les interventions ne sont visibles qu'à partir d'une certaine " +"heure, et uniquement pour le jour même." +msgstr "" + +#: streetup/contracts/models.py:109 +msgid "Heure de début de visibilité" +msgstr "" + +#: streetup/contracts/models.py:110 +msgid "" +"Heure locale à partir de laquelle les interventions du jour deviennent " +"visibles (ex: 06:00:00)." +msgstr "" + +#: streetup/contracts/models.py:114 +#, fuzzy +#| msgid "Voir les interventions assignées" +msgid "Restreindre aux interventions assignées" +msgstr "Voir les interventions assignées" + +#: streetup/contracts/models.py:115 +msgid "" +"Si activé, les membres de cette équipe ne voient que les interventions " +"assignées à l'équipe ou directement à un membre." +msgstr "" + +#: streetup/contracts/models.py:122 +msgid "Équipe de société/organisme" +msgstr "Équipe de société/organisme" + +#: streetup/contracts/models.py:123 +msgid "Équipes de sociétés/organismes" +msgstr "Équipes de sociétés/organismes" + +#: streetup/contracts/models.py:135 +#, fuzzy +#| msgid "Équipe" +msgid "Équipes" +msgstr "Équipe" + +#: streetup/contracts/models.py:136 +msgid "Équipes auxquelles ce membre appartient." +msgstr "" + +#: streetup/contracts/models.py:142 +msgid "Types de maintenance visibles" +msgstr "Types de maintenance visibles" + +#: streetup/contracts/models.py:143 +msgid "" +"Types de maintenance visibles par défaut pour cet utilisateur. Si vide, tous " +"les types sont visibles. Les interventions assignées à l'utilisateur restent " +"toujours visibles." +msgstr "" +"Types de maintenance visibles par défaut pour cet utilisateur. Si vide, tous " +"les types sont visibles. Les interventions assignées à l'utilisateur restent " +"toujours visibles." + +#: streetup/contracts/models.py:178 +msgid "Membre" +msgstr "Membre" + +#: streetup/contracts/models.py:186 streetup/contracts/models.py:222 +#: streetup/contracts/templates/contracts/teams_index.html:349 +msgid "Équipe" +msgstr "Équipe" + +#: streetup/contracts/models.py:188 +msgid "Date d'entrée" +msgstr "Date d'entrée" + +#: streetup/contracts/models.py:189 +msgid "Date de sortie" +msgstr "Date de sortie" + +#: streetup/contracts/models.py:196 +msgid "Modifié par" +msgstr "Modifié par" + +#: streetup/contracts/models.py:205 +msgid "Historique d'équipe d'un membre" +msgstr "Historique d'équipe d'un membre" + +#: streetup/contracts/models.py:206 +msgid "Historiques d'équipe des membres" +msgstr "Historiques d'équipe des membres" + +#: streetup/contracts/models.py:216 +msgid "Configuration utilisateur" +msgstr "Configuration utilisateur" + +#: streetup/contracts/models.py:226 +msgid "Peut consulter" +msgstr "Peut consulter" + +#: streetup/contracts/models.py:227 +msgid "L'utilisateur peut voir cette équipe et ses membres." +msgstr "L'utilisateur peut voir cette équipe et ses membres." + +#: streetup/contracts/models.py:231 +msgid "Peut éditer" +msgstr "Peut éditer" + +#: streetup/contracts/models.py:232 +msgid "" +"L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer " +"des membres entre équipes." +msgstr "" +"L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer " +"des membres entre équipes." + +#: streetup/contracts/models.py:240 +msgid "Accès utilisateur à une équipe" +msgstr "Accès utilisateur à une équipe" + +#: streetup/contracts/models.py:241 +msgid "Accès utilisateurs aux équipes" +msgstr "Accès utilisateurs aux équipes" + +#: streetup/contracts/models.py:259 +msgid "Emails en copie (CC)" +msgstr "Emails en copie (CC)" + +#: streetup/contracts/models.py:260 +msgid "" +"Adresses email à ajouter en copie des notifications (séparées par des " +"virgules ou points-virgules)" +msgstr "" +"Adresses email à ajouter en copie des notifications (séparées par des " +"virgules ou points-virgules)" + +#: streetup/contracts/models.py:264 +msgid "Envoyer des notifications liées aux interventions par email." +msgstr "Envoyer des notifications liées aux interventions par email." + +#: streetup/contracts/models.py:270 +msgid "Nom du signataire (prestataire)" +msgstr "Nom du signataire (prestataire)" + +#: streetup/contracts/models.py:271 +msgid "Nom de la personne qui signe les documents côté prestataire." +msgstr "Nom de la personne qui signe les documents côté prestataire." + +#: streetup/contracts/models.py:277 +msgid "Rôle du signataire (prestataire)" +msgstr "Rôle du signataire (prestataire)" + +#: streetup/contracts/models.py:278 +msgid "Rôle/fonction de la personne qui signe les documents côté prestataire." +msgstr "Rôle/fonction de la personne qui signe les documents côté prestataire." + +#: streetup/contracts/models.py:284 +msgid "Téléphone de contact" +msgstr "Téléphone de contact" + +#: streetup/contracts/models.py:285 +msgid "" +"Numéro de téléphone du technicien de garde ou contact principal pour les " +"urgences." +msgstr "" +"Numéro de téléphone du technicien de garde ou contact principal pour les " +"urgences." + +#: streetup/contracts/models.py:291 +msgid "Thématiques" +msgstr "Thématiques" + +#: streetup/contracts/models.py:292 +msgid "Thématiques associées à ce contrat." +msgstr "Thématiques associées à ce contrat." + +#: streetup/contracts/models.py:296 +msgid "Actif" +msgstr "Actif" + +#: streetup/contracts/models.py:297 +msgid "Indique si le contrat est actif." +msgstr "Indique si le contrat est actif." + +#: streetup/contracts/models.py:301 +msgid "Nécessite une approbation" +msgstr "Nécessite une approbation" + +#: streetup/contracts/models.py:302 +msgid "" +"Indique si les interventions liées à ce contrat nécessitent une approbation " +"avant d'être transmises pour traitement." +msgstr "" +"Indique si les interventions liées à ce contrat nécessitent une approbation " +"avant d'être transmises pour traitement." + +#: streetup/contracts/models.py:306 +msgid "Nécessite une vérification" +msgstr "Nécessite une vérification" + +#: streetup/contracts/models.py:307 +msgid "" +"Indique si les interventions liées à ce contrat nécessitent une vérification " +"avant d'être transmises pour traitement." +msgstr "" +"Indique si les interventions liées à ce contrat nécessitent une vérification " +"avant d'être transmises pour traitement." + +#: streetup/contracts/models.py:311 +msgid "Utilise les occupations" +msgstr "Utilise les occupations" + +#: streetup/contracts/models.py:312 +msgid "" +"Indique si les interventions liées à ce contrat utilisent la gestion des " +"occupations (temps passé par membre)." +msgstr "" +"Indique si les interventions liées à ce contrat utilisent la gestion des " +"occupations (temps passé par membre)." + +#: streetup/contracts/models.py:316 +#, fuzzy +#| msgid "Impossible de charger les interventions." +msgid "Utilise les gestionnaires d'intervention" +msgstr "Impossible de charger les interventions." + +#: streetup/contracts/models.py:317 +#, fuzzy +#| msgid "" +#| "Indique si les interventions liées à ce contrat utilisent la gestion des " +#| "occupations (temps passé par membre)." +msgid "" +"Indique si les interventions liées à ce contrat utilisent un gestionnaire " +"d'intervention." +msgstr "" +"Indique si les interventions liées à ce contrat utilisent la gestion des " +"occupations (temps passé par membre)." + +#: streetup/contracts/models.py:321 +msgid "Occupations obligatoires" +msgstr "Occupations obligatoires" + +#: streetup/contracts/models.py:322 +msgid "" +"Si activé, au moins une occupation doit être enregistrée avant de terminer " +"une intervention corrective liée à ce contrat." +msgstr "" +"Si activé, au moins une occupation doit être enregistrée avant de terminer " +"une intervention corrective liée à ce contrat." + +#: streetup/contracts/models.py:326 +msgid "Avertir si aucune prestation au passage à 'Traité'" +msgstr "Avertir si aucune prestation au passage à 'Traité'" + +#: streetup/contracts/models.py:327 +msgid "" +"Si activé, un avertissement est affiché lorsque l'utilisateur tente de " +"passer une intervention au statut 'Traité' sans avoir encodé de prestation." +msgstr "" +"Si activé, un avertissement est affiché lorsque l'utilisateur tente de " +"passer une intervention au statut 'Traité' sans avoir encodé de prestation." + +#: streetup/contracts/models.py:331 +msgid "Envoi groupé journalier" +msgstr "Envoi groupé journalier" + +#: streetup/contracts/models.py:332 +msgid "" +"Si activé, les fiches d'intervention du jour sont regroupées dans un seul " +"email envoyé en fin de journée, plutôt qu'un email par intervention." +msgstr "" +"Si activé, les fiches d'intervention du jour sont regroupées dans un seul " +"email envoyé en fin de journée, plutôt qu'un email par intervention." + +#: streetup/contracts/models.py:336 +msgid "Heure d'envoi du résumé journalier" +msgstr "Heure d'envoi du résumé journalier" + +#: streetup/contracts/models.py:337 +msgid "" +"Heure (0-23) à laquelle le résumé journalier des interventions est envoyé " +"(heure locale)." +msgstr "" +"Heure (0-23) à laquelle le résumé journalier des interventions est envoyé " +"(heure locale)." + +#: streetup/contracts/models.py:341 +msgid "Emails uniquement si priorité urgente" +msgstr "Emails uniquement si priorité urgente" + +#: streetup/contracts/models.py:342 +msgid "" +"Si activé, les notifications email ne sont envoyées que pour les " +"interventions de priorité 'Urgent'." +msgstr "" +"Si activé, les notifications email ne sont envoyées que pour les " +"interventions de priorité 'Urgent'." + +#: streetup/contracts/models.py:436 +#: streetup/contracts/templates/contracts/contracts_index.html:139 +msgid "En préparation" +msgstr "En préparation" + +#: streetup/contracts/models.py:437 +#: streetup/contracts/templates/contracts/contracts_index.html:144 +msgid "Envoyé" +msgstr "Envoyé" + +#: streetup/contracts/models.py:438 +#: streetup/contracts/templates/contracts/contracts_index.html:149 +msgid "Terminé" +msgstr "Terminé" + +#: streetup/contracts/models.py:439 +#: streetup/contracts/templates/contracts/contracts_index.html:154 +msgid "Comptabilisé" +msgstr "Comptabilisé" + +#: streetup/contracts/models.py:440 +#: streetup/contracts/templates/contracts/contracts_index.html:159 +msgid "Payé" +msgstr "Payé" + +#: streetup/contracts/models.py:441 +#: streetup/contracts/templates/contracts/contracts_index.html:164 +msgid "Annulé" +msgstr "Annulé" + +#: streetup/contracts/models.py:460 +msgid "Projets" +msgstr "Projets" + +#: streetup/contracts/models.py:826 +msgid "État d'avancement associé à ce document, le cas échéant" +msgstr "État d'avancement associé à ce document, le cas échéant" + +#: streetup/contracts/models.py:852 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:47 +#: streetup/contracts/views_pdf_reports.py:432 +msgid "Déclaration de créance" +msgstr "Déclaration de créance" + +#: streetup/contracts/models.py:856 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:52 +#: streetup/contracts/templates/contracts/contracts_detail.html:116 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:768 +#: streetup/contracts/templates/contracts/teams_index.html:741 +msgid "Titre" +msgstr "Titre" + +#: streetup/contracts/models.py:857 +msgid "Titre ou description du justificatif" +msgstr "Titre ou description du justificatif" + +#: streetup/contracts/models.py:864 streetup/contracts/models.py:1045 +msgid "Date d'ajout" +msgstr "Date d'ajout" + +#: streetup/contracts/models.py:871 streetup/contracts/models.py:1052 +msgid "Ajouté par" +msgstr "Ajouté par" + +#: streetup/contracts/models.py:878 +msgid "Pièce jointe de déclaration de créance" +msgstr "Pièce jointe de déclaration de créance" + +#: streetup/contracts/models.py:879 +msgid "Pièces jointes de déclarations de créance" +msgstr "Pièces jointes de déclarations de créance" + +#: streetup/contracts/models.py:886 +msgid "Numéro d'ordre de l'état d'avancement pour cette commande" +msgstr "Numéro d'ordre de l'état d'avancement pour cette commande" + +#: streetup/contracts/models.py:905 +msgid "Quantité consommée à cette date" +msgstr "Quantité consommée à cette date" + +#: streetup/contracts/models.py:926 +msgid "En attente de validation" +msgstr "En attente de validation" + +#: streetup/contracts/models.py:927 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:616 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:926 +msgid "Approuvé" +msgstr "Approuvé" + +#: streetup/contracts/models.py:928 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:621 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:927 +msgid "Refusé" +msgstr "Refusé" + +#: streetup/contracts/models.py:929 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:623 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:929 +msgid "Facturé" +msgstr "Facturé" + +#: streetup/contracts/models.py:939 streetup/contracts/models.py:1029 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:53 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:86 +msgid "Commande" +msgstr "Commande" + +#: streetup/contracts/models.py:945 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:699 +msgid "Poste de marché" +msgstr "Poste de marché" + +#: streetup/contracts/models.py:952 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:591 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:713 +msgid "Commentaire" +msgstr "Commentaire" + +#: streetup/contracts/models.py:957 +msgid "Statut de validation" +msgstr "Statut de validation" + +#: streetup/contracts/models.py:959 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:54 +msgid "Date de création" +msgstr "Date de création" + +#: streetup/contracts/models.py:966 +msgid "Créé par" +msgstr "Créé par" + +#: streetup/contracts/models.py:968 +msgid "Date de validation" +msgstr "Date de validation" + +#: streetup/contracts/models.py:975 +msgid "Validé par" +msgstr "Validé par" + +#: streetup/contracts/models.py:981 +msgid "Quantité déjà facturée" +msgstr "Quantité déjà facturée" + +#: streetup/contracts/models.py:982 +msgid "Quantité cumulée sur les déclarations de créance précédentes." +msgstr "Quantité cumulée sur les déclarations de créance précédentes." + +#: streetup/contracts/models.py:1017 +msgid "Commande directe – poste" +msgstr "Commande directe – poste" + +#: streetup/contracts/models.py:1018 +msgid "Commandes directes – postes" +msgstr "Commandes directes – postes" + +#: streetup/contracts/models.py:1043 +msgid "Vignette" +msgstr "Vignette" + +#: streetup/contracts/models.py:1064 +msgid "Pièce jointe de commande" +msgstr "Pièce jointe de commande" + +#: streetup/contracts/models.py:1065 +msgid "Pièces jointes de commande" +msgstr "Pièces jointes de commande" + +#: streetup/contracts/permissions.py:98 +msgid "Paramètre 'contract_id' manquant." +msgstr "Paramètre 'contract_id' manquant." + +#: streetup/contracts/permissions.py:103 +msgid "Accès au contrat refusé." +msgstr "Accès au contrat refusé." + +#: streetup/contracts/permissions.py:132 +msgid "Paramètre 'order_id' manquant." +msgstr "Paramètre 'order_id' manquant." + +#: streetup/contracts/permissions.py:137 streetup/contracts/views.py:976 +msgid "Accès à la commande refusé." +msgstr "Accès à la commande refusé." + +#: streetup/contracts/templates/admin/contracts/contractpost/change_list.html:7 +msgid "Importer / mettre à jour des postes (Excel)" +msgstr "Importer / mettre à jour des postes (Excel)" + +#: streetup/contracts/templates/admin/contracts/contractpost/import_posts.html:8 +msgid "Accueil" +msgstr "Accueil" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:27 +msgid "Contrats" +msgstr "Contrats" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:29 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:474 +msgid "Justificatifs" +msgstr "Justificatifs" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:33 +msgid "Justificatifs de la déclaration de créance" +msgstr "Justificatifs de la déclaration de créance" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:58 +msgid "Voir le document" +msgstr "Voir le document" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:68 +msgid "Ajouter un justificatif" +msgstr "Ajouter un justificatif" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:90 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:313 +#: streetup/contracts/templates/contracts/teams_index.html:324 +msgid "Ajouter" +msgstr "Ajouter" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:101 +msgid "Justificatifs attachés" +msgstr "Justificatifs attachés" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:117 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:466 +msgid "Ajouté le" +msgstr "Ajouté le" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:119 +msgid "par" +msgstr "par" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:125 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:332 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:333 +msgid "Télécharger" +msgstr "Télécharger" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:128 +msgid "Êtes-vous sûr de vouloir supprimer ce justificatif ?" +msgstr "Êtes-vous sûr de vouloir supprimer ce justificatif ?" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:130 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:238 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:290 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:642 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:743 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:949 +msgid "Supprimer" +msgstr "Supprimer" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:141 +msgid "" +"Aucun justificatif n'a encore été ajouté à cette déclaration de créance." +msgstr "" +"Aucun justificatif n'a encore été ajouté à cette déclaration de créance." + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:149 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:31 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:154 +msgid "Retour à la commande" +msgstr "Retour à la commande" + +#: streetup/contracts/templates/contracts/contracts_detail.html:19 +msgid "Détail du marché / contrat" +msgstr "Détail du marché / contrat" + +#: streetup/contracts/templates/contracts/contracts_detail.html:28 +#: streetup/contracts/templates/contracts/contracts_detail.html:117 +#: streetup/contracts/templates/contracts/contracts_index.html:243 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:769 +msgid "Début" +msgstr "Début" + +#: streetup/contracts/templates/contracts/contracts_detail.html:29 +#: streetup/contracts/templates/contracts/contracts_index.html:244 +msgid "Fin" +msgstr "Fin" + +#: streetup/contracts/templates/contracts/contracts_detail.html:31 +msgid "Fonctionnaire dirigeant" +msgstr "Fonctionnaire dirigeant" + +#: streetup/contracts/templates/contracts/contracts_detail.html:34 +msgid "Notifications email" +msgstr "Notifications email" + +#: streetup/contracts/templates/contracts/contracts_detail.html:36 +msgid "Oui" +msgstr "Oui" + +#: streetup/contracts/templates/contracts/contracts_detail.html:38 +msgid "Non" +msgstr "Non" + +#: streetup/contracts/templates/contracts/contracts_detail.html:45 +msgid "Montant engagé" +msgstr "Montant engagé" + +#: streetup/contracts/templates/contracts/contracts_detail.html:49 +msgid "Montant \"presté" +msgstr "Montant \"presté" + +#: streetup/contracts/templates/contracts/contracts_detail.html:67 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:135 +msgid "Montant validé" +msgstr "Montant validé" + +#: streetup/contracts/templates/contracts/contracts_detail.html:87 +msgid "Dossier interne" +msgstr "Dossier interne" + +#: streetup/contracts/templates/contracts/contracts_detail.html:96 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:212 +msgid "Retour à la liste" +msgstr "Retour à la liste" + +#: streetup/contracts/templates/contracts/contracts_detail.html:104 +#: streetup/contracts/templates/contracts/contracts_index.html:120 +msgid "Commandes" +msgstr "Commandes" + +#: streetup/contracts/templates/contracts/contracts_detail.html:115 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:70 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:496 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:584 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:767 +#: streetup/contracts/templates/contracts/teams_index.html:740 +#: streetup/contracts/views_pdf_reports.py:945 +#: streetup/contracts/views_pdf_reports.py:1138 +msgid "Code" +msgstr "Code" + +#: streetup/contracts/templates/contracts/contracts_detail.html:118 +#: streetup/contracts/templates/contracts/contracts_index.html:200 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:592 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:770 +#: streetup/contracts/templates/contracts/teams_index.html:743 +msgid "Statut" +msgstr "Statut" + +#: streetup/contracts/templates/contracts/contracts_index.html:114 +msgid "Nouvelle commande" +msgstr "Nouvelle commande" + +#: streetup/contracts/templates/contracts/contracts_index.html:123 +msgid "Marchés/Contrats" +msgstr "Marchés/Contrats" + +#: streetup/contracts/templates/contracts/contracts_index.html:134 +msgid "Statuts" +msgstr "Statuts" + +#: streetup/contracts/templates/contracts/contracts_index.html:171 +msgid "Commandes à partir du" +msgstr "Commandes à partir du" + +#: streetup/contracts/templates/contracts/contracts_index.html:175 +msgid "jusqu'au" +msgstr "jusqu'au" + +#: streetup/contracts/templates/contracts/contracts_index.html:181 +msgid "Filtrer" +msgstr "Filtrer" + +#: streetup/contracts/templates/contracts/contracts_index.html:182 +msgid "Réinitialiser" +msgstr "Réinitialiser" + +#: streetup/contracts/templates/contracts/contracts_index.html:197 +msgid "Numéro de commande" +msgstr "Numéro de commande" + +#: streetup/contracts/templates/contracts/contracts_index.html:199 +msgid "Date" +msgstr "Date" + +#: streetup/contracts/templates/contracts/contracts_index.html:202 +msgid "Marché" +msgstr "Marché" + +#: streetup/contracts/templates/contracts/contracts_index.html:240 +msgid "Numéro" +msgstr "Numéro" + +#: streetup/contracts/templates/contracts/contracts_index.html:289 +msgid "Aucune commande trouvée" +msgstr "Aucune commande trouvée" + +#: streetup/contracts/templates/contracts/contracts_index.html:294 +msgid "Aucun marché trouvé" +msgstr "Aucun marché trouvé" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:22 +msgid "Modifier la commande" +msgstr "Modifier la commande" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:24 +msgid "Ajouter une nouvelle commande" +msgstr "Ajouter une nouvelle commande" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:35 +msgid "Mettre à jour" +msgstr "Mettre à jour" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:35 +msgid "Créer la commande" +msgstr "Créer la commande" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:29 +#, python-format +msgid "Devis pour la commande %(order_code)s" +msgstr "Devis pour la commande %(order_code)s" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:112 +msgid "Non renseigné" +msgstr "Non renseigné" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:39 +msgid "Montant total du devis" +msgstr "Montant total du devis" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:54 +#, python-format +msgid "Ce devis est figé depuis le %(finalized_at)s." +msgstr "Ce devis est figé depuis le %(finalized_at)s." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:56 +#: streetup/contracts/views.py:460 +msgid "Ce devis est figé et ne peut plus être modifié." +msgstr "Ce devis est figé et ne peut plus être modifié." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:69 +msgid "N° de poste" +msgstr "N° de poste" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:72 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:586 +#: streetup/contracts/views_pdf_reports.py:947 +#: streetup/contracts/views_pdf_reports.py:1140 +msgid "Unité" +msgstr "Unité" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:73 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:510 +#: streetup/contracts/views_pdf_reports.py:1141 +msgid "Prix unitaire" +msgstr "Prix unitaire" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:75 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:590 +#: streetup/contracts/views_pdf_reports.py:1143 +msgid "Montant" +msgstr "Montant" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:81 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:87 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:93 +msgid "Filtrer…" +msgstr "Filtrer…" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:103 +msgid "Quantité > 0" +msgstr "Quantité > 0" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:140 +msgid "Aucun poste disponible pour ce contrat." +msgstr "Aucun poste disponible pour ce contrat." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:145 +msgid "Aucun poste ne correspond aux filtres." +msgstr "Aucun poste ne correspond aux filtres." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:159 +msgid "Enregistrer le devis" +msgstr "Enregistrer le devis" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:163 +msgid "Figer le devis" +msgstr "Figer le devis" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:64 +msgid "Détail de la commande" +msgstr "Détail de la commande" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:99 +msgid "Contrat lié" +msgstr "Contrat lié" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:105 +msgid "Date de commande" +msgstr "Date de commande" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:111 +msgid "Montant prévisionnel" +msgstr "Montant prévisionnel" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:117 +msgid "Montant \"presté\"" +msgstr "Montant \"presté\"" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:156 +msgid "Devis" +msgstr "Devis" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:158 +msgid "Montant total" +msgstr "Montant total" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:160 +msgid "Devis figé" +msgstr "Devis figé" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:167 +#, python-format +msgid "" +"\n" +" %(item_count)s poste dans le devis.\n" +" " +msgid_plural "" +"\n" +" %(item_count)s postes dans le devis.\n" +" " +msgstr[0] "" +"\n" +" %(item_count)s poste dans le devis.\n" +" " +msgstr[1] "" +"\n" +" %(item_count)s postes dans le devis.\n" +" " + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:174 +msgid "Aucun devis n'est associé à cette commande pour le moment." +msgstr "Aucun devis n'est associé à cette commande pour le moment." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:181 +msgid "Voir le devis" +msgstr "Voir le devis" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:183 +msgid "Gérer le devis" +msgstr "Gérer le devis" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:189 +msgid "Exporter en PDF" +msgstr "Exporter en PDF" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:198 +msgid "Commandé par" +msgstr "Commandé par" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:205 +msgid "Télécharger le fichier" +msgstr "Télécharger le fichier" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:219 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:329 +msgid "Documents" +msgstr "Documents" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:221 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:301 +msgid "Ajouter des documents" +msgstr "Ajouter des documents" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:270 +msgid "Aucun document associé à cette commande." +msgstr "Aucun document associé à cette commande." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:281 +msgid "Supprimer le document" +msgstr "Supprimer le document" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:285 +msgid "Voulez-vous supprimer ce document ?" +msgstr "Voulez-vous supprimer ce document ?" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:289 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:719 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:742 +#: streetup/contracts/templates/contracts/teams_index.html:322 +#: streetup/contracts/templates/contracts/teams_index.html:363 +msgid "Annuler" +msgstr "Annuler" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:374 +msgid "Précédent" +msgstr "Précédent" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:379 +msgid "Suivant" +msgstr "Suivant" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:416 +msgid "Documents financiers" +msgstr "Documents financiers" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:421 +msgid "Générer un draft de l'état de travaux" +msgstr "Générer un draft de l'état de travaux" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:424 +msgid "Générer un draft de la déclaration de créance" +msgstr "Générer un draft de la déclaration de créance" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:430 +msgid "Générer la déclaration de créance" +msgstr "Générer la déclaration de créance" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:434 +msgid "" +"Cette opération va générer un état de travaux, une déclaration de créance et " +"faire passer les interventions validées au statut \"comptabilisé\"." +msgstr "" +"Cette opération va générer un état de travaux, une déclaration de créance et " +"faire passer les interventions validées au statut \"comptabilisé\"." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:444 +#, python-format +msgid "État d'avancement %(num)s" +msgstr "État d'avancement %(num)s" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:448 +msgid "Autres documents" +msgstr "Autres documents" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:465 +msgid "Type" +msgstr "Type" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:468 +msgid "justificatif(s)" +msgstr "justificatif(s)" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:473 +msgid "Gérer les justificatifs" +msgstr "Gérer les justificatifs" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:487 +msgid "Consommation des postes" +msgstr "Consommation des postes" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:495 +msgid "Poste" +msgstr "Poste" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:499 +msgid "Quantité prévue" +msgstr "Quantité prévue" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:502 +msgid "État d avancement du" +msgstr "État d avancement du" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:503 +msgid "État" +msgstr "État" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:506 +msgid "Quantité consommée" +msgstr "Quantité consommée" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:508 +msgid "Quantité restante" +msgstr "Quantité restante" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:511 +msgid "Montant consommé" +msgstr "Montant consommé" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:550 +msgid "Aucune consommation enregistrée pour cette commande." +msgstr "Aucune consommation enregistrée pour cette commande." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:557 +msgid "Projets associés" +msgstr "Projets associés" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:567 +#: streetup/contracts/views_pdf_reports.py:713 +msgid "Commandes directes" +msgstr "Commandes directes" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:569 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:693 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1110 +msgid "Ajouter un poste direct" +msgstr "Ajouter un poste direct" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:577 +msgid "" +"Les commandes directes permettent d'enregistrer des postes de marché " +"consommés sans passer par une intervention. Les postes validés sont inclus " +"dans les totaux, les états d'avancement et les déclarations de créance." +msgstr "" +"Les commandes directes permettent d'enregistrer des postes de marché " +"consommés sans passer par une intervention. Les postes validés sont inclus " +"dans les totaux, les états d'avancement et les déclarations de créance." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:583 +msgid "N°" +msgstr "N°" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:587 +msgid "Prix unit." +msgstr "Prix unit." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:589 +msgid "Qté facturée" +msgstr "Qté facturée" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:593 +msgid "Actions" +msgstr "Actions" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:625 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:928 +msgid "En attente" +msgstr "En attente" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:635 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:944 +msgid "Modifier" +msgstr "Modifier" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:652 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:956 +msgid "Valider" +msgstr "Valider" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:660 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:961 +msgid "Refuser" +msgstr "Refuser" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:668 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:966 +msgid "Remettre en attente" +msgstr "Remettre en attente" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:678 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1213 +msgid "Aucune commande directe enregistrée." +msgstr "Aucune commande directe enregistrée." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:700 +msgid "Rechercher par code, numéro ou description…" +msgstr "Rechercher par code, numéro ou description…" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:721 +#: streetup/contracts/templates/contracts/teams_index.html:365 +msgid "Enregistrer" +msgstr "Enregistrer" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:735 +msgid "Supprimer le poste direct" +msgstr "Supprimer le poste direct" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:739 +msgid "Voulez-vous supprimer ce poste direct ?" +msgstr "Voulez-vous supprimer ce poste direct ?" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:755 +msgid "Ajouter une intervention" +msgstr "Ajouter une intervention" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1107 +msgid "Modifier le poste direct" +msgstr "Modifier le poste direct" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1143 +msgid "Veuillez sélectionner un poste de marché." +msgstr "Veuillez sélectionner un poste de marché." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1173 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1178 +#: streetup/contracts/templates/contracts/teams_index.html:436 +#: streetup/contracts/templates/contracts/teams_index.html:440 +msgid "Une erreur est survenue." +msgstr "Une erreur est survenue." + +#: streetup/contracts/templates/contracts/teams_index.html:130 +msgid "Gestion des équipes" +msgstr "Gestion des équipes" + +#: streetup/contracts/templates/contracts/teams_index.html:137 +msgid "Aucune équipe accessible." +msgstr "Aucune équipe accessible." + +#: streetup/contracts/templates/contracts/teams_index.html:150 +#: streetup/contracts/templates/contracts/teams_index.html:157 +msgid "Non assignés" +msgstr "Non assignés" + +#: streetup/contracts/templates/contracts/teams_index.html:152 +msgid "Aucune équipe" +msgstr "Aucune équipe" + +#: streetup/contracts/templates/contracts/teams_index.html:156 +#: streetup/contracts/templates/contracts/teams_index.html:578 +msgid "Ajouter un membre non assigné" +msgstr "Ajouter un membre non assigné" + +#: streetup/contracts/templates/contracts/teams_index.html:180 +#: streetup/contracts/templates/contracts/teams_index.html:353 +msgid "Assigner à une équipe" +msgstr "Assigner à une équipe" + +#: streetup/contracts/templates/contracts/teams_index.html:185 +#: streetup/contracts/templates/contracts/teams_index.html:258 +msgid "Supprimer ce membre" +msgstr "Supprimer ce membre" + +#: streetup/contracts/templates/contracts/teams_index.html:195 +#: streetup/contracts/templates/contracts/teams_index.html:268 +msgid "Aucun membre" +msgstr "Aucun membre" + +#: streetup/contracts/templates/contracts/teams_index.html:202 +#: streetup/contracts/templates/contracts/teams_index.html:276 +msgid "membre(s)" +msgstr "membre(s)" + +#: streetup/contracts/templates/contracts/teams_index.html:219 +msgid "Voir les interventions assignées" +msgstr "Voir les interventions assignées" + +#: streetup/contracts/templates/contracts/teams_index.html:227 +#: streetup/contracts/templates/contracts/teams_index.html:298 +#: streetup/contracts/templates/contracts/teams_index.html:577 +msgid "Ajouter un membre" +msgstr "Ajouter un membre" + +#: streetup/contracts/templates/contracts/teams_index.html:253 +msgid "Déplacer vers une autre équipe" +msgstr "Déplacer vers une autre équipe" + +#: streetup/contracts/templates/contracts/teams_index.html:305 +#: streetup/contracts/templates/contracts/teams_index.html:345 +msgid "Nom affiché" +msgstr "Nom affiché" + +#: streetup/contracts/templates/contracts/teams_index.html:306 +msgid "Ex : Jean Dupont" +msgstr "Ex : Jean Dupont" + +#: streetup/contracts/templates/contracts/teams_index.html:309 +msgid "Compte utilisateur" +msgstr "Compte utilisateur" + +#: streetup/contracts/templates/contracts/teams_index.html:311 +msgid "Sélectionner un utilisateur" +msgstr "Sélectionner un utilisateur" + +#: streetup/contracts/templates/contracts/teams_index.html:337 +msgid "Modifier le membre" +msgstr "Modifier le membre" + +#: streetup/contracts/templates/contracts/teams_index.html:351 +msgid "Ne pas modifier l'équipe" +msgstr "Ne pas modifier l'équipe" + +#: streetup/contracts/templates/contracts/teams_index.html:352 +msgid "Désassigner (aucune équipe)" +msgstr "Désassigner (aucune équipe)" + +#: streetup/contracts/templates/contracts/teams_index.html:380 +msgid "Interventions" +msgstr "Interventions" + +#: streetup/contracts/templates/contracts/teams_index.html:387 +#: streetup/contracts/templates/contracts/teams_index.html:687 +#: streetup/contracts/templates/contracts/teams_index.html:763 +msgid "Chargement..." +msgstr "Chargement..." + +#: streetup/contracts/templates/contracts/teams_index.html:402 +#: streetup/contracts/templates/contracts/teams_index.html:403 +msgid "Résumé intervention" +msgstr "Résumé intervention" + +#: streetup/contracts/templates/contracts/teams_index.html:410 +msgid "Retour aux interventions" +msgstr "Retour aux interventions" + +#: streetup/contracts/templates/contracts/teams_index.html:412 +msgid "Fermer" +msgstr "Fermer" + +#: streetup/contracts/templates/contracts/teams_index.html:551 +#: streetup/contracts/templates/contracts/teams_index.html:633 +msgid "Le nom du membre est requis." +msgstr "Le nom du membre est requis." + +#: streetup/contracts/templates/contracts/teams_index.html:634 +msgid "L\\'utilisateur est requis." +msgstr "L\\'utilisateur est requis." + +#: streetup/contracts/templates/contracts/teams_index.html:651 +msgid "Supprimer le membre" +msgstr "Supprimer le membre" + +#: streetup/contracts/templates/contracts/teams_index.html:651 +msgid "Cette action est irréversible." +msgstr "Cette action est irréversible." + +#: streetup/contracts/templates/contracts/teams_index.html:700 +msgid "Impossible de charger les interventions." +msgstr "Impossible de charger les interventions." + +#: streetup/contracts/templates/contracts/teams_index.html:706 +msgid "intervention(s)" +msgstr "intervention(s)" + +#: streetup/contracts/templates/contracts/teams_index.html:709 +msgid "Aucune intervention assignée à cette équipe." +msgstr "Aucune intervention assignée à cette équipe." + +#: streetup/contracts/templates/contracts/teams_index.html:721 +msgid "Voir le résumé" +msgstr "Voir le résumé" + +#: streetup/contracts/templates/contracts/teams_index.html:742 +msgid "Membre assigné" +msgstr "Membre assigné" + +#: streetup/contracts/templates/contracts/teams_index.html:779 +msgid "Résumé non disponible." +msgstr "Résumé non disponible." + +#: streetup/contracts/views.py:269 streetup/contracts/views.py:873 +#: streetup/contracts/views.py:917 streetup/contracts/views.py:942 +msgid "Vous n'avez pas accès à cette commande." +msgstr "Vous n'avez pas accès à cette commande." + +#: streetup/contracts/views.py:275 +msgid "" +"Vous n'avez pas de thématique d'intervention autorisée pour cette commande." +msgstr "" +"Vous n'avez pas de thématique d'intervention autorisée pour cette commande." + +#: streetup/contracts/views.py:347 +msgid "Vous n'avez pas le droit d'ajouter une commande pour ce contrat." +msgstr "Vous n'avez pas le droit d'ajouter une commande pour ce contrat." + +#: streetup/contracts/views.py:388 +msgid "" +"Impossible de changer le contrat: des interventions sont déjà liées à cette " +"commande." +msgstr "" +"Impossible de changer le contrat: des interventions sont déjà liées à cette " +"commande." + +#: streetup/contracts/views.py:397 +msgid "Vous n'avez pas le droit de modifier une commande pour ce contrat." +msgstr "Vous n'avez pas le droit de modifier une commande pour ce contrat." + +#: streetup/contracts/views.py:416 +msgid "Vous n'avez pas le droit de modifier le devis de cette commande." +msgstr "Vous n'avez pas le droit de modifier le devis de cette commande." + +#: streetup/contracts/views.py:469 streetup/contracts/views.py:474 +#: streetup/contracts/views.py:480 +msgid "Les données reçues sont invalides." +msgstr "Les données reçues sont invalides." + +#: streetup/contracts/views.py:523 +msgid "Le formulaire contient des erreurs." +msgstr "Le formulaire contient des erreurs." + +#: streetup/contracts/views.py:552 +msgid "Impossible de figer un devis vide." +msgstr "Impossible de figer un devis vide." + +#: streetup/contracts/views.py:555 +msgid "Le devis a été figé." +msgstr "Le devis a été figé." + +#: streetup/contracts/views.py:558 +msgid "Le devis a été enregistré." +msgstr "Le devis a été enregistré." + +#: streetup/contracts/views.py:702 +msgid "Accès refusé au document de la commande." +msgstr "Accès refusé au document de la commande." + +#: streetup/contracts/views.py:707 +msgid "Fichier introuvable" +msgstr "Fichier introuvable" + +#: streetup/contracts/views.py:732 +msgid "Le contrat est requis" +msgstr "Le contrat est requis" + +#: streetup/contracts/views.py:735 +msgid "La référence de commande est requise" +msgstr "La référence de commande est requise" + +#: streetup/contracts/views.py:738 +msgid "La date de commande est requise" +msgstr "La date de commande est requise" + +#: streetup/contracts/views.py:741 +msgid "La date de livraison est requise" +msgstr "La date de livraison est requise" + +#: streetup/contracts/views.py:745 +msgid "Vous n'avez pas le droit d'ajouter une commande pour ce contrat" +msgstr "Vous n'avez pas le droit d'ajouter une commande pour ce contrat" + +#: streetup/contracts/views.py:752 +msgid "Une commande avec cette référence existe déjà pour ce contrat" +msgstr "Une commande avec cette référence existe déjà pour ce contrat" + +#: streetup/contracts/views.py:775 +msgid "Données JSON invalides" +msgstr "Données JSON invalides" + +#: streetup/contracts/views.py:834 +msgid "Vous n'avez pas accès à ce contrat" +msgstr "Vous n'avez pas accès à ce contrat" + +#: streetup/contracts/views.py:882 +msgid "Le justificatif a été ajouté avec succès." +msgstr "Le justificatif a été ajouté avec succès." + +#: streetup/contracts/views.py:927 +#, python-format +msgid "Le justificatif « %(title)s » a été supprimé." +msgstr "Le justificatif « %(title)s » a été supprimé." + +#: streetup/contracts/views.py:947 +msgid "" +"Seuls les managers et administrateurs peuvent changer le statut d'une " +"commande." +msgstr "" +"Seuls les managers et administrateurs peuvent changer le statut d'une " +"commande." + +#: streetup/contracts/views.py:956 streetup/contracts/views.py:1226 +msgid "Statut invalide." +msgstr "Statut invalide." + +#: streetup/contracts/views.py:964 +msgid "Le statut de la commande a été mis à jour avec succès." +msgstr "Le statut de la commande a été mis à jour avec succès." + +#: streetup/contracts/views.py:979 +msgid "Vous n'avez pas la permission d'ajouter des documents à cette commande." +msgstr "" +"Vous n'avez pas la permission d'ajouter des documents à cette commande." + +#: streetup/contracts/views.py:991 +msgid "Document(s) ajouté(s) avec succès." +msgstr "Document(s) ajouté(s) avec succès." + +#: streetup/contracts/views.py:993 +msgid "Erreur lors de l'ajout des documents." +msgstr "Erreur lors de l'ajout des documents." + +#: streetup/contracts/views.py:1005 streetup/contracts/views.py:1043 +#: streetup/contracts/views.py:1074 streetup/contracts/views.py:1142 +#: streetup/contracts/views.py:1192 streetup/contracts/views.py:1212 +msgid "Accès refusé." +msgstr "Accès refusé." + +#: streetup/contracts/views.py:1008 +msgid "Vous n'avez pas la permission de supprimer ce document." +msgstr "Vous n'avez pas la permission de supprimer ce document." + +#: streetup/contracts/views.py:1076 streetup/contracts/views.py:1144 +#: streetup/contracts/views.py:1194 streetup/contracts/views.py:1214 +msgid "Permission insuffisante." +msgstr "Permission insuffisante." + +#: streetup/contracts/views.py:1081 streetup/contracts/views.py:1153 +#: streetup/contracts/views.py:1221 +msgid "Données JSON invalides." +msgstr "Données JSON invalides." + +#: streetup/contracts/views.py:1088 +msgid "Le poste de marché est requis." +msgstr "Le poste de marché est requis." + +#: streetup/contracts/views.py:1095 +msgid "La quantité doit être un nombre positif." +msgstr "La quantité doit être un nombre positif." + +#: streetup/contracts/views.py:1102 +msgid "" +"Un poste direct existe déjà pour ce poste dans cette commande. Modifiez-le " +"plutôt." +msgstr "" +"Un poste direct existe déjà pour ce poste dans cette commande. Modifiez-le " +"plutôt." + +#: streetup/contracts/views.py:1148 +msgid "Impossible de modifier un poste déjà validé." +msgstr "Impossible de modifier un poste déjà validé." + +#: streetup/contracts/views.py:1163 +msgid "La quantité doit être un nombre positif ou nul." +msgstr "La quantité doit être un nombre positif ou nul." + +#: streetup/contracts/views.py:1198 +msgid "Impossible de supprimer un poste déjà validé." +msgstr "Impossible de supprimer un poste déjà validé." + +#: streetup/contracts/views.py:1200 +msgid "Impossible de supprimer un poste déjà facturé." +msgstr "Impossible de supprimer un poste déjà facturé." + +#: streetup/contracts/views_daily_report.py:227 +msgid "Rapport journalier des interventions" +msgstr "Rapport journalier des interventions" + +#: streetup/contracts/views_daily_report.py:248 +#: streetup/contracts/views_daily_report.py:393 +msgid "Synthèse par thématique" +msgstr "Synthèse par thématique" + +#: streetup/contracts/views_daily_report.py:251 +#: streetup/contracts/views_daily_report.py:403 +msgid "Thématique" +msgstr "Thématique" + +#: streetup/contracts/views_daily_report.py:302 +#: streetup/contracts/views_daily_report.py:344 +#: streetup/contracts/views_daily_report.py:433 +#: streetup/contracts/views_daily_report.py:481 +msgid "TOTAL" +msgstr "TOTAL" + +#: streetup/contracts/views_daily_report.py:314 +msgid "Nombre d'interventions par personne" +msgstr "Nombre d'interventions par personne" + +#: streetup/contracts/views_daily_report.py:317 +#: streetup/contracts/views_daily_report.py:459 +msgid "Personne" +msgstr "Personne" + +#: streetup/contracts/views_daily_report.py:317 +#: streetup/contracts/views_daily_report.py:459 +msgid "Nombre" +msgstr "Nombre" + +#: streetup/contracts/views_daily_report.py:397 +#: streetup/contracts/views_daily_report.py:454 +msgid "Rapport journalier" +msgstr "Rapport journalier" + +#: streetup/contracts/views_daily_report.py:451 +msgid "Par personne" +msgstr "Par personne" + +#: streetup/contracts/views_daily_report.py:518 +msgid "Aucune de vos équipes n'est autorisée à générer ce rapport." +msgstr "Aucune de vos équipes n'est autorisée à générer ce rapport." + +#: streetup/contracts/views_pdf_reports.py:301 +#: streetup/contracts/views_pdf_reports.py:481 +msgid "Service public régional de Bruxelles" +msgstr "Service public régional de Bruxelles" + +#: streetup/contracts/views_pdf_reports.py:302 +#: streetup/contracts/views_pdf_reports.py:482 +msgid "Cahier des charges " +msgstr "Cahier des charges " + +#: streetup/contracts/views_pdf_reports.py:307 +#: streetup/contracts/views_pdf_reports.py:487 +msgid "Bruxelles: " +msgstr "Bruxelles: " + +#: streetup/contracts/views_pdf_reports.py:310 +msgid "DECLARATION DE CREANCE" +msgstr "DECLARATION DE CREANCE" + +#: streetup/contracts/views_pdf_reports.py:314 +msgid "Concerne:" +msgstr "Concerne:" + +#: streetup/contracts/views_pdf_reports.py:325 +#, python-format +msgid "" +"Je soussigné %(signatory)s, %(role)s, de la firme %(company)s, déclare que " +"le Service public régional de Bruxelles – Place Saint-Lazare 2 à 1035 " +"Bruxelles, est redevable de la somme de :" +msgstr "" +"Je soussigné %(signatory)s, %(role)s, de la firme %(company)s, déclare que " +"le Service public régional de Bruxelles – Place Saint-Lazare 2 à 1035 " +"Bruxelles, est redevable de la somme de :" + +#: streetup/contracts/views_pdf_reports.py:345 +#: streetup/contracts/views_pdf_reports.py:788 +#: streetup/contracts/views_pdf_reports.py:1295 +msgid "Total général TVA non comprise :" +msgstr "Total général TVA non comprise :" + +#: streetup/contracts/views_pdf_reports.py:350 +#: streetup/contracts/views_pdf_reports.py:793 +#: streetup/contracts/views_pdf_reports.py:1307 +msgid "TVA 21 % :" +msgstr "TVA 21 % :" + +#: streetup/contracts/views_pdf_reports.py:370 +msgid "euro" +msgid_plural "euros" +msgstr[0] "euro" +msgstr[1] "euros" + +#: streetup/contracts/views_pdf_reports.py:374 +msgid "centime" +msgid_plural "centimes" +msgstr[0] "centime" +msgstr[1] "centimes" + +#: streetup/contracts/views_pdf_reports.py:375 +msgid "et" +msgstr "et" + +#: streetup/contracts/views_pdf_reports.py:380 +#, python-format +msgid "Montant total, TVA incluse : %(amount)s" +msgstr "Montant total, TVA incluse : %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:399 +#, python-format +msgid "Declaration_Creance_%(slug_contract_number)s_%(slug_order_code)s.pdf" +msgstr "Declaration_Creance_%(slug_contract_number)s_%(slug_order_code)s.pdf" + +#: streetup/contracts/views_pdf_reports.py:427 +#: streetup/contracts/views_pdf_reports.py:827 +msgid "Declaration_Creance" +msgstr "Declaration_Creance" + +#: streetup/contracts/views_pdf_reports.py:490 +msgid "ETAT DE TRAVAUX" +msgstr "ETAT DE TRAVAUX" + +#: streetup/contracts/views_pdf_reports.py:558 +#, python-format +msgid "Intervention %(code)s du %(date)s" +msgstr "Intervention %(code)s du %(date)s" + +#: streetup/contracts/views_pdf_reports.py:569 +msgid "périodes" +msgstr "périodes" + +#: streetup/contracts/views_pdf_reports.py:579 +#: streetup/contracts/views_pdf_reports.py:717 +msgid "Code poste" +msgstr "Code poste" + +#: streetup/contracts/views_pdf_reports.py:581 +#: streetup/contracts/views_pdf_reports.py:719 +#: streetup/contracts/views_pdf_reports.py:948 +msgid "PU" +msgstr "PU" + +#: streetup/contracts/views_pdf_reports.py:582 +#: streetup/contracts/views_pdf_reports.py:720 +msgid "Qté" +msgstr "Qté" + +#: streetup/contracts/views_pdf_reports.py:583 +#: streetup/contracts/views_pdf_reports.py:721 +#: streetup/contracts/views_pdf_reports.py:950 +msgid "Total" +msgstr "Total" + +#: streetup/contracts/views_pdf_reports.py:681 +msgid "Aucun poste validé pour cette intervention" +msgstr "Aucun poste validé pour cette intervention" + +#: streetup/contracts/views_pdf_reports.py:687 +#: streetup/contracts/views_pdf_reports.py:743 +msgid "Total HTVA :" +msgstr "Total HTVA :" + +#: streetup/contracts/views_pdf_reports.py:798 +#: streetup/contracts/views_pdf_reports.py:1318 +msgid "Total général TVAC :" +msgstr "Total général TVAC :" + +#: streetup/contracts/views_pdf_reports.py:853 +msgid "Etat_de_travaux" +msgstr "Etat_de_travaux" + +#: streetup/contracts/views_pdf_reports.py:856 +msgid "État de travaux" +msgstr "État de travaux" + +#: streetup/contracts/views_pdf_reports.py:914 +#, python-format +msgid "Devis de la commande %(order)s" +msgstr "Devis de la commande %(order)s" + +#: streetup/contracts/views_pdf_reports.py:917 +#, python-format +msgid "Contrat %(contract)s – Commande %(order)s" +msgstr "Contrat %(contract)s – Commande %(order)s" + +#: streetup/contracts/views_pdf_reports.py:927 +msgid "Postes du devis" +msgstr "Postes du devis" + +#: streetup/contracts/views_pdf_reports.py:944 +#: streetup/contracts/views_pdf_reports.py:1137 +msgid "N° poste" +msgstr "N° poste" + +#: streetup/contracts/views_pdf_reports.py:1033 +#, python-format +msgid "Total HTVA : %(amount)s" +msgstr "Total HTVA : %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:1037 +#, python-format +msgid "TVA 21%% : %(amount)s" +msgstr "TVA 21%% : %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:1041 +#, python-format +msgid "Total TVAC : %(amount)s" +msgstr "Total TVAC : %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:1059 +msgid "Aucun devis n'est disponible pour cette commande." +msgstr "Aucun devis n'est disponible pour cette commande." + +#: streetup/contracts/views_pdf_reports.py:1065 +#, python-format +msgid "Devis_%(contract)s_%(order)s.pdf" +msgstr "Devis_%(contract)s_%(order)s.pdf" + +#: streetup/contracts/views_pdf_reports.py:1084 +msgid "Postes" +msgstr "Postes" + +#: streetup/contracts/views_pdf_reports.py:1103 +msgid "Marché :" +msgstr "Marché :" + +#: streetup/contracts/views_pdf_reports.py:1109 +#: streetup/contracts/views_pdf_reports.py:1121 +msgid "Description :" +msgstr "Description :" + +#: streetup/contracts/views_pdf_reports.py:1115 +msgid "Commande :" +msgstr "Commande :" + +#: streetup/contracts/views_pdf_reports.py:1128 +msgid "Date :" +msgstr "Date :" + +#: streetup/contracts/views_pdf_reports.py:1144 +msgid "SAJ" +msgstr "SAJ" + +#: streetup/contracts/views_pdf_reports.py:1239 +msgid "Total général des fournitures et travaux revisables hors TVA :" +msgstr "Total général des fournitures et travaux revisables hors TVA :" + +#: streetup/contracts/views_pdf_reports.py:1250 +msgid "Coefficient de révision :" +msgstr "Coefficient de révision :" + +#: streetup/contracts/views_pdf_reports.py:1265 +msgid "" +"Total général des fournitures et travaux non revisables hors TVA (SAJ) :" +msgstr "" +"Total général des fournitures et travaux non revisables hors TVA (SAJ) :" + +#: streetup/contracts/views_pdf_reports.py:1278 +#, python-format +msgid "" +"Total général des %(type)s non revisables hors TVA (SAJ) avec coefficient " +"%(coef)s :" +msgstr "" +"Total général des %(type)s non revisables hors TVA (SAJ) avec coefficient " +"%(coef)s :" + +#: streetup/contracts/views_pdf_reports.py:1345 +msgid "Detail_postes" +msgstr "Detail_postes" + +#: streetup/contracts/views_pdf_reports.py:1349 +msgid "Détail des postes" +msgstr "Détail des postes" + +#: streetup/contracts/views_pdf_reports.py:1372 +msgid "" +"Vous n'avez pas la permission de générer des documents pour cette commande." +msgstr "" +"Vous n'avez pas la permission de générer des documents pour cette commande." + +#: streetup/contracts/views_pdf_reports.py:1484 +msgid "Déclaration de créance créée et sauvegardée." +msgstr "Déclaration de créance créée et sauvegardée." + +#~ msgid "Laisser vide pour un membre non assigné à une équipe." +#~ msgstr "Laisser vide pour un membre non assigné à une équipe." + +#~| msgid "Nouvelle commande" +#~ msgid "Nouvelle équipe" +#~ msgstr "Nouvelle équipe" + +#~| msgid "Supprimer le document" +#~ msgid "Supprimer l'équipe" +#~ msgstr "Supprimer l'équipe" + +#~ msgid "Nom de l'équipe" +#~ msgstr "Nom de l'équipe" + +#~ msgid "Ex : Équipe Signalisation Nord" +#~ msgstr "Ex : Équipe Signalisation Nord" + +#~ msgid "Société" +#~ msgstr "Société" + +#~ msgid "Sélectionner une société" +#~ msgstr "Sélectionner une société" + +#~ msgid "Créer" +#~ msgstr "Créer" + +#~ msgid "Le nom de l\\'équipe est requis." +#~ msgstr "Le nom de l\\'équipe est requis." + +#~| msgid "Le contrat est requis" +#~ msgid "La société est requise." +#~ msgstr "La société est requise." + +#~ msgid "Types de maintenance autorisés" +#~ msgstr "Types de maintenance autorisés" + +#~ msgid "" +#~ "Types de maintenance que cette équipe peut voir. Si vide, tous les types " +#~ "sont autorisés." +#~ msgstr "" +#~ "Types de maintenance que cette équipe peut voir. Si vide, tous les types " +#~ "sont autorisés." + +#~ msgid "Total général HTVA :" +#~ msgstr "Total général HTVA :" diff --git a/streetup/contracts/locale/fr/LC_MESSAGES/djangojs.mo b/streetup/contracts/locale/fr/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..e94b8ce Binary files /dev/null and b/streetup/contracts/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/contracts/locale/fr/LC_MESSAGES/djangojs.po b/streetup/contracts/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..977ad8b --- /dev/null +++ b/streetup/contracts/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: contracts/static/contracts/timeline.js:115 +msgid "Êtes-vous sûr de vouloir annuler cette commande ?" +msgstr "Êtes-vous sûr de vouloir annuler cette commande ?" + +#: contracts/static/contracts/timeline.js:136 +msgid "Erreur lors de la mise à jour du statut" +msgstr "Erreur lors de la mise à jour du statut" diff --git a/streetup/contracts/locale/nl/LC_MESSAGES/django.mo b/streetup/contracts/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..a1edc20 Binary files /dev/null and b/streetup/contracts/locale/nl/LC_MESSAGES/django.mo differ diff --git a/streetup/contracts/locale/nl/LC_MESSAGES/django.po b/streetup/contracts/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..5fa30b6 --- /dev/null +++ b/streetup/contracts/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,1970 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: streetup/contracts/forms.py:30 +msgid "" +"Ce contrat ne peut plus être modifié car des interventions sont déjà liées." +msgstr "" +"Dit contract kan niet meer worden gewijzigd omdat er al interventies aan " +"gekoppeld zijn." + +#: streetup/contracts/forms.py:67 +msgid "" +"Le contrat ne peut plus être modifié car des interventions sont déjà liées à " +"cette commande." +msgstr "" +"Het contract kan niet meer worden gewijzigd omdat er al interventies aan " +"deze bestelling gekoppeld zijn." + +#: streetup/contracts/forms.py:69 +msgid "Modification du contrat interdite." +msgstr "Wijziging van het contract niet toegestaan." + +#: streetup/contracts/forms.py:79 +msgid "Date de la commande" +msgstr "Besteldatum" + +#: streetup/contracts/forms.py:87 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:106 +msgid "Date de livraison" +msgstr "Leverdatum" + +#: streetup/contracts/forms.py:92 +msgid "Statut de la commande" +msgstr "Status van de bestelling" + +#: streetup/contracts/forms.py:97 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:202 +msgid "Fichier de commande" +msgstr "Bestandsstuk van de bestelling" + +#: streetup/contracts/forms.py:101 +msgid "Montant total (€)" +msgstr "Totaalbedrag (€)" + +#: streetup/contracts/forms.py:124 +#: streetup/contracts/templates/contracts/contracts_detail.html:22 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:37 +msgid "Contrat" +msgstr "Contract" + +#: streetup/contracts/forms.py:125 +msgid "Référence de commande" +msgstr "Bestelreferentie" + +#: streetup/contracts/forms.py:126 +#: streetup/contracts/templates/contracts/contracts_detail.html:25 +#: streetup/contracts/templates/contracts/contracts_index.html:198 +#: streetup/contracts/templates/contracts/contracts_index.html:241 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:71 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:102 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:497 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:585 +#: streetup/contracts/views_pdf_reports.py:580 +#: streetup/contracts/views_pdf_reports.py:718 +#: streetup/contracts/views_pdf_reports.py:946 +#: streetup/contracts/views_pdf_reports.py:1139 +msgid "Description" +msgstr "Beschrijving" + +#: streetup/contracts/forms.py:136 streetup/contracts/models.py:950 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:74 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:588 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:709 +#: streetup/contracts/views_pdf_reports.py:949 +#: streetup/contracts/views_pdf_reports.py:1142 +msgid "Quantité" +msgstr "Hoeveelheid" + +#: streetup/contracts/forms.py:166 streetup/contracts/forms.py:168 +msgid "Poste invalide." +msgstr "Ongeldige post." + +#: streetup/contracts/forms.py:193 +msgid "Ex: Facture fournisseur, Bon de livraison..." +msgstr "Bijv.: Leveranciersfactuur, Leveringsbon..." + +#: streetup/contracts/forms.py:200 +msgid "Titre du justificatif" +msgstr "Titel bewijsstuk" + +#: streetup/contracts/forms.py:201 streetup/contracts/models.py:862 +#: streetup/contracts/models.py:1034 +#: streetup/contracts/templates/contracts/contracts_index.html:201 +msgid "Fichier" +msgstr "Bestand" + +#: streetup/contracts/forms.py:225 +msgid "Fichiers" +msgstr "Bestanden" + +#: streetup/contracts/models.py:39 +msgid "Maintenance corrective" +msgstr "Correctief onderhoud" + +#: streetup/contracts/models.py:40 +msgid "Maintenance préventive" +msgstr "Preventief onderhoud" + +#: streetup/contracts/models.py:41 +msgid "Maintenance améliorative" +msgstr "Verbeterend onderhoud" + +#: streetup/contracts/models.py:47 +msgid "Code du type de maintenance (corrective, preventive, ameliorative)" +msgstr "Code van het onderhoudstype (correctief, preventief, verbeterend)" + +#: streetup/contracts/models.py:48 +msgid "Nom (FR)" +msgstr "Naam (FR)" + +#: streetup/contracts/models.py:49 +msgid "Nom (NL)" +msgstr "Naam (NL)" + +#: streetup/contracts/models.py:64 +msgid "Type de maintenance" +msgstr "Onderhoudstype" + +#: streetup/contracts/models.py:65 +msgid "Types de maintenance" +msgstr "Onderhoudstypen" + +#: streetup/contracts/models.py:78 +msgid "Catégories d'assets gérées" +msgstr "" + +#: streetup/contracts/models.py:79 +msgid "Catégories d'assets dont cette équipe est responsable." +msgstr "" + +#: streetup/contracts/models.py:83 +msgid "Gestionnaire d'assets" +msgstr "" + +#: streetup/contracts/models.py:84 +msgid "" +"Indique si cette équipe est gestionnaire des catégories d'assets associées." +msgstr "" + +#: streetup/contracts/models.py:88 +#: streetup/contracts/templates/contracts/contracts_detail.html:26 +#: streetup/contracts/templates/contracts/contracts_index.html:242 +msgid "Prestataire" +msgstr "Aannemer" + +#: streetup/contracts/models.py:89 +msgid "Indique si cette équipe est un prestataire extérieur." +msgstr "" + +#: streetup/contracts/models.py:93 +msgid "Peut générer le rapport journalier" +msgstr "Kan het dagelijkse rapport genereren" + +#: streetup/contracts/models.py:94 +msgid "" +"Indique si les membres de cette équipe peuvent générer un rapport quotidien " +"des interventions." +msgstr "" +"Geeft aan of de leden van dit team een dagelijks rapport van de interventies " +"kunnen genereren." + +#: streetup/contracts/models.py:98 +msgid "Photos obligatoires" +msgstr "" + +#: streetup/contracts/models.py:99 +msgid "" +"Si activé, au moins une photo doit être ajoutée à l'intervention avant de " +"pouvoir la terminer." +msgstr "" + +#: streetup/contracts/models.py:103 +msgid "Restreindre la visibilité aux interventions du jour" +msgstr "" + +#: streetup/contracts/models.py:104 +msgid "" +"Si activé, les interventions ne sont visibles qu'à partir d'une certaine " +"heure, et uniquement pour le jour même." +msgstr "" + +#: streetup/contracts/models.py:109 +msgid "Heure de début de visibilité" +msgstr "" + +#: streetup/contracts/models.py:110 +msgid "" +"Heure locale à partir de laquelle les interventions du jour deviennent " +"visibles (ex: 06:00:00)." +msgstr "" + +#: streetup/contracts/models.py:114 +#, fuzzy +#| msgid "Nombre d'interventions par personne" +msgid "Restreindre aux interventions assignées" +msgstr "Aantal interventies per persoon" + +#: streetup/contracts/models.py:115 +msgid "" +"Si activé, les membres de cette équipe ne voient que les interventions " +"assignées à l'équipe ou directement à un membre." +msgstr "" + +#: streetup/contracts/models.py:122 +msgid "Équipe de société/organisme" +msgstr "Team van bedrijf/organisatie" + +#: streetup/contracts/models.py:123 +msgid "Équipes de sociétés/organismes" +msgstr "Teams van bedrijven/organisaties" + +#: streetup/contracts/models.py:135 +#, fuzzy +#| msgid "Équipe" +msgid "Équipes" +msgstr "Team" + +#: streetup/contracts/models.py:136 +msgid "Équipes auxquelles ce membre appartient." +msgstr "" + +#: streetup/contracts/models.py:142 +msgid "Types de maintenance visibles" +msgstr "Zichtbare onderhoudstypen" + +#: streetup/contracts/models.py:143 +msgid "" +"Types de maintenance visibles par défaut pour cet utilisateur. Si vide, tous " +"les types sont visibles. Les interventions assignées à l'utilisateur restent " +"toujours visibles." +msgstr "" +"Standaard zichtbare onderhoudstypen voor deze gebruiker. Indien leeg, zijn " +"alle typen zichtbaar. Aan de gebruiker toegewezen interventies blijven " +"altijd zichtbaar." + +#: streetup/contracts/models.py:178 +#, fuzzy +#| msgid "Nombre" +msgid "Membre" +msgstr "Aantal" + +#: streetup/contracts/models.py:186 streetup/contracts/models.py:222 +#: streetup/contracts/templates/contracts/teams_index.html:349 +msgid "Équipe" +msgstr "Team" + +#: streetup/contracts/models.py:188 +#, fuzzy +#| msgid "Date de création" +msgid "Date d'entrée" +msgstr "Aanmaakdatum" + +#: streetup/contracts/models.py:189 +#, fuzzy +#| msgid "Date de création" +msgid "Date de sortie" +msgstr "Aanmaakdatum" + +#: streetup/contracts/models.py:196 +#, fuzzy +#| msgid "Commandé par" +msgid "Modifié par" +msgstr "Besteld door" + +#: streetup/contracts/models.py:205 +msgid "Historique d'équipe d'un membre" +msgstr "" + +#: streetup/contracts/models.py:206 +msgid "Historiques d'équipe des membres" +msgstr "" + +#: streetup/contracts/models.py:216 +msgid "Configuration utilisateur" +msgstr "" + +#: streetup/contracts/models.py:226 +msgid "Peut consulter" +msgstr "Kan raadplegen" + +#: streetup/contracts/models.py:227 +msgid "L'utilisateur peut voir cette équipe et ses membres." +msgstr "" + +#: streetup/contracts/models.py:231 +msgid "Peut éditer" +msgstr "Kan bewerken" + +#: streetup/contracts/models.py:232 +msgid "" +"L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer " +"des membres entre équipes." +msgstr "" + +#: streetup/contracts/models.py:240 +msgid "Accès utilisateur à une équipe" +msgstr "" + +#: streetup/contracts/models.py:241 +msgid "Accès utilisateurs aux équipes" +msgstr "" + +#: streetup/contracts/models.py:259 +msgid "Emails en copie (CC)" +msgstr "E-mails in kopie (CC)" + +#: streetup/contracts/models.py:260 +msgid "" +"Adresses email à ajouter en copie des notifications (séparées par des " +"virgules ou points-virgules)" +msgstr "" +"E-mailadressen toe te voegen in kopie van meldingen (gescheiden door komma's " +"of puntkomma's)" + +#: streetup/contracts/models.py:264 +msgid "Envoyer des notifications liées aux interventions par email." +msgstr "Meldingen over interventies per e-mail versturen." + +#: streetup/contracts/models.py:270 +msgid "Nom du signataire (prestataire)" +msgstr "Naam ondertekenaar (aannemer)" + +#: streetup/contracts/models.py:271 +msgid "Nom de la personne qui signe les documents côté prestataire." +msgstr "Naam van de persoon die namens de aannemer documenten ondertekent." + +#: streetup/contracts/models.py:277 +msgid "Rôle du signataire (prestataire)" +msgstr "Rol ondertekenaar (aannemer)" + +#: streetup/contracts/models.py:278 +msgid "Rôle/fonction de la personne qui signe les documents côté prestataire." +msgstr "" +"Rol/functie van de persoon die namens de aannemer documenten ondertekent." + +#: streetup/contracts/models.py:284 +msgid "Téléphone de contact" +msgstr "Contacttelefoon" + +#: streetup/contracts/models.py:285 +msgid "" +"Numéro de téléphone du technicien de garde ou contact principal pour les " +"urgences." +msgstr "" +"Telefoonnummer van de wachttechnicus of hoofdcontact voor noodgevallen." + +#: streetup/contracts/models.py:291 +msgid "Thématiques" +msgstr "Thema's" + +#: streetup/contracts/models.py:292 +msgid "Thématiques associées à ce contrat." +msgstr "Thema's die aan dit contract zijn gekoppeld." + +#: streetup/contracts/models.py:296 +msgid "Actif" +msgstr "Actief" + +#: streetup/contracts/models.py:297 +msgid "Indique si le contrat est actif." +msgstr "Geeft aan of het contract actief is." + +#: streetup/contracts/models.py:301 +msgid "Nécessite une approbation" +msgstr "" + +#: streetup/contracts/models.py:302 +msgid "" +"Indique si les interventions liées à ce contrat nécessitent une approbation " +"avant d'être transmises pour traitement." +msgstr "" + +#: streetup/contracts/models.py:306 +msgid "Nécessite une vérification" +msgstr "" + +#: streetup/contracts/models.py:307 +msgid "" +"Indique si les interventions liées à ce contrat nécessitent une vérification " +"avant d'être transmises pour traitement." +msgstr "" + +#: streetup/contracts/models.py:311 +msgid "Utilise les occupations" +msgstr "" + +#: streetup/contracts/models.py:312 +msgid "" +"Indique si les interventions liées à ce contrat utilisent la gestion des " +"occupations (temps passé par membre)." +msgstr "" + +#: streetup/contracts/models.py:316 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Utilise les gestionnaires d'intervention" +msgstr "Kan een lege offerte niet bevriezen." + +#: streetup/contracts/models.py:317 +msgid "" +"Indique si les interventions liées à ce contrat utilisent un gestionnaire " +"d'intervention." +msgstr "" + +#: streetup/contracts/models.py:321 +msgid "Occupations obligatoires" +msgstr "" + +#: streetup/contracts/models.py:322 +msgid "" +"Si activé, au moins une occupation doit être enregistrée avant de terminer " +"une intervention corrective liée à ce contrat." +msgstr "" + +#: streetup/contracts/models.py:326 +msgid "Avertir si aucune prestation au passage à 'Traité'" +msgstr "" + +#: streetup/contracts/models.py:327 +msgid "" +"Si activé, un avertissement est affiché lorsque l'utilisateur tente de " +"passer une intervention au statut 'Traité' sans avoir encodé de prestation." +msgstr "" + +#: streetup/contracts/models.py:331 +#, fuzzy +#| msgid "Rapport journalier" +msgid "Envoi groupé journalier" +msgstr "Dagelijks rapport" + +#: streetup/contracts/models.py:332 +msgid "" +"Si activé, les fiches d'intervention du jour sont regroupées dans un seul " +"email envoyé en fin de journée, plutôt qu'un email par intervention." +msgstr "" + +#: streetup/contracts/models.py:336 +msgid "Heure d'envoi du résumé journalier" +msgstr "" + +#: streetup/contracts/models.py:337 +msgid "" +"Heure (0-23) à laquelle le résumé journalier des interventions est envoyé " +"(heure locale)." +msgstr "" + +#: streetup/contracts/models.py:341 +msgid "Emails uniquement si priorité urgente" +msgstr "" + +#: streetup/contracts/models.py:342 +msgid "" +"Si activé, les notifications email ne sont envoyées que pour les " +"interventions de priorité 'Urgent'." +msgstr "" + +#: streetup/contracts/models.py:436 +#: streetup/contracts/templates/contracts/contracts_index.html:139 +msgid "En préparation" +msgstr "In voorbereiding" + +#: streetup/contracts/models.py:437 +#: streetup/contracts/templates/contracts/contracts_index.html:144 +msgid "Envoyé" +msgstr "Verzonden" + +#: streetup/contracts/models.py:438 +#: streetup/contracts/templates/contracts/contracts_index.html:149 +msgid "Terminé" +msgstr "Afgerond" + +#: streetup/contracts/models.py:439 +#: streetup/contracts/templates/contracts/contracts_index.html:154 +msgid "Comptabilisé" +msgstr "Geboekt" + +#: streetup/contracts/models.py:440 +#: streetup/contracts/templates/contracts/contracts_index.html:159 +msgid "Payé" +msgstr "Betaald" + +#: streetup/contracts/models.py:441 +#: streetup/contracts/templates/contracts/contracts_index.html:164 +msgid "Annulé" +msgstr "Geannuleerd" + +#: streetup/contracts/models.py:460 +msgid "Projets" +msgstr "Projecten" + +#: streetup/contracts/models.py:826 +msgid "État d'avancement associé à ce document, le cas échéant" +msgstr "Voortgangsstatus gekoppeld aan dit document, indien van toepassing" + +#: streetup/contracts/models.py:852 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:47 +#: streetup/contracts/views_pdf_reports.py:432 +msgid "Déclaration de créance" +msgstr "Schuldvordering" + +#: streetup/contracts/models.py:856 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:52 +#: streetup/contracts/templates/contracts/contracts_detail.html:116 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:768 +#: streetup/contracts/templates/contracts/teams_index.html:741 +msgid "Titre" +msgstr "Titel" + +#: streetup/contracts/models.py:857 +msgid "Titre ou description du justificatif" +msgstr "Titel of beschrijving van het bewijsstuk" + +#: streetup/contracts/models.py:864 streetup/contracts/models.py:1045 +msgid "Date d'ajout" +msgstr "Toevoegingsdatum" + +#: streetup/contracts/models.py:871 streetup/contracts/models.py:1052 +msgid "Ajouté par" +msgstr "Toegevoegd door" + +#: streetup/contracts/models.py:878 +msgid "Pièce jointe de déclaration de créance" +msgstr "Bijlage schuldvordering" + +#: streetup/contracts/models.py:879 +msgid "Pièces jointes de déclarations de créance" +msgstr "Bijlagen schuldvorderingen" + +#: streetup/contracts/models.py:886 +msgid "Numéro d'ordre de l'état d'avancement pour cette commande" +msgstr "Volgordenummer van de voortgangsstatus voor deze bestelling" + +#: streetup/contracts/models.py:905 +msgid "Quantité consommée à cette date" +msgstr "Op deze datum verbruikte hoeveelheid" + +#: streetup/contracts/models.py:926 +#, fuzzy +#| msgid "Date de livraison" +msgid "En attente de validation" +msgstr "Leverdatum" + +#: streetup/contracts/models.py:927 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:616 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:926 +msgid "Approuvé" +msgstr "" + +#: streetup/contracts/models.py:928 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:621 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:927 +msgid "Refusé" +msgstr "" + +#: streetup/contracts/models.py:929 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:623 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:929 +msgid "Facturé" +msgstr "Gefactureerd" + +#: streetup/contracts/models.py:939 streetup/contracts/models.py:1029 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:53 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:86 +msgid "Commande" +msgstr "Bestelling" + +#: streetup/contracts/models.py:945 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:699 +msgid "Poste de marché" +msgstr "" + +#: streetup/contracts/models.py:952 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:591 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:713 +msgid "Commentaire" +msgstr "Opmerking" + +#: streetup/contracts/models.py:957 +msgid "Statut de validation" +msgstr "Validatiestatus" + +#: streetup/contracts/models.py:959 +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:54 +msgid "Date de création" +msgstr "Aanmaakdatum" + +#: streetup/contracts/models.py:966 +msgid "Créé par" +msgstr "Aangemaakt door" + +#: streetup/contracts/models.py:968 +#, fuzzy +#| msgid "Date de livraison" +msgid "Date de validation" +msgstr "Leverdatum" + +#: streetup/contracts/models.py:975 +msgid "Validé par" +msgstr "Gevalideerd door" + +#: streetup/contracts/models.py:981 +#, fuzzy +#| msgid "Quantité prévue" +msgid "Quantité déjà facturée" +msgstr "Geplande hoeveelheid" + +#: streetup/contracts/models.py:982 +#, fuzzy +#| msgid "Générer la déclaration de créance" +msgid "Quantité cumulée sur les déclarations de créance précédentes." +msgstr "Schuldvordering genereren" + +#: streetup/contracts/models.py:1017 +#, fuzzy +#| msgid "Consommation des postes" +msgid "Commande directe – poste" +msgstr "Verbruik van de posten" + +#: streetup/contracts/models.py:1018 +#, fuzzy +#| msgid "Consommation des postes" +msgid "Commandes directes – postes" +msgstr "Verbruik van de posten" + +#: streetup/contracts/models.py:1043 +msgid "Vignette" +msgstr "Miniatuur" + +#: streetup/contracts/models.py:1064 +msgid "Pièce jointe de commande" +msgstr "Bestelbon bijlage" + +#: streetup/contracts/models.py:1065 +msgid "Pièces jointes de commande" +msgstr "Bestelbon bijlagen" + +#: streetup/contracts/permissions.py:98 +msgid "Paramètre 'contract_id' manquant." +msgstr "Parameter 'contract_id' ontbreekt." + +#: streetup/contracts/permissions.py:103 +msgid "Accès au contrat refusé." +msgstr "Toegang tot het contract geweigerd." + +#: streetup/contracts/permissions.py:132 +msgid "Paramètre 'order_id' manquant." +msgstr "Parameter 'order_id' ontbreekt." + +#: streetup/contracts/permissions.py:137 streetup/contracts/views.py:976 +msgid "Accès à la commande refusé." +msgstr "Toegang tot de bestelling geweigerd." + +#: streetup/contracts/templates/admin/contracts/contractpost/change_list.html:7 +msgid "Importer / mettre à jour des postes (Excel)" +msgstr "" + +#: streetup/contracts/templates/admin/contracts/contractpost/import_posts.html:8 +msgid "Accueil" +msgstr "" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:27 +msgid "Contrats" +msgstr "Contracten" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:29 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:474 +msgid "Justificatifs" +msgstr "Bewijsstukken" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:33 +msgid "Justificatifs de la déclaration de créance" +msgstr "Bewijsstukken van de schuldvordering" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:58 +msgid "Voir le document" +msgstr "Document bekijken" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:68 +msgid "Ajouter un justificatif" +msgstr "Bewijsstuk toevoegen" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:90 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:313 +#: streetup/contracts/templates/contracts/teams_index.html:324 +msgid "Ajouter" +msgstr "Toevoegen" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:101 +msgid "Justificatifs attachés" +msgstr "Bijgevoegde bewijsstukken" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:117 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:466 +msgid "Ajouté le" +msgstr "Toegevoegd op" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:119 +msgid "par" +msgstr "door" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:125 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:332 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:333 +msgid "Télécharger" +msgstr "Downloaden" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:128 +msgid "Êtes-vous sûr de vouloir supprimer ce justificatif ?" +msgstr "Weet u zeker dat u dit bewijsstuk wilt verwijderen?" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:130 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:238 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:290 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:642 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:743 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:949 +msgid "Supprimer" +msgstr "Verwijderen" + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:141 +msgid "" +"Aucun justificatif n'a encore été ajouté à cette déclaration de créance." +msgstr "Er is nog geen bewijsstuk toegevoegd aan deze schuldvordering." + +#: streetup/contracts/templates/contracts/claim_declaration_attachments.html:149 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:31 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:154 +msgid "Retour à la commande" +msgstr "Terug naar de bestelling" + +#: streetup/contracts/templates/contracts/contracts_detail.html:19 +msgid "Détail du marché / contrat" +msgstr "Detail van opdracht/contract" + +#: streetup/contracts/templates/contracts/contracts_detail.html:28 +#: streetup/contracts/templates/contracts/contracts_detail.html:117 +#: streetup/contracts/templates/contracts/contracts_index.html:243 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:769 +msgid "Début" +msgstr "Begin" + +#: streetup/contracts/templates/contracts/contracts_detail.html:29 +#: streetup/contracts/templates/contracts/contracts_index.html:244 +msgid "Fin" +msgstr "Einde" + +#: streetup/contracts/templates/contracts/contracts_detail.html:31 +msgid "Fonctionnaire dirigeant" +msgstr "Leidend ambtenaar" + +#: streetup/contracts/templates/contracts/contracts_detail.html:34 +msgid "Notifications email" +msgstr "E-mailmeldingen" + +#: streetup/contracts/templates/contracts/contracts_detail.html:36 +msgid "Oui" +msgstr "Ja" + +#: streetup/contracts/templates/contracts/contracts_detail.html:38 +msgid "Non" +msgstr "Nee" + +#: streetup/contracts/templates/contracts/contracts_detail.html:45 +msgid "Montant engagé" +msgstr "Vastgelegd bedrag" + +#: streetup/contracts/templates/contracts/contracts_detail.html:49 +msgid "Montant \"presté" +msgstr "Gepresteerd bedrag" + +#: streetup/contracts/templates/contracts/contracts_detail.html:67 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:135 +msgid "Montant validé" +msgstr "Goedgekeurd bedrag" + +#: streetup/contracts/templates/contracts/contracts_detail.html:87 +msgid "Dossier interne" +msgstr "Intern dossier" + +#: streetup/contracts/templates/contracts/contracts_detail.html:96 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:212 +msgid "Retour à la liste" +msgstr "Terug naar de lijst" + +#: streetup/contracts/templates/contracts/contracts_detail.html:104 +#: streetup/contracts/templates/contracts/contracts_index.html:120 +msgid "Commandes" +msgstr "Bestellingen" + +#: streetup/contracts/templates/contracts/contracts_detail.html:115 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:70 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:496 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:584 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:767 +#: streetup/contracts/templates/contracts/teams_index.html:740 +#: streetup/contracts/views_pdf_reports.py:945 +#: streetup/contracts/views_pdf_reports.py:1138 +msgid "Code" +msgstr "Code" + +#: streetup/contracts/templates/contracts/contracts_detail.html:118 +#: streetup/contracts/templates/contracts/contracts_index.html:200 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:592 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:770 +#: streetup/contracts/templates/contracts/teams_index.html:743 +msgid "Statut" +msgstr "Status" + +#: streetup/contracts/templates/contracts/contracts_index.html:114 +msgid "Nouvelle commande" +msgstr "Nieuwe bestelling" + +#: streetup/contracts/templates/contracts/contracts_index.html:123 +msgid "Marchés/Contrats" +msgstr "Opdrachten/Contracten" + +#: streetup/contracts/templates/contracts/contracts_index.html:134 +msgid "Statuts" +msgstr "Statussen" + +#: streetup/contracts/templates/contracts/contracts_index.html:171 +msgid "Commandes à partir du" +msgstr "Bestellingen vanaf" + +#: streetup/contracts/templates/contracts/contracts_index.html:175 +msgid "jusqu'au" +msgstr "tot" + +#: streetup/contracts/templates/contracts/contracts_index.html:181 +msgid "Filtrer" +msgstr "Filteren" + +#: streetup/contracts/templates/contracts/contracts_index.html:182 +msgid "Réinitialiser" +msgstr "Resetten" + +#: streetup/contracts/templates/contracts/contracts_index.html:197 +msgid "Numéro de commande" +msgstr "Bestelnummer" + +#: streetup/contracts/templates/contracts/contracts_index.html:199 +msgid "Date" +msgstr "Datum" + +#: streetup/contracts/templates/contracts/contracts_index.html:202 +msgid "Marché" +msgstr "Opdracht" + +#: streetup/contracts/templates/contracts/contracts_index.html:240 +msgid "Numéro" +msgstr "Nummer" + +#: streetup/contracts/templates/contracts/contracts_index.html:289 +msgid "Aucune commande trouvée" +msgstr "Geen bestelling gevonden" + +#: streetup/contracts/templates/contracts/contracts_index.html:294 +msgid "Aucun marché trouvé" +msgstr "Geen opdracht gevonden" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:22 +msgid "Modifier la commande" +msgstr "Bestelling bewerken" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:24 +msgid "Ajouter une nouvelle commande" +msgstr "Nieuwe bestelling toevoegen" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:35 +msgid "Mettre à jour" +msgstr "Bijwerken" + +#: streetup/contracts/templates/contracts/contracts_order_form.html:35 +msgid "Créer la commande" +msgstr "Bestelling aanmaken" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:29 +#, python-format +msgid "Devis pour la commande %(order_code)s" +msgstr "Offerte voor bestelling %(order_code)s" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:38 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:112 +msgid "Non renseigné" +msgstr "Niet gespecificeerd" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:39 +msgid "Montant total du devis" +msgstr "Totaalbedrag van de offerte" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:54 +#, python-format +msgid "Ce devis est figé depuis le %(finalized_at)s." +msgstr "Deze offerte is bevroren sinds %(finalized_at)s." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:56 +#: streetup/contracts/views.py:460 +msgid "Ce devis est figé et ne peut plus être modifié." +msgstr "Deze offerte is bevroren en kan niet meer worden gewijzigd." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:69 +msgid "N° de poste" +msgstr "Postnr." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:72 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:586 +#: streetup/contracts/views_pdf_reports.py:947 +#: streetup/contracts/views_pdf_reports.py:1140 +msgid "Unité" +msgstr "Eenheid" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:73 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:510 +#: streetup/contracts/views_pdf_reports.py:1141 +msgid "Prix unitaire" +msgstr "Eenheidsprijs" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:75 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:590 +#: streetup/contracts/views_pdf_reports.py:1143 +msgid "Montant" +msgstr "Bedrag" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:81 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:87 +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:93 +msgid "Filtrer…" +msgstr "Filteren…" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:103 +msgid "Quantité > 0" +msgstr "Hoeveelheid > 0" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:140 +msgid "Aucun poste disponible pour ce contrat." +msgstr "Geen post beschikbaar voor dit contract." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:145 +msgid "Aucun poste ne correspond aux filtres." +msgstr "Geen post komt overeen met de filters." + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:159 +msgid "Enregistrer le devis" +msgstr "Offerte opslaan" + +#: streetup/contracts/templates/contracts/contracts_order_quote_form.html:163 +msgid "Figer le devis" +msgstr "Offerte bevriezen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:64 +msgid "Détail de la commande" +msgstr "Detail van de bestelling" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:99 +msgid "Contrat lié" +msgstr "Gekoppeld contract" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:105 +msgid "Date de commande" +msgstr "Besteldatum" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:111 +msgid "Montant prévisionnel" +msgstr "Raming bedrag" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:117 +msgid "Montant \"presté\"" +msgstr "Gepresteerd bedrag" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:156 +msgid "Devis" +msgstr "Offerte" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:158 +msgid "Montant total" +msgstr "Totaalbedrag" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:160 +msgid "Devis figé" +msgstr "Bevroren offerte" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:167 +#, python-format +msgid "" +"\n" +" %(item_count)s poste dans le devis.\n" +" " +msgid_plural "" +"\n" +" %(item_count)s postes dans le devis.\n" +" " +msgstr[0] "" +"\n" +" %(item_count)s post in de offerte.\n" +" " +msgstr[1] "" +"\n" +" %(item_count)s posten in de offerte.\n" +" " + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:174 +msgid "Aucun devis n'est associé à cette commande pour le moment." +msgstr "Momenteel is er geen offerte gekoppeld aan deze bestelling." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:181 +msgid "Voir le devis" +msgstr "Offerte bekijken" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:183 +msgid "Gérer le devis" +msgstr "Offerte beheren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:189 +msgid "Exporter en PDF" +msgstr "Exporteren naar PDF" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:198 +msgid "Commandé par" +msgstr "Besteld door" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:205 +msgid "Télécharger le fichier" +msgstr "Bestand downloaden" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:219 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:329 +msgid "Documents" +msgstr "Documenten" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:221 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:301 +msgid "Ajouter des documents" +msgstr "Documenten toevoegen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:270 +msgid "Aucun document associé à cette commande." +msgstr "Geen document gekoppeld aan deze bestelling." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:281 +msgid "Supprimer le document" +msgstr "Document verwijderen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:285 +msgid "Voulez-vous supprimer ce document ?" +msgstr "Wilt u dit document verwijderen?" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:289 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:719 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:742 +#: streetup/contracts/templates/contracts/teams_index.html:322 +#: streetup/contracts/templates/contracts/teams_index.html:363 +msgid "Annuler" +msgstr "Annuleren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:374 +msgid "Précédent" +msgstr "Vorige" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:379 +msgid "Suivant" +msgstr "Volgende" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:416 +msgid "Documents financiers" +msgstr "Financiële documenten" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:421 +msgid "Générer un draft de l'état de travaux" +msgstr "Draft van de staat van werken genereren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:424 +msgid "Générer un draft de la déclaration de créance" +msgstr "Draft van de schuldvordering genereren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:430 +msgid "Générer la déclaration de créance" +msgstr "Schuldvordering genereren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:434 +msgid "" +"Cette opération va générer un état de travaux, une déclaration de créance et " +"faire passer les interventions validées au statut \"comptabilisé\"." +msgstr "" +"Deze actie zal een staat van werken en een schuldvordering genereren en de " +"gevalideerde interventies op de status \"gefactureerd\" zetten." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:444 +#, python-format +msgid "État d'avancement %(num)s" +msgstr "Voortgangsrapport %(num)s" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:448 +msgid "Autres documents" +msgstr "Andere documenten" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:465 +msgid "Type" +msgstr "Type" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:468 +msgid "justificatif(s)" +msgstr "bewijsstuk(ken)" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:473 +msgid "Gérer les justificatifs" +msgstr "Bewijsstukken beheren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:487 +msgid "Consommation des postes" +msgstr "Verbruik van de posten" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:495 +msgid "Poste" +msgstr "Post" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:499 +msgid "Quantité prévue" +msgstr "Geplande hoeveelheid" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:502 +msgid "État d avancement du" +msgstr "Voortgangsstatus van" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:503 +msgid "État" +msgstr "Status" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:506 +msgid "Quantité consommée" +msgstr "Verbruikte hoeveelheid" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:508 +msgid "Quantité restante" +msgstr "Resterende hoeveelheid" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:511 +msgid "Montant consommé" +msgstr "Verbruikt bedrag" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:550 +msgid "Aucune consommation enregistrée pour cette commande." +msgstr "Geen verbruik geregistreerd voor deze bestelling." + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:557 +msgid "Projets associés" +msgstr "Gekoppelde projecten" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:567 +#: streetup/contracts/views_pdf_reports.py:713 +#, fuzzy +#| msgid "Commandes" +msgid "Commandes directes" +msgstr "Bestellingen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:569 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:693 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1110 +#, fuzzy +#| msgid "Ajouter un justificatif" +msgid "Ajouter un poste direct" +msgstr "Bewijsstuk toevoegen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:577 +msgid "" +"Les commandes directes permettent d'enregistrer des postes de marché " +"consommés sans passer par une intervention. Les postes validés sont inclus " +"dans les totaux, les états d'avancement et les déclarations de créance." +msgstr "" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:583 +msgid "N°" +msgstr "Nr" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:587 +#, fuzzy +#| msgid "Prix unitaire" +msgid "Prix unit." +msgstr "Eenheidsprijs" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:589 +#, fuzzy +#| msgid "Facturé" +msgid "Qté facturée" +msgstr "Gefactureerd" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:593 +msgid "Actions" +msgstr "Acties" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:625 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:928 +msgid "En attente" +msgstr "" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:635 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:944 +msgid "Modifier" +msgstr "Bewerken" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:652 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:956 +msgid "Valider" +msgstr "Valideren" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:660 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:961 +msgid "Refuser" +msgstr "" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:668 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:966 +msgid "Remettre en attente" +msgstr "" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:678 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1213 +#, fuzzy +#| msgid "Aucune commande trouvée" +msgid "Aucune commande directe enregistrée." +msgstr "Geen bestelling gevonden" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:700 +msgid "Rechercher par code, numéro ou description…" +msgstr "" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:721 +#: streetup/contracts/templates/contracts/teams_index.html:365 +msgid "Enregistrer" +msgstr "Opslaan" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:735 +#, fuzzy +#| msgid "Supprimer le document" +msgid "Supprimer le poste direct" +msgstr "Document verwijderen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:739 +#, fuzzy +#| msgid "Voulez-vous supprimer ce document ?" +msgid "Voulez-vous supprimer ce poste direct ?" +msgstr "Wilt u dit document verwijderen?" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:755 +msgid "Ajouter une intervention" +msgstr "Interventie toevoegen" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1107 +#, fuzzy +#| msgid "Modifier la commande" +msgid "Modifier le poste direct" +msgstr "Bestelling bewerken" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1143 +msgid "Veuillez sélectionner un poste de marché." +msgstr "" + +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1173 +#: streetup/contracts/templates/contracts/contracts_orders_detail.html:1178 +#: streetup/contracts/templates/contracts/teams_index.html:436 +#: streetup/contracts/templates/contracts/teams_index.html:440 +msgid "Une erreur est survenue." +msgstr "Er is een fout opgetreden." + +#: streetup/contracts/templates/contracts/teams_index.html:130 +#, fuzzy +#| msgid "Consommation des postes" +msgid "Gestion des équipes" +msgstr "Verbruik van de posten" + +#: streetup/contracts/templates/contracts/teams_index.html:137 +msgid "Aucune équipe accessible." +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:150 +#: streetup/contracts/templates/contracts/teams_index.html:157 +#, fuzzy +#| msgid "Non renseigné" +msgid "Non assignés" +msgstr "Niet gespecificeerd" + +#: streetup/contracts/templates/contracts/teams_index.html:152 +#, fuzzy +#| msgid "Aucun projet" +msgid "Aucune équipe" +msgstr "Geen project" + +#: streetup/contracts/templates/contracts/teams_index.html:156 +#: streetup/contracts/templates/contracts/teams_index.html:578 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Ajouter un membre non assigné" +msgstr "Interventie toevoegen" + +#: streetup/contracts/templates/contracts/teams_index.html:180 +#: streetup/contracts/templates/contracts/teams_index.html:353 +msgid "Assigner à une équipe" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:185 +#: streetup/contracts/templates/contracts/teams_index.html:258 +#, fuzzy +#| msgid "Supprimer le document" +msgid "Supprimer ce membre" +msgstr "Document verwijderen" + +#: streetup/contracts/templates/contracts/teams_index.html:195 +#: streetup/contracts/templates/contracts/teams_index.html:268 +msgid "Aucun membre" +msgstr "Geen lid" + +#: streetup/contracts/templates/contracts/teams_index.html:202 +#: streetup/contracts/templates/contracts/teams_index.html:276 +msgid "membre(s)" +msgstr "lid(s)" + +#: streetup/contracts/templates/contracts/teams_index.html:219 +#, fuzzy +#| msgid "Nombre d'interventions par personne" +msgid "Voir les interventions assignées" +msgstr "Aantal interventies per persoon" + +#: streetup/contracts/templates/contracts/teams_index.html:227 +#: streetup/contracts/templates/contracts/teams_index.html:298 +#: streetup/contracts/templates/contracts/teams_index.html:577 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Ajouter un membre" +msgstr "Interventie toevoegen" + +#: streetup/contracts/templates/contracts/teams_index.html:253 +msgid "Déplacer vers une autre équipe" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:305 +#: streetup/contracts/templates/contracts/teams_index.html:345 +msgid "Nom affiché" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:306 +msgid "Ex : Jean Dupont" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:309 +msgid "Compte utilisateur" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:311 +msgid "Sélectionner un utilisateur" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:337 +#, fuzzy +#| msgid "Modifier la commande" +msgid "Modifier le membre" +msgstr "Bestelling bewerken" + +#: streetup/contracts/templates/contracts/teams_index.html:351 +msgid "Ne pas modifier l'équipe" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:352 +msgid "Désassigner (aucune équipe)" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:380 +msgid "Interventions" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:387 +#: streetup/contracts/templates/contracts/teams_index.html:687 +#: streetup/contracts/templates/contracts/teams_index.html:763 +msgid "Chargement..." +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:402 +#: streetup/contracts/templates/contracts/teams_index.html:403 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Résumé intervention" +msgstr "Interventie toevoegen" + +#: streetup/contracts/templates/contracts/teams_index.html:410 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "Retour aux interventions" +msgstr "Interventie toevoegen" + +#: streetup/contracts/templates/contracts/teams_index.html:412 +msgid "Fermer" +msgstr "Sluiten" + +#: streetup/contracts/templates/contracts/teams_index.html:551 +#: streetup/contracts/templates/contracts/teams_index.html:633 +#, fuzzy +#| msgid "Le contrat est requis" +msgid "Le nom du membre est requis." +msgstr "Contract is vereist" + +#: streetup/contracts/templates/contracts/teams_index.html:634 +#, fuzzy +#| msgid "Le contrat est requis" +msgid "L\\'utilisateur est requis." +msgstr "Contract is vereist" + +#: streetup/contracts/templates/contracts/teams_index.html:651 +#, fuzzy +#| msgid "Supprimer le document" +msgid "Supprimer le membre" +msgstr "Document verwijderen" + +#: streetup/contracts/templates/contracts/teams_index.html:651 +msgid "Cette action est irréversible." +msgstr "Deze actie is onomkeerbaar." + +#: streetup/contracts/templates/contracts/teams_index.html:700 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de charger les interventions." +msgstr "Kan een lege offerte niet bevriezen." + +#: streetup/contracts/templates/contracts/teams_index.html:706 +#, fuzzy +#| msgid "Ajouter une intervention" +msgid "intervention(s)" +msgstr "Interventie toevoegen" + +#: streetup/contracts/templates/contracts/teams_index.html:709 +#, fuzzy +#| msgid "Aucun document associé à cette commande." +msgid "Aucune intervention assignée à cette équipe." +msgstr "Geen document gekoppeld aan deze bestelling." + +#: streetup/contracts/templates/contracts/teams_index.html:721 +#, fuzzy +#| msgid "Voir le document" +msgid "Voir le résumé" +msgstr "Document bekijken" + +#: streetup/contracts/templates/contracts/teams_index.html:742 +msgid "Membre assigné" +msgstr "" + +#: streetup/contracts/templates/contracts/teams_index.html:779 +msgid "Résumé non disponible." +msgstr "" + +#: streetup/contracts/views.py:269 streetup/contracts/views.py:873 +#: streetup/contracts/views.py:917 streetup/contracts/views.py:942 +msgid "Vous n'avez pas accès à cette commande." +msgstr "U hebt geen toegang tot deze bestelling." + +#: streetup/contracts/views.py:275 +msgid "" +"Vous n'avez pas de thématique d'intervention autorisée pour cette commande." +msgstr "U hebt geen toegestaan interventiethema voor deze bestelling." + +#: streetup/contracts/views.py:347 +msgid "Vous n'avez pas le droit d'ajouter une commande pour ce contrat." +msgstr "U hebt geen recht om een bestelling voor dit contract toe te voegen." + +#: streetup/contracts/views.py:388 +msgid "" +"Impossible de changer le contrat: des interventions sont déjà liées à cette " +"commande." +msgstr "" +"Contract kan niet worden gewijzigd: er zijn al interventies aan deze " +"bestelling gekoppeld." + +#: streetup/contracts/views.py:397 +msgid "Vous n'avez pas le droit de modifier une commande pour ce contrat." +msgstr "U hebt geen recht om een bestelling voor dit contract te wijzigen." + +#: streetup/contracts/views.py:416 +msgid "Vous n'avez pas le droit de modifier le devis de cette commande." +msgstr "U hebt geen recht om de offerte van deze bestelling te wijzigen." + +#: streetup/contracts/views.py:469 streetup/contracts/views.py:474 +#: streetup/contracts/views.py:480 +msgid "Les données reçues sont invalides." +msgstr "De ontvangen gegevens zijn ongeldig." + +#: streetup/contracts/views.py:523 +msgid "Le formulaire contient des erreurs." +msgstr "Het formulier bevat fouten." + +#: streetup/contracts/views.py:552 +msgid "Impossible de figer un devis vide." +msgstr "Kan een lege offerte niet bevriezen." + +#: streetup/contracts/views.py:555 +msgid "Le devis a été figé." +msgstr "De offerte is bevroren." + +#: streetup/contracts/views.py:558 +msgid "Le devis a été enregistré." +msgstr "De offerte is opgeslagen." + +#: streetup/contracts/views.py:702 +msgid "Accès refusé au document de la commande." +msgstr "Toegang tot het bestelbestand geweigerd." + +#: streetup/contracts/views.py:707 +msgid "Fichier introuvable" +msgstr "Bestand niet gevonden" + +#: streetup/contracts/views.py:732 +msgid "Le contrat est requis" +msgstr "Contract is vereist" + +#: streetup/contracts/views.py:735 +msgid "La référence de commande est requise" +msgstr "Bestelreferentie is vereist" + +#: streetup/contracts/views.py:738 +msgid "La date de commande est requise" +msgstr "Besteldatum is vereist" + +#: streetup/contracts/views.py:741 +msgid "La date de livraison est requise" +msgstr "Leverdatum is vereist" + +#: streetup/contracts/views.py:745 +msgid "Vous n'avez pas le droit d'ajouter une commande pour ce contrat" +msgstr "U hebt geen recht om een bestelling voor dit contract toe te voegen" + +#: streetup/contracts/views.py:752 +msgid "Une commande avec cette référence existe déjà pour ce contrat" +msgstr "Een bestelling met deze referentie bestaat al voor dit contract" + +#: streetup/contracts/views.py:775 +msgid "Données JSON invalides" +msgstr "Ongeldige JSON-gegevens" + +#: streetup/contracts/views.py:834 +msgid "Vous n'avez pas accès à ce contrat" +msgstr "U hebt geen toegang tot dit contract" + +#: streetup/contracts/views.py:882 +msgid "Le justificatif a été ajouté avec succès." +msgstr "Het bewijsstuk is succesvol toegevoegd." + +#: streetup/contracts/views.py:927 +#, python-format +msgid "Le justificatif « %(title)s » a été supprimé." +msgstr "Het bewijsstuk «%(title)s» is verwijderd." + +#: streetup/contracts/views.py:947 +msgid "" +"Seuls les managers et administrateurs peuvent changer le statut d'une " +"commande." +msgstr "" +"Alleen managers en beheerders kunnen de status van een bestelling wijzigen." + +#: streetup/contracts/views.py:956 streetup/contracts/views.py:1226 +msgid "Statut invalide." +msgstr "Ongeldige status." + +#: streetup/contracts/views.py:964 +msgid "Le statut de la commande a été mis à jour avec succès." +msgstr "De status van de bestelling is succesvol bijgewerkt." + +#: streetup/contracts/views.py:979 +msgid "Vous n'avez pas la permission d'ajouter des documents à cette commande." +msgstr "" +"U hebt geen toestemming om documenten aan deze bestelling toe te voegen." + +#: streetup/contracts/views.py:991 +msgid "Document(s) ajouté(s) avec succès." +msgstr "Document(en) succesvol toegevoegd." + +#: streetup/contracts/views.py:993 +msgid "Erreur lors de l'ajout des documents." +msgstr "Fout bij het toevoegen van documenten." + +#: streetup/contracts/views.py:1005 streetup/contracts/views.py:1043 +#: streetup/contracts/views.py:1074 streetup/contracts/views.py:1142 +#: streetup/contracts/views.py:1192 streetup/contracts/views.py:1212 +msgid "Accès refusé." +msgstr "Toegang geweigerd." + +#: streetup/contracts/views.py:1008 +msgid "Vous n'avez pas la permission de supprimer ce document." +msgstr "U hebt geen toestemming om dit document te verwijderen." + +#: streetup/contracts/views.py:1076 streetup/contracts/views.py:1144 +#: streetup/contracts/views.py:1194 streetup/contracts/views.py:1214 +msgid "Permission insuffisante." +msgstr "" + +#: streetup/contracts/views.py:1081 streetup/contracts/views.py:1153 +#: streetup/contracts/views.py:1221 +#, fuzzy +#| msgid "Données JSON invalides" +msgid "Données JSON invalides." +msgstr "Ongeldige JSON-gegevens" + +#: streetup/contracts/views.py:1088 +#, fuzzy +#| msgid "La date de commande est requise" +msgid "Le poste de marché est requis." +msgstr "Besteldatum is vereist" + +#: streetup/contracts/views.py:1095 +msgid "La quantité doit être un nombre positif." +msgstr "" + +#: streetup/contracts/views.py:1102 +msgid "" +"Un poste direct existe déjà pour ce poste dans cette commande. Modifiez-le " +"plutôt." +msgstr "" + +#: streetup/contracts/views.py:1148 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de modifier un poste déjà validé." +msgstr "Kan een lege offerte niet bevriezen." + +#: streetup/contracts/views.py:1163 +msgid "La quantité doit être un nombre positif ou nul." +msgstr "" + +#: streetup/contracts/views.py:1198 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de supprimer un poste déjà validé." +msgstr "Kan een lege offerte niet bevriezen." + +#: streetup/contracts/views.py:1200 +#, fuzzy +#| msgid "Impossible de figer un devis vide." +msgid "Impossible de supprimer un poste déjà facturé." +msgstr "Kan een lege offerte niet bevriezen." + +#: streetup/contracts/views_daily_report.py:227 +msgid "Rapport journalier des interventions" +msgstr "Dagelijks interventierapport" + +#: streetup/contracts/views_daily_report.py:248 +#: streetup/contracts/views_daily_report.py:393 +msgid "Synthèse par thématique" +msgstr "Samenvatting per thema" + +#: streetup/contracts/views_daily_report.py:251 +#: streetup/contracts/views_daily_report.py:403 +msgid "Thématique" +msgstr "Thema" + +#: streetup/contracts/views_daily_report.py:302 +#: streetup/contracts/views_daily_report.py:344 +#: streetup/contracts/views_daily_report.py:433 +#: streetup/contracts/views_daily_report.py:481 +msgid "TOTAL" +msgstr "TOTAAL" + +#: streetup/contracts/views_daily_report.py:314 +msgid "Nombre d'interventions par personne" +msgstr "Aantal interventies per persoon" + +#: streetup/contracts/views_daily_report.py:317 +#: streetup/contracts/views_daily_report.py:459 +msgid "Personne" +msgstr "Persoon" + +#: streetup/contracts/views_daily_report.py:317 +#: streetup/contracts/views_daily_report.py:459 +msgid "Nombre" +msgstr "Nummer" + +#: streetup/contracts/views_daily_report.py:397 +#: streetup/contracts/views_daily_report.py:454 +msgid "Rapport journalier" +msgstr "Dagelijks rapport" + +#: streetup/contracts/views_daily_report.py:451 +msgid "Par personne" +msgstr "Per persoon" + +#: streetup/contracts/views_daily_report.py:518 +msgid "Aucune de vos équipes n'est autorisée à générer ce rapport." +msgstr "Geen van uw teams is gemachtigd dit rapport te genereren." + +#: streetup/contracts/views_pdf_reports.py:301 +#: streetup/contracts/views_pdf_reports.py:481 +msgid "Service public régional de Bruxelles" +msgstr "Gewestelijke Overheidsdienst Brussel" + +#: streetup/contracts/views_pdf_reports.py:302 +#: streetup/contracts/views_pdf_reports.py:482 +msgid "Cahier des charges " +msgstr "Bestek " + +#: streetup/contracts/views_pdf_reports.py:307 +#: streetup/contracts/views_pdf_reports.py:487 +msgid "Bruxelles: " +msgstr "Brussel: " + +#: streetup/contracts/views_pdf_reports.py:310 +msgid "DECLARATION DE CREANCE" +msgstr "SCHULDVORDERING" + +#: streetup/contracts/views_pdf_reports.py:314 +msgid "Concerne:" +msgstr "Betreft:" + +#: streetup/contracts/views_pdf_reports.py:325 +#, python-format +msgid "" +"Je soussigné %(signatory)s, %(role)s, de la firme %(company)s, déclare que " +"le Service public régional de Bruxelles – Place Saint-Lazare 2 à 1035 " +"Bruxelles, est redevable de la somme de :" +msgstr "" +"Ondergetekende %(signatory)s, %(role)s, van de firma %(company)s, verklaart " +"dat de Gewestelijke Overheidsdienst Brussel – Place Saint-Lazare 2 te 1035 " +"Brussel, het bedrag verschuldigd is van:" + +#: streetup/contracts/views_pdf_reports.py:345 +#: streetup/contracts/views_pdf_reports.py:788 +#: streetup/contracts/views_pdf_reports.py:1295 +msgid "Total général TVA non comprise :" +msgstr "Algemeen totaal incl. btw:" + +#: streetup/contracts/views_pdf_reports.py:350 +#: streetup/contracts/views_pdf_reports.py:793 +#: streetup/contracts/views_pdf_reports.py:1307 +msgid "TVA 21 % :" +msgstr "Btw 21%:" + +#: streetup/contracts/views_pdf_reports.py:370 +msgid "euro" +msgid_plural "euros" +msgstr[0] "euro" +msgstr[1] "euro" + +#: streetup/contracts/views_pdf_reports.py:374 +msgid "centime" +msgid_plural "centimes" +msgstr[0] "cent" +msgstr[1] "cent" + +#: streetup/contracts/views_pdf_reports.py:375 +msgid "et" +msgstr "en" + +#: streetup/contracts/views_pdf_reports.py:380 +#, python-format +msgid "Montant total, TVA incluse : %(amount)s" +msgstr "Totaalbedrag, incl. btw: %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:399 +#, python-format +msgid "Declaration_Creance_%(slug_contract_number)s_%(slug_order_code)s.pdf" +msgstr "Schuldvordering_%(slug_contract_number)s_%(slug_order_code)s.pdf" + +#: streetup/contracts/views_pdf_reports.py:427 +#: streetup/contracts/views_pdf_reports.py:827 +msgid "Declaration_Creance" +msgstr "Schuldvordering" + +#: streetup/contracts/views_pdf_reports.py:490 +msgid "ETAT DE TRAVAUX" +msgstr "STAAT VAN WERKEN" + +#: streetup/contracts/views_pdf_reports.py:558 +#, python-format +msgid "Intervention %(code)s du %(date)s" +msgstr "Interventie %(code)s van %(date)s" + +#: streetup/contracts/views_pdf_reports.py:569 +msgid "périodes" +msgstr "periodes" + +#: streetup/contracts/views_pdf_reports.py:579 +#: streetup/contracts/views_pdf_reports.py:717 +msgid "Code poste" +msgstr "Bestekpost" + +#: streetup/contracts/views_pdf_reports.py:581 +#: streetup/contracts/views_pdf_reports.py:719 +#: streetup/contracts/views_pdf_reports.py:948 +msgid "PU" +msgstr "EP" + +#: streetup/contracts/views_pdf_reports.py:582 +#: streetup/contracts/views_pdf_reports.py:720 +msgid "Qté" +msgstr "Hvh." + +#: streetup/contracts/views_pdf_reports.py:583 +#: streetup/contracts/views_pdf_reports.py:721 +#: streetup/contracts/views_pdf_reports.py:950 +msgid "Total" +msgstr "Totaal" + +#: streetup/contracts/views_pdf_reports.py:681 +msgid "Aucun poste validé pour cette intervention" +msgstr "Geen post beschikbaar voor dit contract." + +#: streetup/contracts/views_pdf_reports.py:687 +#: streetup/contracts/views_pdf_reports.py:743 +msgid "Total HTVA :" +msgstr "Totaal excl. btw:" + +#: streetup/contracts/views_pdf_reports.py:798 +#: streetup/contracts/views_pdf_reports.py:1318 +msgid "Total général TVAC :" +msgstr "Algemeen totaal incl. btw:" + +#: streetup/contracts/views_pdf_reports.py:853 +msgid "Etat_de_travaux" +msgstr "Staat_van_werken" + +#: streetup/contracts/views_pdf_reports.py:856 +msgid "État de travaux" +msgstr "Staat van werken" + +#: streetup/contracts/views_pdf_reports.py:914 +#, python-format +msgid "Devis de la commande %(order)s" +msgstr "Offerte van bestelling %(order)s" + +#: streetup/contracts/views_pdf_reports.py:917 +#, python-format +msgid "Contrat %(contract)s – Commande %(order)s" +msgstr "Contract %(contract)s – Bestelling %(order)s" + +#: streetup/contracts/views_pdf_reports.py:927 +msgid "Postes du devis" +msgstr "Posten van de offerte" + +#: streetup/contracts/views_pdf_reports.py:944 +#: streetup/contracts/views_pdf_reports.py:1137 +msgid "N° poste" +msgstr "Postnr." + +#: streetup/contracts/views_pdf_reports.py:1033 +#, python-format +msgid "Total HTVA : %(amount)s" +msgstr "Totaal excl. btw: %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:1037 +#, python-format +msgid "TVA 21%% : %(amount)s" +msgstr "Btw 21%%: %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:1041 +#, python-format +msgid "Total TVAC : %(amount)s" +msgstr "Totaal incl. btw: %(amount)s" + +#: streetup/contracts/views_pdf_reports.py:1059 +msgid "Aucun devis n'est disponible pour cette commande." +msgstr "Er is geen offerte beschikbaar voor deze bestelling." + +#: streetup/contracts/views_pdf_reports.py:1065 +#, python-format +msgid "Devis_%(contract)s_%(order)s.pdf" +msgstr "Offerte_%(contract)s_%(order)s.pdf" + +#: streetup/contracts/views_pdf_reports.py:1084 +msgid "Postes" +msgstr "Post" + +#: streetup/contracts/views_pdf_reports.py:1103 +msgid "Marché :" +msgstr "Opdracht" + +#: streetup/contracts/views_pdf_reports.py:1109 +#: streetup/contracts/views_pdf_reports.py:1121 +msgid "Description :" +msgstr "Beschrijving" + +#: streetup/contracts/views_pdf_reports.py:1115 +msgid "Commande :" +msgstr "Bestelling" + +#: streetup/contracts/views_pdf_reports.py:1128 +msgid "Date :" +msgstr "Datum" + +#: streetup/contracts/views_pdf_reports.py:1144 +msgid "SAJ" +msgstr "SAJ" + +#: streetup/contracts/views_pdf_reports.py:1239 +msgid "Total général des fournitures et travaux revisables hors TVA :" +msgstr "Totaal excl. btw voor reviseerbare leveringen en werken:" + +#: streetup/contracts/views_pdf_reports.py:1250 +msgid "Coefficient de révision :" +msgstr "Herzieningscoëfficiënt:" + +#: streetup/contracts/views_pdf_reports.py:1265 +msgid "" +"Total général des fournitures et travaux non revisables hors TVA (SAJ) :" +msgstr "Totaal excl. btw voor niet-reviseerbare leveringen en werken (SAJ):" + +#: streetup/contracts/views_pdf_reports.py:1278 +#, python-format +msgid "" +"Total général des %(type)s non revisables hors TVA (SAJ) avec coefficient " +"%(coef)s :" +msgstr "" +"Totaal excl. btw voor niet-reviseerbare %(type)s (SAJ) met coëfficiënt " +"%(coef)s:" + +#: streetup/contracts/views_pdf_reports.py:1345 +msgid "Detail_postes" +msgstr "Detail_posten" + +#: streetup/contracts/views_pdf_reports.py:1349 +msgid "Détail des postes" +msgstr "Detail van de bestelling" + +#: streetup/contracts/views_pdf_reports.py:1372 +msgid "" +"Vous n'avez pas la permission de générer des documents pour cette commande." +msgstr "" +"U hebt geen toestemming om documenten voor deze bestelling te genereren." + +#: streetup/contracts/views_pdf_reports.py:1484 +msgid "Déclaration de créance créée et sauvegardée." +msgstr "Schuldvordering aangemaakt en opgeslagen." + +#~| msgid "Nouvelle commande" +#~ msgid "Nouvelle équipe" +#~ msgstr "Nieuw elftal" + +#, fuzzy +#~| msgid "Supprimer le document" +#~ msgid "Supprimer l'équipe" +#~ msgstr "Document verwijderen" + +#~ msgid "Créer" +#~ msgstr "Aanmaken" + +#, fuzzy +#~| msgid "Le contrat est requis" +#~ msgid "La société est requise." +#~ msgstr "Contract is vereist" + +#~ msgid "Total général HTVA :" +#~ msgstr "Algemeen totaal excl. btw:" diff --git a/streetup/contracts/locale/nl/LC_MESSAGES/djangojs.mo b/streetup/contracts/locale/nl/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..01bfd75 Binary files /dev/null and b/streetup/contracts/locale/nl/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/contracts/locale/nl/LC_MESSAGES/djangojs.po b/streetup/contracts/locale/nl/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..89ac41b --- /dev/null +++ b/streetup/contracts/locale/nl/LC_MESSAGES/djangojs.po @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: contracts/static/contracts/timeline.js:115 +msgid "Êtes-vous sûr de vouloir annuler cette commande ?" +msgstr "Bent u zeker dat u deze bestelling wilt annuleren?" + +#: contracts/static/contracts/timeline.js:136 +msgid "Erreur lors de la mise à jour du statut" +msgstr "Fout bij het bijwerken van de status" diff --git a/streetup/contracts/management/__init__.py b/streetup/contracts/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/contracts/management/commands/__init__.py b/streetup/contracts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/contracts/management/commands/create_monthly_orders_2026.py b/streetup/contracts/management/commands/create_monthly_orders_2026.py new file mode 100644 index 0000000..f33b592 --- /dev/null +++ b/streetup/contracts/management/commands/create_monthly_orders_2026.py @@ -0,0 +1,180 @@ +""" +Commande Django pour créer les commandes mensuelles 2026. + +Pour chaque mois de mars à décembre 2026, crée 3 commandes dans un contrat donné : + - Appels 2026 + - Réparations 2026 + - Préventif 2026 + +Vérifie l'existence avant d'insérer (idempotent). + +Usage : + python manage.py create_monthly_orders_2026 --contract + python manage.py create_monthly_orders_2026 --contract --dry-run +""" + +from django.core.management.base import BaseCommand, CommandError +from django.utils.translation import override as translation_override +from datetime import date + +from contracts.models import Contract, ContractOrder + + +# --------------------------------------------------------------------------- +# Données de référence +# --------------------------------------------------------------------------- + +MONTHS = [ + # (numéro, nom_affichage, préposition) + (3, "Mars", "de mars"), + (4, "Avril", "d'avril"), + (5, "Mai", "de mai"), + (6, "Juin", "de juin"), + (7, "Juillet", "de juillet"), + (8, "Août", "d'août"), + (9, "Septembre", "de septembre"), + (10, "Octobre", "d'octobre"), + (11, "Novembre", "de novembre"), + (12, "Décembre", "de décembre"), +] + +YEAR = 2026 + +# Modèle de commande : (suffixe_code, gabarit_description) +ORDER_TEMPLATES = [ + ("Appels", "Appels du mois {prep} {year}"), + ("Réparations", "Réparations du mois {prep} {year}"), + ("Préventif", "Interventions préventives du mois {prep} {year}"), +] + + +# --------------------------------------------------------------------------- +# Commande +# --------------------------------------------------------------------------- + +class Command(BaseCommand): + help = ( + "Crée les 3 commandes mensuelles (Appels / Réparations / Préventif) " + "pour chaque mois de mars à décembre 2026 dans le contrat spécifié." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--contract", + required=True, + metavar="CONTRACT_NUMBER", + help="Numéro du contrat cible (ex: 2024-MAINT-001).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + default=False, + help="Simule sans écrire en base de données.", + ) + parser.add_argument( + "--status", + default="pending", + metavar="STATUS", + help=( + "Statut des commandes créées. " + "Valeurs possibles : pending, sent, completed, invoiced, paid, cancelled. " + "(défaut : pending)" + ), + ) + + def handle(self, *args, **options): + contract_number = options["contract"] + dry_run = options["dry_run"] + status = options["status"] + + # Forcer le français pour les éventuelles traductions Django + with translation_override("fr"): + try: + contract = Contract.objects.get(contract_number=contract_number) + except Contract.DoesNotExist: + # Afficher les contrats disponibles pour aider l'utilisateur + available = list( + Contract.objects.values_list("contract_number", flat=True).order_by("contract_number") + ) + raise CommandError( + f"Contrat « {contract_number} » introuvable.\n" + f"Contrats disponibles : {', '.join(available) if available else '(aucun)'}" + ) + + self.stdout.write( + self.style.SUCCESS(f"\nContrat trouvé : {contract}\n") + ) + + if dry_run: + self.stdout.write(self.style.WARNING("⚠ Mode simulation (--dry-run) : aucune donnée ne sera écrite.\n")) + + created_count = 0 + skipped_count = 0 + + for month_num, month_name, month_prep in MONTHS: + # Dates : 1er du mois → 1er du mois suivant (ou 1er janvier 2027 pour décembre) + order_date = date(YEAR, month_num, 1) + if month_num == 12: + delivery_date = date(YEAR + 1, 1, 1) + else: + delivery_date = date(YEAR, month_num + 1, 1) + + self.stdout.write(f"\n--- {month_name} {YEAR} ---") + + for prefix, desc_template in ORDER_TEMPLATES: + order_code = f"{prefix} {month_name} {YEAR}" + description = desc_template.format(prep=month_prep, year=YEAR) + + # Vérification de l'existence (case-insensitive sur order_code) + existing = ContractOrder.objects.filter( + contract=contract, + order_code__iexact=order_code, + ).first() + + if existing: + self.stdout.write( + self.style.WARNING( + f" ⏭ EXISTANT — {existing.order_code!r} (id={existing.pk})" + ) + ) + skipped_count += 1 + else: + if dry_run: + self.stdout.write( + self.style.SUCCESS( + f" ✔ À CRÉER — code={order_code!r} | desc={description!r}" + f" | commande={order_date} | livraison={delivery_date}" + ) + ) + else: + obj = ContractOrder.objects.create( + contract=contract, + order_code=order_code, + description=description, + order_date=order_date, + delivery_date=delivery_date, + order_status=status, + ) + self.stdout.write( + self.style.SUCCESS( + f" ✔ CRÉÉ — {obj.order_code!r} (id={obj.pk})" + ) + ) + created_count += 1 + + # Résumé + self.stdout.write("\n" + "=" * 60) + if dry_run: + self.stdout.write( + self.style.WARNING( + f"Simulation terminée : {created_count} commande(s) à créer, " + f"{skipped_count} déjà existante(s)." + ) + ) + else: + self.stdout.write( + self.style.SUCCESS( + f"Terminé : {created_count} commande(s) créée(s), " + f"{skipped_count} déjà existante(s) (ignorée(s))." + ) + ) diff --git a/streetup/contracts/management/commands/import_contract_posts.py b/streetup/contracts/management/commands/import_contract_posts.py new file mode 100644 index 0000000..e13b26c --- /dev/null +++ b/streetup/contracts/management/commands/import_contract_posts.py @@ -0,0 +1,135 @@ +""" +Commande Django pour importer / mettre à jour les postes d'un contrat +depuis un fichier Excel de type « Inventaire fonteinen » (format SPRB-GOB). + +Structure attendue du fichier Excel : + Col A – Nr. Post (entier) → order_number + Col B – Code Fontaine/Article → code + Col C – Beschrijving NL → description_nl + Col D – AO (TVS = à justifier) → is_amount_to_be_justified + Col E – Unité NL → unit (fallback) + Col F – Description FR → description_fr + Col G – NC (ignorée) + Col H – U (unité FR) → unit (prioritaire) + Col I – Quantité présumée (ignorée) + Col J – Prix unitaire → unit_price + Col K – Total par poste (ignorée) + +Usage : + python manage.py import_contract_posts \\ + --contract E24.143 \\ + --excel-path scripts/data/"E24.143 Inventaire_inventaris ingevuld.xlsm" + + # simuler sans écrire : + python manage.py import_contract_posts --contract E24.143 \\ + --excel-path /tmp/inventaire.xlsm --dry-run + + # mettre à jour les postes déjà présents : + python manage.py import_contract_posts --contract E24.143 \\ + --excel-path /tmp/inventaire.xlsm --update +""" + +from django.core.management.base import BaseCommand, CommandError + +from contracts.models import Contract +from contracts.utils_import_posts import import_contract_posts + + +class Command(BaseCommand): + help = ( + "Importe ou met à jour les postes d'un contrat depuis un fichier Excel " + "(format Inventaire fonteinen SPRB-GOB)." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--contract", + required=True, + metavar="CONTRACT_NUMBER", + help="Numéro du contrat cible (ex : E24.143).", + ) + parser.add_argument( + "--excel-path", + required=True, + metavar="PATH", + help="Chemin vers le fichier Excel (.xlsx / .xlsm).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + default=False, + help="Simule l'import sans écrire en base de données.", + ) + parser.add_argument( + "--update", + action="store_true", + default=False, + help=( + "Met à jour les postes déjà présents (même order_number). " + "Sans ce flag, les postes existants sont ignorés." + ), + ) + + def handle(self, *args, **options): + contract_number = options["contract"] + excel_path = options["excel_path"] + dry_run = options["dry_run"] + update_existing = options["update"] + + if dry_run: + self.stdout.write(self.style.WARNING( + "=" * 62 + "\n" + "MODE DRY-RUN – aucune modification ne sera enregistrée\n" + "=" * 62 + )) + + # ------------------------------------------------------------------ + # Récupération du contrat + # ------------------------------------------------------------------ + try: + contract = Contract.objects.get(contract_number=contract_number) + except Contract.DoesNotExist: + raise CommandError( + f"Contrat « {contract_number} » introuvable en base." + ) + + self.stdout.write(f"Contrat : {contract}") + self.stdout.write(f"Fichier : {excel_path}") + self.stdout.write(f"Mode update: {'oui' if update_existing else 'non'}\n") + + # ------------------------------------------------------------------ + # Import + # ------------------------------------------------------------------ + stats = import_contract_posts( + contract=contract, + excel_source=excel_path, + dry_run=dry_run, + update_existing=update_existing, + log=lambda msg: self.stdout.write(msg), + ) + + # ------------------------------------------------------------------ + # Résumé + # ------------------------------------------------------------------ + marker = " [DRY-RUN]" if dry_run else "" + self.stdout.write("\n" + "=" * 62) + self.stdout.write(self.style.SUCCESS(f"RÉSUMÉ{marker}")) + self.stdout.write("=" * 62) + self.stdout.write(f" Lignes avec numéro de poste : {stats['total_rows']}") + self.stdout.write(self.style.SUCCESS( + f" Postes créés : {stats['created']}" + )) + self.stdout.write(self.style.SUCCESS( + f" Postes mis à jour : {stats['updated']}" + )) + self.stdout.write( + f" Ignorés (déjà présents) : {stats['skipped_already_exists']}" + ) + self.stdout.write( + f" Ignorés (prix manquant) : {stats['skipped_no_price']}" + ) + if stats['errors']: + self.stdout.write(self.style.ERROR( + f" Erreurs : {stats['errors']}" + )) + self.stdout.write("=" * 62) diff --git a/streetup/contracts/migrations/0001_initial.py b/streetup/contracts/migrations/0001_initial.py new file mode 100644 index 0000000..16775f5 --- /dev/null +++ b/streetup/contracts/migrations/0001_initial.py @@ -0,0 +1,287 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import common.storages +import contracts.models +import django.db.models.deletion +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('assets', '0001_initial'), + ('common', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('address', models.TextField(blank=True, null=True)), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('phone_number', models.CharField(blank=True, max_length=20, null=True)), + ('logo', models.ImageField(blank=True, null=True, upload_to='company_logos/')), + ], + options={ + 'verbose_name': 'Société/Organisme', + 'verbose_name_plural': 'Sociétés/Organismes', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ContractOrderAttachment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=contracts.models.order_attachment_upload_path, verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to=contracts.models.order_attachment_upload_path, verbose_name='Vignette')), + ('upload_date', models.DateTimeField(auto_now_add=True, verbose_name="Date d'ajout")), + ], + options={ + 'verbose_name': 'Pièce jointe de commande', + 'verbose_name_plural': 'Pièces jointes de commande', + 'ordering': ['upload_date'], + }, + ), + migrations.CreateModel( + name='ContractOrderDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('document', models.FileField(storage=common.storages.PrivateMediaStorage(), upload_to=contracts.models.order_document_upload_path)), + ('document_type', models.CharField(choices=[('invoice', 'Facture'), ('claim_declaration', 'Déclaration de créance'), ('state_of_works', 'État de travaux'), ('posts_detail', 'Détail des postes'), ('other', 'Autre')], default='other', max_length=50)), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='ContractOrderProgressState', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('index', models.PositiveIntegerField(blank=True, help_text="Numéro d'ordre de l'état d'avancement pour cette commande", null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': "État d'avancement", + }, + ), + migrations.CreateModel( + name='ContractOrderProgressStateItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.DecimalField(decimal_places=2, default=0, help_text='Quantité consommée à cette date', max_digits=10)), + ], + options={ + 'verbose_name': "Item d'état d'avancement", + 'verbose_name_plural': "Items d'états d'avancement", + 'ordering': ['contract_post__order_number'], + }, + ), + migrations.CreateModel( + name='ContractOrderQuote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_finalized', models.BooleanField(default=False)), + ('finalized_at', models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='ContractOrderQuoteItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.DecimalField(decimal_places=2, max_digits=10)), + ], + options={ + 'ordering': ['contract_post__order_number'], + }, + ), + migrations.CreateModel( + name='ContractPost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=255)), + ('description_fr', models.TextField()), + ('description_nl', models.TextField()), + ('order_number', models.PositiveIntegerField()), + ('unit', models.CharField(max_length=255)), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10)), + ('total_quantity_used', models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ('post_type', models.CharField(blank=True, max_length=100, null=True)), + ('is_coefficient_multipliable', models.BooleanField(default=False)), + ('is_amount_to_be_justified', models.BooleanField(default=False)), + ('coeff_amount_to_be_justified', models.DecimalField(blank=True, decimal_places=2, default=1, max_digits=10, null=True)), + ], + options={ + 'ordering': ['order_number'], + }, + ), + migrations.CreateModel( + name='DirectOrderPost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Quantité')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Commentaire')), + ('control_status', models.CharField(choices=[('pending', 'En attente de validation'), ('validated', 'Approuvé'), ('refused', 'Refusé'), ('invoiced', 'Facturé')], default='pending', max_length=20, verbose_name='Statut de validation')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Date de création')), + ('validated_at', models.DateTimeField(blank=True, null=True, verbose_name='Date de validation')), + ('invoiced_quantity', models.DecimalField(decimal_places=2, default=Decimal('0'), help_text='Quantité cumulée sur les déclarations de créance précédentes.', max_digits=10, verbose_name='Quantité déjà facturée')), + ], + options={ + 'verbose_name': 'Commande directe – poste', + 'verbose_name_plural': 'Commandes directes – postes', + 'ordering': ['contract_post__order_number'], + }, + ), + migrations.CreateModel( + name='MaintainType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(help_text='Code du type de maintenance (corrective, preventive, ameliorative)', max_length=50, unique=True)), + ('name_fr', models.CharField(max_length=100, verbose_name='Nom (FR)')), + ('name_nl', models.CharField(max_length=100, verbose_name='Nom (NL)')), + ], + options={ + 'verbose_name': 'Type de maintenance', + 'verbose_name_plural': 'Types de maintenance', + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='UserCompanyTeamAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('can_view', models.BooleanField(default=False, help_text="L'utilisateur peut voir cette équipe et ses membres.", verbose_name='Peut consulter')), + ('can_edit', models.BooleanField(default=False, help_text="L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer des membres entre équipes.", verbose_name='Peut éditer')), + ], + options={ + 'verbose_name': 'Accès utilisateur à une équipe', + 'verbose_name_plural': 'Accès utilisateurs aux équipes', + }, + ), + migrations.CreateModel( + name='ClaimDeclarationAttachment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(help_text='Titre ou description du justificatif', max_length=255, verbose_name='Titre')), + ('document', models.FileField(storage=common.storages.PrivateMediaStorage(), upload_to=contracts.models.claim_attachment_upload_path, verbose_name='Fichier')), + ('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name="Date d'ajout")), + ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_claim_attachments', to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')), + ], + options={ + 'verbose_name': 'Pièce jointe de déclaration de créance', + 'verbose_name_plural': 'Pièces jointes de déclarations de créance', + 'ordering': ['uploaded_at'], + }, + ), + migrations.CreateModel( + name='CompanyMember', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Membre de société/organisme', + 'verbose_name_plural': 'Membres de sociétés/organismes', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='CompanyTeam', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('is_assets_manager', models.BooleanField(default=False, help_text="Indique si cette équipe est gestionnaire des catégories d'assets associées.", verbose_name="Gestionnaire d'assets")), + ('is_provider', models.BooleanField(default=False, help_text='Indique si cette équipe est un prestataire extérieur.', verbose_name='Prestataire')), + ('can_generate_daily_report', models.BooleanField(default=False, help_text='Indique si les membres de cette équipe peuvent générer un rapport quotidien des interventions.', verbose_name='Peut générer le rapport journalier')), + ('photos_required', models.BooleanField(default=False, help_text="Si activé, au moins une photo doit être ajoutée à l'intervention avant de pouvoir la terminer.", verbose_name='Photos obligatoires')), + ('asset_categories', models.ManyToManyField(blank=True, help_text="Catégories d'assets dont cette équipe est responsable.", related_name='managing_teams', to='assets.assetcategory', verbose_name="Catégories d'assets gérées")), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='contracts.company')), + ('thematics', models.ManyToManyField(blank=True, related_name='company_teams', to='common.thematic')), + ], + options={ + 'verbose_name': 'Équipe de société/organisme', + 'verbose_name_plural': 'Équipes de sociétés/organismes', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='CompanyMemberHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('join_date', models.DateField(verbose_name="Date d'entrée")), + ('leave_date', models.DateField(blank=True, null=True, verbose_name='Date de sortie')), + ('changed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_team_changes', to=settings.AUTH_USER_MODEL, verbose_name='Modifié par')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_history', to='contracts.companymember', verbose_name='Membre')), + ('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_history', to='contracts.companyteam', verbose_name='Équipe')), + ], + options={ + 'verbose_name': "Historique d'équipe d'un membre", + 'verbose_name_plural': "Historiques d'équipe des membres", + 'ordering': ['member', '-join_date'], + }, + ), + migrations.AddField( + model_name='companymember', + name='team', + field=models.ForeignKey(blank=True, help_text='Laisser vide pour un membre non assigné à une équipe.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='contracts.companyteam', verbose_name='Équipe'), + ), + migrations.CreateModel( + name='Contract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('contract_number', models.CharField(max_length=100, unique=True)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('description', models.TextField(blank=True, null=True)), + ('committed_amount', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, null=True)), + ('internal_link', models.URLField(blank=True, help_text='Lien vers le dossier interne', null=True)), + ('notification_emails', models.CharField(blank=True, max_length=1000, null=True)), + ('cc_notification_emails', models.CharField(blank=True, help_text='Adresses email à ajouter en copie des notifications (séparées par des virgules ou points-virgules)', max_length=1000, null=True, verbose_name='Emails en copie (CC)')), + ('send_email_notifications', models.BooleanField(default=True, help_text='Envoyer des notifications liées aux interventions par email.')), + ('signatory_name', models.CharField(blank=True, help_text='Nom de la personne qui signe les documents côté prestataire.', max_length=255, null=True, verbose_name='Nom du signataire (prestataire)')), + ('signatory_role', models.CharField(blank=True, help_text='Rôle/fonction de la personne qui signe les documents côté prestataire.', max_length=255, null=True, verbose_name='Rôle du signataire (prestataire)')), + ('contact_phone', models.CharField(blank=True, help_text='Numéro de téléphone du technicien de garde ou contact principal pour les urgences.', max_length=50, null=True, verbose_name='Téléphone de contact')), + ('is_active', models.BooleanField(default=True, help_text='Indique si le contrat est actif.', verbose_name='Actif')), + ('needs_approval', models.BooleanField(default=False, help_text="Indique si les interventions liées à ce contrat nécessitent une approbation avant d'être transmises pour traitement.", verbose_name='Nécessite une approbation')), + ('needs_checking', models.BooleanField(default=False, help_text="Indique si les interventions liées à ce contrat nécessitent une vérification avant d'être transmises pour traitement.", verbose_name='Nécessite une vérification')), + ('uses_occupations', models.BooleanField(default=False, help_text='Indique si les interventions liées à ce contrat utilisent la gestion des occupations (temps passé par membre).', verbose_name='Utilise les occupations')), + ('occupations_required', models.BooleanField(default=False, help_text='Si activé, au moins une occupation doit être enregistrée avant de terminer une intervention corrective liée à ce contrat.', verbose_name='Occupations obligatoires')), + ('warn_if_no_prestation_on_processed', models.BooleanField(default=False, help_text="Si activé, un avertissement est affiché lorsque l'utilisateur tente de passer une intervention au statut 'Traité' sans avoir encodé de prestation.", verbose_name="Avertir si aucune prestation au passage à 'Traité'")), + ('send_daily_summary', models.BooleanField(default=False, help_text="Si activé, les fiches d'intervention du jour sont regroupées dans un seul email envoyé en fin de journée, plutôt qu'un email par intervention.", verbose_name='Envoi groupé journalier')), + ('daily_summary_hour', models.PositiveSmallIntegerField(default=18, help_text='Heure (0-23) à laquelle le résumé journalier des interventions est envoyé (heure locale).', verbose_name="Heure d'envoi du résumé journalier")), + ('email_only_if_urgent', models.BooleanField(default=False, help_text="Si activé, les notifications email ne sont envoyées que pour les interventions de priorité 'Urgent'.", verbose_name='Emails uniquement si priorité urgente')), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='contracts.company')), + ('executive_officer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='executive_officer_contracts', to=settings.AUTH_USER_MODEL)), + ('thematics', models.ManyToManyField(blank=True, help_text='Thématiques associées à ce contrat.', related_name='contracts', to='common.thematic', verbose_name='Thématiques')), + ], + options={ + 'ordering': ['contract_number'], + }, + ), + migrations.CreateModel( + name='ContractOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order_code', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, null=True)), + ('order_date', models.DateField()), + ('delivery_date', models.DateField()), + ('total_amount', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=10, null=True)), + ('order_status', models.CharField(choices=[('pending', 'En préparation'), ('sent', 'Envoyé'), ('completed', 'Terminé'), ('invoiced', 'Comptabilisé'), ('paid', 'Payé'), ('cancelled', 'Annulé')], default='pending', max_length=50)), + ('order_file', models.FileField(blank=True, null=True, upload_to=contracts.models.contract_order_upload_path)), + ('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='contracts.contract')), + ('ordered_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contract_orders', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-order_date'], + }, + ), + ] diff --git a/streetup/contracts/migrations/0002_initial.py b/streetup/contracts/migrations/0002_initial.py new file mode 100644 index 0000000..8575287 --- /dev/null +++ b/streetup/contracts/migrations/0002_initial.py @@ -0,0 +1,155 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('common', '0003_initial'), + ('contracts', '0001_initial'), + ('projects', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='contractorder', + name='projects', + field=models.ManyToManyField(blank=True, related_name='orders', to='projects.project', verbose_name='Projets'), + ), + migrations.AddField( + model_name='contractorderattachment', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='contracts.contractorder', verbose_name='Commande'), + ), + migrations.AddField( + model_name='contractorderattachment', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_order_attachments', to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='contractorderdocument', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='contracts.contractorder'), + ), + migrations.AddField( + model_name='claimdeclarationattachment', + name='claim_document', + field=models.ForeignKey(limit_choices_to={'document_type': 'claim_declaration'}, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='contracts.contractorderdocument', verbose_name='Déclaration de créance'), + ), + migrations.AddField( + model_name='contractorderprogressstate', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_progress_states', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='contractorderprogressstate', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_states', to='contracts.contractorder'), + ), + migrations.AddField( + model_name='contractorderdocument', + name='progress_state', + field=models.ForeignKey(blank=True, help_text="État d'avancement associé à ce document, le cas échéant", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ps_documents', to='contracts.contractorderprogressstate'), + ), + migrations.AddField( + model_name='contractorderprogressstateitem', + name='progress_state', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='contracts.contractorderprogressstate'), + ), + migrations.AddField( + model_name='contractorderquote', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_quotes', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='contractorderquote', + name='finalized_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='finalized_quotes', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='contractorderquote', + name='order', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='quote', to='contracts.contractorder'), + ), + migrations.AddField( + model_name='contractorderquoteitem', + name='quote', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='contracts.contractorderquote'), + ), + migrations.AddField( + model_name='contractpost', + name='contract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='contracts.contract'), + ), + migrations.AddField( + model_name='contractorderquoteitem', + name='contract_post', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contracts.contractpost'), + ), + migrations.AddField( + model_name='contractorderprogressstateitem', + name='contract_post', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contracts.contractpost'), + ), + migrations.AddField( + model_name='directorderpost', + name='contract_post', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='direct_order_posts', to='contracts.contractpost', verbose_name='Poste de marché'), + ), + migrations.AddField( + model_name='directorderpost', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_direct_order_posts', to=settings.AUTH_USER_MODEL, verbose_name='Créé par'), + ), + migrations.AddField( + model_name='directorderpost', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='direct_posts', to='contracts.contractorder', verbose_name='Commande'), + ), + migrations.AddField( + model_name='directorderpost', + name='validated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='validated_direct_order_posts', to=settings.AUTH_USER_MODEL, verbose_name='Validé par'), + ), + migrations.AddField( + model_name='companymember', + name='visible_maintain_types', + field=models.ManyToManyField(blank=True, help_text="Types de maintenance visibles par défaut pour cet utilisateur. Si vide, tous les types sont visibles. Les interventions assignées à l'utilisateur restent toujours visibles.", related_name='company_members', to='contracts.maintaintype', verbose_name='Types de maintenance visibles'), + ), + migrations.AddField( + model_name='usercompanyteamaccess', + name='team', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_accesses', to='contracts.companyteam', verbose_name='Équipe'), + ), + migrations.AddField( + model_name='usercompanyteamaccess', + name='user_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_accesses', to='common.userconfig', verbose_name='Configuration utilisateur'), + ), + migrations.AddConstraint( + model_name='contractorder', + constraint=models.UniqueConstraint(fields=('contract', 'order_code'), name='unique_order_per_contract'), + ), + migrations.AlterUniqueTogether( + name='contractorderquoteitem', + unique_together={('quote', 'contract_post')}, + ), + migrations.AlterUniqueTogether( + name='contractorderprogressstateitem', + unique_together={('progress_state', 'contract_post')}, + ), + migrations.AlterUniqueTogether( + name='directorderpost', + unique_together={('order', 'contract_post')}, + ), + migrations.AlterUniqueTogether( + name='usercompanyteamaccess', + unique_together={('user_config', 'team')}, + ), + ] diff --git a/streetup/contracts/migrations/0003_contract_uses_intervention_manager.py b/streetup/contracts/migrations/0003_contract_uses_intervention_manager.py new file mode 100644 index 0000000..e2ec6a6 --- /dev/null +++ b/streetup/contracts/migrations/0003_contract_uses_intervention_manager.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-06-03 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0002_initial'), + ] + + operations = [ + migrations.AddField( + model_name='contract', + name='uses_intervention_manager', + field=models.BooleanField(default=False, help_text="Indique si les interventions liées à ce contrat utilisent un gestionnaire d'intervention.", verbose_name="Utilise les gestionnaires d'intervention"), + ), + ] diff --git a/streetup/contracts/migrations/0004_contract_auto_close_delay_days_and_more.py b/streetup/contracts/migrations/0004_contract_auto_close_delay_days_and_more.py new file mode 100644 index 0000000..fcef582 --- /dev/null +++ b/streetup/contracts/migrations/0004_contract_auto_close_delay_days_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2026-06-07 09:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0003_contract_uses_intervention_manager'), + ] + + operations = [ + migrations.AddField( + model_name='contract', + name='auto_close_delay_days', + field=models.PositiveIntegerField(default=30, help_text='Délai en jours après lequel une intervention est clôturée automatiquement.', verbose_name='Délai de clôture automatique (jours)'), + ), + migrations.AddField( + model_name='contract', + name='auto_close_interventions', + field=models.BooleanField(default=False, help_text='Indique si les interventions liées à ce contrat doivent être automatiquement clôturées après un certain délai.', verbose_name='Clôture automatique des interventions'), + ), + ] diff --git a/streetup/contracts/migrations/0005_remove_contract_auto_close.py b/streetup/contracts/migrations/0005_remove_contract_auto_close.py new file mode 100644 index 0000000..0d2a85d --- /dev/null +++ b/streetup/contracts/migrations/0005_remove_contract_auto_close.py @@ -0,0 +1,19 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0004_contract_auto_close_delay_days_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='contract', + name='auto_close_delay_days', + ), + migrations.RemoveField( + model_name='contract', + name='auto_close_interventions', + ), + ] diff --git a/streetup/contracts/migrations/0006_companymember_teams_m2m.py b/streetup/contracts/migrations/0006_companymember_teams_m2m.py new file mode 100644 index 0000000..30460fd --- /dev/null +++ b/streetup/contracts/migrations/0006_companymember_teams_m2m.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.8 on 2026-06-10 08:31 + +from django.db import migrations, models + +def copy_team_to_teams(apps, schema_editor): + CompanyMember = apps.get_model('contracts', 'CompanyMember') + for member in CompanyMember.objects.all(): + # access the raw DB field team_id to avoid django loading issues if team FK field is already removed or modified + team_id = getattr(member, 'team_id', None) + if team_id is not None: + member.teams.add(team_id) + +def copy_teams_to_team(apps, schema_editor): + CompanyMember = apps.get_model('contracts', 'CompanyMember') + for member in CompanyMember.objects.all(): + first_team = member.teams.first() + if first_team: + member.team = first_team + member.save(update_fields=['team']) + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0005_remove_contract_auto_close'), + ] + + operations = [ + migrations.AddField( + model_name='companymember', + name='teams', + field=models.ManyToManyField(blank=True, help_text='Équipes auxquelles ce membre appartient.', related_name='members', to='contracts.companyteam', verbose_name='Équipes'), + ), + migrations.RunPython(copy_team_to_teams, reverse_code=copy_teams_to_team), + migrations.RemoveField( + model_name='companymember', + name='team', + ), + ] diff --git a/streetup/contracts/migrations/0007_companyteam_restrict_today_interventions_visibility_and_more.py b/streetup/contracts/migrations/0007_companyteam_restrict_today_interventions_visibility_and_more.py new file mode 100644 index 0000000..ae01219 --- /dev/null +++ b/streetup/contracts/migrations/0007_companyteam_restrict_today_interventions_visibility_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2026-06-17 14:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0006_companymember_teams_m2m'), + ] + + operations = [ + migrations.AddField( + model_name='companyteam', + name='restrict_today_interventions_visibility', + field=models.BooleanField(default=False, help_text="Si activé, les interventions ne sont visibles qu'à partir d'une certaine heure, et uniquement pour le jour même.", verbose_name='Restreindre la visibilité aux interventions du jour'), + ), + migrations.AddField( + model_name='companyteam', + name='visibility_start_hour', + field=models.TimeField(blank=True, help_text='Heure locale à partir de laquelle les interventions du jour deviennent visibles (ex: 06:00:00).', null=True, verbose_name='Heure de début de visibilité'), + ), + ] diff --git a/streetup/contracts/migrations/0008_companyteam_restrict_to_assigned_interventions.py b/streetup/contracts/migrations/0008_companyteam_restrict_to_assigned_interventions.py new file mode 100644 index 0000000..6ba2185 --- /dev/null +++ b/streetup/contracts/migrations/0008_companyteam_restrict_to_assigned_interventions.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2026-06-22 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0007_companyteam_restrict_today_interventions_visibility_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='companyteam', + name='restrict_to_assigned_interventions', + field=models.BooleanField(default=False, help_text="Si activé, les membres de cette équipe ne voient que les interventions assignées à l'équipe ou directement à un membre.", verbose_name='Restreindre aux interventions assignées'), + ), + ] diff --git a/streetup/contracts/migrations/__init__.py b/streetup/contracts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/contracts/models.py b/streetup/contracts/models.py new file mode 100644 index 0000000..1dcf2a7 --- /dev/null +++ b/streetup/contracts/models.py @@ -0,0 +1,1067 @@ +from django.db import models +from django.db.models import Sum, F, FloatField, ExpressionWrapper, Q +from django.utils.text import slugify +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import get_language +from django.utils import timezone + +from common.models import Thematic + +import os +from datetime import datetime +from decimal import Decimal + +from common.storages import PrivateMediaStorage +private_storage = PrivateMediaStorage() + + + +class Company(models.Model): + """Modèle représentant une société ou un organisme""" + name = models.CharField(max_length=255, unique=True) + address = models.TextField(blank=True, null=True) + contact_email = models.EmailField(blank=True, null=True) + phone_number = models.CharField(max_length=20, blank=True, null=True) + logo = models.ImageField(upload_to='company_logos/', blank=True, null=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Société/Organisme" + verbose_name_plural = "Sociétés/Organismes" + ordering = ['name'] + + +# Choix des types de maintenance (doit correspondre à MAINTAIN_CHOICES dans interventions.models) +MAINTAIN_TYPE_CHOICES = [ + ('corrective', _('Maintenance corrective')), + ('preventive', _('Maintenance préventive')), + ('ameliorative', _('Maintenance améliorative')), +] + + +class MaintainType(models.Model): + """Modèle représentant un type de maintenance pour les équipes""" + code = models.CharField(max_length=50, unique=True, help_text=_("Code du type de maintenance (corrective, preventive, ameliorative)")) + name_fr = models.CharField(max_length=100, verbose_name=_("Nom (FR)")) + name_nl = models.CharField(max_length=100, verbose_name=_("Nom (NL)")) + + def __str__(self): + lang = get_language() + if lang and lang.startswith('nl'): + return self.name_nl or self.name_fr + return self.name_fr + + def get_name(self): + lang = get_language() + if lang and lang.startswith('nl'): + return self.name_nl or self.name_fr + return self.name_fr + + class Meta: + verbose_name = _("Type de maintenance") + verbose_name_plural = _("Types de maintenance") + ordering = ['code'] + + +class CompanyTeam(models.Model): + """Modèle représentant une équipe au sein d'une société ou d'un organisme""" + name = models.CharField(max_length=255, unique=True) + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="teams") + thematics = models.ManyToManyField('common.Thematic', blank=True, related_name="company_teams") + asset_categories = models.ManyToManyField( + 'assets.AssetCategory', + blank=True, + related_name='managing_teams', + verbose_name=_("Catégories d'assets gérées"), + help_text=_("Catégories d'assets dont cette équipe est responsable."), + ) + is_assets_manager = models.BooleanField( + default=False, + verbose_name=_("Gestionnaire d'assets"), + help_text=_("Indique si cette équipe est gestionnaire des catégories d'assets associées."), + ) + is_provider = models.BooleanField( + default=False, + verbose_name=_("Prestataire"), + help_text=_("Indique si cette équipe est un prestataire extérieur."), + ) + can_generate_daily_report = models.BooleanField( + default=False, + verbose_name=_("Peut générer le rapport journalier"), + help_text=_("Indique si les membres de cette équipe peuvent générer un rapport quotidien des interventions."), + ) + photos_required = models.BooleanField( + default=False, + verbose_name=_("Photos obligatoires"), + help_text=_("Si activé, au moins une photo doit être ajoutée à l'intervention avant de pouvoir la terminer."), + ) + restrict_today_interventions_visibility = models.BooleanField( + default=False, + verbose_name=_("Restreindre la visibilité aux interventions du jour"), + help_text=_("Si activé, les interventions ne sont visibles qu'à partir d'une certaine heure, et uniquement pour le jour même."), + ) + visibility_start_hour = models.TimeField( + null=True, + blank=True, + verbose_name=_("Heure de début de visibilité"), + help_text=_("Heure locale à partir de laquelle les interventions du jour deviennent visibles (ex: 06:00:00)."), + ) + restrict_to_assigned_interventions = models.BooleanField( + default=False, + verbose_name=_("Restreindre aux interventions assignées"), + help_text=_("Si activé, les membres de cette équipe ne voient que les interventions assignées à l'équipe ou directement à un membre."), + ) + + def __str__(self): + return f"{self.name} ({self.company.name})" + + class Meta: + verbose_name = _("Équipe de société/organisme") + verbose_name_plural = _("Équipes de sociétés/organismes") + ordering = ['name'] + + +class CompanyMember(models.Model): + """Modèle représentant un membre d'une équipe de société/organisme""" + name = models.CharField(max_length=255) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + teams = models.ManyToManyField( + CompanyTeam, + blank=True, + related_name="members", + verbose_name=_("Équipes"), + help_text=_("Équipes auxquelles ce membre appartient."), + ) + visible_maintain_types = models.ManyToManyField( + MaintainType, + blank=True, + related_name="company_members", + verbose_name=_("Types de maintenance visibles"), + help_text=_("Types de maintenance visibles par défaut pour cet utilisateur. Si vide, tous les types sont visibles. Les interventions assignées à l'utilisateur restent toujours visibles.") + ) + + def __str__(self): + teams_list = ", ".join(t.name for t in self.teams.all()) + return f"{self.name} ({teams_list or '—'})" + + @property + def teams_display(self): + """Retourne les noms de toutes les équipes associées séparés par une virgule.""" + return ", ".join(t.name for t in self.teams.all()) + + @property + def company_ids_str(self): + """Retourne une liste d'identifiants d'entreprises des équipes sous forme de chaîne de caractères.""" + return ",".join(str(t.company_id) for t in self.teams.all() if t.company_id) + + def get_visible_maintain_type_codes(self): + """Retourne la liste des codes de types de maintenance visibles. + Si aucun n'est défini, retourne None (tous visibles).""" + codes = list(self.visible_maintain_types.values_list('code', flat=True)) + return codes if codes else None + + class Meta: + verbose_name = "Membre de société/organisme" + verbose_name_plural = "Membres de sociétés/organismes" + ordering = ['name'] + + +class CompanyMemberHistory(models.Model): + """Historique des appartenances d'un membre à une équipe""" + member = models.ForeignKey( + CompanyMember, + on_delete=models.CASCADE, + related_name='team_history', + verbose_name=_("Membre"), + ) + team = models.ForeignKey( + CompanyTeam, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='member_history', + verbose_name=_("Équipe"), + ) + join_date = models.DateField(verbose_name=_("Date d'entrée")) + leave_date = models.DateField(null=True, blank=True, verbose_name=_("Date de sortie")) + changed_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='member_team_changes', + verbose_name=_("Modifié par"), + ) + + def __str__(self): + team_name = self.team.name if self.team else "—" + leave = self.leave_date.isoformat() if self.leave_date else "…" + return f"{self.member.name} → {team_name} ({self.join_date} / {leave})" + + class Meta: + verbose_name = _("Historique d'équipe d'un membre") + verbose_name_plural = _("Historiques d'équipe des membres") + ordering = ['member', '-join_date'] + + +class UserCompanyTeamAccess(models.Model): + """Permissions d'accès d'un utilisateur à une équipe de société/organisme.""" + user_config = models.ForeignKey( + 'common.UserConfig', + on_delete=models.CASCADE, + related_name='team_accesses', + verbose_name=_("Configuration utilisateur"), + ) + team = models.ForeignKey( + CompanyTeam, + on_delete=models.CASCADE, + related_name='user_accesses', + verbose_name=_("Équipe"), + ) + can_view = models.BooleanField( + default=False, + verbose_name=_("Peut consulter"), + help_text=_("L'utilisateur peut voir cette équipe et ses membres."), + ) + can_edit = models.BooleanField( + default=False, + verbose_name=_("Peut éditer"), + help_text=_("L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer des membres entre équipes."), + ) + + def __str__(self): + return f"{self.user_config.user.username} → {self.team.name}" + + class Meta: + unique_together = ('user_config', 'team') + verbose_name = _("Accès utilisateur à une équipe") + verbose_name_plural = _("Accès utilisateurs aux équipes") + + +class Contract(models.Model): + """Modèle représentant un contrat""" + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="contracts") + contract_number = models.CharField(max_length=100, unique=True) + start_date = models.DateField() + end_date = models.DateField() + description = models.TextField(blank=True, null=True) + executive_officer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="executive_officer_contracts") + committed_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0, blank=True, null=True) + internal_link = models.URLField(blank=True, null=True, help_text="Lien vers le dossier interne") + notification_emails = models.CharField(blank=True, null=True, max_length=1000) + cc_notification_emails = models.CharField( + blank=True, + null=True, + max_length=1000, + verbose_name=_("Emails en copie (CC)"), + help_text=_("Adresses email à ajouter en copie des notifications (séparées par des virgules ou points-virgules)"), + ) + send_email_notifications = models.BooleanField( + default=True, + help_text=_("Envoyer des notifications liées aux interventions par email."), + ) + signatory_name = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name=_("Nom du signataire (prestataire)"), + help_text=_("Nom de la personne qui signe les documents côté prestataire."), + ) + signatory_role = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name=_("Rôle du signataire (prestataire)"), + help_text=_("Rôle/fonction de la personne qui signe les documents côté prestataire."), + ) + contact_phone = models.CharField( + max_length=50, + blank=True, + null=True, + verbose_name=_("Téléphone de contact"), + help_text=_("Numéro de téléphone du technicien de garde ou contact principal pour les urgences."), + ) + thematics = models.ManyToManyField( + Thematic, + blank=True, + related_name='contracts', + verbose_name=_("Thématiques"), + help_text=_("Thématiques associées à ce contrat."), + ) + is_active = models.BooleanField( + default=True, + verbose_name=_("Actif"), + help_text=_("Indique si le contrat est actif."), + ) + needs_approval = models.BooleanField( + default=False, + verbose_name=_("Nécessite une approbation"), + help_text=_("Indique si les interventions liées à ce contrat nécessitent une approbation avant d'être transmises pour traitement."), + ) + needs_checking = models.BooleanField( + default=False, + verbose_name=_("Nécessite une vérification"), + help_text=_("Indique si les interventions liées à ce contrat nécessitent une vérification avant d'être transmises pour traitement."), + ) + uses_occupations = models.BooleanField( + default=False, + verbose_name=_("Utilise les occupations"), + help_text=_("Indique si les interventions liées à ce contrat utilisent la gestion des occupations (temps passé par membre)."), + ) + uses_intervention_manager = models.BooleanField( + default=False, + verbose_name=_("Utilise les gestionnaires d'intervention"), + help_text=_("Indique si les interventions liées à ce contrat utilisent un gestionnaire d'intervention."), + ) + occupations_required = models.BooleanField( + default=False, + verbose_name=_("Occupations obligatoires"), + help_text=_("Si activé, au moins une occupation doit être enregistrée avant de terminer une intervention corrective liée à ce contrat."), + ) + warn_if_no_prestation_on_processed = models.BooleanField( + default=False, + verbose_name=_("Avertir si aucune prestation au passage à 'Traité'"), + help_text=_("Si activé, un avertissement est affiché lorsque l'utilisateur tente de passer une intervention au statut 'Traité' sans avoir encodé de prestation."), + ) + send_daily_summary = models.BooleanField( + default=False, + verbose_name=_("Envoi groupé journalier"), + help_text=_("Si activé, les fiches d'intervention du jour sont regroupées dans un seul email envoyé en fin de journée, plutôt qu'un email par intervention."), + ) + daily_summary_hour = models.PositiveSmallIntegerField( + default=18, + verbose_name=_("Heure d'envoi du résumé journalier"), + help_text=_("Heure (0-23) à laquelle le résumé journalier des interventions est envoyé (heure locale)."), + ) + email_only_if_urgent = models.BooleanField( + default=False, + verbose_name=_("Emails uniquement si priorité urgente"), + help_text=_("Si activé, les notifications email ne sont envoyées que pour les interventions de priorité 'Urgent'."), + ) + + def __str__(self): + return f"Contrat {self.contract_number} - {self.company.name}" + + class Meta: + ordering = ['contract_number'] + + def get_total_realized_amount(self): + """ + Retourne la somme de tous les montants prestés sur les interventions + associées à toutes les commandes du contrat. + Prend en compte les postes avec coefficient multipliable. + """ + from interventions.models import InterventionContractPost + posts = InterventionContractPost.objects.filter( + intervention__order__contract=self + ).select_related('contract_post', 'intervention') + + total = Decimal('0') + for post in posts: + total += post.total_price + + return float(total) + + def get_total_validated_amount(self): + """ + Retourne la somme des montants validés (statut = 'validated') + sur toutes les interventions liées aux commandes du contrat (peu importe leur statut). + Prend en compte les postes avec coefficient multipliable. + """ + from interventions.models import InterventionContractPost + posts = InterventionContractPost.objects.filter( + intervention__order__contract=self, + control_status='validated' + ).select_related('contract_post', 'intervention') + + total = Decimal('0') + for post in posts: + total += post.total_price + + return float(total) + + def get_total_remaining_validated_amount(self): + """ + Retourne la somme des montants validés (statut = 'validated') + sur les interventions liées aux commandes du contrat au statut "validated". + Prend en compte les postes avec coefficient multipliable. + """ + from interventions.models import InterventionContractPost + posts = InterventionContractPost.objects.filter( + intervention__order__contract=self, + intervention__status='validated', + control_status='validated' + ).select_related('contract_post', 'intervention') + + total = Decimal('0') + for post in posts: + total += post.total_price + + return float(total) + + +class ContractPost(models.Model): + """Modèle représentant un poste dans un contrat""" + contract = models.ForeignKey(Contract, on_delete=models.CASCADE, related_name="posts") + code = models.CharField(max_length=255) + description_fr = models.TextField() + description_nl = models.TextField() + order_number = models.PositiveIntegerField() + unit = models.CharField(max_length=255) + unit_price = models.DecimalField(max_digits=10, decimal_places=2) + total_quantity_used = models.DecimalField(max_digits=10, decimal_places=2, default=0) + post_type = models.CharField(max_length=100, blank=True, null=True) + is_coefficient_multipliable = models.BooleanField(default=False) + is_amount_to_be_justified = models.BooleanField(default=False) + coeff_amount_to_be_justified = models.DecimalField(max_digits=10, decimal_places=2, default=1, blank=True, null=True) + + def __str__(self): + return f"{self.contract.contract_number} --- {self.code}" + + def get_description_for_language(self, lang=None): + current_lang = lang or get_language() or 'fr' + if current_lang.startswith('nl'): + return self.description_nl + return self.description_fr + + class Meta: + ordering = ['order_number'] + + + +CONTRACT_ORDER_STATUS_CHOICES = [ + ('pending', _('En préparation')), + ('sent', _('Envoyé')), + ('completed', _('Terminé')), + ('invoiced', _('Comptabilisé')), + ('paid', _('Payé')), + ('cancelled', _('Annulé')), +] + +def contract_order_upload_path(instance, filename): + # Exemple : "orders/CONTRAT-123/bon_commande.pdf" + contract_code = slugify(instance.contract.contract_number) # ou tout autre champ unique du contrat + return f"orders/{contract_code}/{filename}" + +class ContractOrder(models.Model): + """Modèle représentant une commande dans un contrat""" + contract = models.ForeignKey(Contract, on_delete=models.CASCADE, related_name="orders") + order_code = models.CharField(max_length=100) + description = models.TextField(blank=True, null=True) + order_date = models.DateField() + delivery_date = models.DateField() + total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0, blank=True, null=True) + order_status = models.CharField(max_length=50, choices=CONTRACT_ORDER_STATUS_CHOICES, default='pending') + order_file = models.FileField(upload_to=contract_order_upload_path, blank=True, null=True) + ordered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="contract_orders") + projects = models.ManyToManyField('projects.Project', blank=True, related_name="orders", verbose_name=_("Projets")) + + def __str__(self): + return f"Contrat {self.contract.contract_number} - {self.order_code}" + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['contract', 'order_code'], name='unique_order_per_contract') + ] + + ordering = ['-order_date'] + + def get_total_realized_amount(self): + """ + Montant presté : somme de toutes les quantités * prix unitaire + pour les interventions liées à cette commande, indépendamment du contrôle. + Inclut également les commandes directes (DirectOrderPost). + Prend en compte les postes avec coefficient multipliable. + """ + from interventions.models import InterventionContractPost + posts = InterventionContractPost.objects.filter( + intervention__order=self + ).select_related('contract_post', 'intervention') + + total = Decimal('0') + for post in posts: + total += post.total_price + + for direct_post in self.direct_posts.select_related('contract_post'): + total += direct_post.total_price + + return float(total) + + def get_total_validated_amount(self): + """ + Montant validé : uniquement les postes dont le contrôle a été validé + et dont l'intervention est au statut validé. + Inclut les commandes directes validées. + Prend en compte les postes avec coefficient multipliable. + """ + from interventions.models import InterventionContractPost + posts = InterventionContractPost.objects.filter( + intervention__order=self, + intervention__status__in=['validated', 'invoiced', 'closed'], # Inclure les interventions déjà facturées ou payées + control_status='validated' + ).select_related('contract_post', 'intervention') + + total = Decimal('0') + for post in posts: + total += post.total_price + + for direct_post in self.direct_posts.filter(control_status='validated').select_related('contract_post'): + total += direct_post.total_price + + return float(total) + + def get_total_remaining_validated_amount(self): + """ + Retourne la somme des montants validés (statut = 'validated') + sur les interventions liées aux commandes du contrat au statut "validated". + Inclut les commandes directes validées. + Prend en compte les postes avec coefficient multipliable. + """ + from interventions.models import InterventionContractPost + posts = InterventionContractPost.objects.filter( + intervention__order=self, + intervention__status='validated', + control_status='validated' + ).select_related('contract_post', 'intervention') + + total = Decimal('0') + for post in posts: + total += post.total_price + + for direct_post in self.direct_posts.filter(control_status='validated').select_related('contract_post'): + total += direct_post.total_price + + return float(total) + + def get_quote_total_amount(self): + quote = getattr(self, "quote", None) + if not quote: + return Decimal("0") + return quote.get_total_amount() + + def get_post_consumption_summary(self, include_progress_states=False): + """Retourne un récapitulatif de la consommation des postes. + Prend en compte les postes avec coefficient multipliable et les commandes directes.""" + from interventions.models import InterventionContractPost, InterventionPostPeriodQuantity + from django.db.models import Sum + + # Récupérer tous les postes avec leurs interventions pour calculer les montants + all_posts = InterventionContractPost.objects.filter( + intervention__order=self + ).select_related('contract_post', 'intervention') + + # Calculer les quantités consommées par poste + consumption_map = {} + # Calculer les montants consommés (en utilisant total_price qui gère les coefficients) + amount_map = {} + + # Récupérer les interventions qui ont des périodes + interventions_with_periods = set( + InterventionPostPeriodQuantity.objects.filter( + intervention__order=self + ).values_list('intervention_id', flat=True).distinct() + ) + + # Récupérer les quantités des périodes par intervention et par poste + period_quantities = {} + if interventions_with_periods: + period_data = InterventionPostPeriodQuantity.objects.filter( + intervention__order=self + ).values('intervention_id', 'contract_post_id').annotate( + total_quantity=Sum('quantity') + ) + for row in period_data: + key = (row['intervention_id'], row['contract_post_id']) + period_quantities[key] = row['total_quantity'] or Decimal("0") + + for post in all_posts: + post_id = post.contract_post_id + intervention_id = post.intervention_id + + # Si l'intervention a des périodes, utiliser la somme des périodes + # sinon utiliser la quantité du poste + if intervention_id in interventions_with_periods: + key = (intervention_id, post_id) + quantity = period_quantities.get(key, Decimal("0")) + if post.intervention.status_order < 50: # Intervention pas encore validée + quantity += Decimal(post.quantity or 0) + total_price = quantity * (post.contract_post.unit_price or Decimal("0")) + if post.contract_post.is_amount_to_be_justified and post.contract_post.coeff_amount_to_be_justified: + total_price = total_price * post.contract_post.coeff_amount_to_be_justified + else: + quantity = post.quantity or Decimal("0") + total_price = post.total_price or Decimal("0") + + consumption_map[post_id] = consumption_map.get(post_id, Decimal("0")) + quantity + amount_map[post_id] = amount_map.get(post_id, Decimal("0")) + total_price + + # Ajouter les commandes directes au récapitulatif + for direct_post in self.direct_posts.select_related('contract_post'): + post_id = direct_post.contract_post_id + # Include both current quantity and already-invoiced quantity + quantity = (direct_post.quantity or Decimal("0")) + (direct_post.invoiced_quantity or Decimal("0")) + unit_price = direct_post.contract_post.unit_price or Decimal("0") + total_price = quantity * unit_price + consumption_map[post_id] = consumption_map.get(post_id, Decimal("0")) + quantity + amount_map[post_id] = amount_map.get(post_id, Decimal("0")) + total_price + + quote = getattr(self, "quote", None) + quote_items = {} + if quote: + for item in quote.items.select_related("contract_post"): + quote_items[item.contract_post_id] = item + + # Récupérer les états d'avancement si demandé + progress_states_data = {} + if include_progress_states: + progress_states = self.progress_states.all().order_by('created_at') + for ps in progress_states: + ps_items_map = {} + for item in ps.items.select_related('contract_post'): + ps_items_map[item.contract_post_id] = item.quantity + progress_states_data[ps.id] = { + 'state': ps, + 'items': ps_items_map + } + + post_ids = set(consumption_map.keys()) | set(quote_items.keys()) + + # Ajouter les postes des états d'avancement + if include_progress_states: + for ps_data in progress_states_data.values(): + post_ids.update(ps_data['items'].keys()) + + if not post_ids: + return [], [] + + posts = ( + ContractPost.objects.filter(id__in=post_ids) + .select_related("contract") + ) + + summary = [] + for post in posts: + consumed = Decimal(consumption_map.get(post.id) or 0) + quote_item = quote_items.get(post.id) + planned = Decimal("0") + if quote_item and quote_item.quantity is not None: + planned = Decimal(quote_item.quantity) + + remaining = planned - consumed + consumed_amount = amount_map.get(post.id, Decimal("0")) + + item_data = { + "contract_post": post, + "consumed_quantity": consumed, + "planned_quantity": planned, + "remaining_quantity": remaining, + "consumed_amount": consumed_amount, + } + + # Ajouter les quantités des états d'avancement pour ce poste + if include_progress_states: + item_data["progress_states"] = {} + for ps_id, ps_data in progress_states_data.items(): + item_data["progress_states"][ps_id] = Decimal(ps_data['items'].get(post.id, 0)) + + summary.append(item_data) + + summary.sort(key=lambda item: item["contract_post"].order_number) + + if include_progress_states: + # Retourner aussi la liste des états d'avancement + states_list = [ps_data['state'] for ps_data in progress_states_data.values()] + return summary, states_list + + return summary + + +class ContractOrderQuote(models.Model): + order = models.OneToOneField(ContractOrder, on_delete=models.CASCADE, related_name="quote") + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="created_quotes", + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_finalized = models.BooleanField(default=False) + finalized_at = models.DateTimeField(null=True, blank=True) + finalized_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="finalized_quotes", + ) + + def __str__(self): + return f"Devis {self.order.order_code}" + + def get_total_amount(self): + total = Decimal("0") + for item in self.items.select_related("contract_post"): + total += item.get_total_amount() + return total + + def finalize(self, user=None): + if self.is_finalized: + return + + from django.utils import timezone + + self.is_finalized = True + self.finalized_at = timezone.now() + if user is not None: + self.finalized_by = user + self.save(update_fields=["is_finalized", "finalized_at", "finalized_by", "updated_at"]) + + def get_consumed_quantities(self, exclude_intervention_id=None, include_current_intervention_periods=None): + from interventions.models import InterventionContractPost, InterventionPostPeriodQuantity + + # Calculer les quantités consommées par les autres interventions + qs = InterventionContractPost.objects.filter(intervention__order=self.order) + if exclude_intervention_id: + qs = qs.exclude(intervention_id=exclude_intervention_id) + + rows = qs.values("contract_post").annotate(total_quantity=Sum("quantity")) + consumption_map = { + row["contract_post"]: row["total_quantity"] or Decimal("0") + for row in rows + } + + # Ajouter les quantités validées des périodes précédentes de l'intervention en cours + if include_current_intervention_periods: + period_qs = InterventionPostPeriodQuantity.objects.filter( + intervention_id=include_current_intervention_periods, + intervention__order=self.order + ) + period_rows = period_qs.values("contract_post").annotate(total_quantity=Sum("quantity")) + + for row in period_rows: + post_id = row["contract_post"] + period_qty = row["total_quantity"] or Decimal("0") + consumption_map[post_id] = consumption_map.get(post_id, Decimal("0")) + period_qty + + return consumption_map + + def get_remaining_items(self, exclude_intervention_id=None, include_zero=False, include_current_intervention_periods=None): + consumption_map = self.get_consumed_quantities( + exclude_intervention_id=exclude_intervention_id, + include_current_intervention_periods=include_current_intervention_periods + ) + items = [] + for quote_item in self.items.select_related("contract_post"): + planned = Decimal(quote_item.quantity or 0) + consumed = Decimal(consumption_map.get(quote_item.contract_post_id) or 0) + remaining = planned - consumed + if include_zero or remaining != 0: + items.append({ + "quote_item": quote_item, + "contract_post": quote_item.contract_post, + "planned_quantity": planned, + "consumed_quantity": consumed, + "remaining_quantity": remaining, + }) + + items.sort(key=lambda item: item["contract_post"].order_number) + return items + + +class ContractOrderQuoteItem(models.Model): + quote = models.ForeignKey(ContractOrderQuote, on_delete=models.CASCADE, related_name="items") + contract_post = models.ForeignKey(ContractPost, on_delete=models.CASCADE) + quantity = models.DecimalField(max_digits=10, decimal_places=2) + + class Meta: + unique_together = ("quote", "contract_post") + ordering = ["contract_post__order_number"] + + def __str__(self): + return f"{self.contract_post.code} - {self.quantity}" + + def get_total_amount(self): + unit_price = self.contract_post.unit_price or Decimal("0") + quantity = Decimal(self.quantity or 0) + return (unit_price * quantity).quantize(Decimal("0.01")) + + + +ORDER_DOCUMENT_TYPE_CHOICES = [ + ('invoice', 'Facture'), + ('claim_declaration', 'Déclaration de créance'), + ('state_of_works', "État de travaux"), + ('posts_detail', "Détail des postes"), + ('other', 'Autre'), +] + +def order_document_upload_path(instance, filename): + # On récupère l'année et le mois actuels + now = timezone.now() + year = now.strftime("%Y") + month = now.strftime("%m") + + # On construit le chemin avec l'ID de l'intervention (attention : doit être déjà enregistrée) + order_code = instance.order.order_code if instance.order else "unknown" + return f"intervention_documents/{year}/{order_code}/{filename}" + +class ContractOrderDocument(models.Model): + """Modèle représentant un document attaché à une commande de contrat""" + order = models.ForeignKey(ContractOrder, on_delete=models.CASCADE, related_name="documents") + title = models.CharField(max_length=255) + document = models.FileField(upload_to=order_document_upload_path, storage=private_storage) + document_type = models.CharField(max_length=50, choices=ORDER_DOCUMENT_TYPE_CHOICES, default='other') + uploaded_at = models.DateTimeField(auto_now_add=True) + progress_state = models.ForeignKey( + 'ContractOrderProgressState', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='ps_documents', + help_text=_("État d'avancement associé à ce document, le cas échéant"), + ) + + def __str__(self): + return f"{self.title} ({self.order.order_code})" + + def is_claim_declaration(self): + """Retourne True si ce document est une déclaration de créance""" + return self.document_type == 'claim_declaration' + + +def claim_attachment_upload_path(instance, filename): + """Chemin de stockage pour les pièces jointes des déclarations de créance""" + now = timezone.now() + year = now.strftime("%Y") + order_code = instance.claim_document.order.order_code if instance.claim_document else "unknown" + return f"claim_attachments/{year}/{order_code}/{filename}" + + +class ClaimDeclarationAttachment(models.Model): + """Modèle représentant une pièce jointe (justificatif) pour une déclaration de créance""" + claim_document = models.ForeignKey( + ContractOrderDocument, + on_delete=models.CASCADE, + related_name="attachments", + limit_choices_to={'document_type': 'claim_declaration'}, + verbose_name=_("Déclaration de créance"), + ) + title = models.CharField( + max_length=255, + verbose_name=_("Titre"), + help_text=_("Titre ou description du justificatif"), + ) + document = models.FileField( + upload_to=claim_attachment_upload_path, + storage=private_storage, + verbose_name=_("Fichier"), + ) + uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date d'ajout")) + uploaded_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="uploaded_claim_attachments", + verbose_name=_("Ajouté par"), + ) + + def __str__(self): + return f"{self.title} ({self.claim_document.title})" + + class Meta: + verbose_name = _("Pièce jointe de déclaration de créance") + verbose_name_plural = _("Pièces jointes de déclarations de créance") + ordering = ['uploaded_at'] + + +class ContractOrderProgressState(models.Model): + """Modèle représentant un état d'avancement pour une commande de contrat""" + order = models.ForeignKey(ContractOrder, on_delete=models.CASCADE, related_name="progress_states") + index = models.PositiveIntegerField(null=True, blank=True, help_text=_("Numéro d'ordre de l'état d'avancement pour cette commande")) + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, + related_name="created_progress_states") + + def __str__(self): + return f"État d'avancement - {self.order.order_code} - {self.created_at.strftime('%d/%m/%Y')}" + + class Meta: + verbose_name = "État d'avancement" + verbose_name_plural = "États d'avancement" + ordering = ['created_at'] + + +class ContractOrderProgressStateItem(models.Model): + """Modèle représentant les quantités d'un poste dans un état d'avancement""" + progress_state = models.ForeignKey(ContractOrderProgressState, on_delete=models.CASCADE, related_name="items") + contract_post = models.ForeignKey(ContractPost, on_delete=models.CASCADE) + quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0, + help_text=_("Quantité consommée à cette date")) + + def __str__(self): + return f"{self.contract_post.code} - {self.quantity}" + + class Meta: + verbose_name = "Item d'état d'avancement" + verbose_name_plural = "Items d'états d'avancement" + unique_together = ("progress_state", "contract_post") + ordering = ['contract_post__order_number'] + + +def order_attachment_upload_path(instance, filename): + now = timezone.now() + year = now.strftime("%Y") + month = now.strftime("%m") + order_code = slugify(instance.order.order_code) if instance.order else "unknown" + return f"order_attachments/{year}/{month}/{order_code}/{filename}" + + +DIRECT_ORDER_POST_STATUS_CHOICES = [ + ('pending', _('En attente de validation')), + ('validated', _('Approuvé')), + ('refused', _('Refusé')), + ('invoiced', _('Facturé')), +] + + +class DirectOrderPost(models.Model): + """Commande directe d'un poste de marché sans passer par une intervention.""" + order = models.ForeignKey( + ContractOrder, + on_delete=models.CASCADE, + related_name='direct_posts', + verbose_name=_("Commande"), + ) + contract_post = models.ForeignKey( + ContractPost, + on_delete=models.CASCADE, + related_name='direct_order_posts', + verbose_name=_("Poste de marché"), + ) + quantity = models.DecimalField( + max_digits=10, + decimal_places=2, + verbose_name=_("Quantité"), + ) + comment = models.TextField(blank=True, null=True, verbose_name=_("Commentaire")) + control_status = models.CharField( + max_length=20, + choices=DIRECT_ORDER_POST_STATUS_CHOICES, + default='pending', + verbose_name=_("Statut de validation"), + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date de création")) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='created_direct_order_posts', + verbose_name=_("Créé par"), + ) + validated_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de validation")) + validated_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='validated_direct_order_posts', + verbose_name=_("Validé par"), + ) + invoiced_quantity = models.DecimalField( + max_digits=10, + decimal_places=2, + default=Decimal('0'), + verbose_name=_("Quantité déjà facturée"), + help_text=_("Quantité cumulée sur les déclarations de créance précédentes."), + ) + + @property + def total_price(self): + """Calcule le montant total du poste direct. + Reprend la même logique que InterventionContractPost.total_price, + mais appliquée aux autres postes directs de la même commande.""" + cp = self.contract_post + qty = Decimal(self.quantity or 0) + unit_price = cp.unit_price or Decimal('0') + + if cp.is_coefficient_multipliable and cp.post_type: + effective_qty = Decimal('1') if qty > 0 else Decimal('0') + same_type_posts = DirectOrderPost.objects.filter( + order=self.order, + contract_post__post_type=cp.post_type, + ).exclude( + contract_post__is_coefficient_multipliable=True + ).select_related('contract_post') + total_same_type_cost = sum( + Decimal(p.quantity or 0) * (p.contract_post.unit_price or Decimal('0')) + for p in same_type_posts + ) + return effective_qty * unit_price * total_same_type_cost + + base = qty * unit_price + if cp.is_amount_to_be_justified and cp.coeff_amount_to_be_justified: + return base * cp.coeff_amount_to_be_justified + return base + + def __str__(self): + return f"{self.contract_post.code} - {self.quantity} ({self.order.order_code})" + + class Meta: + verbose_name = _("Commande directe – poste") + verbose_name_plural = _("Commandes directes – postes") + ordering = ['contract_post__order_number'] + unique_together = ('order', 'contract_post') + + +class ContractOrderAttachment(models.Model): + """Document générique (photo, PDF, …) associé à une commande, avec vignette.""" + order = models.ForeignKey( + ContractOrder, + on_delete=models.CASCADE, + related_name="attachments", + verbose_name=_("Commande"), + ) + file = models.FileField( + upload_to=order_attachment_upload_path, + storage=private_storage, + verbose_name=_("Fichier"), + max_length=255, + ) + thumbnail = models.ImageField( + upload_to=order_attachment_upload_path, + storage=private_storage, + null=True, + blank=True, + max_length=255, + verbose_name=_("Vignette"), + ) + upload_date = models.DateTimeField(auto_now_add=True, verbose_name=_("Date d'ajout")) + uploaded_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="uploaded_order_attachments", + verbose_name=_("Ajouté par"), + ) + + @property + def filename(self): + import os + return os.path.basename(self.file.name) + + def __str__(self): + return f"{self.filename} ({self.order.order_code})" + + class Meta: + verbose_name = _("Pièce jointe de commande") + verbose_name_plural = _("Pièces jointes de commande") + ordering = ["upload_date"] + \ No newline at end of file diff --git a/streetup/contracts/permissions.py b/streetup/contracts/permissions.py new file mode 100644 index 0000000..3375386 --- /dev/null +++ b/streetup/contracts/permissions.py @@ -0,0 +1,419 @@ +from django.core.exceptions import PermissionDenied +from django.utils.translation import gettext as _ + +from functools import wraps + +from common.models import UserContractAccess, UserContractAccess, UserConfig, UserThematics +from contracts.models import Contract, ContractOrder, ContractOrderDocument, ClaimDeclarationAttachment + + + + +ALLOWED_ROLES_FOR_CONTRACT_ACCESS = {"admin", "manager", "external_manager", "controller", "top_manager"} + +def get_allowed_contracts_for_user(user): + """ + Retourne une queryset des contrats accessibles à un utilisateur + s'il a le droit de voir les interventions ET un rôle autorisé. + """ + try: + config = user.config + except AttributeError: + return Contract.objects.none() + + # Vérifie les rôles + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS): + return Contract.objects.none() + + if "admin" in user_roles or "top_manager" in user_roles: + return Contract.objects.all() + + # Filtre les UserContractAccess valides + allowed_contract_ids = UserContractAccess.objects.filter( + user_config=config, + can_view_interventions=True + ).values_list("contract_id", flat=True) + + return Contract.objects.filter(id__in=allowed_contract_ids) + + + +def user_has_access_to_contract(user, contract_id): + """ + Vérifie si un utilisateur peut accéder à un contrat donné : + - A un rôle autorisé + - A un accès UserContractAccess avec can_view_interventions=True + """ + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS): + return False + + if "admin" in user_roles or "top_manager" in user_roles: + return True + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_interventions=True + ).exists() + + +def user_can_view_contract_prices(user, contract_id): + """Vrai si l'utilisateur est admin ou dispose du droit can_view_prices sur le contrat.""" + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS): + return False + + if "admin" in user_roles or "top_manager" in user_roles: + return True + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_prices=True + ).exists() + + +def contract_view_required(view_func): + """ + Décorateur basé sur user_has_access_to_contract. + La vue doit avoir un paramètre `contract_id` (en keyword arg). + """ + + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + contract_id = kwargs.get("contract_id") + if contract_id is None: + raise PermissionDenied(_("Paramètre 'contract_id' manquant.")) + + from .permissions import user_has_access_to_contract # éviter import circulaire + + if not user_has_access_to_contract(request.user, contract_id): + raise PermissionDenied(_("Accès au contrat refusé.")) + + return view_func(request, *args, **kwargs) + + return _wrapped_view + + +def user_has_access_to_contract_order(user, order_id): + """ + Vérifie que l'utilisateur a accès au contrat lié à une ContractOrder. + """ + try: + order = ContractOrder.objects.select_related('contract').get(id=order_id) + except ContractOrder.DoesNotExist: + return False + + return user_has_access_to_contract(user, order.contract.id) + + +def order_view_required(view_func): + """ + Décorateur pour restreindre l'accès à une vue de ContractOrder. + Nécessite un paramètre keyword `order_id`. + """ + + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + order_id = kwargs.get("order_id") + if order_id is None: + raise PermissionDenied(_("Paramètre 'order_id' manquant.")) + + from .permissions import user_has_access_to_contract_order + + if not user_has_access_to_contract_order(request.user, order_id): + raise PermissionDenied(_("Accès à la commande refusé.")) + + return view_func(request, *args, **kwargs) + + return _wrapped_view + + +ALLOWED_ROLES_FOR_ORDER_CREATION = {"admin", "manager"} + +def user_can_add_order(user): + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if user_roles.intersection(ALLOWED_ROLES_FOR_ORDER_CREATION): + return True + + return False + + +def user_can_add_order_for_contract(user, contract_id): + """ + Vérifie que l'utilisateur a le droit d'ajouter une commande à un contrat donné : + - Il doit avoir un rôle 'admin' ou 'manager' + - Il doit avoir un accès UserContractAccess avec can_view_interventions=True + """ + if not user.is_authenticated: + return False + + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_ORDER_CREATION): + return False + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_interventions=True + ).exists() + + +def user_has_access_to_contract_posts(user, contract_id): + """ + Vérifie que l'utilisateur a accès aux postes du contrat. + """ + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_CONTRACT_ACCESS): + return False + + if "admin" in user_roles or "top_manager" in user_roles: + return True + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_posts=True + ).exists() + + +ALLOWED_ROLES_FOR_QUOTE_MANAGEMENT = {"admin", "external_manager"} + +def user_can_manage_quote_for_contract(user, contract_id): + """ + Vérifie que l'utilisateur a le droit de gérer le devis d'une commande : + - Il doit avoir un rôle 'admin' ou 'external_manager' + - Il doit avoir un accès UserContractAccess avec can_view_interventions=True + """ + if not user.is_authenticated: + return False + + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_QUOTE_MANAGEMENT): + return False + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_interventions=True + ).exists() + + +def user_can_manage_direct_order_posts(user, contract_id): + """ + Vérifie que l'utilisateur peut créer/modifier/supprimer des commandes directes : + - Il doit avoir le rôle 'external_manager' + - Il doit avoir un accès UserContractAccess avec can_view_interventions=True + """ + if not user.is_authenticated: + return False + + try: + config = user.config + except AttributeError: + return False + + if not config.roles.filter(name='external_manager').exists(): + return False + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_interventions=True + ).exists() + + +ALLOWED_ROLES_FOR_DIRECT_POST_VALIDATION = {"admin", "manager", "controller"} + +def user_can_validate_direct_order_posts(user, contract_id): + """ + Vérifie que l'utilisateur peut valider/refuser des commandes directes : + - Il doit avoir un rôle 'admin', 'manager' ou 'controller' + - Il doit avoir un accès UserContractAccess avec can_view_interventions=True + """ + if not user.is_authenticated: + return False + + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection(ALLOWED_ROLES_FOR_DIRECT_POST_VALIDATION): + return False + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=contract_id, + can_view_interventions=True + ).exists() + + +def user_can_generate_order_documents(user, order): + """ + Vérifie que l'utilisateur a le droit de générer des documents pour une commande donnée : + - Il doit avoir un rôle 'admin' ou 'external_manager' + - Il doit avoir un accès UserContractAccess avec can_view_interventions=True + """ + if not user.is_authenticated: + return False + + try: + config = user.config + except AttributeError: + return False + + user_roles = set(config.roles.values_list("name", flat=True)) + if not user_roles.intersection({"admin", "external_manager"}): + return False + + return UserContractAccess.objects.filter( + user_config=config, + contract_id=order.contract.id, + can_view_interventions=True + ).exists() + + +# --------------------------------------------------------------------------- +# Permissions équipes (CompanyTeam / CompanyMember) +# --------------------------------------------------------------------------- + +def get_viewable_teams_for_user(user): + """ + Retourne le queryset des équipes que l'utilisateur peut consulter. + Les admins voient toutes les équipes ; les autres ont besoin d'un + UserCompanyTeamAccess avec can_view=True. + """ + from contracts.models import CompanyTeam, UserCompanyTeamAccess + if not user.is_authenticated: + return CompanyTeam.objects.none() + try: + config = user.config + except AttributeError: + return CompanyTeam.objects.none() + user_roles = set(config.roles.values_list("name", flat=True)) + if "admin" in user_roles: + return CompanyTeam.objects.all() + return CompanyTeam.objects.filter( + user_accesses__user_config=config, + user_accesses__can_view=True, + ) + + +def user_can_view_teams(user): + """Vérifie si l'utilisateur peut accéder à au moins une équipe.""" + from contracts.models import UserCompanyTeamAccess + if not user.is_authenticated: + return False + try: + config = user.config + except AttributeError: + return False + user_roles = set(config.roles.values_list("name", flat=True)) + if "admin" in user_roles: + return True + return UserCompanyTeamAccess.objects.filter( + user_config=config, + can_view=True, + ).exists() + + +def user_can_edit_teams(user): + """ + Vérifie si l'utilisateur a le droit d'éditer des équipes. + Les admins ont toujours le droit ; les autres ont besoin d'un + UserCompanyTeamAccess avec can_edit=True. + """ + from contracts.models import UserCompanyTeamAccess + if not user.is_authenticated: + return False + try: + config = user.config + except AttributeError: + return False + user_roles = set(config.roles.values_list("name", flat=True)) + if "admin" in user_roles: + return True + return UserCompanyTeamAccess.objects.filter( + user_config=config, + can_edit=True, + ).exists() + + +def user_can_edit_specific_team(user, team_id): + """ + Vérifie si l'utilisateur peut éditer une équipe précise (créer/supprimer + des membres, déplacer des membres vers/depuis cette équipe). + """ + from contracts.models import UserCompanyTeamAccess + if not user.is_authenticated: + return False + try: + config = user.config + except AttributeError: + return False + user_roles = set(config.roles.values_list("name", flat=True)) + if "admin" in user_roles: + return True + return UserCompanyTeamAccess.objects.filter( + user_config=config, + team_id=team_id, + can_edit=True, + ).exists() + + + + +def guard_contract_order_file(obj: ContractOrder, user) -> bool: + # Accès si l’utilisateur a accès à la commande + return user_has_access_to_contract_order(user, obj.id) + +def guard_contract_file(obj: Contract, user) -> bool: + # Si un jour tu ajoutes un FileField sur Contract + return user_has_access_to_contract(user, obj.id) + +def guard_order_document_file(obj: ContractOrderDocument, user) -> bool: + # Document rattaché à une commande → on réutilise la même permission + return user_has_access_to_contract_order(user, obj.order_id) + + +def guard_claim_attachment_file(obj: ClaimDeclarationAttachment, user) -> bool: + # Pièce jointe de déclaration de créance → accès via la commande du document parent + return user_has_access_to_contract_order(user, obj.claim_document.order_id) + +def guard_order_attachment_file(obj, user) -> bool: + # Pièce jointe générique d'une commande (photos, PDF, …) + return user_has_access_to_contract_order(user, obj.order_id) \ No newline at end of file diff --git a/streetup/contracts/signals.py b/streetup/contracts/signals.py new file mode 100644 index 0000000..24fe10b --- /dev/null +++ b/streetup/contracts/signals.py @@ -0,0 +1,84 @@ +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver +from contracts.models import ContractOrderQuote, ContractOrderQuoteItem + + +@receiver(post_save, sender=ContractOrderQuote) +def update_order_total_on_quote_save(sender, instance, **kwargs): + """ + Met à jour le total_amount de la commande lorsqu'un devis est créé ou modifié. + """ + order = instance.order + order.total_amount = instance.get_total_amount() + order.save(update_fields=['total_amount']) + + +@receiver(post_delete, sender=ContractOrderQuote) +def clear_order_total_on_quote_delete(sender, instance, **kwargs): + """ + Réinitialise le total_amount de la commande lorsqu'un devis est supprimé. + """ + order = instance.order + order.total_amount = 0 + order.save(update_fields=['total_amount']) + + +@receiver(post_save, sender=ContractOrderQuoteItem) +def update_order_total_on_quote_item_save(sender, instance, **kwargs): + """ + Met à jour le total_amount de la commande lorsqu'un item du devis est créé ou modifié. + """ + quote = instance.quote + order = quote.order + order.total_amount = quote.get_total_amount() + order.save(update_fields=['total_amount']) + + +@receiver(post_delete, sender=ContractOrderQuoteItem) +def update_order_total_on_quote_item_delete(sender, instance, **kwargs): + """ + Met à jour le total_amount de la commande lorsqu'un item du devis est supprimé. + """ + quote = instance.quote + order = quote.order + order.total_amount = quote.get_total_amount() + order.save(update_fields=['total_amount']) + + +from django.db.models.signals import m2m_changed +from contracts.models import CompanyMember, CompanyMemberHistory +from django.utils import timezone + +@receiver(m2m_changed, sender=CompanyMember.teams.through) +def update_member_team_history(sender, instance, action, reverse, pk_set, **kwargs): + if reverse: + return + + today = timezone.now().date() + + if action == "post_add": + # Pour chaque équipe ajoutée, on crée un enregistrement d'historique s'il n'existe pas déjà de période active + for team_id in pk_set: + CompanyMemberHistory.objects.get_or_create( + member=instance, + team_id=team_id, + leave_date__isnull=True, + defaults={'join_date': today} + ) + + elif action == "post_remove": + # Pour chaque équipe retirée, on clôture la période d'appartenance active + for team_id in pk_set: + CompanyMemberHistory.objects.filter( + member=instance, + team_id=team_id, + leave_date__isnull=True + ).update(leave_date=today) + + elif action == "pre_clear": + # Avant d'effacer toutes les équipes d'un membre, on clôture toutes les périodes actives + CompanyMemberHistory.objects.filter( + member=instance, + leave_date__isnull=True + ).update(leave_date=today) + diff --git a/streetup/contracts/static/contracts/contracts_order_quote_form.js b/streetup/contracts/static/contracts/contracts_order_quote_form.js new file mode 100644 index 0000000..707787f --- /dev/null +++ b/streetup/contracts/static/contracts/contracts_order_quote_form.js @@ -0,0 +1,259 @@ +function getQuoteQuantityData(input) { + if (!input) { + return { normalized: '', numeric: 0 }; + } + + const rawValue = typeof input.value === 'string' ? input.value.trim() : ''; + if (!rawValue) { + return { normalized: '', numeric: 0 }; + } + + const normalized = rawValue.replace(',', '.'); + const numeric = Number.parseFloat(normalized); + + if (Number.isNaN(numeric) || numeric < 0) { + return { normalized, numeric: 0 }; + } + + return { normalized, numeric }; +} + +function parseQuoteQuantity(input) { + const { numeric } = getQuoteQuantityData(input); + return numeric; +} + +(function() { + const table = document.getElementById('quote-items-table'); + if (!table) { + return; + } + + const orderFilterInput = table.querySelector('[data-filter-input="order_number"]'); + const codeFilterInput = table.querySelector('[data-filter-input="code"]'); + const descriptionFilterInput = table.querySelector('[data-filter-input="description"]'); + const quantityToggle = table.querySelector('[data-filter-quantity]'); + const quantityInputs = table.querySelectorAll('.quote-quantity-input'); + const rows = Array.from(table.querySelectorAll('tbody tr[data-post-id]')); + const noResultsRow = document.getElementById('quote-no-results'); + + function normalize(value) { + if (value === undefined || value === null) { + return ''; + } + return value + .toString() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); + } + + function applyFilters() { + const orderFilter = normalize(orderFilterInput ? orderFilterInput.value : ''); + const codeFilter = normalize(codeFilterInput ? codeFilterInput.value : ''); + const descriptionFilter = normalize(descriptionFilterInput ? descriptionFilterInput.value : ''); + const mustHaveQuantity = Boolean(quantityToggle && quantityToggle.checked); + + let visibleCount = 0; + + rows.forEach((row) => { + const rowOrder = normalize(row.dataset.orderNumber); + const rowCode = normalize(row.dataset.code); + const rowDescription = normalize(row.dataset.description); + const quantityInput = row.querySelector('.quote-quantity-input'); + const quantityValue = parseQuoteQuantity(quantityInput); + + const matchesOrder = !orderFilter || rowOrder.includes(orderFilter); + const matchesCode = !codeFilter || rowCode.includes(codeFilter); + const matchesDescription = !descriptionFilter || rowDescription.includes(descriptionFilter); + const matchesQuantity = !mustHaveQuantity || quantityValue > 0; + + const shouldShow = matchesOrder && matchesCode && matchesDescription && matchesQuantity; + row.classList.toggle('d-none', !shouldShow); + if (shouldShow) { + visibleCount += 1; + } + }); + + if (noResultsRow) { + if (visibleCount === 0 && rows.length > 0) { + noResultsRow.classList.add('show'); + } else { + noResultsRow.classList.remove('show'); + } + } + } + + if (orderFilterInput) { + orderFilterInput.addEventListener('input', applyFilters); + } + if (codeFilterInput) { + codeFilterInput.addEventListener('input', applyFilters); + } + if (descriptionFilterInput) { + descriptionFilterInput.addEventListener('input', applyFilters); + } + if (quantityToggle) { + quantityToggle.addEventListener('change', applyFilters); + } + quantityInputs.forEach((input) => { + input.addEventListener('input', applyFilters); + input.addEventListener('change', applyFilters); + }); + + applyFilters(); +})(); + + + +document.addEventListener('DOMContentLoaded', function() { + const table = document.getElementById('quote-items-table'); + if (!table) return; + + // Fixe la largeur des colonnes après le premier rendu + const ths = table.querySelectorAll('thead tr:first-child th'); + ths.forEach((th, idx) => { + const width = th.offsetWidth; + th.style.width = width + 'px'; + // Fixe aussi la largeur des td correspondants + table.querySelectorAll('tbody tr td:nth-child(' + (idx + 1) + ')').forEach(td => { + td.style.width = width + 'px'; + }); + }); + + const form = document.getElementById('quoteItemsForm'); + const payloadInput = document.getElementById('quoteItemsPayload'); + if (!form || !payloadInput) { + return; + } + + // Helper to format and display value with two decimals without changing numeric meaning + function formatQuantityDisplay(input) { + if (!input || input.disabled) return; + const { normalized } = getQuoteQuantityData(input); + if (!normalized) { + input.value = ''; + return; + } + const num = Number.parseFloat(normalized); + if (Number.isNaN(num)) return; + // Always display two decimals + input.value = num.toFixed(2); + } + + // Function to update the total amount for a row + function updateRowTotal(input) { + const row = input.closest('tr[data-post-id]'); + if (!row) return; + + const totalCell = row.querySelector('.quote-total-amount'); + if (!totalCell) return; + + const unitPrice = parseFloat(row.dataset.unitPrice) || 0; + const quantity = parseQuoteQuantity(input); + + if (quantity > 0) { + const total = unitPrice * quantity; + totalCell.textContent = total.toFixed(2) + ' €'; + } else { + totalCell.textContent = '—'; + } + updateGrandTotal(); + } + + // Function to update the grand total by summing all row totals + function updateGrandTotal() { + const grandTotalEl = document.querySelector('[data-quote-grand-total]'); + if (!grandTotalEl) return; + + const allRows = Array.from(table.querySelectorAll('tbody tr[data-post-id]')); + let sum = 0; + allRows.forEach((row) => { + const unitPrice = parseFloat(row.dataset.unitPrice) || 0; + const quantityInput = row.querySelector('.quote-quantity-input'); + const quantity = parseQuoteQuantity(quantityInput); + if (quantity > 0) { + // Round each line to 2dp before summing (same logic as Python backend) + sum += Math.round(unitPrice * quantity * 100) / 100; + } + }); + + grandTotalEl.textContent = sum.toFixed(2) + ' €'; + } + + // Ensure current values show two decimals on load + const qtyInputs = table.querySelectorAll('.quote-quantity-input'); + qtyInputs.forEach((input) => { + formatQuantityDisplay(input); + updateRowTotal(input); // Calculate initial total (also calls updateGrandTotal) + + // Detect spinner usage via keyboard arrows + let spinByKeys = false; + input.addEventListener('keydown', (e) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + spinByKeys = true; + } + }); + input.addEventListener('keyup', () => { + spinByKeys = false; + }); + + // Heuristic: a recent mousedown likely indicates clicking the spinner + let spinByMouse = false; + let spinMouseTimer = null; + input.addEventListener('mousedown', () => { + spinByMouse = true; + if (spinMouseTimer) window.clearTimeout(spinMouseTimer); + spinMouseTimer = window.setTimeout(() => { + spinByMouse = false; + }, 400); + }); + + // While using spinner (keyboard or mouse), keep two decimals live + input.addEventListener('input', () => { + if (spinByKeys || spinByMouse) { + formatQuantityDisplay(input); + } + updateRowTotal(input); // Update total on input + }); + + // Always normalize display on change and when leaving the field + input.addEventListener('change', () => { + formatQuantityDisplay(input); + updateRowTotal(input); // Update total on change + }); + input.addEventListener('blur', () => { + formatQuantityDisplay(input); + updateRowTotal(input); // Update total on blur + }); + }); + + form.addEventListener('submit', function() { + const rows = Array.from(table.querySelectorAll('tbody tr[data-post-id]')); + const payload = []; + + rows.forEach((row) => { + const quantityInput = row.querySelector('.quote-quantity-input'); + if (!quantityInput || quantityInput.disabled) { + return; + } + + const { normalized, numeric } = getQuoteQuantityData(quantityInput); + if (!normalized || numeric <= 0) { + return; + } + + const postId = Number.parseInt(row.dataset.postId, 10); + if (Number.isNaN(postId)) { + return; + } + + payload.push({ + contract_post_id: postId, + quantity: normalized, + }); + }); + + payloadInput.value = JSON.stringify(payload); + }); +}); \ No newline at end of file diff --git a/streetup/contracts/static/contracts/timeline.css b/streetup/contracts/static/contracts/timeline.css new file mode 100644 index 0000000..f5b569d --- /dev/null +++ b/streetup/contracts/static/contracts/timeline.css @@ -0,0 +1,107 @@ +.timeline { + display: flex; + align-items: flex-start; /* Aligner les cercles correctement */ + overflow-x: auto; + padding: 20px; +} + +.step { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + margin-right: 30px; +} + +.step:last-child { + margin-right: 0; + padding-left: 3px; + padding-right: 3px; +} + +/* Ligne entre les cercles */ +.step::after { + content: ""; + position: absolute; + width: 30px; + height: 5px; + background-color: #ccc; + top: 15px; /* Ajuste la position de la ligne */ + left: 100%; + /* transform: translateX(15px); */ +} + +.step:last-child::after { + display: none; +} + +/* Style des cercles */ +.circle { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + color: white; + background-color: #ccc; +} + +.past .circle { + background-color: #ccc; +} + +.current .circle { + background-color: #febf00; +} + +.future { + display: none; +} + +/* Cercle d'action supplémentaire */ +.action { + background-color: #0d6efd !important; + cursor: pointer; +} +.action:hover { + background-color: #073c8b !important; +} + +.action.cancelled { + background-color: #fc4848 !important; + +} +.action.cancelled:hover { + background-color: #8b1d07 !important; +} + + + +/* Labels uniquement sur le dernier élément et nextStep */ +.steplabel { + font-size: 12px; + text-align: center; + max-width: 80px; + color: #333; +} + +/* Spacer invisible pour aligner tous les cercles */ +.spacer { + height: 17px; /* Compense la hauteur du label */ + visibility: hidden; +} + +/* Ligne en pointillé après le next step si ce n'est pas le dernier élément */ +.next-step-dashed::after { + content: ""; + position: absolute; + width: 30px; + height: 5px; + background: repeating-linear-gradient(90deg,#ccc 0 8px,#fff0 0 11px); /*5px red then 2px transparent -> repeat this!*/ + top: 15px; + left: 100%; + display: block !important; /* Force l'affichage */ +} diff --git a/streetup/contracts/static/contracts/timeline.js b/streetup/contracts/static/contracts/timeline.js new file mode 100644 index 0000000..edc2b49 --- /dev/null +++ b/streetup/contracts/static/contracts/timeline.js @@ -0,0 +1,148 @@ +const statusTimeline = document.getElementById("statusTimeline"); + +// Fonction pour récupérer le token CSRF depuis les cookies +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +const csrftoken = getCookie('csrftoken'); +const orderId = document.getElementById('orderId').value; + +let currentIndex = -1; + +// Trouver l'index du statut actuel +STATUS_CHOICES.forEach((status, index) => { + if (status.key === currentStatus) { + currentIndex = index; + } +}); + +let nextActionsIndex = []; +if (!['cancelled'].includes(currentStatus)) { + nextActionsIndex = [currentIndex + 1]; +} +let pastActionsIndex = []; + +// Définir les actions possibles selon le statut actuel +// if (['sent'].includes(currentStatus)) { +// nextActionsIndex.push(currentIndex + 2); // completed +// } +// else if (['completed'].includes(currentStatus)) { +// nextActionsIndex.push(currentIndex + 2); // invoiced +// } +// else if (['invoiced'].includes(currentStatus)) { +// nextActionsIndex.push(currentIndex + 2); // paid +// } + +// Ajouter la possibilité d'annuler depuis n'importe quel statut sauf 'completed', 'invoiced', 'paid' et 'cancelled' +if (!['completed', 'invoiced', 'paid', 'cancelled'].includes(currentStatus)) { + const cancelledIndex = STATUS_CHOICES.findIndex(s => s.key === 'cancelled'); + if (cancelledIndex !== -1) { + nextActionsIndex.push(cancelledIndex); + } +} + +// Générer la timeline +STATUS_CHOICES.forEach((status, index) => { + let statusClass = "hide"; + let actionClass = ""; + let dashedClass = ""; + let buttonClass = ""; + let label = status.label; + let icon = ""; + const isAllowed = allowedStatuses.includes(status.key); + + if (index === currentIndex) { + statusClass = "current"; + dashedClass = "next-step-dashed"; + } else if (index < currentIndex) { + statusClass = "past"; + if (pastActionsIndex.includes(index)) { + if (isAllowed) { + actionClass = "action"; + buttonClass = "button"; + icon = "bi-arrow-left"; + } + } else { + label = ""; + } + } else { + if (nextActionsIndex.includes(index)) { + statusClass = "next"; + if (isAllowed) { + actionClass = "action"; + buttonClass = "button"; + dashedClass = "next-step-dashed"; + icon = "bi-arrow-right"; + } + } + } + + if (index === (STATUS_CHOICES.length - 1)) { + dashedClass = ""; + } + + if (statusClass !== "hide") { + const step = document.createElement("div"); + step.className = `step ${statusClass} ${dashedClass}`; + step.innerHTML = ` +
          + ${icon ? '' : (index + 1)} +
          +
          ${label}
          + `; + statusTimeline.appendChild(step); + } +}); + +// Gérer les clics sur les cercles d'action avec délégation d'événements +$(document).on('click', '.circle.action', function () { + const newStatus = $(this).attr('status'); + + // Confirmer l'action pour l'annulation + if (newStatus === 'cancelled') { + if (!confirm(gettext("Êtes-vous sûr de vouloir annuler cette commande ?"))) { + return; + } + } + + updateStatus(newStatus); +}); + +function updateStatus(newStatus) { + $.ajax({ + url: `/contracts/orders/${orderId}/update-status/`, + method: 'POST', + headers: {'X-CSRFToken': csrftoken}, + data: { + order_id: orderId, + new_status: newStatus + }, + success: function (response) { + location.reload(); + }, + error: function (xhr) { + let message = gettext("Erreur lors de la mise à jour du statut"); + try { + const errorData = JSON.parse(xhr.responseText); + if (errorData.message) { + message = errorData.message; + } + } catch (e) { + // Ignore JSON parse errors + } + alert(message); + } + }); +} diff --git a/streetup/contracts/templates/admin/contracts/contractpost/change_list.html b/streetup/contracts/templates/admin/contracts/contractpost/change_list.html new file mode 100644 index 0000000..8612467 --- /dev/null +++ b/streetup/contracts/templates/admin/contracts/contractpost/change_list.html @@ -0,0 +1,11 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} + +{% block object-tools-items %} +
        • + + {% trans "Importer / mettre à jour des postes (Excel)" %} + +
        • + {{ block.super }} +{% endblock %} diff --git a/streetup/contracts/templates/admin/contracts/contractpost/import_posts.html b/streetup/contracts/templates/admin/contracts/contractpost/import_posts.html new file mode 100644 index 0000000..03b137b --- /dev/null +++ b/streetup/contracts/templates/admin/contracts/contractpost/import_posts.html @@ -0,0 +1,94 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block title %}Importer des postes de contrat{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

          Importer / mettre à jour des postes de contrat depuis Excel

          + +

          + Format attendu : colonnes Nr. Post (A), Code (B), + Description NL (C), AO (D), Unité (E), Description FR (F), + NC (G), U (H), Quantité (I), Prix unitaire (J).
          + Si Mettre à jour les postes existants est coché, les postes déjà présents + (identifiés par leur numéro de poste) seront mis à jour ; sinon ils sont ignorés. +

          + +{% if messages %} +
            + {% for message in messages %} +
          • {{ message }}
          • + {% endfor %} +
          +{% endif %} + +{% if stats %} +
          +

          Résultat de l'import{% if dry_run %} (dry-run – rien n'a été enregistré){% endif %}

          + + + + + + + + {% if stats.errors %} + + {% endif %} + +
          Lignes avec numéro de poste{{ stats.total_rows }}
          Postes créés{{ stats.created }}
          Postes mis à jour{{ stats.updated }}
          Postes ignorés (déjà présents){{ stats.skipped_already_exists }}
          Postes ignorés (prix manquant){{ stats.skipped_no_price }}
          Erreurs{{ stats.errors }}
          + {% if log_lines %} +
          + Journal détaillé ({{ log_lines|length }} lignes) +
          {{ log_lines|join:"\n" }}
          +
          + {% endif %} +
          +{% endif %} + +
          + {% csrf_token %} +
          +

          Paramètres d'import

          + +
          + + {{ form.contract }} + {% if form.contract.errors %}
            {% for e in form.contract.errors %}
          • {{ e }}
          • {% endfor %}
          {% endif %} +
          + +
          + + {{ form.excel_file }} +

          Fichiers acceptés : .xlsx, .xlsm

          + {% if form.excel_file.errors %}
            {% for e in form.excel_file.errors %}
          • {{ e }}
          • {% endfor %}
          {% endif %} +
          + +
          + + {{ form.update_existing }} +

          {{ form.update_existing.help_text }}

          +
          + +
          + + {{ form.dry_run }} +

          {{ form.dry_run.help_text }}

          +
          +
          + +
          + + Annuler +
          +
          +{% endblock %} diff --git a/streetup/contracts/templates/contracts/claim_declaration_attachments.html b/streetup/contracts/templates/contracts/claim_declaration_attachments.html new file mode 100644 index 0000000..0040edf --- /dev/null +++ b/streetup/contracts/templates/contracts/claim_declaration_attachments.html @@ -0,0 +1,153 @@ +{% extends "base.html" %} +{% load i18n %} +{% load short_name %} + +{% block head %} + +{% endblock head %} + +{% block content %} +
          + + +

          {% translate "Justificatifs de la déclaration de créance" %}

          + + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + +
          +
          +
          {% translate "Déclaration de créance" %}
          +
          +
          +
          +
          +

          {% translate "Titre" %} : {{ claim_doc.title }}

          +

          {% translate "Commande" %} : {{ order.order_code }}

          +

          {% translate "Date de création" %} : {{ claim_doc.uploaded_at|date:"d/m/Y H:i" }}

          +
          + +
          +
          +
          + + +
          +
          +
          {% translate "Ajouter un justificatif" %}
          +
          +
          +
          + {% csrf_token %} +
          +
          + + {{ form.title }} + {% if form.title.errors %} +
          {{ form.title.errors }}
          + {% endif %} +
          +
          + + {{ form.document }} + {% if form.document.errors %} +
          {{ form.document.errors }}
          + {% endif %} +
          +
          + +
          +
          +
          +
          +
          + + +
          +
          +
          {% translate "Justificatifs attachés" %}
          + {{ attachments|length }} +
          +
          + {% if attachments %} +
          + {% for att in attachments %} +
          +
          + +
          + + {{ att.title }} + +
          + + {% translate "Ajouté le" %} {{ att.uploaded_at|date:"d/m/Y H:i" }} + {% if att.uploaded_by %} + {% translate "par" %} {{ att.uploaded_by|short_name }} + {% endif %} + +
          +
          +
          + + + +
          + {% csrf_token %} + +
          +
          +
          + {% endfor %} +
          + {% else %} +

          + + {% translate "Aucun justificatif n'a encore été ajouté à cette déclaration de créance." %} +

          + {% endif %} +
          +
          + + +
          +{% endblock %} diff --git a/streetup/contracts/templates/contracts/contracts_detail.html b/streetup/contracts/templates/contracts/contracts_detail.html new file mode 100644 index 0000000..2fce608 --- /dev/null +++ b/streetup/contracts/templates/contracts/contracts_detail.html @@ -0,0 +1,146 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block head %} + + + +{% endblock head %} + +{% block content %} +
          +

          {% translate "Détail du marché / contrat" %}

          +
          +
          + {% translate "Contrat" %} : {{ contract.contract_number }} +
          +
          +

          {% translate "Description" %} : {{ contract.description }}

          +

          {% translate "Prestataire" %} : {{ contract.company }}

          + +

          {% translate "Début" %} : {{ contract.start_date|date:"d/m/Y" }}

          +

          {% translate "Fin" %} : {{ contract.end_date|date:"d/m/Y" }}

          + +

          {% translate "Fonctionnaire dirigeant" %} : {{ contract.executive_officer }}

          + +

          + {% translate "Notifications email" %} : + {% if contract.send_email_notifications %} + {% translate "Oui" %} + {% else %} + {% translate "Non" %} + {% endif %} +

          + +
          + +

          + {% translate "Montant engagé" %} : {{ contract.committed_amount|default:"Non renseigné" }} € +

          + +
          +
          {% translate 'Montant "presté' %}
          +
          {{ contract.get_total_realized_amount|floatformat:2 }} €
          +
          +
          +
          +
          +
          +
          +
          + {% if order.committed_amount %} {{ contract.realized_percent|floatformat:0 }} {% endif %} +
          +
          + +
          +
          {% translate "Montant validé" %}
          +
          {{ contract.get_total_validated_amount|floatformat:2 }} €
          +
          +
          +
          +
          +
          +
          +
          + {{ contract.validated_percent|floatformat:0 }}% +
          +
          + + {% if is_intern %} +
          +

          + {% translate "Dossier interne" %} : + + {% if contract.internal_link %} Evia {% endif %} + +

          + {% endif %} + +
          + +
          + + + +
          +
          +

          {% translate "Commandes" %}

          +
          +
          + + + + + + + + + + + {% for order in contract.orders.all %} + + + + + + + + {% endfor %} + + +
          {% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
          + + + + {{ order.order_code }}{{ order.description }}{{ order.order_date|date:"d-m-Y" }} + + {{ order.get_order_status_display }} + +
          +
          +
          + +
          + +{% endblock %} \ No newline at end of file diff --git a/streetup/contracts/templates/contracts/contracts_index.html b/streetup/contracts/templates/contracts/contracts_index.html new file mode 100644 index 0000000..9e71896 --- /dev/null +++ b/streetup/contracts/templates/contracts/contracts_index.html @@ -0,0 +1,300 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block head %} + + + +{% endblock head %} + + +{% block content %} + +
          +
          + {% if is_add_order_allowed == True %} + + {% translate "Nouvelle commande" %} + + {% endif %} +
          + +
          +
          + +
          +
          +
          + +
          + +
          + +
          + + +
          + +
          + + +
          + +
          + + +
          + +
          + + +
          + +
          + + +
          + +
          + + +
          +
          +
          + + +
          + + +
          +
          + + +
          + + +
          + + {% translate "Réinitialiser" %} +
          +
          +
          +
          +
          + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + {% endfor %} + +
          {% translate "Numéro de commande" %}{% translate "Description" %}{% translate "Date" %}{% translate "Statut" %}{% translate "Fichier" %}{% translate "Marché" %}
          + + + + {{ order.order_code }}{{ order.description|truncatechars:100 }}{{ order.order_date }} + + {{ order.get_order_status_display }} + + {% if order.order_file %}{% endif %}{{ order.contract }}
          +
          +
          +
          +
          + + + + + + + + + + + + + {% for contract in contracts %} + + + + + + + + + {% endfor %} + +
          {% translate "Numéro" %}{% translate "Description" %}{% translate "Prestataire" %}{% translate "Début" %}{% translate "Fin" %}
          + + + + {{ contract.contract_number }}{{ contract.description|truncatechars:100 }}{{ contract.company.name }}{{ contract.start_date }}{{ contract.end_date }}
          +
          +
          +
          +
          + +{% endblock content%} + + +{% block script %} + + + +{% endblock script %} \ No newline at end of file diff --git a/streetup/contracts/templates/contracts/contracts_order_form.html b/streetup/contracts/templates/contracts/contracts_order_form.html new file mode 100644 index 0000000..1fe2a64 --- /dev/null +++ b/streetup/contracts/templates/contracts/contracts_order_form.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block head %} + + + +{% endblock head %} + + +{% block content %} +
          +

          + {% if order %} + {% translate "Modifier la commande" %} {{ order.order_code }} + {% else %} + {% translate "Ajouter une nouvelle commande" %} + {% endif %} +

          + +
          + {% csrf_token %} + {% if project_id %} + + {% endif %} + {{ form.as_p }} + +
          +
          + +{% endblock content%} + + +{% block script %} + +{% endblock script %} \ No newline at end of file diff --git a/streetup/contracts/templates/contracts/contracts_order_quote_form.html b/streetup/contracts/templates/contracts/contracts_order_quote_form.html new file mode 100644 index 0000000..737b6b3 --- /dev/null +++ b/streetup/contracts/templates/contracts/contracts_order_quote_form.html @@ -0,0 +1,175 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load l10n %} + +{% block head %} + +{% endblock head %} + +{% block content %} +
          +
          +

          {% blocktrans with order_code=order.order_code %}Devis pour la commande {{ order_code }}{% endblocktrans %}

          + + {% translate "Retour à la commande" %} + +
          + +
          +
          +

          {% translate "Contrat" %} : {{ order.contract.contract_number }}

          +

          {% translate "Description" %} : {{ order.description|default:_("Non renseigné") }}

          +

          {% translate "Montant total du devis" %} : {{ quote_total|floatformat:2 }} €

          +
          +
          + + {% if messages %} + {% for message in messages %} +
          {{ message }}
          + {% endfor %} + {% endif %} + + {% if quote.is_finalized %} + + {% endif %} + +
          + {% csrf_token %} + +
          + + + + + + + + + + + + + + + + + + + + + + + {% for row in rows %} + {% with post=row.post %} + + + + + + + + + + {% endwith %} + {% empty %} + + + + {% endfor %} + + + + +
          {% translate "N° de poste" %}{% translate "Code" %}{% translate "Description" %}{% translate "Unité" %}{% translate "Prix unitaire" %}{% translate "Quantité" %}{% translate "Montant" %}
          + + + + + + +
          + + +
          +
          {{ post.order_number }}{{ post.code }}{{ post.get_description_for_language }}{{ post.unit }}{{ post.unit_price|floatformat:2 }} € + + {% if row.errors %} +
          {{ row.errors|join:', ' }}
          + {% endif %} +
          —
          {% translate "Aucun poste disponible pour ce contrat." %}
          + {% translate "Aucun poste ne correspond aux filtres." %} +
          +
          + +
          + + {% translate "Retour à la commande" %} + + {% if can_edit_quote %} +
          + + {% if can_finalize_quote %} + + {% endif %} +
          + {% endif %} +
          +
          +
          +{% endblock content %} + +{% block script %} + +{% endblock script %} \ No newline at end of file diff --git a/streetup/contracts/templates/contracts/contracts_orders_detail.html b/streetup/contracts/templates/contracts/contracts_orders_detail.html new file mode 100644 index 0000000..9adcc39 --- /dev/null +++ b/streetup/contracts/templates/contracts/contracts_orders_detail.html @@ -0,0 +1,1225 @@ +{% extends "base.html" %} +{% load i18n %} +{% load short_name %} +{% load contract_filters %} +{% load static %} + +{% block head %} + + + + +{% endblock head %} + +{% block content %} +
          +

          {% translate "Détail de la commande" %}

          + + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + +
          +
          +
          +
          +
          +
          +
          + +
          +
          + {% translate "Commande" %} : {{ order.order_code }} +
          + + {{ order.get_order_status_display }} + + {% if order.order_status == 'pending' and is_order_edit_allowed == True %} + + + + {% endif %} +
          +
          +
          +

          {% translate "Contrat lié" %} : {{ order.contract.contract_number }} - {{ order.contract }}

          + + {% if order.description %} +

          {% translate "Description" %} : {{ order.description }}

          + {% endif %} + +

          {% translate "Date de commande" %} : {{ order.order_date|date:"d/m/Y" }}

          +

          {% translate "Date de livraison" %} : {{ order.delivery_date|date:"d/m/Y" }}

          + +
          + +

          + {% translate "Montant prévisionnel" %} : + {% translate "Non renseigné" as not_set %} + {{ order.total_amount|default:not_set }} € +

          + +
          +
          {% translate 'Montant "presté"' %}
          +
          {{ order.get_total_realized_amount|floatformat:2 }} €
          +
          +
          +
          +
          +
          +
          +
          + {% if order.total_amount %} {{ order.realized_percent|floatformat:0 }}% {% endif %} +
          +
          + +
          +
          {% translate "Montant validé" %}
          +
          {{ order.get_total_validated_amount|floatformat:2 }} €
          +
          +
          +
          +
          +
          +
          +
          + {{ order.validated_percent|floatformat:0 }}% +
          +
          + +
          + +
          +
          +
          {% translate "Devis" %}
          + {% if quote %} +

          {% translate "Montant total" %} : {{ quote_total|floatformat:2 }} €

          + {% if quote.is_finalized %} +

          {% translate "Devis figé" %} + {% if quote.finalized_at %} + ({{ quote.finalized_at|date:'d/m/Y H:i' }}) + {% endif %} +

          + {% endif %} +

          + {% blocktrans count item_count=quote.items.count %} + {{ item_count }} poste dans le devis. + {% plural %} + {{ item_count }} postes dans le devis. + {% endblocktrans %} +

          + {% else %} +

          {% translate "Aucun devis n'est associé à cette commande pour le moment." %}

          + {% endif %} +
          + +
          + +
          + + {% if order.ordered_by %} +

          {% translate "Commandé par" %} : {{ order.ordered_by|short_name }}

          + {% endif %} + +

          + {% translate "Fichier de commande" %} : + {% if order.order_file %} + + 📄 {% translate "Télécharger le fichier" %} + + {% endif %} +

          + +
          + +
          + + +
          +
          +

          {% translate "Documents" %}

          + {% if is_order_edit_allowed %} + + {% endif %} +
          +
          + {% if attachments %} +
          + {% for att in attachments %} +
          + +
          + {{ att.upload_date|date:"d/m/Y H:i" }}
          + {{ att.uploaded_by|short_name }} +
          +
          + {% endfor %} +
          + {% else %} +

          {% translate "Aucun document associé à cette commande." %}

          + {% endif %} +
          +
          + + {% if is_order_edit_allowed %} + + + + + + {% endif %} + + + + + +
          +
          +

          {% translate "Documents financiers" %}

          +
          +
          +

          + + {% translate "Générer un draft de l'état de travaux" %} + + + {% translate "Générer un draft de la déclaration de créance" %} + +

          + {% if is_generate_documents_allowed %} +

          + + {% translate "Générer la déclaration de créance" %} + +
          + + {% translate 'Cette opération va générer un état de travaux, une déclaration de créance et faire passer les interventions validées au statut "comptabilisé".' %} + +

          + {% endif %} +
            + {% regroup documents by progress_state.index as docs_by_state %} + + {% for group in docs_by_state %} + {% if group.grouper %} +
          • + {% blocktrans with num=group.grouper %}État d'avancement {{ num }}{% endblocktrans %} +
          • + {% else %} +
          • + {% translate "Autres documents" %} +
          • + {% endif %} + + {% for doc in group.list %} +
          • +
            + {% if doc.document.name|lower|slice:"-5:" == ".xlsx" or doc.document.name|lower|slice:"-4:" == ".xls" %} + + {% else %} + + {% endif %} + + {{ doc.title }} + +
            + + {% translate "Type" %} : {{ doc.get_document_type_display }} | + {% translate "Ajouté le" %} {{ doc.uploaded_at|date:"d/m/Y H:i" }} + {% if doc.document_type == 'claim_declaration' %} + | {{ doc.attachments.count }} {% translate "justificatif(s)" %} + {% endif %} + +
            + {% if doc.document_type == 'claim_declaration' %} + + {% translate "Justificatifs" %}{% if doc.attachments.count %} {{ doc.attachments.count }}{% endif %} + + {% endif %} +
          • + {% endfor %} + {% endfor %} +
          +
          +
          + + +
          +
          +

          {% translate "Consommation des postes" %}

          +
          +
          + {% if consumption_summary %} +
          + + + + + + + {% if quote %} + + {% endif %} + {% for ps in progress_states %} + + {% endfor %} + + {% if quote %} + + {% endif %} + + + + + + {% for item in consumption_summary %} + + + + + {% if quote %} + + {% endif %} + {% for ps in progress_states %} + + {% endfor %} + + {% if quote %} + + {% endif %} + + + + {% endfor %} + +
          {% translate "Poste" %}{% translate "Code" %}{% translate "Description" %}{% translate "Quantité prévue" %} + {% translate "État" %} {{ forloop.counter }}
          {{ ps.created_at|date:"d/m/Y" }}
          +
          {% translate "Quantité consommée" %}{% translate "Quantité restante" %}{% translate "Prix unitaire" %}{% translate "Montant consommé" %}
          {{ item.contract_post.order_number }}{{ item.contract_post.code }} + {% if LANGUAGE_CODE == "nl" %} + {{ item.contract_post.description_nl|default:item.contract_post.description_fr }} + {% else %} + {{ item.contract_post.description_fr|default:item.contract_post.description_nl }} + {% endif %} + {{ item.planned_quantity|floatformat:2 }} + {% if item.progress_states %} + {{ item.progress_states|dict_get:ps.id|floatformat:2|default:"0.00" }} + {% else %} + 0.00 + {% endif %} + {{ item.consumed_quantity|floatformat:2 }}{{ item.remaining_quantity|floatformat:2 }}{{ item.contract_post.unit_price|floatformat:2 }} €{{ item.consumed_amount|floatformat:2 }} €
          +
          + {% else %} +

          {% translate "Aucune consommation enregistrée pour cette commande." %}

          + {% endif %} +
          +
          + + + {% if order.projects.exists %} +
          {% translate "Projets associés" %} : + {% for project in order.projects.all %} + {{ project.code }} - {{ project.name }}{% if not forloop.last %}, {% endif %} + {% endfor %} +
          + {% endif %} + + +
          +
          +

          {% translate "Commandes directes" %}

          + {% if can_manage_direct_posts %} + + {% endif %} +
          +
          +

          + + {% translate "Les commandes directes permettent d'enregistrer des postes de marché consommés sans passer par une intervention. Les postes validés sont inclus dans les totaux, les états d'avancement et les déclarations de créance." %} +

          +
          + + + + + + + + + + + + + + + + + + {% for dp in direct_posts %} + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
          {% translate "N°" %}{% translate "Code" %}{% translate "Description" %}{% translate "Unité" %}{% translate "Prix unit." %}{% translate "Quantité" %}{% translate "Qté facturée" %}{% translate "Montant" %}{% translate "Commentaire" %}{% translate "Statut" %}{% translate "Actions" %}
          {{ dp.contract_post.order_number }}{{ dp.contract_post.code }} + {% if LANGUAGE_CODE == "nl" %} + {{ dp.contract_post.description_nl|default:dp.contract_post.description_fr }} + {% else %} + {{ dp.contract_post.description_fr|default:dp.contract_post.description_nl }} + {% endif %} + {{ dp.contract_post.unit }}{{ dp.contract_post.unit_price|floatformat:2 }} €{{ dp.quantity|floatformat:2 }}{{ dp.invoiced_quantity|floatformat:2 }}{{ dp.total_price|floatformat:2 }} €{{ dp.comment|default:"" }} + {% if dp.control_status == 'validated' %} + {% translate "Approuvé" %} + {% if dp.validated_by %} +
          {{ dp.validated_by.get_full_name }} – {{ dp.validated_at|date:"d/m/Y" }} + {% endif %} + {% elif dp.control_status == 'refused' %} + {% translate "Refusé" %} + {% elif dp.control_status == 'invoiced' %} + {% translate "Facturé" %} + {% else %} + {% translate "En attente" %} + {% endif %} +
          +
          + {% if can_manage_direct_posts and dp.control_status != 'validated' %} + + {% if not dp.invoiced_quantity or dp.invoiced_quantity <= 0 %} + + {% endif %} + {% endif %} + {% if can_validate_direct_posts %} + {% if dp.control_status != 'validated' %} + + {% endif %} + {% if dp.control_status != 'refused' %} + + {% endif %} + {% if dp.control_status != 'pending' %} + + {% endif %} + {% endif %} +
          +
          {% translate "Aucune commande directe enregistrée." %}
          +
          +
          +
          + + {% if can_manage_direct_posts %} + + + {% endif %} + + + + + +
          +
          +

          Interventions

          + {% if can_add_intervention_for_order %} + + {% endif %} +
          +
          + + + + + + + + + + {% for intervention in interventions %} + + + + + + + {% endfor %} + + +
          {% translate "Code" %}{% translate "Titre" %}{% translate "Début" %}{% translate "Statut" %}
          {{ intervention.code }}{{ intervention.title }}{{ intervention.begin_time|date:"d-m-Y H:i" }}{{ intervention.get_status_display }}
          +
          +
          + {% if can_add_intervention_for_order %} + {% with order.id|stringformat:"s" as order_id %} + {% with extra_query_params='&order_id='|add:order_id|add:'&redirect_to_order=true' %} + {% include "home/_intervention_type_modal.html" %} + {% endwith %} + {% endwith %} + {% endif %} +
          + +{% endblock %} + +{% block script %} + + + {{ status_choices|json_script:"status_choices" }} + + + + +{% endblock script %} diff --git a/streetup/contracts/templates/contracts/teams_index.html b/streetup/contracts/templates/contracts/teams_index.html new file mode 100644 index 0000000..a4d83a0 --- /dev/null +++ b/streetup/contracts/templates/contracts/teams_index.html @@ -0,0 +1,812 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load roles %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
          + + +
          +

          + {% trans "Gestion des équipes" %} +

          + +
          + + {% if not teams %} +
          + {% trans "Aucune équipe accessible." %} +
          + {% else %} + + +
          + + +
          +
          +
          +
          +
          + {% trans "Non assignés" %} +
          + {% trans "Aucune équipe" %} +
          + {% if can_edit %} + + {% endif %} +
          +
          +
            + {% for member in unassigned_members %} +
          • + {% if can_edit %} + + {% endif %} +
            + {{ member.name }} + {% if member.user %} + {{ member.user.get_full_name|default:member.user.username }} + {% endif %} +
            + {% if can_edit %} +
            + + +
            + {% endif %} +
          • + {% empty %} +
          • + + {% trans "Aucun membre" %} +
          • + {% endfor %} +
          +
          + +
          +
          + + + {% for team in teams %} +
          +
          +
          +
          +
          {{ team.name }}
          + {{ team.company.name }} +
          +
          + {% if team.active_interventions_count > 0 %} + + {% endif %} + {% if can_edit %} + + {% endif %} +
          +
          + +
          +
            + {% for member in team.members.all %} +
          • + {% if can_edit %} + + {% endif %} +
            + {{ member.name }} + {% if member.user %} + {{ member.user.get_full_name|default:member.user.username }} + {% endif %} +
            + {% if can_edit %} +
            + + +
            + {% endif %} +
          • + {% empty %} +
          • + + {% trans "Aucun membre" %} +
          • + {% endfor %} +
          +
          + + +
          +
          + {% endfor %} +
          + {% endif %} + +
          + + +{% if can_edit %} + + + + + + + + +{% endif %} + + + + + + + +{% endblock content %} + +{% block script %} + + +{% endblock script %} diff --git a/streetup/contracts/templatetags/__init__.py b/streetup/contracts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/contracts/templatetags/contract_filters.py b/streetup/contracts/templatetags/contract_filters.py new file mode 100644 index 0000000..2c2a18a --- /dev/null +++ b/streetup/contracts/templatetags/contract_filters.py @@ -0,0 +1,14 @@ +from django import template + +register = template.Library() + + +@register.filter +def dict_get(dictionary, key): + """ + Template filter to get a value from a dictionary by key. + Usage: {{ my_dict|dict_get:my_key }} + """ + if dictionary is None: + return None + return dictionary.get(key) diff --git a/streetup/contracts/tests.py b/streetup/contracts/tests.py new file mode 100644 index 0000000..b49ea74 --- /dev/null +++ b/streetup/contracts/tests.py @@ -0,0 +1,197 @@ +from django.test import TestCase, Client +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.urls import reverse +import json + +from contracts.models import Company, CompanyTeam, CompanyMember, CompanyMemberHistory +from common.models import UserConfig, Role + +User = get_user_model() + +class CompanyMemberTeamsTests(TestCase): + def setUp(self): + # Create user and configuration + self.user = User.objects.create_user(username="testuser", password="password123") + self.user_config = UserConfig.objects.create(user=self.user, is_intern=True) + + # Create company and teams + self.company = Company.objects.create(name="Test Company") + self.team1 = CompanyTeam.objects.create(name="Team Alpha", company=self.company) + self.team2 = CompanyTeam.objects.create(name="Team Beta", company=self.company) + + # Create member + self.member = CompanyMember.objects.create( + name="Jean Dupont", + user=self.user + ) + + def test_assign_multiple_teams(self): + """Test that a member can be assigned to multiple teams and history tracks it.""" + today = timezone.now().date() + + # Add to Team 1 + self.member.teams.add(self.team1) + self.assertEqual(self.member.teams.count(), 1) + + # Verify history entry created + histories = CompanyMemberHistory.objects.filter(member=self.member, team=self.team1) + self.assertEqual(histories.count(), 1) + self.assertEqual(histories.first().join_date, today) + self.assertIsNone(histories.first().leave_date) + + # Add to Team 2 + self.member.teams.add(self.team2) + self.assertEqual(self.member.teams.count(), 2) + + # Verify history for Team 2 + histories_team2 = CompanyMemberHistory.objects.filter(member=self.member, team=self.team2) + self.assertEqual(histories_team2.count(), 1) + self.assertEqual(histories_team2.first().join_date, today) + self.assertIsNone(histories_team2.first().leave_date) + + def test_remove_team(self): + """Test that removing a team closes the history period.""" + today = timezone.now().date() + self.member.teams.add(self.team1) + + # Remove from Team 1 + self.member.teams.remove(self.team1) + self.assertEqual(self.member.teams.count(), 0) + + # Verify history leave_date updated + history = CompanyMemberHistory.objects.get(member=self.member, team=self.team1) + self.assertEqual(history.leave_date, today) + + def test_clear_teams(self): + """Test that clearing teams closes all active history periods.""" + today = timezone.now().date() + self.member.teams.add(self.team1) + self.member.teams.add(self.team2) + + # Clear teams + self.member.teams.clear() + self.assertEqual(self.member.teams.count(), 0) + + # Verify history leave_date updated for all + h1 = CompanyMemberHistory.objects.get(member=self.member, team=self.team1) + h2 = CompanyMemberHistory.objects.get(member=self.member, team=self.team2) + self.assertEqual(h1.leave_date, today) + self.assertEqual(h2.leave_date, today) + + +class TeamViewsAjaxTests(TestCase): + def setUp(self): + self.client = Client() + self.user = User.objects.create_user(username="adminuser", password="password") + self.user_config = UserConfig.objects.create(user=self.user, is_intern=True) + # Give admin role to bypass permission denials + self.admin_role, _ = Role.objects.get_or_create(name="admin") + self.user_config.roles.add(self.admin_role) + + self.client.login(username="adminuser", password="password") + + self.company = Company.objects.create(name="Test Company") + self.team_a = CompanyTeam.objects.create(name="Team A", company=self.company) + self.team_b = CompanyTeam.objects.create(name="Team B", company=self.company) + + self.member = CompanyMember.objects.create(name="Tech A", user=self.user) + self.member.teams.add(self.team_a) + + def test_teams_move_member_transfer(self): + """Test moving a member transfers them (removes old, adds new).""" + url = reverse('contracts:teams_move_member', args=[self.member.id]) + payload = { + 'old_team_id': self.team_a.id, + 'team_id': self.team_b.id + } + + response = self.client.post( + url, + data=json.dumps(payload), + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + + self.member.refresh_from_db() + # Should only belong to Team B now + self.assertEqual(list(self.member.teams.all()), [self.team_b]) + + # Verify history was updated + today = timezone.now().date() + h_old = CompanyMemberHistory.objects.get(member=self.member, team=self.team_a) + self.assertEqual(h_old.leave_date, today) + h_new = CompanyMemberHistory.objects.get(member=self.member, team=self.team_b) + self.assertEqual(h_new.join_date, today) + self.assertIsNone(h_new.leave_date) + + def test_teams_update_member_modal(self): + """Test updating a member via modal view with transfer.""" + url = reverse('contracts:teams_update_member', args=[self.member.id]) + payload = { + 'name': 'Tech A Modified', + 'old_team_id': self.team_a.id, + 'team_id': self.team_b.id + } + + response = self.client.post( + url, + data=json.dumps(payload), + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + + self.member.refresh_from_db() + self.assertEqual(self.member.name, 'Tech A Modified') + self.assertEqual(list(self.member.teams.all()), [self.team_b]) + + def test_teams_create_member_different_company_allowed_for_team_company(self): + """Test that a non-admin user can add a member belonging to the target team's company, even if different from user's company.""" + from django.contrib.auth.models import User + from common.models import UserConfig, Role + from contracts.models import Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess + + # Create user of Company A + other_user = User.objects.create_user(username="otheruser", password="password") + company_a = Company.objects.create(name="Company A") + other_user_config = UserConfig.objects.create(user=other_user, company=company_a) + + # Create team belonging to Company B + company_b = Company.objects.create(name="Company B") + team_b = CompanyTeam.objects.create(name="Team Company B", company=company_b) + + # Grant user edit permission on team_b + UserCompanyTeamAccess.objects.create( + user_config=other_user_config, + team=team_b, + can_view=True, + can_edit=True + ) + + # Create target user to add, belonging to Company B + target_user = User.objects.create_user(username="targetuser", password="password") + UserConfig.objects.create(user=target_user, company=company_b) + + # Login other_user + self.client.logout() + self.client.login(username="otheruser", password="password") + + # Call teams_create_member + url = reverse('contracts:teams_create_member') + payload = { + 'name': 'New Target Member', + 'user_id': target_user.id, + 'team_id': team_b.id + } + + response = self.client.post( + url, + data=json.dumps(payload), + content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + + # Verify member was created and added to team_b + member = CompanyMember.objects.get(user=target_user) + self.assertEqual(member.name, 'New Target Member') + self.assertIn(team_b, member.teams.all()) diff --git a/streetup/contracts/urls.py b/streetup/contracts/urls.py new file mode 100644 index 0000000..ef0e21f --- /dev/null +++ b/streetup/contracts/urls.py @@ -0,0 +1,49 @@ +from django.urls import path + +from . import views +from . import views_teams +from .views_daily_report import daily_report_download + +app_name = "contracts" +urlpatterns = [ + # Gestion des équipes + path('teams/', views_teams.teams_index, name='teams_index'), + path('teams/members/create/', views_teams.teams_create_member, name='teams_create_member'), + path('teams/members//move/', views_teams.teams_move_member, name='teams_move_member'), + path('teams/members//update/', views_teams.teams_update_member, name='teams_update_member'), + path('teams/members//delete/', views_teams.teams_delete_member, name='teams_delete_member'), + path('teams//interventions/', views_teams.team_interventions_json, name='team_interventions_json'), + path("", views.contracts_index, name="contracts_index"), + path('/', views.contracts_detail, name='contracts_detail'), + path('orders/available/', views.available_orders, name='available_orders'), + path('orders//', views.contracts_orders_detail, name='contracts_orders_detail'), + path('orders//update-status/', views.update_order_status, name='update_order_status'), + path('orders//change/', views.contracts_orders_change, name='contracts_orders_change'), + path('orders//quote/', views.contracts_orders_quote, name='contracts_orders_quote'), + path('orders//quote/preview/', views.preview_order_quote_pdf, name='contracts_orders_quote_pdf'), + path("orders//preview-claim-declaration/", views.preview_claim_declaration_pdf, name="order_claim_declaration_pdf_preview"), + path("orders//preview-state-of-works/", views.preview_state_of_works_pdf, name="order_state_of_works_pdf_preview"), + path("orders//generate-order-documents/", views.generate_documents_for_order, name="generate_documents_for_order"), + path('orders//documents//download/', views.order_document_download, name='order_document_download'), + path('orders/add/', views.contracts_orders_add, name='contracts_orders_add'), + path('orders/create-ajax/', views.contracts_orders_create_ajax, name='contracts_orders_create_ajax'), + path('orders/get/', views.contracts_orders_get, name='contracts_orders_get'), + path('autocomplete-posts/', views.autocomplete_posts, name='autocomplete-posts'), + path('contracts//posts/', views.contract_posts_list, name='contract_posts_list'), + # Créer une intervention à partir d'une commande + path('orders//add-intervention/', views.contracts_orders_add_intervention, name='contracts_orders_add_intervention'), + # Gestion des pièces jointes des déclarations de créance + path('documents//attachments/', views.claim_declaration_attachments, name='claim_declaration_attachments'), + path('attachments//delete/', views.delete_claim_attachment, name='delete_claim_attachment'), + # Pièces jointes génériques d'une commande (photos, PDF, …) + path('orders//attachments/add/', views.order_add_attachment, name='order_add_attachment'), + path('orders//attachments//delete/', views.order_delete_attachment, name='order_delete_attachment'), + # Commandes directes (postes sans intervention) + path('orders//direct-posts/', views.order_direct_posts_list, name='order_direct_posts_list'), + path('orders//direct-posts/add/', views.order_direct_post_add, name='order_direct_post_add'), + path('orders//direct-posts//update/', views.order_direct_post_update, name='order_direct_post_update'), + path('orders//direct-posts//delete/', views.order_direct_post_delete, name='order_direct_post_delete'), + path('orders//direct-posts//validate/', views.order_direct_post_validate, name='order_direct_post_validate'), + # Rapport journalier + path('daily-report/', daily_report_download, name='daily_report'), +] \ No newline at end of file diff --git a/streetup/contracts/utils_import_posts.py b/streetup/contracts/utils_import_posts.py new file mode 100644 index 0000000..5b02439 --- /dev/null +++ b/streetup/contracts/utils_import_posts.py @@ -0,0 +1,195 @@ +""" +Logique partagée d'import / mise à jour de ContractPost depuis un fichier +Excel de type « Inventaire E24.143 » (format SPRB-GOB/Fonteinen). + +Structure des colonnes (0-indexé) : + 0 – Nr. Post → order_number + 1 – Code Fontaine → code + 2 – Beschrijving NL → description_nl + 3 – AO/TVS → is_amount_to_be_justified (TVS = True) + 4 – Unité NL → fallback unit + 5 – Description FR → description_fr + 6 – NC (ignoré) + 7 – U (unité FR) → unit (prioritaire) + 8 – Quantité présumée (ignorée : pas mappée au modèle) + 9 – Prix unitaire → unit_price + 10 – Total par poste (ignoré : calculable) + +Utilisé depuis : + - contracts/management/commands/import_contract_posts.py + - contracts/admin.py (vue d'import) +""" + +from decimal import Decimal, InvalidOperation + +import openpyxl + +from .models import Contract, ContractPost + + +# --------------------------------------------------------------------------- +# Utilitaires internes +# --------------------------------------------------------------------------- + +def _to_decimal(value): + if value is None: + return None + try: + return Decimal(str(value)) + except (InvalidOperation, ValueError): + return None + + +def _clean(value): + if value is None: + return '' + return str(value).strip() + + +# --------------------------------------------------------------------------- +# Fonction principale +# --------------------------------------------------------------------------- + +def import_contract_posts(contract: Contract, excel_source, *, dry_run: bool = False, + update_existing: bool = False, log=None) -> dict: + """ + Importe (ou met à jour) les postes d'un contrat depuis un fichier Excel. + + Paramètres + ---------- + contract : instance Contract cible + excel_source : chemin fichier (str/Path) ou objet fichier ouvert (bytes-like) + dry_run : si True, ne persiste rien en base + update_existing : si True, met à jour les postes existants (même order_number) ; + si False, les ignore + log : callable(str) pour afficher des messages ; défaut = print + + Retour + ------ + dict avec les clés : + total_rows, created, updated, skipped_already_exists, skipped_no_price, errors + """ + if log is None: + log = print + + stats = { + 'total_rows': 0, + 'created': 0, + 'updated': 0, + 'skipped_already_exists': 0, + 'skipped_no_price': 0, + 'errors': 0, + } + + # ------------------------------------------------------------------ + # Ouverture du classeur (chemin ou objet fichier) + # ------------------------------------------------------------------ + try: + wb = openpyxl.load_workbook( + excel_source, read_only=True, data_only=True, keep_vba=False + ) + except Exception as exc: + log(f"ERREUR ouverture Excel : {exc}") + stats['errors'] += 1 + return stats + + ws = wb.active + log(f"Feuille lue : {ws.title}") + + # ------------------------------------------------------------------ + # Map order_number → ContractPost existants pour le contrat + # ------------------------------------------------------------------ + existing_map = { + p.order_number: p + for p in ContractPost.objects.filter(contract=contract) + } + + posts_to_create = [] + posts_to_update = [] + fields_to_update = [ + 'code', 'description_fr', 'description_nl', + 'unit', 'unit_price', 'is_amount_to_be_justified', + ] + + for row_num, row in enumerate(ws.iter_rows(values_only=True), start=1): + # Ne garder que les lignes avec un entier en colonne A (Nr. Post) + raw_num = row[0] if len(row) > 0 else None + if not isinstance(raw_num, (int, float)) or raw_num != int(raw_num): + continue + + stats['total_rows'] += 1 + order_number = int(raw_num) + + code = _clean(row[1] if len(row) > 1 else None) + description_nl = _clean(row[2] if len(row) > 2 else None) + ao = _clean(row[3] if len(row) > 3 else None) + unit_nl = _clean(row[4] if len(row) > 4 else None) + description_fr = _clean(row[5] if len(row) > 5 else None) + unit_fr = _clean(row[7] if len(row) > 7 else None) + unit_price = _to_decimal(row[9] if len(row) > 9 else None) + + unit = unit_fr if unit_fr else unit_nl + is_amount_to_be_justified = ao.upper() == 'TVS' + + if unit_price is None: + log(f" Ligne {row_num}: poste {order_number} – prix unitaire manquant, ignoré") + stats['skipped_no_price'] += 1 + continue + + if order_number in existing_map: + if not update_existing: + log(f" Ligne {row_num}: poste {order_number} ({code}) – déjà présent, ignoré") + stats['skipped_already_exists'] += 1 + continue + + # Mise à jour + post = existing_map[order_number] + post.code = code + post.description_fr = description_fr + post.description_nl = description_nl + post.unit = unit + post.unit_price = unit_price + post.is_amount_to_be_justified = is_amount_to_be_justified + posts_to_update.append(post) + log( + f" [UPDATE] poste {order_number:3d} | {code:<12} | " + f"{unit_price:>10} | {description_fr[:60]}" + ) + stats['updated'] += 1 + else: + posts_to_create.append(ContractPost( + contract=contract, + code=code, + description_fr=description_fr, + description_nl=description_nl, + order_number=order_number, + unit=unit, + unit_price=unit_price, + is_amount_to_be_justified=is_amount_to_be_justified, + )) + log( + f" [CREATE] poste {order_number:3d} | {code:<12} | " + f"{unit_price:>10} | {description_fr[:60]}" + ) + stats['created'] += 1 + + # ------------------------------------------------------------------ + # Persistance + # ------------------------------------------------------------------ + if not dry_run: + from django.db import transaction + try: + with transaction.atomic(): + if posts_to_create: + ContractPost.objects.bulk_create(posts_to_create) + if posts_to_update: + ContractPost.objects.bulk_update(posts_to_update, fields_to_update) + except Exception as exc: + import traceback + log(f"\nERREUR lors de l'écriture en base : {exc}") + traceback.print_exc() + stats['errors'] += stats['created'] + stats['updated'] + stats['created'] = 0 + stats['updated'] = 0 + + return stats diff --git a/streetup/contracts/views.py b/streetup/contracts/views.py new file mode 100644 index 0000000..93c8449 --- /dev/null +++ b/streetup/contracts/views.py @@ -0,0 +1,1250 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_GET, require_POST +from django.http import JsonResponse, FileResponse, Http404 +from django.core.exceptions import PermissionDenied +from django.urls import reverse +from django.utils.encoding import smart_str +from django.utils.translation import get_language, gettext as _, ngettext +from django.db.models import F, Q +from django.db.models.functions import Cast +from django.db.models import CharField + +import json +import os +from decimal import Decimal, InvalidOperation +from urllib.parse import urlencode + +from contracts.models import ( + Contract, + ContractPost, + ContractOrder, + ContractOrderDocument, + ContractOrderQuote, + ClaimDeclarationAttachment, + ContractOrderAttachment, + DirectOrderPost, + DIRECT_ORDER_POST_STATUS_CHOICES, +) +from contracts.forms import ContractOrderForm, ContractOrderQuoteQuantityForm, ClaimDeclarationAttachmentForm, ContractOrderAttachmentForm +from contracts.permissions import get_allowed_contracts_for_user, contract_view_required, order_view_required, user_can_generate_order_documents +from contracts.permissions import user_can_add_order, user_can_add_order_for_contract, user_has_access_to_contract, user_has_access_to_contract_posts, user_has_access_to_contract_order, user_can_manage_direct_order_posts, user_can_validate_direct_order_posts +from contracts.permissions import user_can_manage_quote_for_contract +from contracts.views_pdf_reports import ( + preview_claim_declaration_pdf, + preview_state_of_works_pdf, + preview_order_quote_pdf, + generate_documents_for_order, +) + +from common.models import UserConfig, UserThematics +from common.private_files.helpers import build_private_url +from interventions.permissions import can_add_intervention + + +def contracts_index(request): + contracts = get_allowed_contracts_for_user(request.user) + + # Filtrage par statut(s) + status_filters = request.GET.getlist('status') + if status_filters == []: + # Par défaut, afficher les commandes en 'pending' et 'sent' + status_filters = ['pending', 'sent', 'completed', 'invoiced'] + + orders = orders = ContractOrder.objects.filter(contract__in=contracts, order_status__in=status_filters) + + # Filtrage par date + date_from = request.GET.get('date_from', '') + if date_from: + orders = orders.filter(order_date__gte=date_from) + + date_to = request.GET.get('date_to', '') + if date_to: + orders = orders.filter(order_date__lte=date_to) + + # Tri par date décroissante + orders = orders.order_by('-order_date') + + context = { + 'contracts': contracts, + 'orders': orders, + 'is_add_order_allowed': user_can_add_order(request.user), + 'selected_statuses': status_filters, # Passer les statuts sélectionnés au template + } + return render(request, "contracts/contracts_index.html", context) + + +@contract_view_required +def contracts_detail(request, contract_id): + contract = Contract.objects.get(id=contract_id) + posts = ContractPost.objects.filter(contract=contract) + + # Normalize numeric values to Decimal to avoid float/Decimal mixing errors + total = contract.committed_amount + if total is None: + total = Decimal('0') + elif not isinstance(total, Decimal): + total = Decimal(str(total)) + + realized_raw = contract.get_total_realized_amount() or 0 + validated_raw = contract.get_total_validated_amount() or 0 + realized = realized_raw if isinstance(realized_raw, Decimal) else Decimal(str(realized_raw)) + validated = validated_raw if isinstance(validated_raw, Decimal) else Decimal(str(validated_raw)) + + contract.realized_percent = (realized / total * Decimal('100')) if total else Decimal('100') + # Use Decimal('1') in fallback max to keep types consistent + contract.validated_percent = (validated / total * Decimal('100')) if total else (validated / max(realized, validated, Decimal('1')) * Decimal('100')) + + is_intern = UserConfig.objects.filter(user=request.user, is_intern=True).exists() + + context = { + 'contract': contract, + 'posts': posts, + 'is_intern': is_intern, + } + return render(request, "contracts/contracts_detail.html", context) + + +@order_view_required +def contracts_orders_detail(request, order_id): + order = ContractOrder.objects.get(id=order_id) + interventions = order.interventions.all() + + # Normalize numeric values to Decimal to avoid float/Decimal mixing errors + total = order.total_amount + if total is None: + total = Decimal('0') + elif not isinstance(total, Decimal): + total = Decimal(str(total)) + + realized_raw = order.get_total_realized_amount() or 0 + validated_raw = order.get_total_validated_amount() or 0 + realized = realized_raw if isinstance(realized_raw, Decimal) else Decimal(str(realized_raw)) + validated = validated_raw if isinstance(validated_raw, Decimal) else Decimal(str(validated_raw)) + + order.realized_percent = (realized / total * Decimal('100')) if total else Decimal('100') + order.validated_percent = (validated / total * Decimal('100')) if total else (validated / max(realized, validated, Decimal('1')) * Decimal('100')) + + private_url_order_file = None + if order.order_file: + private_url_order_file = build_private_url("contracts", "ContractOrder", order.id, "order_file") + + # URLs pour chaque document financier + docs = [] + for d in order.documents.all().order_by('uploaded_at'): + d.private_url = build_private_url("contracts", "ContractOrderDocument", d.id, "document") + docs.append(d) + + # URLs pour chaque pièce jointe (photos / fichiers génériques) + attachments = [] + for a in order.attachments.all().order_by('upload_date'): + a.file_url = build_private_url("contracts", "ContractOrderAttachment", a.id, "file") + a.thumbnail_url = ( + build_private_url("contracts", "ContractOrderAttachment", a.id, "thumbnail") + if a.thumbnail + else a.file_url + ) + attachments.append(a) + + quote = getattr(order, "quote", None) + quote_total = order.get_quote_total_amount() + + # Récupérer le résumé de consommation avec les états d'avancement + result = order.get_post_consumption_summary(include_progress_states=True) + if isinstance(result, tuple): + consumption_summary, progress_states = result + else: + consumption_summary = result + progress_states = [] + + # Détermination du droit de créer une intervention pour cette commande + can_add_intervention_for_order = False + user_config = getattr(request.user, "config", None) + if user_config is not None: + # L'utilisateur doit pouvoir voir des interventions sur au moins une thématique + editable_thematics = UserThematics.objects.filter( + user_config=user_config, + can_edit_interventions=True, + ) + if editable_thematics.exists() and user_has_access_to_contract_order(request.user, order.id): + can_add_intervention_for_order = True + + # Préparer les données pour la timeline de statut + from contracts.models import CONTRACT_ORDER_STATUS_CHOICES + status_choices = [ + {'key': choice[0], 'label': str(choice[1])} + for choice in CONTRACT_ORDER_STATUS_CHOICES + ] + + # Déterminer les statuts accessibles (managers et admins peuvent changer le statut) + allowed_statuses = [] + if user_config and user_config.roles.filter(name__in=['manager', 'admin']).exists(): + # Inclure 'cancelled' seulement si le statut actuel est 'pending' ou 'sent' + if order.order_status in ['pending', 'sent']: + allowed_statuses = [choice[0] for choice in CONTRACT_ORDER_STATUS_CHOICES] + else: + allowed_statuses = [choice[0] for choice in CONTRACT_ORDER_STATUS_CHOICES if choice[0] != 'cancelled'] + + # Déterminer les droits de création d'interventions par type + user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set() + can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller'}) + can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager'}) + + context = { + 'order': order, + 'interventions': interventions, + 'is_order_edit_allowed': user_can_add_order_for_contract(request.user, order.contract_id), + 'is_quote_edit_allowed': user_can_manage_quote_for_contract(request.user, order.contract_id), + 'is_generate_documents_allowed': user_can_generate_order_documents(request.user, order), + 'private_url_order_file': private_url_order_file, + 'documents': docs, + 'attachments': attachments, + 'add_attachment_form': ContractOrderAttachmentForm(), + 'quote': quote, + 'quote_total': quote_total, + 'consumption_summary': consumption_summary, + 'progress_states': progress_states, + 'can_add_intervention_for_order': can_add_intervention_for_order, + 'can_create_preventive': can_create_preventive, + 'can_create_ameliorative': can_create_ameliorative, + 'status_choices': status_choices, + 'allowed_statuses': json.dumps(allowed_statuses), + 'can_manage_direct_posts': _user_can_manage_direct_posts(request.user, order), + 'can_validate_direct_posts': _user_can_validate_direct_posts(request.user, order), + 'direct_posts': list(order.direct_posts.select_related('contract_post', 'created_by', 'validated_by')), + } + return render(request, "contracts/contracts_orders_detail.html", context) + + +def _get_allowed_thematics_for_order_project(user, order): + """Retourne l'ensemble des thématiques autorisées pour l'utilisateur, croisées + avec celles du projet de la commande si présent. + + - L'utilisateur doit avoir des UserThematics(can_edit_interventions=True) + - Si order.project est défini, on intersecte avec project.thematics + """ + user_config = getattr(user, "config", None) + if not user_config: + return [] + + editable_links = ( + UserThematics.objects + .filter(user_config=user_config, can_edit_interventions=True) + .select_related('thematic') + ) + user_thematics = {ut.thematic for ut in editable_links} + + # Si la commande a des projets, on intersecte avec les thématiques de tous les projets + order_projects = order.projects.all() + if order_projects.exists(): + # Union de toutes les thématiques des projets + project_thematics = set() + for project in order_projects: + project_thematics.update(project.thematics.all()) + return list(user_thematics & project_thematics) + + return list(user_thematics) + + +@order_view_required +def contracts_orders_add_intervention(request, order_id): + """Redirige vers le bon flux de création d'intervention à partir d'une commande. + + Règles : + - L'utilisateur doit avoir accès à la commande (user_has_access_to_contract_order). + - Si la commande est liée à un projet : + * intervention améliorative (maintain_type=ameliorative) + * on croise les thématiques éditables avec celles du projet + * si une seule thématique -> redirection directe vers interventions:add + * sinon -> passage par l'écran de choix de thématique (restreint). + - Si la commande n'a pas de projet : + * on passe par choose_thematic_before_add comme depuis la home, + en propageant order_id. + """ + order = get_object_or_404(ContractOrder, pk=order_id) + + # Vérification de l'accès à la commande / contrat + if not user_has_access_to_contract_order(request.user, order_id): + raise PermissionDenied(_("Vous n'avez pas accès à cette commande.")) + + order_projects = order.projects.all() + allowed_thematics = _get_allowed_thematics_for_order_project(request.user, order) + + if not allowed_thematics: + messages.error(request, _("Vous n'avez pas de thématique d'intervention autorisée pour cette commande.")) + return redirect('contracts:contracts_orders_detail', order_id=order.id) + + # Si la commande est liée à des projets -> intervention améliorative + if order_projects.exists(): + # Prendre le premier projet si plusieurs (ou adapter selon la logique métier) + project = order_projects.first() + if len(allowed_thematics) == 1: + thematic = allowed_thematics[0] + base_url = reverse('interventions:intervention_add', kwargs={'thematic_code': thematic.code}) + params = { + 'maintain_type': 'ameliorative', + 'project_id': project.id, + 'order_id': order.id, + 'redirect_to_order': 'true', + } + return redirect(f"{base_url}?{urlencode(params)}") + + # Plusieurs thématiques possibles : on passe par l'écran de choix, + # en restreignant les thématiques côté interventions. + base_url = reverse('interventions:intervention_add_choose_thematic') + params = { + 'type': 'ameliorative', + 'order_id': order.id, + 'project_id': project.id, + 'redirect_to_order': 'true', + } + # On ajoute les codes thématiques autorisées pour filtrage côté vue + for thematic in allowed_thematics: + params.setdefault('allowed_thematic', []) + params['allowed_thematic'].append(thematic.code) + + # urlencode ne gère pas directement les listes imbriquées avec setdefault, + # on reconstruit proprement la query string. + flat_params = [] + for key, value in params.items(): + if isinstance(value, list): + for v in value: + flat_params.append((key, str(v))) + else: + flat_params.append((key, str(value))) + + query_string = urlencode(flat_params) + return redirect(f"{base_url}?{query_string}") + + # Pas de projet : on laisse l'utilisateur choisir type + thématique comme depuis la home. + # Si un type est déjà fourni (via la modale réutilisée), on le propage. + base_url = reverse('interventions:intervention_add_choose_thematic') + intervention_type = request.GET.get('type') + + params = { + 'order_id': order.id, + 'redirect_to_order': 'true', + } + if intervention_type in ('corrective', 'preventive', 'ameliorative'): + params['type'] = intervention_type + + query_string = urlencode(params) + return redirect(f"{base_url}?{query_string}") + + + +def contracts_orders_add(request): + project_id = request.GET.get('project_id') or request.POST.get('project_id') + + if request.method == 'POST': + form = ContractOrderForm(request.POST, request.FILES, user=request.user, project_id=project_id) + if form.is_valid(): + contract_order = form.save(commit=False) + + # permissions + if not user_can_add_order_for_contract(request.user, contract_order.contract_id): + raise PermissionDenied(_("Vous n'avez pas le droit d'ajouter une commande pour ce contrat.")) + + contract_order.ordered_by = request.user # Attribue l'utilisateur connecté + + contract_order.save() + + # Associate with project if project_id is provided + if project_id: + from projects.models import Project + try: + project = Project.objects.get(id=project_id) + contract_order.projects.add(project) + except Project.DoesNotExist: + pass + + # Redirect to project detail if coming from a project + if project_id: + return redirect('projects:projects_detail', project_id=project_id) + return redirect('contracts:contracts_index') + else: + form = ContractOrderForm(user=request.user, project_id=project_id) + + return render(request, 'contracts/contracts_order_form.html', { + 'form': form, + 'project_id': project_id, + }) + + + +def contracts_orders_change(request, order_id): + order = get_object_or_404(ContractOrder, pk=order_id) + original_contract_id = order.contract_id + first_project = order.projects.first() + project_id = first_project.id if first_project else None + + if request.method == 'POST': + form = ContractOrderForm(request.POST, request.FILES, instance=order, user=request.user, project_id=project_id) + if form.is_valid(): + # Sécurité supplémentaire côté vue pour empêcher une modification de contrat si des interventions sont déjà liées. + new_contract = form.cleaned_data.get('contract') + if order.interventions.exists() and new_contract and new_contract.id != original_contract_id: + form.add_error('contract', _("Impossible de changer le contrat: des interventions sont déjà liées à cette commande.")) + else: + contract_order = form.save(commit=False) + + + contract_order = form.save(commit=False) + + # permissions + if not user_can_add_order_for_contract(request.user, contract_order.contract_id): + raise PermissionDenied(_("Vous n'avez pas le droit de modifier une commande pour ce contrat.")) + + contract_order.ordered_by = request.user # Attribue l'utilisateur connecté + contract_order.save() + return redirect('contracts:contracts_orders_detail', order_id=order.id) + else: + form = ContractOrderForm(instance=order, user=request.user, project_id=project_id) + + return render(request, 'contracts/contracts_order_form.html', { + 'form': form, + 'order': order, + }) + + +@order_view_required +def contracts_orders_quote(request, order_id): + order = get_object_or_404(ContractOrder, pk=order_id) + + if not user_can_manage_quote_for_contract(request.user, order.contract_id): + raise PermissionDenied(_("Vous n'avez pas le droit de modifier le devis de cette commande.")) + + quote, created = ContractOrderQuote.objects.get_or_create( + order=order, + defaults={'created_by': request.user}, + ) + + can_edit_quote = not quote.is_finalized + + posts = list(order.contract.posts.order_by('order_number')) + allowed_post_ids = {post.id for post in posts} + + existing_items = { + item.contract_post_id: item + for item in quote.items.select_related('contract_post') + } + + def format_quantity_for_input(value): + if value in (None, ''): + return '' + if isinstance(value, Decimal): + if value <= 0: + return '' + quantized = value.quantize(Decimal('0.01')) + if quantized <= 0: + return '' + return format(quantized, 'f') + try: + decimal_value = Decimal(str(value)) + except (InvalidOperation, ValueError): + return str(value) + if decimal_value <= 0: + return '' + return format(decimal_value.quantize(Decimal('0.01')), 'f') + + submitted_values = {} + row_errors = {} + general_errors = [] + action = request.POST.get('action', 'save') if request.method == 'POST' else 'save' + raw_payload_value = request.POST.get('items_payload', '') if request.method == 'POST' else None + quantities_to_save = {} + + if request.method == 'POST': + if not can_edit_quote: + messages.error(request, _("Ce devis est figé et ne peut plus être modifié.")) + return redirect('contracts:contracts_orders_quote', order_id=order.id) + + payload_data = [] + payload_source = raw_payload_value.strip() if raw_payload_value else '' + if payload_source: + try: + payload_data = json.loads(payload_source) + except json.JSONDecodeError: + general_errors.append(_("Les données reçues sont invalides.")) + else: + payload_data = [] + + if not general_errors and not isinstance(payload_data, list): + general_errors.append(_("Les données reçues sont invalides.")) + payload_data = [] + + if not general_errors: + for entry in payload_data: + if not isinstance(entry, dict): + general_errors.append(_("Les données reçues sont invalides.")) + continue + + contract_post_raw = entry.get('contract_post_id') + quantity_raw = entry.get('quantity') + contract_post_int = None + if contract_post_raw is not None: + try: + contract_post_int = int(contract_post_raw) + except (TypeError, ValueError): + contract_post_int = None + + if contract_post_int is not None: + submitted_values[contract_post_int] = '' if quantity_raw in (None, '') else str(quantity_raw) + + form_data = {} + if contract_post_raw is not None: + form_data['contract_post_id'] = str(contract_post_raw) + if quantity_raw not in (None, ''): + form_data['quantity'] = str(quantity_raw) + + form = ContractOrderQuoteQuantityForm( + data=form_data, + allowed_post_ids=allowed_post_ids, + ) + + if form.is_valid(): + contract_post_id = form.cleaned_data['contract_post_id'] + quantity = form.cleaned_data['quantity'] + if quantity > 0: + quantities_to_save[contract_post_id] = quantity + else: + errors = [] + for field_errors in form.errors.values(): + errors.extend(field_errors) + if contract_post_int is not None: + row_errors[contract_post_int] = errors + else: + general_errors.extend(errors) + + if general_errors or row_errors: + for error_message in general_errors: + messages.error(request, error_message) + messages.error(request, _("Le formulaire contient des erreurs.")) + else: + if quote.created_by_id is None: + quote.created_by = request.user + quote.save() + + posts_by_id = {post.id: post for post in posts} + + ids_to_delete = set(existing_items.keys()) - set(quantities_to_save.keys()) + if ids_to_delete: + quote.items.filter(contract_post_id__in=ids_to_delete).delete() + + for contract_post_id, quantity in quantities_to_save.items(): + item = existing_items.get(contract_post_id) + if item is None: + contract_post = posts_by_id.get(contract_post_id) + if contract_post is None: + continue + item = quote.items.create( + contract_post=contract_post, + quantity=quantity, + ) + existing_items[contract_post_id] = item + elif item.quantity != quantity: + item.quantity = quantity + item.save(update_fields=['quantity']) + + if action == 'finalize': + if not quote.items.exists(): + messages.error(request, _("Impossible de figer un devis vide.")) + else: + quote.finalize(request.user) + messages.success(request, _("Le devis a été figé.")) + return redirect('contracts:contracts_orders_quote', order_id=order.id) + + messages.success(request, _("Le devis a été enregistré.")) + return redirect('contracts:contracts_orders_quote', order_id=order.id) + + def get_row_quantity(post): + if post.id in submitted_values: + return format_quantity_for_input(submitted_values[post.id]) + item = existing_items.get(post.id) + if item and item.quantity is not None: + return format_quantity_for_input(item.quantity) + return '' + + rows = [ + { + 'post': post, + 'quantity': get_row_quantity(post), + 'errors': row_errors.get(post.id, []), + } + for post in posts + ] + + if raw_payload_value is None: + payload_items = [ + { + 'contract_post_id': item.contract_post_id, + 'quantity': format_quantity_for_input(item.quantity), + } + for item in quote.items.select_related('contract_post') + if item.quantity and item.quantity > 0 + ] + raw_payload_value = json.dumps(payload_items) + elif not raw_payload_value: + raw_payload_value = '[]' + + context = { + 'order': order, + 'quote': quote, + 'rows': rows, + 'quote_total': quote.get_total_amount() if quote else Decimal('0'), + 'can_edit_quote': can_edit_quote, + 'can_finalize_quote': can_edit_quote and quote.items.exists(), + 'items_payload': raw_payload_value, + } + return render(request, 'contracts/contracts_order_quote_form.html', context) + + +@require_GET +def contracts_orders_get(request): + contract_id = request.GET.get("contract_id") + + if not contract_id: + return JsonResponse({'error': 'Missing contract_id parameter'}, status=400) + + # permissions + if not user_has_access_to_contract(request.user, contract_id): + return JsonResponse({'error': 'Unauthorized'}, status=403) + + orders = ContractOrder.objects.filter(contract_id=contract_id).order_by("order_code") + data = [{"id": o.id, "order_code": o.order_code} for o in orders] + return JsonResponse(data, safe=False) + + + +@require_GET +def autocomplete_posts(request): + q = request.GET.get('q', '') + contract_id = request.GET.get('contract_id', None) + + lang = get_language() + + + field = request.GET.get('field', None) + + if not q or not contract_id: + return JsonResponse([], safe=False) + + # permissions + if not user_has_access_to_contract_posts(request.user, contract_id): + return JsonResponse({'error': 'Unauthorized'}, status=403) + + results = ContractPost.objects.filter(contract=contract_id) + + # Diviser la requête en mots pour permettre une recherche multi-termes + # Chaque mot doit être présent dans au moins un des champs recherchés + words = q.split() + + if field is None: + # Pour chaque mot, on crée un filtre OR sur tous les champs + # Ensuite on combine tous les filtres avec AND (chaque mot doit matcher) + for word in words: + word_filter = ( + Q(order_number__icontains=word) | + Q(code__icontains=word) | + Q(description_fr__icontains=word) | + Q(description_nl__icontains=word) + ) + results = results.filter(word_filter) + + results = ( + results + .annotate(description=F(f"description_{lang}")) + .values("id", "order_number", "code", "description", "unit_price", "post_type", "is_coefficient_multipliable", "is_amount_to_be_justified", "coeff_amount_to_be_justified")[:10] + ) + + + else: + # Choix du champ de description selon la langue + if field == 'description': + if lang == 'fr': + field = 'description_fr' + elif lang == 'nl': + field = 'description_nl' + else: + field = 'description_fr' + + + if field == "order_number": + results = results.annotate(order_number_str=Cast("order_number", CharField())) + # Chaque mot doit être présent dans le champ + for word in words: + results = results.filter(order_number_str__icontains=word) + else: + # Chaque mot doit être présent dans le champ spécifié + for word in words: + results = results.filter(**{f"{field}__icontains": word}) + + results = ( + results + .annotate(description=F(f"description_{lang}")) # alias + .values("id", "order_number", "code", "description", "unit_price", "post_type", "is_coefficient_multipliable", "is_amount_to_be_justified", "coeff_amount_to_be_justified")[:10] + ) + + return JsonResponse(list(results), safe=False) + + + + +def order_document_download(request, order_id:int, doc_id: int): + doc = get_object_or_404( + ContractOrderDocument.objects.select_related("order"), + pk=doc_id + ) + + # Vérif d’accès : même logique que pour les commandes + if not user_has_access_to_contract_order(request.user, order_id): + raise PermissionDenied(_("Accès refusé au document de la commande.")) # :contentReference[oaicite:4]{index=4} + + # Récupération du chemin réel + f = doc.document + if not f or not f.storage.exists(f.name): + raise Http404(_("Fichier introuvable")) + + response = FileResponse(f.open("rb")) # support du streaming + # Optionnel : forcer le téléchargement ou non + response["Content-Disposition"] = f'inline; filename="{smart_str(os.path.basename(f.name))}"' + # Optionnel : type mime (sinon deviné) + # response["Content-Type"] = "application/pdf" + return response + + +@require_POST +def contracts_orders_create_ajax(request): + """Vue AJAX pour créer une commande depuis le formulaire d'intervention préventive""" + + try: + data = json.loads(request.body) + contract_id = data.get('contract_id') + order_code = data.get('order_code', '').strip() + description = data.get('description', '').strip() + order_date = data.get('order_date') + delivery_date = data.get('delivery_date') + total_amount = data.get('total_amount') + + # Validation + if not contract_id: + return JsonResponse({'success': False, 'error': _("Le contrat est requis")}, status=400) + + if not order_code: + return JsonResponse({'success': False, 'error': _("La référence de commande est requise")}, status=400) + + if not order_date: + return JsonResponse({'success': False, 'error': _("La date de commande est requise")}, status=400) + + if not delivery_date: + return JsonResponse({'success': False, 'error': _("La date de livraison est requise")}, status=400) + + # Vérifier les permissions + if not user_can_add_order_for_contract(request.user, contract_id): + return JsonResponse({'success': False, 'error': _("Vous n'avez pas le droit d'ajouter une commande pour ce contrat")}, status=403) + + # Vérifier que le contrat existe + contract = get_object_or_404(Contract, pk=contract_id) + + # Vérifier l'unicité de order_code pour ce contrat + if ContractOrder.objects.filter(contract=contract, order_code=order_code).exists(): + return JsonResponse({'success': False, 'error': _("Une commande avec cette référence existe déjà pour ce contrat")}, status=400) + + # Créer la commande + order = ContractOrder.objects.create( + contract=contract, + order_code=order_code, + description=description, + order_date=order_date, + delivery_date=delivery_date, + total_amount=Decimal(total_amount) if total_amount else None, + order_status='pending', + ordered_by=request.user + ) + + return JsonResponse({ + 'success': True, + 'order': { + 'id': order.id, + 'order_code': order.order_code + } + }) + + except json.JSONDecodeError: + return JsonResponse({'success': False, 'error': _("Données JSON invalides")}, status=400) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=500) + + +@require_GET +def available_orders(request): + """Retourne la liste des commandes (ContractOrder) accessibles à l'utilisateur. + Par défaut, filtre sur les statuts 'pending' ou 'sent', mais accepte un paramètre + 'status' pour personnaliser (ex: ?status=pending&status=sent&status=completed). + + Format JSON: [{id, order_code, label, status, status_display, contract_id, contract_number}] + """ + if not request.user.is_authenticated: + return JsonResponse({'orders': []}) + + allowed_contracts = get_allowed_contracts_for_user(request.user) + if not allowed_contracts.exists(): + return JsonResponse({'orders': []}) + + # Statuts par défaut: pending, sent + statuses = request.GET.getlist('status') + if not statuses: + statuses = ['pending', 'sent'] + + qs = (ContractOrder.objects + .select_related('contract') + .filter(contract__in=allowed_contracts, order_status__in=statuses)) + + # Recherche optionnelle + term = request.GET.get('q') + if term: + qs = qs.filter(Q(order_code__icontains=term) | Q(description__icontains=term)) + + data = [] + for o in qs.order_by('-order_date')[:200]: # limite raisonnable + data.append({ + 'id': o.id, + 'order_code': o.order_code, + 'label': f"{o.order_code} ({o.contract.contract_number})", + 'status': o.order_status, + 'status_display': o.get_order_status_display(), + 'contract_id': o.contract_id, + 'contract_number': o.contract.contract_number, + }) + return JsonResponse({'orders': data}) + + +@require_GET +def contract_posts_list(request, contract_id): + """ + Retourne la liste de tous les postes d'un contrat. + Utilisé pour afficher tous les postes disponibles lors de l'édition d'une intervention. + + Format JSON: {success: bool, posts: [{id, order_number, code, description_fr, description_nl, unit, unit_price}]} + """ + try: + # Vérifier l'accès au contrat + if not user_has_access_to_contract(request.user, contract_id): + return JsonResponse({'success': False, 'error': _("Vous n'avez pas accès à ce contrat")}, status=403) + + contract = get_object_or_404(Contract, id=contract_id) + posts = ContractPost.objects.filter(contract=contract).order_by('order_number') + + posts_data = [] + for post in posts: + posts_data.append({ + 'id': post.id, + 'order_number': post.order_number, + 'code': post.code, + 'description_fr': post.description_fr, + 'description_nl': post.description_nl, + 'unit': post.unit, + 'unit_price': str(post.unit_price) if post.unit_price else '0.00', + 'post_type': post.post_type or '', + 'is_coefficient_multipliable': post.is_coefficient_multipliable, + }) + + return JsonResponse({ + 'success': True, + 'posts': posts_data + }) + + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=500) + + +@login_required +def claim_declaration_attachments(request, doc_id): + """ + Vue pour gérer les pièces jointes d'une déclaration de créance. + Affiche la liste des pièces jointes et permet d'en ajouter. + """ + claim_doc = get_object_or_404(ContractOrderDocument, id=doc_id, document_type='claim_declaration') + order = claim_doc.order + + # Vérifier l'accès à la commande + if not user_has_access_to_contract_order(request.user, order.id): + raise PermissionDenied(_("Vous n'avez pas accès à cette commande.")) + + if request.method == 'POST': + form = ClaimDeclarationAttachmentForm(request.POST, request.FILES) + if form.is_valid(): + attachment = form.save(commit=False) + attachment.claim_document = claim_doc + attachment.uploaded_by = request.user + attachment.save() + messages.success(request, _("Le justificatif a été ajouté avec succès.")) + return redirect('contracts:claim_declaration_attachments', doc_id=doc_id) + else: + form = ClaimDeclarationAttachmentForm() + + attachments = claim_doc.attachments.all().order_by('-uploaded_at') + + # URLs privées pour chaque pièce jointe + for att in attachments: + att.private_url = build_private_url("contracts", "ClaimDeclarationAttachment", att.id, "document") + + # URL privée pour le document de déclaration de créance + claim_doc_private_url = build_private_url("contracts", "ContractOrderDocument", claim_doc.id, "document") + + context = { + 'claim_doc': claim_doc, + 'claim_doc_private_url': claim_doc_private_url, + 'order': order, + 'attachments': attachments, + 'form': form, + } + return render(request, 'contracts/claim_declaration_attachments.html', context) + + +@login_required +@require_POST +def delete_claim_attachment(request, attachment_id): + """ + Supprime une pièce jointe d'une déclaration de créance. + """ + attachment = get_object_or_404(ClaimDeclarationAttachment, id=attachment_id) + order = attachment.claim_document.order + + # Vérifier l'accès à la commande + if not user_has_access_to_contract_order(request.user, order.id): + raise PermissionDenied(_("Vous n'avez pas accès à cette commande.")) + + doc_id = attachment.claim_document.id + attachment_title = attachment.title + + # Supprimer le fichier physique + if attachment.document: + attachment.document.delete(save=False) + attachment.delete() + + messages.success(request, _("Le justificatif « %(title)s » a été supprimé.") % {'title': attachment_title}) + return redirect('contracts:claim_declaration_attachments', doc_id=doc_id) + + +@login_required +@require_POST +def update_order_status(request, order_id): + """ + Met à jour le statut d'une commande. + Seuls les managers et admins peuvent changer le statut. + """ + order = get_object_or_404(ContractOrder, id=order_id) + + # Vérifier l'accès à la commande + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'message': _("Vous n'avez pas accès à cette commande.")}, status=403) + + # Vérifier que l'utilisateur est manager ou admin + user_config = getattr(request.user, "config", None) + if not user_config or not user_config.roles.filter(name__in=['manager', 'admin']).exists(): + return JsonResponse({'message': _("Seuls les managers et administrateurs peuvent changer le statut d'une commande.")}, status=403) + + new_status = request.POST.get('new_status') + + # Valider que le nouveau statut existe + from contracts.models import CONTRACT_ORDER_STATUS_CHOICES + valid_statuses = [choice[0] for choice in CONTRACT_ORDER_STATUS_CHOICES] + + if new_status not in valid_statuses: + return JsonResponse({'message': _("Statut invalide.")}, status=400) + + # Mettre à jour le statut + old_status = order.order_status + order.order_status = new_status + order.save() + + return JsonResponse({ + 'message': _("Le statut de la commande a été mis à jour avec succès."), + 'old_status': old_status, + 'new_status': new_status + }) + + +@login_required +def order_add_attachment(request, order_id): + """Ajoute une ou plusieurs pièces jointes (photos, PDF, …) à une commande.""" + order = get_object_or_404(ContractOrder, id=order_id) + + if not user_has_access_to_contract_order(request.user, order_id): + raise PermissionDenied(_("Accès à la commande refusé.")) + + if not user_can_add_order_for_contract(request.user, order.contract_id): + raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter des documents à cette commande.")) + + if request.method == 'POST': + form = ContractOrderAttachmentForm(request.POST, request.FILES) + if form.is_valid(): + files = form.cleaned_data['files'] + for f in files: + ContractOrderAttachment.objects.create( + order=order, + file=f, + uploaded_by=request.user, + ) + messages.success(request, _("Document(s) ajouté(s) avec succès.")) + else: + messages.error(request, _("Erreur lors de l'ajout des documents.")) + + return redirect('contracts:contracts_orders_detail', order_id=order_id) + + +@login_required +@require_POST +def order_delete_attachment(request, order_id, attachment_id): + """Supprime une pièce jointe d'une commande (réponse JSON).""" + order = get_object_or_404(ContractOrder, id=order_id) + + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'success': False, 'error': _("Accès refusé.")}, status=403) + + if not user_can_add_order_for_contract(request.user, order.contract_id): + return JsonResponse({'success': False, 'error': _("Vous n'avez pas la permission de supprimer ce document.")}, status=403) + + attachment = get_object_or_404(ContractOrderAttachment, id=attachment_id, order=order) + + if attachment.file: + attachment.file.delete(save=False) + if attachment.thumbnail: + attachment.thumbnail.delete(save=False) + attachment.delete() + + return JsonResponse({'success': True}) + + +# ───────────────────────────────────────────────────────────────────────────── +# Commandes directes (DirectOrderPost) +# ───────────────────────────────────────────────────────────────────────────── + +def _user_can_manage_direct_posts(user, order): + """L'utilisateur peut créer/modifier/supprimer des commandes directes s'il + est external_manager ET a accès au contrat associé à la commande.""" + return user_can_manage_direct_order_posts(user, order.contract_id) + + +def _user_can_validate_direct_posts(user, order): + """L'utilisateur peut valider/refuser des commandes directes + s'il est admin, manager ou controller ET a accès au contrat associé à la commande.""" + return user_can_validate_direct_order_posts(user, order.contract_id) + + +@login_required +@require_GET +def order_direct_posts_list(request, order_id): + """Retourne la liste JSON des commandes directes d'une commande.""" + order = get_object_or_404(ContractOrder, id=order_id) + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'error': _('Accès refusé.')}, status=403) + + lang = get_language() or 'fr' + posts = [ + { + 'id': dp.id, + 'contract_post_id': dp.contract_post_id, + 'post_code': dp.contract_post.code, + 'post_description': dp.contract_post.get_description_for_language(lang), + 'post_order_number': dp.contract_post.order_number, + 'unit': dp.contract_post.unit, + 'unit_price': str(dp.contract_post.unit_price or '0.00'), + 'quantity': str(dp.quantity), + 'invoiced_quantity': str(dp.invoiced_quantity or '0.00'), + 'total_price': str(dp.total_price), + } + for dp in order.direct_posts.select_related('contract_post', 'created_by', 'validated_by') + ] + return JsonResponse({ + 'posts': posts, + 'can_manage': _user_can_manage_direct_posts(request.user, order), + 'can_validate': _user_can_validate_direct_posts(request.user, order), + }) + + +@login_required +@require_POST +def order_direct_post_add(request, order_id): + """Ajoute un poste de commande directe (AJAX JSON).""" + order = get_object_or_404(ContractOrder, id=order_id) + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'success': False, 'error': _('Accès refusé.')}, status=403) + if not _user_can_manage_direct_posts(request.user, order): + return JsonResponse({'success': False, 'error': _('Permission insuffisante.')}, status=403) + + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({'success': False, 'error': _('Données JSON invalides.')}, status=400) + + contract_post_id = data.get('contract_post_id') + quantity_raw = data.get('quantity') + comment = (data.get('comment') or '').strip() + + if not contract_post_id: + return JsonResponse({'success': False, 'error': _('Le poste de marché est requis.')}, status=400) + + try: + quantity = Decimal(str(quantity_raw)) + if quantity <= 0: + raise ValueError + except (InvalidOperation, ValueError, TypeError): + return JsonResponse({'success': False, 'error': _('La quantité doit être un nombre positif.')}, status=400) + + contract_post = get_object_or_404(ContractPost, id=contract_post_id, contract=order.contract) + + if DirectOrderPost.objects.filter(order=order, contract_post=contract_post).exists(): + return JsonResponse({ + 'success': False, + 'error': _('Un poste direct existe déjà pour ce poste dans cette commande. Modifiez-le plutôt.'), + }, status=400) + + dp = DirectOrderPost.objects.create( + order=order, + contract_post=contract_post, + quantity=quantity, + comment=comment or None, + created_by=request.user, + ) + lang = get_language() or 'fr' + return JsonResponse({ + 'success': True, + 'post': { + 'id': dp.id, + 'contract_post_id': dp.contract_post_id, + 'post_code': dp.contract_post.code, + 'post_description': dp.contract_post.get_description_for_language(lang), + 'post_order_number': dp.contract_post.order_number, + 'unit': dp.contract_post.unit, + 'unit_price': str(dp.contract_post.unit_price or '0.00'), + 'quantity': str(dp.quantity), + 'invoiced_quantity': str(dp.invoiced_quantity or '0.00'), + 'total_price': str(dp.total_price), + 'comment': dp.comment or '', + 'control_status': dp.control_status, + 'created_at': dp.created_at.strftime('%d/%m/%Y') if dp.created_at else '', + 'created_by': dp.created_by.get_full_name() if dp.created_by else '', + 'validated_at': '', + 'validated_by': '', + }, + }) + + +@login_required +@require_POST +def order_direct_post_update(request, order_id, direct_post_id): + """Met à jour la quantité/commentaire d'un poste de commande directe (AJAX JSON).""" + order = get_object_or_404(ContractOrder, id=order_id) + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'success': False, 'error': _('Accès refusé.')}, status=403) + if not _user_can_manage_direct_posts(request.user, order): + return JsonResponse({'success': False, 'error': _('Permission insuffisante.')}, status=403) + + dp = get_object_or_404(DirectOrderPost, id=direct_post_id, order=order) + if dp.control_status == 'validated': + return JsonResponse({'success': False, 'error': _('Impossible de modifier un poste déjà validé.')}, status=400) + + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({'success': False, 'error': _('Données JSON invalides.')}, status=400) + + quantity_raw = data.get('quantity') + comment = (data.get('comment') or '').strip() + + try: + quantity = Decimal(str(quantity_raw)) + if quantity < 0: + raise ValueError + except (InvalidOperation, ValueError, TypeError): + return JsonResponse({'success': False, 'error': _('La quantité doit être un nombre positif ou nul.')}, status=400) + + dp.quantity = quantity + dp.comment = comment or None + # Si le poste était facturé, le repasser en attente à la modification + if dp.control_status == 'invoiced': + dp.control_status = 'pending' + dp.save(update_fields=['quantity', 'comment', 'control_status']) + + lang = get_language() or 'fr' + return JsonResponse({ + 'success': True, + 'post': { + 'id': dp.id, + 'quantity': str(dp.quantity), + 'invoiced_quantity': str(dp.invoiced_quantity or '0.00'), + 'total_price': str(dp.total_price), + 'comment': dp.comment or '', + 'control_status': dp.control_status, + }, + }) + + +@login_required +@require_POST +def order_direct_post_delete(request, order_id, direct_post_id): + """Supprime un poste de commande directe (AJAX JSON).""" + order = get_object_or_404(ContractOrder, id=order_id) + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'success': False, 'error': _('Accès refusé.')}, status=403) + if not _user_can_manage_direct_posts(request.user, order): + return JsonResponse({'success': False, 'error': _('Permission insuffisante.')}, status=403) + + dp = get_object_or_404(DirectOrderPost, id=direct_post_id, order=order) + if dp.control_status == 'validated': + return JsonResponse({'success': False, 'error': _('Impossible de supprimer un poste déjà validé.')}, status=400) + if dp.invoiced_quantity and dp.invoiced_quantity > 0: + return JsonResponse({'success': False, 'error': _('Impossible de supprimer un poste déjà facturé.')}, status=400) + + dp.delete() + return JsonResponse({'success': True}) + + +@login_required +@require_POST +def order_direct_post_validate(request, order_id, direct_post_id): + """Valide ou refuse un poste de commande directe (AJAX JSON).""" + order = get_object_or_404(ContractOrder, id=order_id) + if not user_has_access_to_contract_order(request.user, order_id): + return JsonResponse({'success': False, 'error': _('Accès refusé.')}, status=403) + if not _user_can_validate_direct_posts(request.user, order): + return JsonResponse({'success': False, 'error': _('Permission insuffisante.')}, status=403) + + dp = get_object_or_404(DirectOrderPost, id=direct_post_id, order=order) + + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({'success': False, 'error': _('Données JSON invalides.')}, status=400) + + new_status = data.get('status') + valid_statuses = [s[0] for s in DIRECT_ORDER_POST_STATUS_CHOICES] + if new_status not in valid_statuses: + return JsonResponse({'success': False, 'error': _('Statut invalide.')}, status=400) + + from django.utils import timezone + dp.control_status = new_status + if new_status == 'validated': + dp.validated_at = timezone.now() + dp.validated_by = request.user + elif new_status == 'pending': + dp.validated_at = None + dp.validated_by = None + dp.save(update_fields=['control_status', 'validated_at', 'validated_by']) + + return JsonResponse({ + 'success': True, + 'post': { + 'id': dp.id, + 'quantity': str(dp.quantity), + 'invoiced_quantity': str(dp.invoiced_quantity or '0.00'), + 'control_status': dp.control_status, + 'total_price': str(dp.total_price), + 'validated_at': dp.validated_at.strftime('%d/%m/%Y') if dp.validated_at else '', + 'validated_by': dp.validated_by.get_full_name() if dp.validated_by else '', + }, + }) + diff --git a/streetup/contracts/views_daily_report.py b/streetup/contracts/views_daily_report.py new file mode 100644 index 0000000..e2af144 --- /dev/null +++ b/streetup/contracts/views_daily_report.py @@ -0,0 +1,565 @@ +""" +Rapport journalier des interventions encodées par les membres d'une Company. + +Génère un PDF et un Excel contenant : +- Un tableau de synthèse par thématique (lignes) avec colonnes : + Avarie | Défaut/Panne | Demande | StreetUp | PREGO | Terminées | Autres | Total +- Un tableau par personne (nombre d'interventions créées) +""" + +import io +import os +import zipfile +from collections import defaultdict +from datetime import datetime, timedelta + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse, Http404 +from django.utils import timezone +from django.utils.translation import gettext as _ + +from fpdf import FPDF +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill, Border, Side +from openpyxl.utils import get_column_letter + +from contracts.models import CompanyMember, CompanyTeam +from common.models import Thematic +from interventions.models import Intervention, STATUS_ORDERS + + +# --------------------------------------------------------------------------- +# Helpers : collecte des données +# --------------------------------------------------------------------------- + +def _get_authorized_companies_for_user(user): + """ + Retourne la liste des Company distinctes dont l'utilisateur est membre + via une équipe ayant can_generate_daily_report=True. + Un utilisateur peut appartenir à plusieurs équipes ; toutes sont vérifiées. + """ + members = ( + CompanyMember.objects + .filter(user=user, teams__can_generate_daily_report=True) + .prefetch_related('teams__company') + ) + seen = {} + for m in members: + for t in m.teams.all(): + if t.can_generate_daily_report: + c = t.company + seen[c.id] = c + return list(seen.values()) + + +def _collect_report_data(companies, report_date): + """ + Collecte les données du rapport journalier pour une liste de Company. + + La fenêtre temporelle couvre de 20h00 (j-1) à 20h00 (j), heure de Bruxelles. + + Retourne un dict : + { + 'by_thematic': {thematic_name: {col_key: count, ...}, ...}, + 'by_person': {person_name: count, ...}, + 'totals': {col_key: count, ...}, # ligne totale + 'total_all': int, + } + """ + import pytz + brussels = pytz.timezone('Europe/Brussels') + + # Fenêtre : 20h00 j-1 → 20h00 j (heure de Bruxelles) + prev_day = report_date - timedelta(days=1) + period_start = brussels.localize(datetime(prev_day.year, prev_day.month, prev_day.day, 20, 0, 0)) + period_end = brussels.localize(datetime(report_date.year, report_date.month, report_date.day, 20, 0, 0)) + + company_ids = [c.id for c in companies] + + # Récupérer tous les utilisateurs membres des Companies concernées + company_members = CompanyMember.objects.filter( + teams__company_id__in=company_ids + ).select_related('user').prefetch_related('teams__company') + + member_user_ids = list(company_members.values_list('user_id', flat=True)) + # En cas de doublons (même user dans plusieurs équipes), on garde le premier nom trouvé + member_name_map = {} + for m in company_members: + if m.user_id not in member_name_map: + member_name_map[m.user_id] = m.name + + # Interventions créées dans la fenêtre par des membres des Companies + # Uniquement les thématiques avec include_in_daily_report=True ; annulées exclues + interventions = ( + Intervention.objects + .filter( + created_by_id__in=member_user_ids, + creation_time__gte=period_start, + creation_time__lt=period_end, + thematic__include_in_daily_report=True, + ) + .exclude(status__in=['in_preparation','canceled']) + .select_related('thematic', 'source_category') + ) + + # Colonnes + COL_KEYS = [ + 'damage', 'defect', 'demand', + 'observation', 'prego', + 'finished', 'other_status', + 'total', + ] + + by_thematic = defaultdict(lambda: {k: 0 for k in COL_KEYS}) + by_person = defaultdict(int) + totals = {k: 0 for k in COL_KEYS} + + # Pré-peupler avec toutes les thématiques marquées "inclure dans rapport journalier" + for thematic in Thematic.objects.filter(include_in_daily_report=True).order_by('name_fr'): + _ = by_thematic[thematic.name_fr] # initialise la ligne à zéros + + for itv in interventions: + thematic_name = itv.thematic.name_fr if itv.thematic else "Sans thématique" + row = by_thematic[thematic_name] + + # --- Origine --- + origin = itv.origin_type or '' + if origin == 'damage': + row['damage'] += 1 + totals['damage'] += 1 + elif origin == 'defect': + row['defect'] += 1 + totals['defect'] += 1 + elif origin == 'demand': + row['demand'] += 1 + totals['demand'] += 1 + + # --- Source category --- + sc = itv.source_category + if sc: + sc_name = (sc.name_fr or '').strip().lower() + if sc_name == 'observation': + row['observation'] += 1 + totals['observation'] += 1 + elif sc_name == 'prego': + row['prego'] += 1 + totals['prego'] += 1 + + # --- Statut --- + if itv.status_order >= STATUS_ORDERS['finished']: + row['finished'] += 1 + totals['finished'] += 1 + else: + row['other_status'] += 1 + totals['other_status'] += 1 + + # --- Total --- + row['total'] += 1 + totals['total'] += 1 + + # --- Par personne --- + person_name = member_name_map.get(itv.created_by_id, str(itv.created_by)) + by_person[person_name] += 1 + + return { + 'by_thematic': dict(by_thematic), + 'by_person': dict(by_person), + 'totals': totals, + 'total_all': totals['total'], + } + + +# --------------------------------------------------------------------------- +# Colonnes du tableau de synthèse (labels) +# --------------------------------------------------------------------------- + +COLUMN_HEADERS = [ + ('damage', 'Avarie'), + ('defect', 'Défaut/Panne'), + ('demand', 'Demande'), + ('observation', 'StreetUp'), + ('prego', 'Issu de Prego'), + ('finished', 'Résolu'), + ('other_status', 'En cours'), + ('total', 'Total'), +] + +# Couleurs spéciales (R, G, B) pour l'en-tête et la ligne totale de certaines colonnes +# Les colonnes absentes de ce dict héritent de la couleur par défaut. +COLUMN_SPECIAL_COLORS_RGB = { + 'prego': (255, 102, 0), # Orange + 'finished': (112, 173, 71), # Vert + 'other_status': ( 0, 176, 240), # Bleu ciel +} +# Version hex pour Excel (AARRGGBB sans alpha, format RRGGBB) +COLUMN_SPECIAL_COLORS_HEX = { + 'prego': 'FF6600', + 'finished': '70AD47', + 'other_status': '00B0F0', +} + + +# --------------------------------------------------------------------------- +# PDF generation +# --------------------------------------------------------------------------- + +class DailyReportPDF(FPDF): + + def __init__(self, company_name, report_date, *args, **kwargs): + super().__init__(orientation='L', *args, **kwargs) + self.company_name = company_name + self.report_date = report_date + + # ---- header / footer ---- + def header(self): + font_dir = os.path.join(settings.BASE_DIR, "common", "static", "common") + self.add_font("arial", style="", fname=os.path.join(font_dir, "arial.ttf")) + self.add_font("arial", style="B", fname=os.path.join(font_dir, "arial_bold.ttf")) + self.add_font("arial", style="I", fname=os.path.join(font_dir, "arial_italic.ttf")) + + logo_path = os.path.join(settings.BASE_DIR, "common", "static", settings.ORGANIZATION_LOGO_PNG) + if os.path.exists(logo_path): + self.image(logo_path, x=10, y=8, w=50) + + self.set_font("arial", "B", 14) + self.set_y(10) + self.cell(0, 8, _("Rapport journalier des interventions"), align="C", new_x="LMARGIN", new_y="NEXT") + self.set_font("arial", "", 11) + self.cell(0, 7, f"{self.company_name} — {self.report_date.strftime('%d/%m/%Y')}", align="C", new_x="LMARGIN", new_y="NEXT") + self.ln(4) + + def footer(self): + self.set_y(-15) + self.set_font("arial", "I", 8) + self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C") + + # ---- section title ---- + def _section_title(self, text): + self.set_font("arial", "B", 11) + self.set_fill_color(*settings.ORGANIZATION_PRIMARY_COLOR_RGB) + self.set_text_color(255, 255, 255) + self.cell(0, 8, f" {text}", fill=True, new_x="LMARGIN", new_y="NEXT") + self.set_text_color(0, 0, 0) + self.ln(2) + + # ---- table : synthèse par thématique ---- + def add_thematic_table(self, data): + self._section_title(_("Synthèse par thématique")) + + col_widths = [60] + [25] * len(COLUMN_HEADERS) # thématique + cols + headers = [_("Thématique")] + [label for _lbl, label in COLUMN_HEADERS] + + # Header row — hauteur fixe pour toutes les cellules, texte centré verticalement + HEADER_ROW_H = 14 # mm + LINE_H = 4.5 # hauteur par ligne de texte + self.set_font("arial", "B", 8) + self.set_draw_color(0, 0, 0) + x0 = self.get_x() + y0 = self.get_y() + cumulative_x = x0 + for i, h in enumerate(headers): + # Couleur de fond : spéciale si colonne concernée, sinon bleu clair + if i >= 1: + col_key = COLUMN_HEADERS[i - 1][0] + r, g, b = COLUMN_SPECIAL_COLORS_RGB.get(col_key, (220, 230, 241)) + else: + r, g, b = 220, 230, 241 + self.set_fill_color(r, g, b) + # Rectangle plein + bordure à hauteur fixe + self.rect(cumulative_x, y0, col_widths[i], HEADER_ROW_H, style='FD') + # Centrage vertical du texte + lines = str(h).split('\n') + text_block_h = len(lines) * LINE_H + text_y = y0 + (HEADER_ROW_H - text_block_h) / 2 + for k, line in enumerate(lines): + self.set_xy(cumulative_x, text_y + k * LINE_H) + self.cell(col_widths[i], LINE_H, line, align='C') + cumulative_x += col_widths[i] + self.set_xy(x0, y0 + HEADER_ROW_H) + + # Data rows + self.set_font("arial", "", 8) + fill = False + for thematic_name in sorted(data['by_thematic'].keys()): + row = data['by_thematic'][thematic_name] + if fill: + self.set_fill_color(245, 245, 245) + else: + self.set_fill_color(255, 255, 255) + + self.cell(col_widths[0], 6, str(thematic_name), border=1, fill=True, align="L") + for j, (key, _lbl) in enumerate(COLUMN_HEADERS): + val = row.get(key, 0) + self.cell(col_widths[j + 1], 6, str(val), border=1, fill=True, align="C") + self.ln() + fill = not fill + + # Totals row + self.set_font("arial", "B", 8) + self.set_text_color(255, 255, 255) + self.set_fill_color(0, 91, 153) + self.cell(col_widths[0], 7, _("TOTAL"), border=1, fill=True, align="L") + for j, (key, _lbl) in enumerate(COLUMN_HEADERS): + r, g, b = COLUMN_SPECIAL_COLORS_RGB.get(key, (0, 91, 153)) + self.set_fill_color(r, g, b) + val = data['totals'].get(key, 0) + self.cell(col_widths[j + 1], 7, str(val), border=1, fill=True, align="C") + self.ln() + self.set_text_color(0, 0, 0) + self.ln(4) + + # ---- table : par personne ---- + def add_person_table(self, data): + self._section_title(_("Nombre d'interventions par personne")) + + col_widths = [100, 40] + headers = [_("Personne"), _("Nombre")] + + self.set_font("arial", "B", 9) + self.set_fill_color(220, 230, 241) + for i, h in enumerate(headers): + self.cell(col_widths[i], 7, str(h), border=1, fill=True, align="C") + self.ln() + + self.set_font("arial", "", 9) + fill = False + total = 0 + for person_name in sorted(data['by_person'].keys()): + count = data['by_person'][person_name] + total += count + if fill: + self.set_fill_color(245, 245, 245) + else: + self.set_fill_color(255, 255, 255) + self.cell(col_widths[0], 6, str(person_name), border=1, fill=True, align="L") + self.cell(col_widths[1], 6, str(count), border=1, fill=True, align="C") + self.ln() + fill = not fill + + # Total + self.set_font("arial", "B", 9) + self.set_fill_color(0, 91, 153) + self.set_text_color(255, 255, 255) + self.cell(col_widths[0], 7, _("TOTAL"), border=1, fill=True, align="L") + self.cell(col_widths[1], 7, str(total), border=1, fill=True, align="C") + self.ln() + self.set_text_color(0, 0, 0) + + +def generate_daily_report_pdf(company_name, report_date, data): + """Génère le PDF et retourne les bytes.""" + pdf = DailyReportPDF(company_name, report_date) + pdf.alias_nb_pages() + pdf.add_page() + pdf.add_thematic_table(data) + pdf.add_person_table(data) + buffer = io.BytesIO() + pdf.output(buffer) + buffer.seek(0) + return buffer.read() + + +# --------------------------------------------------------------------------- +# Excel generation +# --------------------------------------------------------------------------- + +def generate_daily_report_excel(company_name, report_date, data): + """Génère le fichier Excel et retourne les bytes.""" + wb = Workbook() + + # --- Styles --- + header_font = Font(bold=True, color="FFFFFF", size=10) + header_fill = PatternFill(start_color="005B99", end_color="005B99", fill_type="solid") + header_alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + total_font = Font(bold=True, color="FFFFFF", size=10) + total_fill = PatternFill(start_color="005B99", end_color="005B99", fill_type="solid") + thin_border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin'), + ) + alt_fill = PatternFill(start_color="F5F5F5", end_color="F5F5F5", fill_type="solid") + + # Couleurs spéciales pour certaines colonnes (en-têtes et totaux) + col_special_fills = { + key: PatternFill(start_color=hex_color, end_color=hex_color, fill_type="solid") + for key, hex_color in COLUMN_SPECIAL_COLORS_HEX.items() + } + + # =========== Feuille 1 : Synthèse par thématique =========== + ws1 = wb.active + ws1.title = _("Synthèse par thématique") + + # Title row + ws1.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(COLUMN_HEADERS) + 1) + title_cell = ws1.cell(row=1, column=1, value=f"{_('Rapport journalier')} — {company_name} — {report_date.strftime('%d/%m/%Y')}") + title_cell.font = Font(bold=True, size=14) + title_cell.alignment = Alignment(horizontal="center") + + # Headers + row_num = 3 + col_headers = [_("Thématique")] + [label for _lbl, label in COLUMN_HEADERS] + for col_idx, header_text in enumerate(col_headers, 1): + cell = ws1.cell(row=row_num, column=col_idx, value=str(header_text)) + cell.font = header_font + if col_idx >= 2: + cell.fill = col_special_fills.get(COLUMN_HEADERS[col_idx - 2][0], header_fill) + else: + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + + # Data + row_num = 4 + for idx, thematic_name in enumerate(sorted(data['by_thematic'].keys())): + row_data = data['by_thematic'][thematic_name] + cell = ws1.cell(row=row_num, column=1, value=str(thematic_name)) + cell.border = thin_border + if idx % 2: + cell.fill = alt_fill + + for col_idx, (key, _lbl) in enumerate(COLUMN_HEADERS, 2): + cell = ws1.cell(row=row_num, column=col_idx, value=row_data.get(key, 0)) + cell.alignment = Alignment(horizontal="center") + cell.border = thin_border + if idx % 2: + cell.fill = alt_fill + row_num += 1 + + # Total row (formules SUM pour recalcul automatique) + data_first_row = 4 + cell = ws1.cell(row=row_num, column=1, value=_("TOTAL")) + cell.font = total_font + cell.fill = total_fill + cell.border = thin_border + for col_idx, (key, _lbl) in enumerate(COLUMN_HEADERS, 2): + col_letter = get_column_letter(col_idx) + cell = ws1.cell(row=row_num, column=col_idx, + value=f"=SUM({col_letter}{data_first_row}:{col_letter}{row_num - 1})") + cell.font = total_font + cell.fill = col_special_fills.get(key, total_fill) + cell.alignment = Alignment(horizontal="center") + cell.border = thin_border + + # Auto-adjust column widths + for col in range(1, len(col_headers) + 1): + ws1.column_dimensions[chr(64 + col) if col <= 26 else 'A'].width = 18 if col == 1 else 14 + + # =========== Feuille 2 : Par personne =========== + ws2 = wb.create_sheet(title=_("Par personne")) + + ws2.merge_cells(start_row=1, start_column=1, end_row=1, end_column=2) + title_cell = ws2.cell(row=1, column=1, value=f"{_('Rapport journalier')} — {company_name} — {report_date.strftime('%d/%m/%Y')}") + title_cell.font = Font(bold=True, size=14) + title_cell.alignment = Alignment(horizontal="center") + + row_num = 3 + for col_idx, header_text in enumerate([_("Personne"), _("Nombre")], 1): + cell = ws2.cell(row=row_num, column=col_idx, value=str(header_text)) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + + row_num = 4 + for idx, person_name in enumerate(sorted(data['by_person'].keys())): + count = data['by_person'][person_name] + cell = ws2.cell(row=row_num, column=1, value=str(person_name)) + cell.border = thin_border + if idx % 2: + cell.fill = alt_fill + cell = ws2.cell(row=row_num, column=2, value=count) + cell.alignment = Alignment(horizontal="center") + cell.border = thin_border + if idx % 2: + cell.fill = alt_fill + row_num += 1 + + # Total (formule SUM pour recalcul automatique) + cell = ws2.cell(row=row_num, column=1, value=_("TOTAL")) + cell.font = total_font + cell.fill = total_fill + cell.border = thin_border + cell = ws2.cell(row=row_num, column=2, value=f"=SUM(B4:B{row_num - 1})") + cell.font = total_font + cell.fill = total_fill + cell.alignment = Alignment(horizontal="center") + cell.border = thin_border + + ws2.column_dimensions['A'].width = 35 + ws2.column_dimensions['B'].width = 15 + + # Output + buffer = io.BytesIO() + wb.save(buffer) + return buffer.getvalue() + + +# --------------------------------------------------------------------------- +# Vue Django +# --------------------------------------------------------------------------- + +@login_required +def daily_report_download(request): + """ + Génère un ZIP contenant le PDF et l'Excel du rapport journalier. + Accessible uniquement aux membres d'une CompanyTeam ayant can_generate_daily_report=True. + + Query params optionnels : + - date : date au format YYYY-MM-DD (défaut = aujourd'hui) + - format : 'pdf', 'excel' ou 'zip' (défaut = 'zip') + """ + # Vérifier que l'utilisateur appartient à au moins une équipe autorisée + # (toutes ses équipes sont vérifiées, pas seulement la première) + companies = _get_authorized_companies_for_user(request.user) + if not companies: + raise Http404(_("Aucune de vos équipes n'est autorisée à générer ce rapport.")) + + # Date du rapport + date_str = request.GET.get('date') + if date_str: + try: + report_date = datetime.strptime(date_str, '%Y-%m-%d').date() + except ValueError: + report_date = timezone.now().date() + else: + report_date = timezone.now().date() + + # Collecter les données + data = _collect_report_data(companies, report_date) + company_name = " / ".join(sorted(c.name for c in companies)) + date_label = report_date.strftime('%Y%m%d') + + output_format = request.GET.get('format', 'zip') + + if output_format == 'pdf': + pdf_bytes = generate_daily_report_pdf(company_name, report_date, data) + response = HttpResponse(pdf_bytes, content_type='application/pdf') + response['Content-Disposition'] = f'inline; filename="rapport_journalier_{date_label}.pdf"' + return response + + elif output_format == 'excel': + excel_bytes = generate_daily_report_excel(company_name, report_date, data) + response = HttpResponse( + excel_bytes, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ) + response['Content-Disposition'] = f'attachment; filename="rapport_journalier_{date_label}.xlsx"' + return response + + else: + # ZIP contenant les deux + pdf_bytes = generate_daily_report_pdf(company_name, report_date, data) + excel_bytes = generate_daily_report_excel(company_name, report_date, data) + + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: + zf.writestr(f"rapport_journalier_{date_label}.pdf", pdf_bytes) + zf.writestr(f"rapport_journalier_{date_label}.xlsx", excel_bytes) + zip_buffer.seek(0) + + response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename="rapport_journalier_{date_label}.zip"' + return response diff --git a/streetup/contracts/views_pdf_reports.py b/streetup/contracts/views_pdf_reports.py new file mode 100644 index 0000000..ec81474 --- /dev/null +++ b/streetup/contracts/views_pdf_reports.py @@ -0,0 +1,1542 @@ +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404, redirect +from django.core.files.base import ContentFile +from django.conf import settings +from django.utils.text import slugify +from django.contrib import messages +from django.utils.translation import gettext as _, ngettext, get_language +from django.utils import timezone +from django.utils.formats import date_format +from django.db.models import Sum + +import os +import io +import pytz +from datetime import datetime +from decimal import Decimal +from num2words import num2words +from fpdf import FPDF +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill, Border, Side +from openpyxl.utils import get_column_letter + +from contracts.models import Company, Contract, ContractOrder, ContractOrderDocument, ContractOrderProgressState, ContractOrderProgressStateItem + +from contracts.permissions import user_can_generate_order_documents + + +def calculate_order_totals(order, interventions_filter=None, include_validated_periods=False): + """ + Calcule les totaux ventilés pour une commande en tenant compte des coefficients + multiplicateurs et des sommes à justifier (SAJ). + + Les postes coefficient multiplicateur (is_coefficient_multipliable=True) représentent + un supplément calculé par intervention. Leur total_price inclut déjà le calcul: + 1 × coefficient × somme_des_coûts_postes_même_type + + Structure des totaux: + - Total revisable: postes normaux (sans les coefficients multiplicateurs ni SAJ) + - Postes coefficient multiplicateur: suppléments calculés par intervention + - SAJ sans coefficient: sommes à justifier sans coefficient applicable + - SAJ avec coefficient: sommes à justifier regroupées par type avec coefficient + + Args: + order: ContractOrder + interventions_filter: filtre optionnel pour les interventions (ex: {'status': 'validated'}) + include_validated_periods: si True, inclut les périodes validées des interventions + + Returns: + dict avec les différents totaux et informations sur les périodes + """ + from interventions.models import InterventionContractPost, InterventionPostPeriodQuantity + + # Filtrer les interventions + interventions_qs = order.interventions.all() + if interventions_filter: + interventions_qs = interventions_qs.filter(**interventions_filter) + + intervention_ids = list(interventions_qs.values_list('id', flat=True)) + + # Structure pour suivre les périodes incluses + included_periods = {} # intervention_id -> set of period_numbers + + # Si demandé, inclure aussi les périodes validées (non facturées) + period_posts = [] + interventions_with_periods = set() + + if include_validated_periods: + # Récupérer les périodes validées de TOUTES les interventions de la commande + # (pas seulement celles filtrées) car on veut inclure les interventions + # qui ont des périodes validées même si l'intervention n'est pas validée + all_intervention_ids = list(order.interventions.values_list('id', flat=True)) + + period_quantities = InterventionPostPeriodQuantity.objects.filter( + intervention_id__in=all_intervention_ids, + status='validated' + ).select_related('contract_post', 'intervention') + + # Grouper par intervention et période pour tracking + for pq in period_quantities: + if pq.intervention_id not in included_periods: + included_periods[pq.intervention_id] = set() + included_periods[pq.intervention_id].add(pq.period_number) + interventions_with_periods.add(pq.intervention_id) + + # Créer un objet simulé pour compatibilité avec le code existant + class PeriodPost: + def __init__(self, period_quantity): + self.contract_post = period_quantity.contract_post + self.quantity = period_quantity.quantity + self.intervention = period_quantity.intervention + self.control_status = 'validated' + + @property + def total_price(self): + if not self.contract_post.unit_price: + return Decimal("0.00") + + qty = Decimal(self.quantity or 0) + + # Si le poste est un coefficient multiplicateur, calculer le total en fonction des postes du même type + if self.contract_post.is_coefficient_multipliable and self.contract_post.post_type: + # Pour les postes avec coefficient, la quantité doit être 1 (sauf si 0 ou vide) + effective_qty = Decimal('1') if qty > 0 else Decimal('0') + + # Ajouter les périodes validées du même type (exclure les coefficients) + from collections import defaultdict + same_type_periods = InterventionPostPeriodQuantity.objects.filter( + intervention=self.intervention, + contract_post__post_type=self.contract_post.post_type, + status='validated' + ).exclude( + contract_post__is_coefficient_multipliable=True + ).select_related('contract_post') + + # Grouper par poste pour éviter de compter plusieurs fois le même poste + period_totals_by_post = defaultdict(Decimal) + for p in same_type_periods: + period_totals_by_post[p.contract_post_id] += Decimal(str(p.quantity or 0)) + + # Calculer le coût total des périodes + total_same_type_cost = Decimal('0') + for post_id, total_qty in period_totals_by_post.items(): + # Trouver le contract_post correspondant + cp = next((p.contract_post for p in same_type_periods if p.contract_post_id == post_id), None) + if cp: + total_same_type_cost += total_qty * (cp.unit_price or Decimal('0')) + + # Total = effective_qty × prix unitaire (coefficient) × somme des coûts + return effective_qty * self.contract_post.unit_price * total_same_type_cost + + # Calcul standard + base = qty * self.contract_post.unit_price + if self.contract_post.is_amount_to_be_justified and self.contract_post.coeff_amount_to_be_justified: + return base * self.contract_post.coeff_amount_to_be_justified + return base + + period_posts.append(PeriodPost(pq)) + + # Récupérer tous les postes validés pour ces interventions + # MAIS exclure les interventions qui ont des périodes validées (pour éviter le doublon) + posts = InterventionContractPost.objects.filter( + intervention_id__in=intervention_ids, + control_status='validated' + ).exclude( + intervention_id__in=interventions_with_periods + ).select_related('contract_post', 'intervention') + + # Combiner les postes d'intervention et les périodes + all_posts = list(posts) + period_posts + + # Ajouter les commandes directes validées + from contracts.models import DirectOrderPost + direct_posts_qs = DirectOrderPost.objects.filter( + order=order, + control_status='validated', + ).select_related('contract_post') + all_posts = all_posts + list(direct_posts_qs) + + # Initialiser les accumulateurs + total_revisable = Decimal('0') # Postes revisables (non SAJ, non coefficient) + + # Postes coefficient multiplicateur par code (suppléments) + coefficient_posts_totals = {} # code -> {total, description, coefficient_value, post_type} + + # SAJ sans coefficient + saj_without_coef = {} # code -> {total, description} + + # D'abord, identifier tous les postes coefficient et leurs types + coefficient_post_types = set() + for post in all_posts: + cp = post.contract_post + if cp.is_coefficient_multipliable and cp.post_type: + coefficient_post_types.add(cp.post_type) + if cp.code not in coefficient_posts_totals: + coefficient_posts_totals[cp.code] = { + 'code': cp.code, + 'description': cp.get_description_for_language(), + 'coefficient_value': cp.unit_price or Decimal('0'), # Le prix unitaire est le coefficient + 'post_type': cp.post_type, + 'total': Decimal('0'), + } + # Le total_price est calculé par la propriété du modèle + # = coefficient × somme_des_coûts_postes_même_type (par intervention) + coefficient_posts_totals[cp.code]['total'] += post.total_price or Decimal('0') + + # SAJ avec coefficient - regrouper par post_type + saj_with_coef = {} # post_type -> {total, description, coefficient} + + # Calculer les totaux pour les autres postes + for post in all_posts: + cp = post.contract_post + qty = post.quantity or Decimal('0') + unit_price = cp.unit_price or Decimal('0') + + # Ignorer les postes coefficient (déjà traités) + if cp.is_coefficient_multipliable: + continue + + base_amount = qty * unit_price + + if cp.is_amount_to_be_justified: + # Appliquer le coefficient coeff_amount_to_be_justified si défini + coeff_saj = cp.coeff_amount_to_be_justified if cp.coeff_amount_to_be_justified else Decimal('1') + saj_amount = base_amount * coeff_saj + # Poste SAJ - vérifier si un coefficient s'applique à son type + if cp.post_type and cp.post_type in coefficient_post_types: + # SAJ avec coefficient - regrouper par post_type + if cp.post_type not in saj_with_coef: + # Trouver le coefficient correspondant + coef_info = next( + (c for c in coefficient_posts_totals.values() if c['post_type'] == cp.post_type), + None + ) + coefficient_display = Decimal('1') + (coef_info['coefficient_value'] if coef_info else Decimal('0')) + saj_with_coef[cp.post_type] = { + 'post_type': cp.post_type, + 'description': f"SAJ {cp.post_type}", # Description générique par type + 'coefficient': coefficient_display, + 'total': Decimal('0'), + } + saj_with_coef[cp.post_type]['total'] += saj_amount + else: + # SAJ sans coefficient + if cp.code not in saj_without_coef: + saj_without_coef[cp.code] = { + 'code': cp.code, + 'description': cp.get_description_for_language(), + 'total': Decimal('0'), + } + saj_without_coef[cp.code]['total'] += saj_amount + else: + # Poste revisable normal (montant de base, sans multiplication) + total_revisable += base_amount + + # Calculer les totaux + total_coefficient_supplements = sum(d['total'] for d in coefficient_posts_totals.values()) + total_saj_without_coef = sum(d['total'] for d in saj_without_coef.values()) + total_saj_with_coef_base = sum(d['total'] for d in saj_with_coef.values()) + + # Pour les SAJ avec coefficient: + # - La base est dans saj_with_coef[post_type]['total'] + # - Le supplément (coefficient × base) est dans coefficient_posts_totals + # Donc pour l'affichage, on montre base × (1 + coef), mais dans le total général, + # le supplément est déjà compté dans coefficient_posts_totals. + + # Total général HTVA + # = revisables + suppléments coefficients + SAJ sans coef + SAJ avec coef (base seulement) + # Note: le supplément des SAJ avec coef est inclus dans total_coefficient_supplements + total_general_htva = ( + total_revisable + + total_coefficient_supplements + # Suppléments des postes coefficient (inclut les suppléments SAJ) + total_saj_without_coef + + total_saj_with_coef_base # Base SAJ (le supplément est dans coefficient_posts_totals) + ) + + return { + 'total_revisable': total_revisable, # Postes revisables de base + 'total_coefficient_supplements': total_coefficient_supplements, # Total des suppléments coefficient + 'coefficient_posts_totals': coefficient_posts_totals, # Détails par poste coefficient + 'total_saj_without_coef': total_saj_without_coef, # SAJ sans coefficient + 'saj_without_coef': saj_without_coef, # Détails SAJ sans coef + 'total_saj_with_coef_base': total_saj_with_coef_base, # SAJ avec coefficient (base seulement) + 'saj_with_coef': saj_with_coef, # Détails SAJ avec coef (pour affichage: base × coefficient) + 'total_general_htva': total_general_htva, + 'included_periods': included_periods, # intervention_id -> set of period_numbers + } + + +### Déclaration de créance ### + +class ClaimDeclarationPDF(FPDF): + + def __init__(self, company: Company, show_draft: bool = False, *args, **kwargs): + super().__init__(*args, **kwargs) + self.company = company # Stocker l’instance de Company + self.show_draft = show_draft + self.set_margins(left=15, top=10, right=15) # marges en mm + + def header(self): + # Fonts + self.add_font("arial", style="", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial.ttf")) + self.add_font("arial", style="B", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial_bold.ttf")) + self.add_font("arial", style="I", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial_italic.ttf")) + + # Logo de la société (s'il existe) + if self.company.logo: + logo_path = os.path.join(settings.MEDIA_ROOT, self.company.logo.name) + if os.path.exists(logo_path): + self.image(logo_path, x=10, y=8, w=40) + + # Badge DRAFT en haut à droite (preview seulement) + if self.show_draft: + _draw_draft_ribbon(self, text="DRAFT") + + + def add_order_data(self, order: ContractOrder): + brussels = pytz.timezone("Europe/Brussels") + + self.set_y(30) + self.set_font(family="arial", style="B", size=14) + self.cell(0, 10, _("Service public régional de Bruxelles"), new_y="NEXT", new_x="LMARGIN", align="C") + self.cell(0, 10, _("Cahier des charges ") + order.contract.contract_number, new_y="NEXT", new_x="LMARGIN", align="C") + + self.set_font(family="arial", style="", size=12) + today_str = datetime.today().strftime('%d/%m/%Y') + self.set_y(55) + self.cell(0, 10, _("Bruxelles: ") + today_str, new_y="NEXT", new_x="LMARGIN", align="L") + self.set_font(family="arial", style="BU", size=14) + self.ln(10) + self.cell(0, 10, _("DECLARATION DE CREANCE"), align="C", new_y="NEXT", new_x="LMARGIN") + + self.set_font(family="arial", style="", size=12) + self.ln(10) + self.cell(25, 7, _("Concerne:")) + self.set_font(family="arial", style="B", size=12) + self.multi_cell(160, 7, order.contract.contract_number + " - " + order.order_code + " - " + order.description, align="L", new_y="NEXT", new_x="LMARGIN") + + self.set_font(family="arial", style="", size=12) + self.ln(10) + # Récupérer le nom et rôle du signataire depuis le contrat + signatory_name = order.contract.signatory_name or "..." + signatory_role = order.contract.signatory_role or "..." + + content = _( + "Je soussigné %(signatory)s, %(role)s, de la firme %(company)s, " + "déclare que le Service public régional de Bruxelles – Place Saint-Lazare 2 à 1035 Bruxelles, " + "est redevable de la somme de :" + ) % { + "signatory": signatory_name, + "role": signatory_role, + "company": self.company.name, + } + self.multi_cell(0, 7, content, align="L", new_y="LAST", new_x="LMARGIN") + self.ln(10) + + # Calculer les totaux ventilés (inclut les périodes validées) + totals = calculate_order_totals(order, interventions_filter={'status': 'validated'}, include_validated_periods=True) + + # Afficher le détail des totaux + self.set_font(family="arial", style="", size=10) + line_height = 6 + + + # Total général HTVA + self.cell(130, line_height, _("Total général TVA non comprise :")) + self.cell(50, line_height, f"{totals['total_general_htva']:,.2f} €".replace(",", " ").replace(".", ","), align="R", new_y="NEXT", new_x="LMARGIN") + + # TVA + tva_amount = totals['total_general_htva'] * Decimal('0.21') + self.cell(130, line_height, _("TVA 21 % :")) + self.cell(50, line_height, f"{tva_amount:,.2f} €".replace(",", " ").replace(".", ","), align="R", new_y="NEXT", new_x="LMARGIN") + + # Total TVAC + total_tvac = totals['total_general_htva'] + tva_amount + self.ln(3) + self.set_font(family="arial", style="B", size=12) + amount = float(total_tvac) + formatted_amount = f"{amount:,.2f}".replace(",", " ").replace(".", ",") + self.cell(0, 10, f"{formatted_amount} € TVAC", align="R", new_y="NEXT", new_x="LMARGIN") + + self.set_font(family="arial", style="", size=12) + euros = int(amount) + centimes = int(round((amount - euros) * 100)) + + lang = (get_language() or "fr")[:2] + num_lang = "nl" if lang == "nl" else "fr" + + # Parties en toutes lettres + euros_words = num2words(euros, lang=num_lang) + euro_label = ngettext("euro", "euros", euros) + + if centimes > 0: + cent_words = num2words(centimes, lang=num_lang) + cent_label = ngettext("centime", "centimes", centimes) + conj = _("et") + amount_words = f"{euros_words} {euro_label} {conj} {cent_words} {cent_label}" + else: + amount_words = f"{euros_words} {euro_label}" + + self.multi_cell(0, 7, _("Montant total, TVA incluse : %(amount)s") % {"amount": amount_words.capitalize()}, align="L") + + + +def generate_claim_declaration_pdf(order, output_path, *, show_draft: bool = False): + company = order.contract.company + pdf = ClaimDeclarationPDF(company=company, show_draft=show_draft) + pdf.add_page() + pdf.add_order_data(order) + pdf.output(output_path) + + +def preview_claim_declaration_pdf(request, order_id): + order = get_object_or_404(ContractOrder, id=order_id) + + buffer = io.BytesIO() + generate_claim_declaration_pdf(order, buffer, show_draft=True) + buffer.seek(0) + + filename = _("Declaration_Creance_%(slug_contract_number)s_%(slug_order_code)s.pdf") % { + "slug_contract_number": slugify(order.contract.contract_number), "slug_order_code": slugify(order.order_code) + } + + response = HttpResponse(buffer.read(), content_type="application/pdf") + response['Content-Disposition'] = f'inline; filename="{filename}"' + + return response + + +def create_claim_declaration_document(order_id): + """ + Génère la Déclaration de créance en PDF, la sauvegarde au stockage, + et crée un ContractOrderDocument lié à la commande. + Retourne le document créé. + """ + order = get_object_or_404(ContractOrder, id=order_id) + + # 1) Génération PDF en mémoire + buffer = io.BytesIO() + generate_claim_declaration_pdf(order, buffer) # génère sans 'Draft' + buffer.seek(0) + + # 2) Métadonnées document + today = timezone.localdate() + date_token = today.strftime("%Y%m%d") # pour le nom de fichier + + # Préfixe traduisible ET sûr pour un fichier (slugifié) + fname_prefix = slugify(_("Declaration_Creance")) + filename = f"{fname_prefix}_{slugify(order.contract.contract_number)}_{slugify(order.order_code)}_{date_token}.pdf" + + # Titre lisible avec date localisée + title = "%(decl)s - %(code)s - %(date)s" % { + "decl": _("Déclaration de créance"), + "code": order.order_code, + "date": date_format(timezone.now(), "d/m/Y"), # ou "DATE_FORMAT" si tu veux le format local + } + + # 3) Création ContractOrderDocument + sauvegarde du fichier + doc = ContractOrderDocument( + order=order, + title=title, + document_type="claim_declaration", + ) + doc.document.save(filename, ContentFile(buffer.getvalue())) + doc.save() + + return doc + + +### Etat de travaux ### + +class StateOfWorksPDF(FPDF): + + def __init__(self, company: Company, show_draft: bool = False, *args, **kwargs): + super().__init__(*args, **kwargs) + self.company = company # Stocker l’instance de Company + self.show_draft = show_draft + self.set_margins(left=10, top=20, right=10) # marges en mm + self.set_auto_page_break(auto=True, margin=20) + + def header(self): + # Fonts + self.add_font("arial", style="", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial.ttf")) + self.add_font("arial", style="B", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial_bold.ttf")) + self.add_font("arial", style="I", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial_italic.ttf")) + + # Logo de la société (s'il existe) + if self.company.logo: + logo_path = os.path.join(settings.MEDIA_ROOT, self.company.logo.name) + if os.path.exists(logo_path): + self.image(logo_path, x=10, y=8, w=40) + + if self.show_draft: + _draw_draft_ribbon(self, text="DRAFT") + + + def add_first_page(self, order: ContractOrder): + brussels = pytz.timezone("Europe/Brussels") + + self.set_y(30) + self.set_font(family="arial", style="B", size=14) + self.cell(0, 10, _("Service public régional de Bruxelles"), new_y="NEXT", new_x="LMARGIN", align="C") + self.cell(0, 10, _("Cahier des charges ") + order.contract.contract_number, new_y="NEXT", new_x="LMARGIN", align="C") + + self.set_font(family="arial", style="", size=12) + today_str = datetime.today().strftime('%d/%m/%Y') + self.set_y(55) + self.cell(0, 10, _("Bruxelles: ") + today_str, new_y="NEXT", new_x="LMARGIN", align="L") + self.set_font(family="arial", style="BU", size=14) + self.ln(10) + self.cell(0, 10, _("ETAT DE TRAVAUX"), align="C", new_y="NEXT", new_x="LMARGIN") + self.set_font(family="arial", style="B", size=12) + self.ln(10) + self.multi_cell(0, 7, order.contract.contract_number + " - " + order.order_code + " - " + order.description, align="C", new_y="NEXT", new_x="LMARGIN") + + + def add_state_of_works_data(self, order: ContractOrder, included_periods=None): + if included_periods is None: + included_periods = {} + + total_general_htva = 0 # Accumulateur du total global + + # Logique d'inclusion des interventions : + # 1. Toutes les interventions avec status='validated' (même sans postes validés) + # 2. Toutes les interventions avec des périodes validées (quel que soit leur statut) + from interventions.models import InterventionContractPost, InterventionPostPeriodQuantity + + # Toutes les interventions validées + validated_interventions = order.interventions.filter(status='validated') + + # Toutes les interventions avec périodes validées (quel que soit leur statut) + intervention_ids_with_validated_periods = InterventionPostPeriodQuantity.objects.filter( + intervention__order=order, + status='validated' + ).values_list('intervention_id', flat=True).distinct() + + interventions_with_periods = order.interventions.filter(id__in=intervention_ids_with_validated_periods) + + # Combiner les deux querysets et éliminer les doublons + interventions = (validated_interventions | interventions_with_periods).distinct().order_by('begin_time') + + for intervention in interventions: + # Calculer la hauteur du bloc en fonction des postes validés et des périodes validées + validated_posts_count = intervention.interv_contract_posts.filter(control_status='validated').count() + + periods_posts_count = 0 + if intervention.id in included_periods: + # Compter le nombre de postes distincts dans les périodes validées + from interventions.models import InterventionPostPeriodQuantity + periods_posts_count = InterventionPostPeriodQuantity.objects.filter( + intervention=intervention, + status='validated' + ).values('contract_post_id').distinct().count() + + # Total des lignes de postes à afficher + total_posts_lines = validated_posts_count + periods_posts_count + + # Si aucun poste ni période validée, prévoir une ligne pour le message + if total_posts_lines == 0 and intervention.status == 'validated': + total_posts_lines = 1 + + bloc_height = 40 + 7 * total_posts_lines + + if self.will_page_break(bloc_height): + self.add_page() + + self.ln(5) + + # En-tête de l’intervention + bt = intervention.begin_time + if bt: + local_dt = timezone.localtime(bt) + # Choisis un format : "d/m/Y H:i" ou un format local Django comme "SHORT_DATETIME_FORMAT" + formatted_dt = date_format(local_dt, "d/m/Y H:i") + else: + formatted_dt = "-" + + # Construire le texte du titre avec les périodes si elles existent + title_interv_text = _("Intervention %(code)s du %(date)s") % { + "code": intervention.code, + "date": formatted_dt, + } + + # Ajouter les périodes si elles existent pour cette intervention + periods_for_intervention = included_periods.get(intervention.id, set()) + if periods_for_intervention: + # Trier les périodes et les formatter comme P1, P2, P3... + sorted_periods = sorted(periods_for_intervention) + periods_str = ", ".join([f"P{p}" for p in sorted_periods]) + title_interv_text += f" ({_('périodes')} {periods_str})" + + self.set_font("arial", style="B", size=12) + self.cell(0, 8, title_interv_text, ln=True) + self.set_font("arial", style="", size=12) + self.multi_cell(0, 7, intervention.title or "-", align="L") + self.ln(2) + + # Table des postes + self.set_font("arial", style="", size=10) + self.cell(25, 7, _("Code poste")) + self.cell(85, 7, _("Description")) + self.cell(25, 7, _("PU"), align="R") + self.cell(20, 7, _("Qté"), align="R") + self.cell(25, 7, _("Total"), ln=True, align="R") + + # Ligne en pointillés sous les en-têtes + current_y = self.get_y() + self.set_draw_color(200, 200, 200) # gris clair + self.set_dash_pattern(1, 1) # 1 mm trait / 1 mm espace + self.line(10, current_y, 200, current_y) + self.set_dash_pattern() # reset (ligne pleine ensuite) + + self.set_font("arial", style="", size=8) + + # Afficher les postes validés de l'intervention + validated_posts = intervention.interv_contract_posts.filter(control_status='validated') + total_htva = Decimal('0.00') + has_content = False + + # Prioriser l'affichage des périodes validées si elles existent + if intervention.id in included_periods: + # Cas 1: L'intervention a des périodes validées - afficher le cumul des périodes + from interventions.models import InterventionPostPeriodQuantity, InterventionContractPost + + period_quantities = InterventionPostPeriodQuantity.objects.filter( + intervention=intervention, + status='validated' + ).select_related('contract_post') + + if period_quantities.exists(): + has_content = True + # Grouper par poste pour additionner les quantités de toutes les périodes + from collections import defaultdict + post_totals = defaultdict(lambda: {'quantity': Decimal('0.00'), 'contract_post': None}) + + for pq in period_quantities: + post_totals[pq.contract_post_id]['quantity'] += Decimal(str(pq.quantity or 0)) + post_totals[pq.contract_post_id]['contract_post'] = pq.contract_post + + # Afficher les postes groupés + for post_id, data in post_totals.items(): + contract_post = data['contract_post'] + quantity = data['quantity'] + unit_price = contract_post.unit_price or Decimal('0.00') + + # Vérifier si le poste est un coefficient multiplicateur + if contract_post.is_coefficient_multipliable and contract_post.post_type: + # Pour les postes avec coefficient, la quantité doit être 1 (sauf si 0 ou vide) + effective_qty = Decimal('1') if quantity > 0 else Decimal('0') + + total_same_type_cost = 0 + # Ajouter les périodes validées du même type (exclure les coefficients) + same_type_periods = InterventionPostPeriodQuantity.objects.filter( + intervention=intervention, + contract_post__post_type=contract_post.post_type, + status='validated' + ).exclude( + contract_post__is_coefficient_multipliable=True + ).select_related('contract_post') + + # Grouper les périodes par poste et sommer + period_totals = defaultdict(Decimal) + for period in same_type_periods: + period_totals[period.contract_post_id] += Decimal(str(period.quantity or 0)) + + # Ajouter le coût des périodes + for period_post_id, period_qty in period_totals.items(): + # Trouver le contract_post correspondant + period_cp = next((p.contract_post for p in same_type_periods if p.contract_post_id == period_post_id), None) + if period_cp: + total_same_type_cost += period_qty * (period_cp.unit_price or Decimal('0')) + + # Total = effective_qty × prix unitaire (coefficient) × somme des coûts + post_total = effective_qty * unit_price * total_same_type_cost + else: + # Calcul standard + post_total = quantity * unit_price + if contract_post.is_amount_to_be_justified and contract_post.coeff_amount_to_be_justified: + post_total = post_total * contract_post.coeff_amount_to_be_justified + + self.cell(25, 7, contract_post.code) + self.cell(85, 7, contract_post.get_description_for_language()[:50] + ("..." if len(contract_post.get_description_for_language()) > 40 else "")) + self.cell(25, 7, f"{unit_price:,.2f} €".replace(",", " ").replace(".", ","), align="R") + self.cell(20, 7, f"{quantity:.2f}", align="R") + self.cell(25, 7, f"{post_total:,.2f} €".replace(",", " ").replace(".", ","), ln=True, align="R") + total_htva += post_total + elif validated_posts.exists(): + # Cas 2: L'intervention a des postes validés (sans périodes validées) + has_content = True + for interv_post in validated_posts: + self.cell(25, 7, interv_post.contract_post.code) + self.cell(85, 7, interv_post.contract_post.get_description_for_language()[:50] + ("..." if len(interv_post.contract_post.get_description_for_language()) > 40 else "")) + self.cell(25, 7, f"{interv_post.contract_post.unit_price:,.2f} €".replace(",", " ").replace(".", ","), align="R") + self.cell(20, 7, f"{interv_post.quantity:.2f}" if interv_post.quantity else "0", align="R") + # Utiliser total_price qui gère automatiquement les coefficients multipliables + self.cell(25, 7, f"{interv_post.total_price:,.2f} €".replace(",", " ").replace(".", ","), ln=True, align="R") + total_htva += interv_post.total_price + + # Si l'intervention est validée mais n'a ni postes ni périodes validées, afficher un message + if not has_content and intervention.status == 'validated': + self.set_font("arial", style="I", size=8) + self.cell(0, 7, _("Aucun poste validé pour cette intervention"), ln=True) + self.set_font("arial", style="", size=8) + + # Total de l'intervention + total_general_htva += total_htva + self.set_font("arial", style="B", size=10) + self.cell(155, 7, _("Total HTVA :"), border=0, align="R") + self.cell(25, 7, f"{total_htva:,.2f} €".replace(",", " ").replace(".", ","), border=0, align="R", new_x="LMARGIN", new_y="NEXT") + + # Ligne horizontale de séparation + self.ln(5) + current_y = self.get_y() + self.set_draw_color(100, 100, 100) + self.line(x1=10, y1=current_y, x2=200, y2=current_y) + + + # Section Commandes directes + from contracts.models import DirectOrderPost + direct_posts_qs = list( + order.direct_posts + .filter(control_status='validated') + .select_related('contract_post') + .order_by('contract_post__order_number') + ) + if direct_posts_qs: + direct_posts_total = Decimal('0.00') + bloc_height = 40 + 7 * len(direct_posts_qs) + if self.will_page_break(bloc_height): + self.add_page() + + self.ln(5) + self.set_font("arial", style="B", size=12) + self.cell(0, 8, _("Commandes directes"), ln=True) + self.ln(2) + + self.set_font("arial", style="", size=10) + self.cell(25, 7, _("Code poste")) + self.cell(85, 7, _("Description")) + self.cell(25, 7, _("PU"), align="R") + self.cell(20, 7, _("Qté"), align="R") + self.cell(25, 7, _("Total"), ln=True, align="R") + + current_y = self.get_y() + self.set_draw_color(200, 200, 200) + self.set_dash_pattern(1, 1) + self.line(10, current_y, 200, current_y) + self.set_dash_pattern() + + self.set_font("arial", style="", size=8) + for dp in direct_posts_qs: + cp = dp.contract_post + unit_price = cp.unit_price or Decimal('0.00') + post_total = dp.total_price + desc = cp.get_description_for_language() + self.cell(25, 7, cp.code) + self.cell(85, 7, desc[:50] + ("..." if len(desc) > 50 else "")) + self.cell(25, 7, f"{unit_price:,.2f} €".replace(",", " ").replace(".", ","), align="R") + self.cell(20, 7, f"{dp.quantity:.2f}", align="R") + self.cell(25, 7, f"{post_total:,.2f} €".replace(",", " ").replace(".", ","), ln=True, align="R") + direct_posts_total += post_total + + self.set_font("arial", style="B", size=10) + self.cell(155, 7, _("Total HTVA :"), border=0, align="R") + self.cell(25, 7, f"{direct_posts_total:,.2f} €".replace(",", " ").replace(".", ","), border=0, align="R", new_x="LMARGIN", new_y="NEXT") + + self.ln(5) + current_y = self.get_y() + self.set_draw_color(100, 100, 100) + self.line(x1=10, y1=current_y, x2=200, y2=current_y) + + total_general_htva += direct_posts_total + + # Total général HTVA + TVAC à la fin + if self.will_page_break(40): + self.add_page() + + # Utiliser le total_general_htva accumulé depuis les interventions affichées + tva_amount = total_general_htva * Decimal('0.21') + total_tvac = total_general_htva + tva_amount + + from fpdf.enums import XPos, YPos + + self.ln(5) + self.set_font("arial", style="B", size=10) + + # Coordonnées de départ + start_x = 10 + start_y = self.get_y() + box_width = 190 + line_height = 7 + + # Calculer la hauteur nécessaire pour le rectangle (seulement 3 lignes simples) + num_lines = 3 # Total HTVA, TVA, TVAC + + # Dessin du fond gris clair + self.set_fill_color(240, 240, 240) + self.rect(x=start_x, y=start_y, w=box_width, h=line_height * num_lines + 5, style='F') + + label_x = 15 + value_x = 160 + value_width = 35 + + self.set_y(start_y + 2) + self.set_font("arial", style="B", size=10) + + # Total général HTVA + self.set_x(label_x) + self.cell(value_x - label_x, line_height, _("Total général TVA non comprise :"), border=0) + self.cell(value_width, line_height, f"{total_general_htva:,.2f} €".replace(",", " ").replace(".", ","), border=0, align="R", new_y="NEXT", new_x="LMARGIN") + + # TVA 21% + self.set_x(label_x) + self.cell(value_x - label_x, line_height, _("TVA 21 % :"), border=0) + self.cell(value_width, line_height, f"{tva_amount:,.2f} €".replace(",", " ").replace(".", ","), border=0, align="R", new_y="NEXT", new_x="LMARGIN") + + # TOTAL TVAC + self.set_x(label_x) + self.cell(value_x - label_x, line_height, _("Total général TVAC :"), border=0) + self.cell(value_width, line_height, f"{total_tvac:,.2f} €".replace(",", " ").replace(".", ","), border=0, align="R", new_y="NEXT", new_x="LMARGIN") + self.set_text_color(0, 0, 0) # Reset couleur + + + +def generate_state_of_works_pdf(order, output_path, *, show_draft: bool = False): + company = order.contract.company + + # Calculer les totaux pour obtenir included_periods + totals = calculate_order_totals(order, interventions_filter={'status': 'validated'}, include_validated_periods=True) + included_periods = totals.get('included_periods', {}) + + pdf = StateOfWorksPDF(company=company, show_draft=show_draft) + pdf.add_page() + pdf.add_first_page(order) + pdf.add_page() + pdf.add_state_of_works_data(order, included_periods) + pdf.output(output_path) + + + +def preview_state_of_works_pdf(request, order_id): + order = get_object_or_404(ContractOrder, id=order_id) + + buffer = io.BytesIO() + generate_state_of_works_pdf(order, buffer, show_draft=True) + buffer.seek(0) + + fname_prefix = slugify(_("Declaration_Creance")) + filename = f"{fname_prefix}_{slugify(order.contract.contract_number)}_{slugify(order.order_code)}.pdf" + + response = HttpResponse(buffer.read(), content_type="application/pdf") + response['Content-Disposition'] = f'inline; filename="{filename}"' + + return response + + +def create_state_of_works_document(order_id): + """ + Génère l'État de travaux en PDF, la sauvegarde au stockage, + et crée un ContractOrderDocument lié à la commande. + """ + order = get_object_or_404(ContractOrder, id=order_id) + + # 1) Génération PDF en mémoire + buffer = io.BytesIO() + generate_state_of_works_pdf(order, buffer) # génère sans 'Draft' + buffer.seek(0) + + # 2) Métadonnées document + today = timezone.localdate() + date_token = today.strftime("%Y%m%d") # pour le nom de fichier + + # Préfixe traduisible ET sûr pour un fichier (slugifié) + fname_prefix = slugify(_("Etat_de_travaux")) + filename = f"{fname_prefix}_{slugify(order.contract.contract_number)}_{slugify(order.order_code)}_{date_token}.pdf" + + title_prefix = _("État de travaux") + title = f"{title_prefix} - {order.order_code} - {datetime.now().strftime('%d/%m/%Y')}" + + # 3) Création ContractOrderDocument + sauvegarde du fichier + doc = ContractOrderDocument( + order=order, + title=title, + document_type="state_of_works", + ) + doc.document.save(filename, ContentFile(buffer.getvalue())) + doc.save() + + return doc + + + +### Devis ### + +class OrderQuotePDF(FPDF): + + def __init__(self, company: Company, show_draft: bool = False, *args, **kwargs): + super().__init__(*args, **kwargs) + self.company = company + self.show_draft = show_draft + self.set_margins(left=15, top=20, right=15) + self.set_auto_page_break(auto=True, margin=20) + + def header(self): + self.add_font("arial", style="", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial.ttf")) + self.add_font("arial", style="B", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial_bold.ttf")) + self.add_font("arial", style="I", fname=os.path.join(settings.BASE_DIR, "common", "static", "common", "arial_italic.ttf")) + + if self.company.logo: + logo_path = os.path.join(settings.MEDIA_ROOT, self.company.logo.name) + if os.path.exists(logo_path): + self.image(logo_path, x=10, y=8, w=40) + + if self.show_draft: + _draw_draft_ribbon(self, text="DRAFT") + + def footer(self): + # Numéro de page centré en bas + self.set_y(-12) + self.set_font("arial", style="I", size=8) + self.set_text_color(120, 120, 120) + self.cell(0, 8, f"Page {self.page_no()}", align="C") + + def add_quote(self, order: ContractOrder, quote): + """Render the quote as a table with columns: + Poste (order number), Code, Description, Unité, PU, Qté, Total. + """ + lang = (get_language() or "fr")[:2] + + def format_money(val: Decimal) -> str: + return f"{val:,.2f} €".replace(",", " ").replace(".", ",") + + self.set_y(35) + self.set_font("arial", style="B", size=16) + self.cell(0, 10, _("Devis de la commande %(order)s") % {"order": order.order_code}, align="C", new_x="LMARGIN", new_y="NEXT") + + self.set_font("arial", style="", size=11) + contract_line = _("Contrat %(contract)s – Commande %(order)s") % { + "contract": order.contract.contract_number, + "order": order.order_code, + } + self.cell(0, 7, contract_line, new_x="LMARGIN", new_y="NEXT") + if order.description: + self.multi_cell(0, 6, order.description, align="L") + self.ln(3) + + self.set_font("arial", style="B", size=12) + self.cell(0, 7, _("Postes du devis"), new_x="LMARGIN", new_y="NEXT") + self.ln(1) + + # Table header + # Define column widths (landscape A4: effective width ~ 297 - margins (15*2) = 267 mm) + col_w = { + "order_number": 18, + "code": 28, + "description": 110, + "unit": 16, + "unit_price": 30, + "quantity": 30, + "total": 35, + } + header_h = 8 + self.set_font("arial", style="B", size=9) + self.set_fill_color(240, 240, 240) + self.cell(col_w["order_number"], header_h, _("N° poste"), border=1, align="C", fill=True) + self.cell(col_w["code"], header_h, _("Code"), border=1, align="C", fill=True) + self.cell(col_w["description"], header_h, _("Description"), border=1, align="C", fill=True) + self.cell(col_w["unit"], header_h, _("Unité"), border=1, align="C", fill=True) + self.cell(col_w["unit_price"], header_h, _("PU"), border=1, align="C", fill=True) + self.cell(col_w["quantity"], header_h, _("Quantité"), border=1, align="C", fill=True) + self.cell(col_w["total"], header_h, _("Total"), border=1, align="C", fill=True, new_x="LMARGIN", new_y="NEXT") + + self.set_font("arial", style="", size=8) + total_amount = Decimal("0") + + # Alternating row colors + row_fill_colors = [(255,255,255), (245,245,245)] + row_idx = 0 + + for item in quote.items.select_related("contract_post").order_by("contract_post__order_number"): + post = item.contract_post + description = post.description_fr or "" + if lang == "nl": + description = post.description_nl or post.description_fr or "" + quantity = Decimal(item.quantity or 0) + unit_price = Decimal(post.unit_price or 0) + line_total = (unit_price * quantity).quantize(Decimal("0.01")) + total_amount += line_total + + desc_text = description.strip() if description else "-" + self.set_font("arial", style="", size=8) + fill_rgb = row_fill_colors[row_idx % 2] + self.set_fill_color(*fill_rgb) + + # Save current position for the row + x_row = self.get_x() + y_row = self.get_y() + + # Calculate description height using word wrap + def wrap_text_fpdf(pdf, text, col_width): + words = text.split() + lines = [] + current = '' + for word in words: + test_line = (current + ' ' + word) if current else word + if pdf.get_string_width(test_line) < col_width - 2: + current = test_line + else: + if current: + lines.append(current) + current = word + if current: + lines.append(current) + return lines + + desc_lines = wrap_text_fpdf(self, desc_text, col_w["description"]) + line_height = 4 + padding_bottom = 2 + desc_height = max(len(desc_lines) * line_height + padding_bottom, 8) + + # Draw order_number and code + self.cell(col_w["order_number"], desc_height, str(post.order_number), border=1, align="C", fill=True) + self.cell(col_w["code"], desc_height, post.code or "", border=1, align="L", fill=True) + + # Draw description cell (single cell, text manually word-wrapped) + x_desc = self.get_x() + y_desc = self.get_y() + self.set_xy(x_desc, y_desc) + self.rect(x_desc, y_desc, col_w["description"], desc_height, style='DF') + for i, line in enumerate(desc_lines): + self.set_xy(x_desc + 1, y_desc + 1 + i * line_height) + self.cell(col_w["description"] - 2, line_height, line, border=0, align="L") + # Add bottom padding + self.set_xy(x_desc + col_w["description"], y_desc) + + # Draw the rest of the cells with the same height + self.cell(col_w["unit"], desc_height, post.unit or "", border=1, align="C", fill=True) + self.cell(col_w["unit_price"], desc_height, format_money(unit_price), border=1, align="R", fill=True) + self.cell(col_w["quantity"], desc_height, f"{quantity:.2f}" if quantity else "0.00", border=1, align="R", fill=True) + self.cell(col_w["total"], desc_height, format_money(line_total), border=1, align="R", fill=True, new_x="LMARGIN", new_y="NEXT") + + row_idx += 1 + + # Summary total, TVA, TVAC (aligned right under PU, Quantité, Total) + self.ln(4) + self.set_font("arial", style="B", size=10) + tva = (total_amount * Decimal("0.21")).quantize(Decimal("0.01")) + total_tvac = (total_amount + tva).quantize(Decimal("0.01")) + # Position: start at right edge of PU column + x_totals = self.l_margin + col_w["order_number"] + col_w["code"] + col_w["description"] + col_w["unit"] + self.set_x(x_totals) + self.cell(col_w["unit_price"], 6, "", border=0) + self.cell(col_w["quantity"], 6, "", border=0) + self.cell(col_w["total"], 6, _("Total HTVA : %(amount)s") % {"amount": format_money(total_amount)}, border=0, align="R", new_x="LMARGIN", new_y="NEXT") + self.set_x(x_totals) + self.cell(col_w["unit_price"], 6, "", border=0) + self.cell(col_w["quantity"], 6, "", border=0) + self.cell(col_w["total"], 6, _("TVA 21%% : %(amount)s") % {"amount": format_money(tva)}, border=0, align="R", new_x="LMARGIN", new_y="NEXT") + self.set_x(x_totals) + self.cell(col_w["unit_price"], 6, "", border=0) + self.cell(col_w["quantity"], 6, "", border=0) + self.cell(col_w["total"], 6, _("Total TVAC : %(amount)s") % {"amount": format_money(total_tvac)}, border=0, align="R", new_x="LMARGIN", new_y="NEXT") + + +def generate_order_quote_pdf(order: ContractOrder, output_path, *, show_draft: bool = False): + quote = getattr(order, "quote", None) + if not quote or not quote.items.exists(): + raise ValueError("No quote available for this order") + # Landscape orientation for better table width + pdf = OrderQuotePDF(company=order.contract.company, show_draft=show_draft, orientation='L') + pdf.add_page() + pdf.add_quote(order, quote) + pdf.output(output_path) + + +def preview_order_quote_pdf(request, order_id): + order = get_object_or_404(ContractOrder, id=order_id) + quote = getattr(order, "quote", None) + if not quote or not quote.items.exists(): + raise Http404(_("Aucun devis n'est disponible pour cette commande.")) + + buffer = io.BytesIO() + generate_order_quote_pdf(order, buffer, show_draft=True) + buffer.seek(0) + + filename = _("Devis_%(contract)s_%(order)s.pdf") % { + "contract": slugify(order.contract.contract_number), + "order": slugify(order.order_code), + } + + response = HttpResponse(buffer.read(), content_type="application/pdf") + response['Content-Disposition'] = f'inline; filename="{filename}"' + return response + + +def generate_posts_excel(order, incremental_map, output_path): + """ + Génère un fichier Excel contenant tous les postes du contrat avec leurs quantités incrémentales. + Inclut tous les postes, même ceux avec quantité nulle. + """ + lang = (get_language() or "fr")[:2] + + wb = Workbook() + ws = wb.active + ws.title = _("Postes") + + # Styles + header_font = Font(bold=True, color="FFFFFF") + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + header_alignment = Alignment(horizontal="center", vertical="center") + metadata_font = Font(bold=True, size=11) + border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin') + ) + # Couleurs alternées pour les lignes + light_fill = PatternFill(start_color="FFFFFF", end_color="FFFFFF", fill_type="solid") + gray_fill = PatternFill(start_color="F2F2F2", end_color="F2F2F2", fill_type="solid") + + # Métadonnées en en-tête + row_num = 1 + ws.cell(row=row_num, column=1).value = _("Marché :") + ws.cell(row=row_num, column=1).font = metadata_font + ws.cell(row=row_num, column=2).value = order.contract.contract_number + ws.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=7) + row_num += 1 + + ws.cell(row=row_num, column=1).value = _("Description :") + ws.cell(row=row_num, column=1).font = metadata_font + ws.cell(row=row_num, column=2).value = order.contract.description or "" + ws.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=7) + row_num += 1 + + ws.cell(row=row_num, column=1).value = _("Commande :") + ws.cell(row=row_num, column=1).font = metadata_font + ws.cell(row=row_num, column=2).value = order.order_code + ws.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=7) + row_num += 1 + + ws.cell(row=row_num, column=1).value = _("Description :") + ws.cell(row=row_num, column=1).font = metadata_font + ws.cell(row=row_num, column=2).value = order.description or "" + ws.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=7) + row_num += 1 + + today = timezone.localdate() + ws.cell(row=row_num, column=1).value = _("Date :") + ws.cell(row=row_num, column=1).font = metadata_font + ws.cell(row=row_num, column=2).value = date_format(today, "d/m/Y") + ws.merge_cells(start_row=row_num, start_column=2, end_row=row_num, end_column=7) + row_num += 2 # Ligne vide avant les en-têtes + + # En-têtes de colonnes + header_row = row_num + headers = [ + _("N° poste"), + _("Code"), + _("Description"), + _("Unité"), + _("Prix unitaire"), + _("Quantité"), + _("Montant"), + _("SAJ"), + ] + + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=header_row, column=col_num) + cell.value = header + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = border + + # Largeurs de colonnes + ws.column_dimensions['A'].width = 12 # N° poste + ws.column_dimensions['B'].width = 15 # Code + ws.column_dimensions['C'].width = 50 # Description + ws.column_dimensions['D'].width = 10 # Unité + ws.column_dimensions['E'].width = 14 # Prix unitaire + ws.column_dimensions['F'].width = 10 # Quantité + ws.column_dimensions['G'].width = 14 # Montant + ws.column_dimensions['H'].width = 6 # SAJ + + # Récupérer tous les postes du contrat + contract_posts = order.contract.posts.all().order_by('order_number') + + # Utiliser calculate_order_totals pour obtenir les totaux des postes coefficient (inclut les périodes validées) + totals = calculate_order_totals(order, interventions_filter={'status': 'validated'}, include_validated_periods=True) + + row_num = header_row + 1 + line_idx = 0 # Pour l'alternance des couleurs + + # D'abord, identifier les types de postes qui ont des coefficients + coefficient_post_types = set() + for post in contract_posts: + if post.is_coefficient_multipliable and post.post_type: + coefficient_post_types.add(post.post_type) + + for post in contract_posts: + # Récupérer la quantité incrémentale pour ce poste + incremental_qty = Decimal(incremental_map.get(post.id, 0)) + + # Description selon la langue + description = post.description_fr or "" + if lang == "nl": + description = post.description_nl or post.description_fr or "" + + # Calcul du montant de base + unit_price = Decimal(post.unit_price or 0) + base_amount = unit_price * incremental_qty + # Pour les postes SAJ, appliquer le coefficient coeff_amount_to_be_justified + if post.is_amount_to_be_justified and post.coeff_amount_to_be_justified: + actual_amount = base_amount * post.coeff_amount_to_be_justified + else: + actual_amount = base_amount + + # Marqueur SAJ + is_saj = post.is_amount_to_be_justified + is_coefficient_post = post.is_coefficient_multipliable + + # Couleur alternée + row_fill = gray_fill if line_idx % 2 == 1 else light_fill + + # Remplir la ligne + ws.cell(row=row_num, column=1).value = post.order_number + ws.cell(row=row_num, column=2).value = post.code + ws.cell(row=row_num, column=3).value = description + ws.cell(row=row_num, column=4).value = post.unit or "" + ws.cell(row=row_num, column=5).value = float(unit_price) + ws.cell(row=row_num, column=5).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=6).value = float(incremental_qty) + ws.cell(row=row_num, column=6).number_format = '0.00' + + # Pour les postes coefficient, afficher le supplément calculé + if is_coefficient_post: + coef_total = totals['coefficient_posts_totals'].get(post.code, {}).get('total', Decimal('0')) + ws.cell(row=row_num, column=7).value = float(coef_total) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + else: + ws.cell(row=row_num, column=7).value = float(actual_amount) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=8).value = "Oui" if is_saj else ("Coef." if is_coefficient_post else "") + + # Appliquer les bordures et la couleur de fond + for col in range(1, 9): + ws.cell(row=row_num, column=col).border = border + ws.cell(row=row_num, column=col).alignment = Alignment(vertical="center") + ws.cell(row=row_num, column=col).fill = row_fill + + row_num += 1 + line_idx += 1 + + # Section des totaux ventilés + row_num += 2 + bold_font = Font(bold=True) + + # Total revisable de base + ws.cell(row=row_num, column=5).value = _("Total général des fournitures et travaux revisables hors TVA :") + ws.cell(row=row_num, column=5).font = bold_font + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(totals['total_revisable']) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=7).font = bold_font + row_num += 1 + + # Coefficients de révision (suppléments) + if totals['coefficient_posts_totals']: + ws.cell(row=row_num, column=5).value = _("Coefficient de révision :") + ws.cell(row=row_num, column=5).font = bold_font + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + row_num += 1 + for code, coef_info in totals['coefficient_posts_totals'].items(): + ws.cell(row=row_num, column=5).value = f" {coef_info['description']} :" + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(coef_info['total']) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + row_num += 1 + + # SAJ sans coefficient + if totals['saj_without_coef']: + ws.cell(row=row_num, column=5).value = _("Total général des fournitures et travaux non revisables hors TVA (SAJ) :") + ws.cell(row=row_num, column=5).font = bold_font + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(totals['total_saj_without_coef']) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=7).font = bold_font + row_num += 1 + + # SAJ avec coefficient + for post_type, saj_data in totals['saj_with_coef'].items(): + coef_formatted = f"{saj_data['coefficient']:.2f}".replace(".", ",") + total_with_coef = saj_data['total'] * saj_data['coefficient'] + label = _("Total général des %(type)s non revisables hors TVA (SAJ) avec coefficient %(coef)s :") % { + "type": post_type, + "coef": coef_formatted, + } + ws.cell(row=row_num, column=5).value = label + ws.cell(row=row_num, column=5).font = bold_font + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(total_with_coef) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=7).font = bold_font + row_num += 1 + + # Ligne de séparation + row_num += 1 + + # Total général HTVA + ws.cell(row=row_num, column=5).value = _("Total général TVA non comprise :") + ws.cell(row=row_num, column=5).font = Font(bold=True, size=12) + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(totals['total_general_htva']) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=7).font = Font(bold=True, size=12) + ws.cell(row=row_num, column=7).border = Border(top=Side(style='double')) + row_num += 1 + + # TVA + tva_amount = totals['total_general_htva'] * Decimal('0.21') + ws.cell(row=row_num, column=5).value = _("TVA 21 % :") + ws.cell(row=row_num, column=5).font = Font(bold=True, size=12) + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(tva_amount) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=7).font = Font(bold=True, size=12) + row_num += 1 + + # Total TVAC + total_tvac = totals['total_general_htva'] + tva_amount + ws.cell(row=row_num, column=5).value = _("Total général TVAC :") + ws.cell(row=row_num, column=5).font = Font(bold=True, size=12) + ws.cell(row=row_num, column=5).alignment = Alignment(horizontal="right") + ws.merge_cells(start_row=row_num, start_column=5, end_row=row_num, end_column=6) + ws.cell(row=row_num, column=7).value = float(total_tvac) + ws.cell(row=row_num, column=7).number_format = '#,##0.00 €' + ws.cell(row=row_num, column=7).font = Font(bold=True, size=12) + + # Sauvegarder + wb.save(output_path) + + +def create_posts_excel_document(order_id, incremental_map): + """ + Génère le fichier Excel des postes et crée un ContractOrderDocument lié à la commande. + """ + order = get_object_or_404(ContractOrder, id=order_id) + + # Génération Excel en mémoire + buffer = io.BytesIO() + generate_posts_excel(order, incremental_map, buffer) + buffer.seek(0) + + # Métadonnées document + today = timezone.localdate() + date_token = today.strftime("%Y%m%d") + + fname_prefix = slugify(_("Detail_postes")) + filename = f"{fname_prefix}_{slugify(order.contract.contract_number)}_{slugify(order.order_code)}_{date_token}.xlsx" + + title = "%(detail)s - %(code)s - %(date)s" % { + "detail": _("Détail des postes"), + "code": order.order_code, + "date": date_format(timezone.now(), "d/m/Y"), + } + + # Création ContractOrderDocument + sauvegarde du fichier + doc = ContractOrderDocument( + order=order, + title=title, + document_type="posts_detail", + ) + doc.document.save(filename, ContentFile(buffer.getvalue())) + doc.save() + + return doc + + + + +def generate_documents_for_order(request, order_id): + order = get_object_or_404(ContractOrder, id=order_id) + + if not user_can_generate_order_documents(request.user, order): + messages.error(request, _("Vous n'avez pas la permission de générer des documents pour cette commande.")) + return redirect("contracts:contracts_orders_detail", order_id=order.id) + + from interventions.models import InterventionContractPost, InterventionPostPeriodQuantity + + # Collecter les commandes directes validées AVANT génération des documents (pour les inclure + # dans l'état de travaux/Excel et les réinitialiser seulement après) + validated_direct_posts = list(order.direct_posts.filter(control_status='validated').select_related('contract_post')) + + # Créer le document de déclaration de créance + claim_doc = create_claim_declaration_document(order.id) + + # Créer l'état d'avancement avec les quantités **incrémentales** depuis le dernier état + # Déterminer le prochain index d'état d'avancement pour cette commande + last_state = order.progress_states.order_by('-created_at').first() + next_index = (last_state.index or 0) + 1 if last_state else 1 + + progress_state = ContractOrderProgressState.objects.create( + order=order, + created_by=request.user, + index=next_index, + ) + + # Associer la déclaration de créance et l'état de travaux à cet état d'avancement + claim_doc.progress_state = progress_state + claim_doc.save(update_fields=["progress_state"]) + + # Générer et lier l'état de travaux à ce même état d'avancement + sow_doc = create_state_of_works_document(order.id) + sow_doc.progress_state = progress_state + sow_doc.save(update_fields=["progress_state"]) + + # Récupérer les quantités des postes pour les interventions VALIDÉES de cette déclaration + # et les périodes validées (non facturées) + incremental_map = {} + validated_interventions = order.interventions.filter(status='validated') + + # Postes validés des interventions validées + for intervention in validated_interventions: + for interv_post in intervention.interv_contract_posts.filter(control_status='validated'): + contract_post_id = interv_post.contract_post_id + quantity = interv_post.quantity or 0 + if contract_post_id in incremental_map: + incremental_map[contract_post_id] += quantity + else: + incremental_map[contract_post_id] = quantity + + # Périodes validées (status='validated') de toutes les interventions de la commande + all_intervention_ids = list(order.interventions.values_list('id', flat=True)) + validated_periods = InterventionPostPeriodQuantity.objects.filter( + intervention_id__in=all_intervention_ids, + status='validated' + ) + + for period_qty in validated_periods: + contract_post_id = period_qty.contract_post_id + quantity = period_qty.quantity or 0 + if contract_post_id in incremental_map: + incremental_map[contract_post_id] += quantity + else: + incremental_map[contract_post_id] = quantity + + # Ajouter les commandes directes validées à la carte incrémentale + for dp in validated_direct_posts: + contract_post_id = dp.contract_post_id + quantity = dp.quantity or 0 + if contract_post_id in incremental_map: + incremental_map[contract_post_id] += quantity + else: + incremental_map[contract_post_id] = quantity + + # Générer et lier le fichier Excel des postes à cet état d'avancement + excel_doc = create_posts_excel_document(order.id, incremental_map) + excel_doc.progress_state = progress_state + excel_doc.save(update_fields=["progress_state"]) + + # Créer les items d'état d'avancement uniquement pour le delta de cette déclaration + for contract_post_id, incremental_qty in incremental_map.items(): + ContractOrderProgressStateItem.objects.create( + progress_state=progress_state, + contract_post_id=contract_post_id, + quantity=incremental_qty, + ) + + # Mettre à jour le statut des interventions : + # 1. Toutes les interventions validées (même sans postes validés) + validated_interventions = order.interventions.filter(status='validated') + for intervention in validated_interventions: + intervention.status = 'invoiced' + intervention.save() + + # 2. Toutes les interventions avec périodes validées (quel que soit leur statut) + intervention_ids_with_validated_periods = validated_periods.values_list('intervention_id', flat=True).distinct() + interventions_with_periods = order.interventions.filter(id__in=intervention_ids_with_validated_periods) + for intervention in interventions_with_periods: + # Passer à 'invoiced' seulement si pas déjà dans cet état + if intervention.status != 'invoiced': + intervention.status = 'invoiced' + intervention.save() + + # Marquer les périodes validées comme 'invoiced' + validated_periods.update(status='invoiced') + + # Enregistrer la quantité facturée et réinitialiser les commandes directes validées + for dp in validated_direct_posts: + dp.invoiced_quantity = (dp.invoiced_quantity or Decimal('0')) + dp.quantity + dp.quantity = Decimal('0') + dp.control_status = 'invoiced' + dp.validated_at = None + dp.validated_by = None + dp.save(update_fields=['invoiced_quantity', 'quantity', 'control_status', 'validated_at', 'validated_by']) + + messages.success(request, _("Déclaration de créance créée et sauvegardée.")) + return redirect("contracts:contracts_orders_detail", order_id=order.id) + + + + + +def _safe_set_alpha(pdf, alpha: float): + """Set the transparency (alpha) for the PDF if supported by the FPDF instance.""" + if hasattr(pdf, "set_alpha"): + pdf.set_alpha(alpha) + return True + return False + + +def _draw_draft_ribbon(pdf, text="DRAFT"): + # Sauvegarder état + prev_draw = (pdf.draw_color, pdf.fill_color, pdf.text_color) + prev_font = (pdf.font_family, pdf.font_style, pdf.font_size_pt) + + # Géométrie du ruban (en mm) + ribbon_w = 120 # longueur du bandeau + ribbon_h = 18 # épaisseur du bandeau + margin = 6 # marge par rapport aux bords + + # Point d’ancrage: coin supérieur droit + xr = pdf.w - pdf.r_margin - margin + yr = pdf.t_margin + margin + + # Transparence (si dispo) + had_alpha = _safe_set_alpha(pdf, 0.25) # 25% opaque (75% transparent) + + # Rotation & dessin + try: + # fpdf2: contexte de rotation + with pdf.rotation(-45, x=xr, y=yr): + # Rectangle rouge (ou rose clair si pas d’alpha) + if not had_alpha: + pdf.set_fill_color(255, 200, 200) # fallback 'pseudo-transparence' + else: + pdf.set_fill_color(220, 0, 0) + + pdf.rect(x=xr - ribbon_w/2, y=yr - ribbon_h/2, w=ribbon_w, h=ribbon_h, style='F') + + # Texte + pdf.set_font("arial", style="B", size=16) + pdf.set_text_color(255, 255, 255) # texte blanc pour contraste + pdf.set_xy(xr - ribbon_w/2, yr - ribbon_h/2 + 2) + pdf.cell(w=ribbon_w, h=ribbon_h-4, txt=text, align="C") + finally: + # Reset transparence + if had_alpha: + pdf.set_alpha(1.0) + + # Reset couleurs & police + pdf.set_draw_color(*prev_draw) + pdf.set_fill_color(*prev_draw) # inoffensif si draw=fill couplés par fpdf + pdf.set_text_color(*prev_draw) + pdf.set_font(prev_font[0], style=prev_font[1], size=prev_font[2]) \ No newline at end of file diff --git a/streetup/contracts/views_teams.py b/streetup/contracts/views_teams.py new file mode 100644 index 0000000..11d8087 --- /dev/null +++ b/streetup/contracts/views_teams.py @@ -0,0 +1,371 @@ +import json + +from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, render +from django.db.models import Count, Q +from django.views.decorators.http import require_GET, require_POST + +from contracts.models import CompanyMember, CompanyTeam +from contracts.permissions import ( + get_viewable_teams_for_user, + user_can_edit_specific_team, + user_can_edit_teams, + user_can_view_teams, +) + +User = get_user_model() + + +def _get_user_company(user): + """Retourne la Company associée à l'utilisateur, ou None.""" + try: + return user.config.company + except AttributeError: + return None + + +def _is_admin(user): + try: + return user.config.roles.filter(name="admin").exists() + except AttributeError: + return False + + +@login_required +def teams_index(request): + """Vue principale de gestion des équipes.""" + if not user_can_view_teams(request.user): + raise PermissionDenied("Vous n'avez pas accès à la gestion des équipes.") + + teams = ( + get_viewable_teams_for_user(request.user) + .select_related("company") + .prefetch_related("members__user") + .annotate( + active_interventions_count=Count( + 'assigned_interventions', + filter=Q(assigned_interventions__status_order__lte=51) + ) + ) + .order_by("company__name", "name") + ) + can_edit = user_can_edit_teams(request.user) + user_company = _get_user_company(request.user) + is_admin_user = _is_admin(request.user) + + # Les non-admins ne voient que les utilisateurs de leur propre société et des sociétés des équipes qu'ils ont le droit d'éditer + if is_admin_user or user_company is None: + available_users = User.objects.all().select_related("config__company").order_by("first_name", "last_name", "username") + else: + from contracts.models import UserCompanyTeamAccess + editable_team_companies = CompanyTeam.objects.filter( + user_accesses__user_config=request.user.config, + user_accesses__can_edit=True + ).values_list('company_id', flat=True) + + allowed_company_ids = set(editable_team_companies) + allowed_company_ids.add(user_company.id) + + available_users = User.objects.filter( + config__company_id__in=allowed_company_ids + ).select_related("config__company").order_by("first_name", "last_name", "username") + + # Membres non assignés à une équipe + unassigned_qs = CompanyMember.objects.filter(teams__isnull=True).select_related("user") + if not is_admin_user and user_company is not None: + unassigned_qs = unassigned_qs.filter(user__config__company=user_company) + unassigned_members = unassigned_qs.order_by("name") + + return render( + request, + "contracts/teams_index.html", + { + "teams": teams, + "can_edit": can_edit, + "available_users": available_users, + "unassigned_members": unassigned_members, + "user_company": user_company, + "is_admin": is_admin_user, + }, + ) + + +@login_required +@require_POST +def teams_move_member(request, member_id): + """Déplacer un membre vers une autre équipe (AJAX).""" + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({"error": "JSON invalide"}, status=400) + + # team_id peut être None / absent pour désassigner + raw_team_id = data.get("team_id") + new_team_id = int(raw_team_id) if raw_team_id is not None else None + + # old_team_id represents the team column they are dragged out of + old_team_id = data.get("old_team_id") + old_team_id = int(old_team_id) if old_team_id is not None else None + + member = get_object_or_404(CompanyMember, id=member_id) + viewable = get_viewable_teams_for_user(request.user) + + # Vérifier l'accès à l'équipe source (si le membre était dans une équipe) + if old_team_id is not None: + if not viewable.filter(id=old_team_id).exists(): + raise PermissionDenied("Accès refusé.") + if not user_can_edit_specific_team(request.user, old_team_id): + raise PermissionDenied("Droits d'édition insuffisants.") + else: + if not user_can_edit_teams(request.user): + raise PermissionDenied("Droits d'édition insuffisants.") + + if new_team_id is not None: + new_team = get_object_or_404(CompanyTeam, id=new_team_id) + if not viewable.filter(id=new_team_id).exists(): + raise PermissionDenied("Accès refusé.") + if not user_can_edit_specific_team(request.user, new_team_id): + raise PermissionDenied("Droits d'édition insuffisants.") + else: + new_team = None + + old_team = None + if old_team_id is not None: + old_team = get_object_or_404(CompanyTeam, id=old_team_id) + member.teams.remove(old_team) + + if new_team_id is not None: + member.teams.add(new_team) + + member.save() + + return JsonResponse( + { + "success": True, + "member_id": member.id, + "member_name": member.name, + "old_team_name": old_team.name if old_team else None, + "new_team_name": new_team.name if new_team else None, + } + ) + + + + + + + +@login_required +@require_POST +def teams_create_member(request): + """Créer un nouveau membre, optionnellement dans une équipe (AJAX).""" + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({"error": "JSON invalide"}, status=400) + + name = data.get("name", "").strip() + user_id = data.get("user_id") + raw_team_id = data.get("team_id") + team_id = int(raw_team_id) if raw_team_id is not None else None + + if not name: + return JsonResponse({"error": "Le nom du membre est requis."}, status=400) + if not user_id: + return JsonResponse({"error": "L'utilisateur est requis."}, status=400) + + if team_id is not None: + if not user_can_edit_specific_team(request.user, team_id): + raise PermissionDenied("Droits d'édition insuffisants.") + viewable = get_viewable_teams_for_user(request.user) + if not viewable.filter(id=team_id).exists(): + raise PermissionDenied("Accès refusé.") + team = get_object_or_404(CompanyTeam, id=team_id) + else: + if not user_can_edit_teams(request.user): + raise PermissionDenied("Droits d'édition insuffisants.") + team = None + + user_obj = get_object_or_404(User, id=user_id) + + # Les non-admins ne peuvent ajouter que des utilisateurs de leur société, ou de la société de l'équipe concernée + user_company = _get_user_company(request.user) + if not _is_admin(request.user) and user_company is not None: + target_company = _get_user_company(user_obj) + allowed_companies = {user_company} + if team is not None: + allowed_companies.add(team.company) + + if target_company not in allowed_companies: + return JsonResponse( + {"error": "Vous ne pouvez ajouter que des utilisateurs de votre société ou de la société de l'équipe."}, + status=403, + ) + + # Check if there is already a CompanyMember for this user + member = CompanyMember.objects.filter(user=user_obj).first() + if member: + if team: + if member.teams.filter(id=team.id).exists(): + return JsonResponse( + {"error": "Cet utilisateur fait déjà partie de cette équipe."}, + status=400, + ) + else: + if not member.teams.exists(): + return JsonResponse( + {"error": "Cet utilisateur est déjà dans les membres non assignés."}, + status=400, + ) + if name: + member.name = name + member.save() + else: + member = CompanyMember.objects.create(name=name, user=user_obj) + + if team: + member.teams.add(team) + + return JsonResponse( + { + "success": True, + "member": { + "id": member.id, + "name": member.name, + "team_id": team.id if team else None, + "team_name": team.name if team else None, + "user_display": user_obj.get_full_name() or user_obj.username, + }, + } + ) + + +@login_required +@require_POST +def teams_delete_member(request, member_id): + """Supprimer un membre (AJAX).""" + member = get_object_or_404(CompanyMember, id=member_id) + + member_team_ids = list(member.teams.values_list('id', flat=True)) + if member_team_ids: + viewable = get_viewable_teams_for_user(request.user) + for t_id in member_team_ids: + if not viewable.filter(id=t_id).exists(): + raise PermissionDenied("Accès refusé.") + if not user_can_edit_specific_team(request.user, t_id): + raise PermissionDenied("Droits d'édition insuffisants.") + else: + # Membre non assigné : droits d'édition généraux suffisent + if not user_can_edit_teams(request.user): + raise PermissionDenied("Droits d'édition insuffisants.") + + member_name = member.name + member.delete() + + return JsonResponse({"success": True, "member_name": member_name}) + + +@login_required +@require_POST +def teams_update_member(request, member_id): + """Modifier le nom et/ou l'équipe d'un membre (AJAX).""" + member = get_object_or_404(CompanyMember, id=member_id) + viewable = get_viewable_teams_for_user(request.user) + + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({"error": "JSON invalide"}, status=400) + + # old_team_id represents the team column they clicked edit on (or None if unassigned) + old_team_id = data.get("old_team_id") + old_team_id = int(old_team_id) if old_team_id is not None else None + + # Vérifier l'accès à l'équipe source + if old_team_id is not None: + if not viewable.filter(id=old_team_id).exists(): + raise PermissionDenied("Accès refusé.") + if not user_can_edit_specific_team(request.user, old_team_id): + raise PermissionDenied("Droits d'édition insuffisants.") + else: + if not user_can_edit_teams(request.user): + raise PermissionDenied("Droits d'édition insuffisants.") + + new_name = data.get("name", "").strip() + if not new_name: + return JsonResponse({"error": "Le nom du membre est requis."}, status=400) + + raw_team_id = data.get("team_id") # peut être absent (non modifié) ou None (désassigner) + team_changed = "team_id" in data + + new_team_id = None + new_team = None + + if team_changed: + new_team_id = int(raw_team_id) if raw_team_id is not None else None + if new_team_id is not None: + new_team = get_object_or_404(CompanyTeam, id=new_team_id) + if not viewable.filter(id=new_team_id).exists(): + raise PermissionDenied("Accès refusé.") + if not user_can_edit_specific_team(request.user, new_team_id): + raise PermissionDenied("Droits d'édition insuffisants.") + else: + new_team = None + + # Effectuer le transfert + if old_team_id is not None: + old_team = get_object_or_404(CompanyTeam, id=old_team_id) + member.teams.remove(old_team) + if new_team_id is not None: + member.teams.add(new_team) + + member.name = new_name + member.save() + + return JsonResponse({ + "success": True, + "member_id": member.id, + "member_name": member.name, + "team_id": new_team_id if team_changed else old_team_id, + "team_name": new_team.name if team_changed and new_team else None, + }) + + +@login_required +@require_GET +def team_interventions_json(request, team_id): + """Retourne les interventions assignées à une équipe (JSON, pour la modale).""" + from django.urls import reverse + from interventions.models import Intervention + + team = get_object_or_404(CompanyTeam, id=team_id) + viewable = get_viewable_teams_for_user(request.user) + if not viewable.filter(id=team_id).exists(): + raise PermissionDenied("Accès refusé.") + + interventions = ( + Intervention.objects + .filter(assigned_team=team, status_order__lte=51) + .select_related('assigned_member') + .order_by('-creation_time') + [:500] + ) + + data = [] + for i in interventions: + data.append({ + 'id': i.id, + 'code': i.code, + 'title': i.title, + 'status': i.status, + 'status_display': i.get_status_display(), + 'status_color': i.get_status_color(), + 'assigned_member_name': i.assigned_member.name if i.assigned_member else None, + 'summary_url': reverse('interventions:intervention_summary_modal', args=[i.id]), + 'detail_url': reverse('interventions:intervention_detail', args=[i.id]), + }) + + return JsonResponse({'interventions': data, 'team_name': team.name}) diff --git a/streetup/controls/__init__.py b/streetup/controls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/controls/admin.py b/streetup/controls/admin.py new file mode 100644 index 0000000..0f1cee8 --- /dev/null +++ b/streetup/controls/admin.py @@ -0,0 +1,133 @@ +from django.contrib import admin +from django.utils.html import format_html +from .models import ( + Control, Category, ControlCheck, DocumentCheck, CheckList, + CheckListControlCheck, CheckListDocumentCheck, Remark, RemarkImage, + RemarkTag, ControlReport, ControlReferencePlan, ControlReportTemplate +) +from common.private_files.helpers import build_private_url + +@admin.register(Control) +class ControlAdmin(admin.ModelAdmin): + list_display = ("control_code", "control_type", "status", "project", "phase", "control_author", "updated_at") + list_filter = ("status", "control_type", "project", "phase", "interventions") + search_fields = ("control_code", "description", "location", "control_author__username") + ordering = ("-updated_at",) + autocomplete_fields = ["project", "phase", "interventions", "control_author"] + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("cat_code", "get_name", "thematic") + search_fields = ("cat_code", "name_fr", "name_nl") + list_filter = ("thematic",) + + +@admin.register(ControlCheck) +class ControlCheckAdmin(admin.ModelAdmin): + list_display = ("control", "category", "status") + list_filter = ("status", "category") + autocomplete_fields = ["control", "category"] + + +@admin.register(DocumentCheck) +class DocumentCheckAdmin(admin.ModelAdmin): + list_display = ("control", "doc_type", "status") + list_filter = ("status", "doc_type") + autocomplete_fields = ["control"] + + +@admin.register(CheckList) +class CheckListAdmin(admin.ModelAdmin): + list_display = ("checklist_code", "get_name", "thematic") + search_fields = ("checklist_code", "name_fr", "name_nl") + list_filter = ("thematic",) + + +@admin.register(CheckListControlCheck) +class CheckListControlCheckAdmin(admin.ModelAdmin): + list_display = ("checklist", "category") + autocomplete_fields = ["checklist", "category"] + + +@admin.register(CheckListDocumentCheck) +class CheckListDocumentCheckAdmin(admin.ModelAdmin): + list_display = ("checklist", "doc_type") + list_filter = ("doc_type",) + autocomplete_fields = ["checklist"] + + +@admin.register(Remark) +class RemarkAdmin(admin.ModelAdmin): + list_display = ("control", "remark_number", "description", "internal", "status", "created_at", "updated_at") + list_filter = ("status", "internal") + search_fields = ("description",) + autocomplete_fields = ["control"] + + +@admin.register(RemarkImage) +class RemarkImageAdmin(admin.ModelAdmin): + list_display = ("remark", "file") + autocomplete_fields = ["remark"] + + +@admin.register(RemarkTag) +class RemarkTagAdmin(admin.ModelAdmin): + list_display = ("remark", "category") + autocomplete_fields = ["remark", "category"] + + +@admin.register(ControlReport) +class ControlReportAdmin(admin.ModelAdmin): + list_display = ("control", "created_at", "created_by", "status", "document") + list_filter = ("status",) + search_fields = ("control__control_code",) + autocomplete_fields = ["control", "created_by"] + + +@admin.register(ControlReferencePlan) +class ControlReferencePlanAdmin(admin.ModelAdmin): + # Liste des champs à afficher dans la liste des objets + list_display = ('name', 'control', 'uploaded_by', 'plan_preview', 'max_x', 'max_y', 'uploaded_at') + + # Filtres pour faciliter la navigation + list_filter = ('control', 'uploaded_by') + + # Champs de recherche + search_fields = ('name', 'control__name', 'uploaded_by__username') + + # Champs en lecture seule (si nécessaire) + readonly_fields = ('max_x', 'max_y', 'uploaded_at', 'plan_preview') + + # Personnalisation du formulaire d'édition + fieldsets = ( + (None, { + 'fields': ('control', 'name', 'plan', 'plan_preview', 'uploaded_by', 'uploaded_at') + }), + ('Dimensions normalisées', { + 'fields': ('max_x', 'max_y'), + 'classes': ('collapse',) + }), + ) + + @admin.display(description='Plan') + def plan_preview(self, obj): + if not obj.plan: + return '-' + url = build_private_url('controls', 'ControlReferencePlan', obj.pk, 'plan') + return format_html( + '', + url, url + ) + + def save_model(self, request, obj, form, change): + obj.uploaded_by = request.user # Associer l'utilisateur connecté + super().save_model(request, obj, form, change) + + + +@admin.register(ControlReportTemplate) +class ControlReportTemplateAdmin(admin.ModelAdmin): + list_display = ("id", "control", "name", "updated_by", "updated_at") + search_fields = ("control__description",) + autocomplete_fields = ["control"] \ No newline at end of file diff --git a/streetup/controls/apps.py b/streetup/controls/apps.py new file mode 100644 index 0000000..932d538 --- /dev/null +++ b/streetup/controls/apps.py @@ -0,0 +1,24 @@ +from django.apps import AppConfig + + +class ControlsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'controls' + + def ready(self): + import controls.signals + + from common.private_files.registry import register + from .permissions import ( + guard_control_report_document, + guard_control_reference_plan, + guard_remark_image_file, + ) + # ControlReport.document + register("controls", "ControlReport", "document", guard_control_report_document) + # ControlReferencePlan.plan + register("controls", "ControlReferencePlan", "plan", guard_control_reference_plan) + # RemarkImage.file + register("controls", "RemarkImage", "file", guard_remark_image_file) + # RemarkImage.thumbnail + register("controls", "RemarkImage", "thumbnail", guard_remark_image_file) diff --git a/streetup/controls/forms.py b/streetup/controls/forms.py new file mode 100644 index 0000000..060cb21 --- /dev/null +++ b/streetup/controls/forms.py @@ -0,0 +1,243 @@ +from django import forms +from django.utils.translation import get_language, gettext as _ +from django.forms import inlineformset_factory, BaseInlineFormSet + +from .models import Control, Remark, RemarkImage, ControlReferencePlan, ControlReportTemplate, ControlQuantity +from projects.models import ProjectPhase +from common.models import Thematic, UserContractAccess +from assets.models import AssetCategory +from contracts.models import Contract +from interventions.models import Intervention + + +class ControlForm(forms.ModelForm): + class Meta: + model = Control + fields = [ + 'description', 'status', 'control_type', + 'project', 'phase', + 'thematics', 'categories', 'interventions', 'visibility', 'shared_edition', + 'location', 'lon', 'lat', 'geojson' + ] + widgets = { + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'autocomplete': 'off'}), + 'status': forms.Select(attrs={'class': 'form-select'}), + 'control_type': forms.Select(attrs={'class': 'form-select'}), + 'visibility': forms.Select(attrs={'class': 'form-select'}), + 'shared_edition': forms.CheckboxInput(attrs={'class': 'form-check'}), + 'thematics': forms.CheckboxSelectMultiple(), + 'categories': forms.CheckboxSelectMultiple(), + 'project': forms.Select(attrs={'class': 'form-select', 'id': 'project-select'}), + 'phase': forms.Select(attrs={'class': 'form-select', 'id': 'phase-select'}), + 'location': forms.TextInput(attrs={'class': 'form-control', 'autocomplete': 'off'}), + 'interventions': forms.SelectMultiple(attrs={'style': 'display:none;'}), + 'lon': forms.HiddenInput(), + 'lat': forms.HiddenInput(), + 'geojson': forms.HiddenInput(), + } + labels = { + 'location': _('Localisation') + ' (' + _('facultatif') + ')', + 'categories': _('Catégories') + ' (' + _('facultatif') + ')', + 'interventions': _('Interventions liées') + ' (' + _('facultatif') + ')', + 'project': _('Projet') + ' (' + _('facultatif') + ')', + 'phase': _('Phase du projet') + ' (' + _('facultatif') + ')', + 'description': _('Description') + ' (' + _('facultatif') + ')', + } + + def __init__(self, *args, user=None, **kwargs): + super(ControlForm, self).__init__(*args, **kwargs) + self.fields['phase'].queryset = ProjectPhase.objects.none() + self.fields['categories'].queryset = AssetCategory.objects.none() + + lang = get_language() + if lang == 'nl': + self.fields['thematics'].queryset = Thematic.objects.all().order_by('name_nl') + else: + self.fields['thematics'].queryset = Thematic.objects.all().order_by('name_fr') + + # Si un utilisateur est passé au form et qu'il a une thématique par défaut + if ( + not self.is_bound + and not self.instance.pk # uniquement à la création + and user + and hasattr(user, "config") + and user.config.default_thematic + and not self.initial.get('thematics') + ): + self.initial['thematics'] = [user.config.default_thematic.id] + + # Vérification des catégories pour la validation + if 'thematics' in self.data: + thematic_ids = self.data.getlist('thematics') + self.fields['categories'].queryset = AssetCategory.objects.filter( + thematic__id__in=thematic_ids + ).distinct() + elif self.instance.pk: + # Si modification d’un objet existant, afficher toutes les catégories possibles + # en fonction des thématiques du contrôle (pas seulement les sélectionnées) + thematic_ids = list(self.instance.thematics.values_list('id', flat=True)) + if thematic_ids: + self.fields['categories'].queryset = AssetCategory.objects.filter( + thematic__id__in=thematic_ids + ).distinct() + else: + self.fields['categories'].queryset = AssetCategory.objects.none() + # Vérification des phases pour la validation + if 'project' in self.data and self.data.get('project'): + project_id = self.data.get('project') + self.fields['phase'].queryset = ProjectPhase.objects.filter(project_id=project_id) + elif self.instance.pk and self.instance.project: + # Si modification d'un objet existant + self.fields['phase'].queryset = ProjectPhase.objects.filter(project=self.instance.project) + + # Interventions : éviter de charger toutes les options (liste potentiellement volumineuse) + if 'interventions' in self.data: + ids = [x for x in self.data.getlist('interventions') if str(x).strip().isdigit()] + self.fields['interventions'].queryset = Intervention.objects.filter(id__in=ids) + elif self.instance.pk: + self.fields['interventions'].queryset = self.instance.interventions.all() + else: + self.fields['interventions'].queryset = Intervention.objects.none() + + + +class MultipleFileInput(forms.ClearableFileInput): + allow_multiple_selected = True + + +class MultipleFileField(forms.FileField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", MultipleFileInput()) + super().__init__(*args, **kwargs) + + def clean(self, data, initial=None): + single_file_clean = super().clean + if isinstance(data, (list, tuple)): + result = [single_file_clean(d, initial) for d in data] + else: + result = [single_file_clean(data, initial)] + return result + + +class RemarkForm(forms.ModelForm): + images = MultipleFileField(required=False) + + class Meta: + model = Remark + fields = ["control", "status", "description", "images", "lon", "lat", "x", "y"] + + widgets = { + "description": forms.Textarea(attrs={ + "class": "form-control", # Ajoute la classe Bootstrap + "rows": 2, # Limite à deux lignes + }), + } + + def __init__(self, *args, **kwargs): + super(RemarkForm, self).__init__(*args, **kwargs) + self.fields['images'].widget.attrs.update({'class': 'form-control'}) + + +class ControlReferencePlanForm(forms.ModelForm): + class Meta: + model = ControlReferencePlan + fields = ["name", "plan"] + + +class ControlReportTemplateForm(forms.ModelForm): + class Meta: + model = ControlReportTemplate + fields = ['header_notes', 'footer_notes'] + widgets = { + 'header_notes': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 4, + 'placeholder': _("Entrez les notes de début ..."), + }), + 'footer_notes': forms.Textarea(attrs={ + 'class': 'form-control', + 'rows': 4, + 'placeholder': _("Entrez les notes de fin ..."), + }), + } + + + +class ControlQuantityForm(forms.ModelForm): + # Champ “recherchable” pour le poste: on utilisera un input visible + un hidden pour l’ID + contract_post_label = forms.CharField( + required=False, + widget=forms.TextInput(attrs={ + "class": "form-control", + "placeholder": _("Chercher un poste (autocomplete)..."), + "autocomplete": "off", + "autocorrect": "off", # (iOS) + "autocapitalize": "none", + "spellcheck": "false", + }) + ) + contract_post_id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + + class Meta: + model = ControlQuantity + fields = [ + "contract", "description", + "q1", "q2", "q3", + ] + widgets = { + "contract": forms.Select(attrs={"class": "form-select", "id": "quantity-contract-select"}), + "description": forms.TextInput(attrs={"class": "form-control", "placeholder": _("Description si pas de marché")}), + "q1": forms.NumberInput(attrs={"class": "form-control", "step": "0.01", "inputmode": "decimal"}), + "q2": forms.NumberInput(attrs={"class": "form-control", "step": "0.01", "inputmode": "decimal"}), + "q3": forms.NumberInput(attrs={"class": "form-control", "step": "0.01", "inputmode": "decimal"}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.is_bound and self.instance and self.instance.pk: + cp = getattr(self.instance, "contract_post", None) + if cp: + # ID caché + self.fields["contract_post_id"].initial = cp.id + + # Libellé visible (NL/FR selon la langue active) + lang = (get_language() or "fr").lower() + desc = cp.description_nl if lang.startswith("nl") else cp.description_fr + label = f"{cp.order_number} - {cp.code} - {desc}" + self.fields["contract_post_label"].initial = label + + + def clean(self): + cleaned = super().clean() + # si contract_post_id est fourni, on le rattache dans la view (post-clean) pour éviter import circulaire + return cleaned + + + +class ControlQuantityInlineFormSet(BaseInlineFormSet): + def __init__(self, *args, user=None, **kwargs): + super().__init__(*args, **kwargs) + + qs = Contract.objects.none() + if user and hasattr(user, "config"): + allowed_ids = UserContractAccess.objects.filter( + user_config=user.config, can_view_posts=True + ).values_list("contract_id", flat=True) + qs = Contract.objects.filter(id__in=allowed_ids).order_by("contract_number") + + # Injecter le même queryset dans chaque form + for form in self.forms: + form.fields["contract"].queryset = qs + form.fields["contract"].empty_label = _("Sélectionner un marché") + + + +QuantityFormSet = inlineformset_factory( + Remark, + ControlQuantity, + form=ControlQuantityForm, + formset=ControlQuantityInlineFormSet, + fields=["contract", "description", "q1", "q2", "q3"], + extra=0, + can_delete=True +) \ No newline at end of file diff --git a/streetup/controls/locale/en/LC_MESSAGES/django.mo b/streetup/controls/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..0487e28 Binary files /dev/null and b/streetup/controls/locale/en/LC_MESSAGES/django.mo differ diff --git a/streetup/controls/locale/en/LC_MESSAGES/django.po b/streetup/controls/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..564dc5d --- /dev/null +++ b/streetup/controls/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,1049 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-06 10:57+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: controls/forms.py:39 controls/models.py:53 controls/views_reports_pdf.py:414 +msgid "Localisation" +msgstr "Location" + +#: controls/forms.py:39 controls/forms.py:40 controls/forms.py:41 +#: controls/forms.py:42 controls/forms.py:43 controls/forms.py:44 +#: controls/templates/controls/controls_form_content.html:374 +msgid "facultatif" +msgstr "optional" + +#: controls/forms.py:40 controls/models.py:49 +#: controls/templates/controls/controls_detail_content.html:49 +msgid "Catégories" +msgstr "Categories" + +#: controls/forms.py:41 +#: controls/templates/controls/controls_form_content.html:271 +msgid "Interventions liées" +msgstr "Linked interventions" + +#: controls/forms.py:42 controls/models.py:50 +#: controls/templates/controls/controls_detail_content.html:60 +#: controls/templates/controls/controls_detail_mobile.html:95 +#: controls/templates/controls/controls_index_mobile.html:40 +msgid "Projet" +msgstr "Project" + +#: controls/forms.py:43 +msgid "Phase du projet" +msgstr "Project phase" + +#: controls/forms.py:44 controls/models.py:45 +#: controls/templates/controls/controls_detail_content.html:89 +#: controls/templates/controls/controls_detail_content.html:132 +#: controls/templates/controls/controls_detail_mobile.html:126 +#: controls/templates/controls/controls_detail_mobile.html:172 +#: controls/templates/controls/controls_index.html:34 +msgid "Description" +msgstr "Description" + +#: controls/forms.py:155 +msgid "Entrez les notes de début ..." +msgstr "Enter start notes..." + +#: controls/forms.py:160 +msgid "Entrez les notes de fin ..." +msgstr "Enter end notes..." + +#: controls/forms.py:172 +msgid "Chercher un poste (autocomplete)..." +msgstr "Search for a position (autocomplete)..." + +#: controls/forms.py:189 +msgid "Description si pas de marché" +msgstr "Description if no contract" + +#: controls/forms.py:231 +msgid "Sélectionner un marché" +msgstr "Select a contract" + +#: controls/models.py:21 +msgid "En cours" +msgstr "In progress" + +#: controls/models.py:22 +msgid "Information" +msgstr "Information" + +#: controls/models.py:23 +msgid "A traiter" +msgstr "To be processed" + +#: controls/models.py:24 controls/models.py:229 +msgid "Traité" +msgstr "Processed" + +#: controls/models.py:25 controls/models.py:175 +msgid "Validé" +msgstr "Validated" + +#: controls/models.py:26 controls/models.py:177 controls/models.py:231 +msgid "Annulé" +msgstr "Cancelled" + +#: controls/models.py:30 controls/templates/controls/controls_detail.html:33 +#: controls/templates/controls/controls_detail_content.html:21 +#: controls/views_reports_pdf.py:402 +msgid "Contrôle" +msgstr "Control" + +#: controls/models.py:31 +msgid "Rapport de chantier" +msgstr "Site report" + +#: controls/models.py:32 +msgid "Constat" +msgstr "Statement" + +#: controls/models.py:33 +msgid "Rapport de réunion" +msgstr "Meeting report" + +#: controls/models.py:37 +msgid "Privé" +msgstr "Private" + +#: controls/models.py:38 +msgid "Interne" +msgstr "Internal" + +#: controls/models.py:39 +msgid "Interne et externe" +msgstr "Internal and external" + +#: controls/models.py:44 +#: controls/templates/controls/controls_form_content.html:191 +msgid "Code du contrôle" +msgstr "Control code" + +#: controls/models.py:46 +#: controls/templates/controls/controls_detail_content.html:33 +#: controls/templates/controls/controls_detail_mobile.html:84 +#: controls/views_reports_pdf.py:704 +msgid "Statut" +msgstr "Status" + +#: controls/models.py:47 +msgid "Type de contrôle" +msgstr "Control type" + +#: controls/models.py:48 +#: controls/templates/controls/controls_detail_content.html:41 +msgid "Thématiques" +msgstr "Themes" + +#: controls/models.py:51 +#: controls/templates/controls/controls_detail_content.html:68 +#: controls/templates/controls/controls_detail_mobile.html:103 +msgid "Phase" +msgstr "Phase" + +#: controls/models.py:52 +msgid "Interventions" +msgstr "Interventions" + +#: controls/models.py:54 +msgid "Géométrie" +msgstr "Geometry" + +#: controls/models.py:55 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: controls/models.py:56 +msgid "Longitude" +msgstr "Longitude" + +#: controls/models.py:57 +msgid "Latitude" +msgstr "Latitude" + +#: controls/models.py:61 +msgid "Visibilité" +msgstr "Visibility" + +#: controls/models.py:62 +msgid "Edition partagée" +msgstr "Shared editing" + +#: controls/models.py:136 +msgid "Plan as-built" +msgstr "As-built plan" + +#: controls/models.py:137 +msgid "Fiches techniques" +msgstr "Technical sheets" + +#: controls/models.py:174 +msgid "Actif" +msgstr "Active" + +#: controls/models.py:176 +msgid "Archivé" +msgstr "Archived" + +#: controls/models.py:227 +#: controls/templates/controls/controls_detail_content.html:125 +#: controls/templates/controls/controls_detail_mobile.html:163 +#: controls/templates/controls/controls_detail_mobile.html:956 +msgid "Info" +msgstr "Info" + +#: controls/models.py:228 +#: controls/templates/controls/controls_detail_content.html:127 +#: controls/templates/controls/controls_detail_mobile.html:165 +#: controls/templates/controls/controls_detail_mobile.html:956 +msgid "NOK" +msgstr "NOK" + +#: controls/models.py:230 +#: controls/templates/controls/controls_detail_content.html:126 +#: controls/templates/controls/controls_detail_mobile.html:164 +#: controls/templates/controls/controls_detail_mobile.html:955 +msgid "OK" +msgstr "OK" + +#: controls/models.py:258 +#, python-format +msgid "Remarque %(num)s pour %(control)s" +msgstr "Remark %(num)s for %(control)s" + +#: controls/permissions.py:56 +msgid "Vous n'êtes pas autorisé à accéder à ce contrôle." +msgstr "You are not authorized to access this control." + +#: controls/permissions.py:73 +msgid "Vous n'êtes pas autorisé à ajouter un contrôle." +msgstr "You are not authorized to add a control." + +#: controls/permissions.py:108 +msgid "Vous n'êtes pas autorisé à modifier ce contrôle." +msgstr "You are not authorized to modify this control." + +#: controls/templates/controls/change_template.html:10 +#: controls/templates/controls/change_template_mobile.html:21 +msgid "Modifier le template de rapport" +msgstr "Edit report template" + +#: controls/templates/controls/change_template.html:18 +#: controls/templates/controls/change_template_mobile.html:29 +msgid "Notes de début" +msgstr "Start notes" + +#: controls/templates/controls/change_template.html:24 +#: controls/templates/controls/change_template_mobile.html:35 +msgid "Notes de fin" +msgstr "End notes" + +#: controls/templates/controls/change_template.html:30 +#: controls/templates/controls/change_template_mobile.html:41 +#: controls/templates/controls/controls_detail_content.html:725 +#: controls/templates/controls/controls_detail_mobile.html:765 +#: controls/templates/controls/controls_form_content.html:407 +#: controls/templates/controls/remark_form_content.html:161 +#: controls/templates/controls/remark_form_content.html:163 +msgid "Annuler" +msgstr "Cancel" + +#: controls/templates/controls/change_template.html:31 +#: controls/templates/controls/change_template_mobile.html:42 +#: controls/templates/controls/controls_form_content.html:403 +#: controls/templates/controls/remark_form_content.html:155 +msgid "Enregistrer" +msgstr "Save" + +#: controls/templates/controls/control_report.html:10 +#: controls/templates/controls/control_report.html:39 +msgid "Rapport de Contrôle" +msgstr "Control Report" + +#: controls/templates/controls/control_report.html:41 +msgid "Numéro du rapport" +msgstr "Report number" + +#: controls/templates/controls/control_report.html:42 +#: controls/views_reports_pdf.py:415 controls/views_reports_pdf.py:702 +msgid "Date" +msgstr "Date" + +#: controls/templates/controls/control_report.html:48 +msgid "Statut du Projet" +msgstr "Project Status" + +#: controls/templates/controls/control_report.html:53 +msgid "Photos d'Avancement" +msgstr "Progress Photos" + +#: controls/templates/controls/control_report.html:62 +msgid "Observations" +msgstr "Observations" + +#: controls/templates/controls/control_report.html:77 +msgid "Rapport généré par" +msgstr "Report generated by" + +#: controls/templates/controls/control_report.html:80 +msgid "Télécharger le PDF" +msgstr "Download PDF" + +#: controls/templates/controls/controls_base_mobile.html:12 +#: controls/templates/controls/controls_base_mobile.html:63 +#: controls/templates/controls/controls_index.html:19 +#: controls/templates/controls/controls_index_mobile.html:13 +msgid "Contrôles" +msgstr "Controls" + +#: controls/templates/controls/controls_base_mobile.html:66 +#: controls/templates/controls/controls_projects_mobile.html:17 +msgid "Projets" +msgstr "Projects" + +#: controls/templates/controls/controls_detail_content.html:13 +#: controls/templates/controls/controls_detail_content.html:719 +#: controls/templates/controls/controls_detail_mobile.html:68 +#: controls/templates/controls/controls_detail_mobile.html:136 +#: controls/templates/controls/controls_detail_mobile.html:734 +#: controls/templates/controls/controls_detail_mobile.html:759 +#: controls/templates/controls/controls_detail_mobile.html:780 +#: controls/templates/controls/remarks_form.html:72 +#: controls/templates/controls/remarks_form_mobile.html:72 +msgid "Fermer" +msgstr "Close" + +#: controls/templates/controls/controls_detail_content.html:26 +#: controls/templates/controls/controls_detail_mobile.html:78 +msgid "Modifier le template" +msgstr "Edit template" + +#: controls/templates/controls/controls_detail_content.html:37 +#: controls/templates/controls/controls_detail_mobile.html:88 +#: controls/templates/controls/controls_index.html:36 +#: controls/templates/controls/controls_index_mobile.html:33 +msgid "Type" +msgstr "Type" + +#: controls/templates/controls/controls_detail_content.html:45 +#: controls/templates/controls/controls_detail_content.html:54 +msgid "Aucune" +msgstr "None" + +#: controls/templates/controls/controls_detail_content.html:74 +#: controls/templates/controls/controls_detail_mobile.html:109 +msgid "Intervention" +msgstr "Intervention" + +#: controls/templates/controls/controls_detail_content.html:80 +#: controls/templates/controls/controls_detail_mobile.html:117 +msgid "Créé le" +msgstr "Created on" + +#: controls/templates/controls/controls_detail_content.html:81 +#: controls/templates/controls/controls_detail_mobile.html:118 +#: controls/templates/controls/controls_index_mobile.html:37 +msgid "Mis à jour le" +msgstr "Updated on" + +#: controls/templates/controls/controls_detail_content.html:85 +#: controls/templates/controls/controls_detail_content.html:567 +#: controls/templates/controls/controls_detail_mobile.html:122 +#: controls/templates/controls/controls_detail_mobile.html:611 +#: controls/templates/controls/controls_index.html:35 +msgid "Auteur" +msgstr "Author" + +#: controls/templates/controls/controls_detail_content.html:92 +#: controls/templates/controls/controls_detail_mobile.html:114 +#: controls/templates/controls/controls_index_mobile.html:36 +msgid "Lieu" +msgstr "Location" + +#: controls/templates/controls/controls_detail_content.html:108 +#: controls/templates/controls/controls_detail_mobile.html:145 +#: controls/templates/controls/remark_form_content.html:12 +msgid "Carte" +msgstr "Map" + +#: controls/templates/controls/controls_detail_content.html:114 +#: controls/templates/controls/controls_detail_content.html:316 +#: controls/templates/controls/controls_detail_mobile.html:151 +#: controls/templates/controls/controls_detail_mobile.html:362 +msgid "Ajouter une remarque" +msgstr "Add a remark" + +#: controls/templates/controls/controls_detail_content.html:135 +#: controls/templates/controls/controls_detail_mobile.html:175 +msgid "Veuillez remplir la description." +msgstr "Please fill in the description." + +#: controls/templates/controls/controls_detail_content.html:142 +#: controls/templates/controls/controls_detail_mobile.html:182 +#: controls/templates/controls/remark_form_content.html:85 +msgid "Images" +msgstr "Images" + +#: controls/templates/controls/controls_detail_content.html:145 +#: controls/templates/controls/controls_detail_mobile.html:194 +msgid "Veuillez ajouter au moins une image." +msgstr "Please add at least one image." + +#: controls/templates/controls/controls_detail_content.html:154 +#: controls/templates/controls/controls_detail_mobile.html:203 +msgid "Tags" +msgstr "Tags" + +#: controls/templates/controls/controls_detail_content.html:170 +#: controls/templates/controls/controls_detail_mobile.html:224 +#: controls/templates/controls/remark_form_content.html:117 +msgid "Quantités liées à la remarque" +msgstr "Quantities linked to the remark" + +#: controls/templates/controls/controls_detail_content.html:172 +#: controls/templates/controls/controls_detail_mobile.html:226 +#: controls/templates/controls/remark_form_content.html:119 +msgid "Ajouter des quantités" +msgstr "Add quantities" + +#: controls/templates/controls/controls_detail_content.html:178 +#: controls/templates/controls/controls_detail_mobile.html:232 +#: controls/templates/controls/remark_form_content.html:125 +msgid "Aucune quantité pour l'instant." +msgstr "No quantity for now." + +#: controls/templates/controls/controls_detail_content.html:207 +#: controls/templates/controls/controls_detail_mobile.html:255 +msgid "Ajouter" +msgstr "Add" + +#: controls/templates/controls/controls_detail_content.html:218 +#: controls/templates/controls/controls_detail_mobile.html:266 +#: controls/templates/controls/remark_form_content.html:171 +msgid "Quantités de la remarque" +msgstr "Remark quantities" + +#: controls/templates/controls/controls_detail_content.html:233 +#: controls/templates/controls/controls_detail_mobile.html:281 +#: controls/templates/controls/remark_form_content.html:186 +msgid "Ligne" +msgstr "Line" + +#: controls/templates/controls/controls_detail_content.html:235 +#: controls/templates/controls/controls_detail_content.html:419 +#: controls/templates/controls/controls_detail_content.html:456 +#: controls/templates/controls/controls_detail_content.html:657 +#: controls/templates/controls/controls_detail_content.html:726 +#: controls/templates/controls/controls_detail_mobile.html:283 +#: controls/templates/controls/controls_detail_mobile.html:465 +#: controls/templates/controls/controls_detail_mobile.html:502 +#: controls/templates/controls/controls_detail_mobile.html:697 +#: controls/templates/controls/controls_detail_mobile.html:766 +#: controls/templates/controls/controls_detail_mobile.html:969 +#: controls/templates/controls/controls_detail_mobile.html:998 +#: controls/templates/controls/remark_form_content.html:188 +msgid "Supprimer" +msgstr "Delete" + +#: controls/templates/controls/controls_detail_content.html:241 +#: controls/templates/controls/controls_detail_mobile.html:289 +#: controls/templates/controls/remark_form_content.html:194 +msgid "Marché (optionnel)" +msgstr "Contract (optional)" + +#: controls/templates/controls/controls_detail_content.html:247 +#: controls/templates/controls/controls_detail_mobile.html:295 +#: controls/templates/controls/remark_form_content.html:200 +msgid "Poste (autocomplete si un marché est choisi)" +msgstr "Position (autocomplete if a contract is chosen)" + +#: controls/templates/controls/controls_detail_content.html:255 +#: controls/templates/controls/controls_detail_mobile.html:303 +#: controls/templates/controls/remark_form_content.html:208 +msgid "Description (si pas de marché/poste)" +msgstr "Description (if no contract/position)" + +#: controls/templates/controls/controls_detail_content.html:262 +#: controls/templates/controls/controls_detail_mobile.html:310 +#: controls/templates/controls/remark_form_content.html:215 +msgid "Quantité (1)" +msgstr "Quantity (1)" + +#: controls/templates/controls/controls_detail_content.html:266 +#: controls/templates/controls/controls_detail_mobile.html:314 +#: controls/templates/controls/remark_form_content.html:219 +msgid "Quantité (2)" +msgstr "Quantity (2)" + +#: controls/templates/controls/controls_detail_content.html:270 +#: controls/templates/controls/controls_detail_mobile.html:318 +#: controls/templates/controls/remark_form_content.html:223 +msgid "Quantité (3)" +msgstr "Quantity (3)" + +#: controls/templates/controls/controls_detail_content.html:277 +#: controls/templates/controls/controls_detail_mobile.html:325 +#: controls/templates/controls/remark_form_content.html:230 +msgid "Résultat" +msgstr "Result" + +#: controls/templates/controls/controls_detail_content.html:293 +#: controls/templates/controls/controls_detail_mobile.html:341 +#: controls/templates/controls/remark_form_content.html:246 +msgid "Ajouter une ligne" +msgstr "Add a line" + +#: controls/templates/controls/controls_detail_content.html:296 +#: controls/templates/controls/controls_detail_mobile.html:344 +#: controls/templates/controls/remark_form_content.html:249 +msgid "Terminer et revenir" +msgstr "Finish and return" + +#: controls/templates/controls/controls_detail_content.html:323 +#: controls/templates/controls/controls_detail_mobile.html:368 +msgid "Remarques" +msgstr "Remarks" + +#: controls/templates/controls/controls_detail_content.html:354 +#: controls/templates/controls/controls_detail_mobile.html:402 +msgid "Éditer la remarque" +msgstr "Edit remark" + +#: controls/templates/controls/controls_detail_content.html:358 +#: controls/templates/controls/controls_detail_mobile.html:406 +msgid "Archiver la remarque" +msgstr "Archive remark" + +#: controls/templates/controls/controls_detail_content.html:383 +#: controls/templates/controls/controls_detail_content.html:639 +#: controls/templates/controls/controls_detail_mobile.html:429 +#: controls/templates/controls/controls_detail_mobile.html:679 +msgid "Mise à jour par" +msgstr "Updated by" + +#: controls/templates/controls/controls_detail_content.html:384 +#: controls/templates/controls/controls_detail_content.html:640 +#: controls/templates/controls/controls_detail_mobile.html:430 +#: controls/templates/controls/controls_detail_mobile.html:680 +msgid "le" +msgstr "on" + +#: controls/templates/controls/controls_detail_content.html:389 +#: controls/templates/controls/controls_detail_mobile.html:435 +#, fuzzy +#| msgid "Quantités de la remarque" +msgid "Suivis de la remarque" +msgstr "Remark quantities" + +#: controls/templates/controls/controls_detail_content.html:399 +#: controls/templates/controls/controls_detail_content.html:436 +#: controls/templates/controls/controls_detail_mobile.html:445 +#: controls/templates/controls/controls_detail_mobile.html:482 +#: controls/templates/controls/remark_form_content.html:39 +msgid "par" +msgstr "" + +#: controls/templates/controls/controls_detail_content.html:412 +#: controls/templates/controls/controls_detail_content.html:449 +#: controls/templates/controls/controls_detail_content.html:650 +#: controls/templates/controls/controls_detail_mobile.html:458 +#: controls/templates/controls/controls_detail_mobile.html:495 +#: controls/templates/controls/controls_detail_mobile.html:690 +msgid "Image de la remarque" +msgstr "Remark image" + +#: controls/templates/controls/controls_detail_content.html:483 +#: controls/templates/controls/controls_detail_mobile.html:530 +msgid "Quantités" +msgstr "Quantities" + +#: controls/templates/controls/controls_detail_content.html:489 +#: controls/templates/controls/controls_detail_mobile.html:536 +msgid "Poste / Description" +msgstr "Position / Description" + +#: controls/templates/controls/controls_detail_content.html:490 +#: controls/templates/controls/controls_detail_mobile.html:537 +msgid "Total" +msgstr "Total" + +#: controls/templates/controls/controls_detail_content.html:533 +#: controls/templates/controls/controls_detail_mobile.html:579 +msgid "Aucune remarque enregistrée." +msgstr "No remarks recorded." + +#: controls/templates/controls/controls_detail_content.html:549 +#: controls/templates/controls/controls_detail_mobile.html:594 +msgid "Produire un rapport PDF" +msgstr "Generate a PDF report" + +#: controls/templates/controls/controls_detail_content.html:559 +#: controls/templates/controls/controls_detail_mobile.html:603 +msgid "Rapports" +msgstr "Reports" + +#: controls/templates/controls/controls_detail_content.html:566 +#: controls/templates/controls/controls_detail_mobile.html:610 +msgid "Généré le" +msgstr "Generated on" + +#: controls/templates/controls/controls_detail_content.html:573 +#: controls/templates/controls/controls_detail_mobile.html:617 +msgid "Télécharger" +msgstr "Download" + +#: controls/templates/controls/controls_detail_content.html:576 +#: controls/templates/controls/controls_detail_mobile.html:620 +msgid "Partager" +msgstr "Share" + +#: controls/templates/controls/controls_detail_content.html:583 +#: controls/templates/controls/controls_detail_mobile.html:627 +msgid "Aucun rapport sauvegardé." +msgstr "No report saved." + +#: controls/templates/controls/controls_detail_content.html:591 +#: controls/templates/controls/controls_detail_mobile.html:635 +msgid "Template" +msgstr "Template" + +#: controls/templates/controls/controls_detail_content.html:593 +#: controls/templates/controls/controls_detail_mobile.html:637 +msgid "Éditer le template" +msgstr "Edit template" + +#: controls/templates/controls/controls_detail_content.html:603 +#: controls/templates/controls/controls_detail_mobile.html:647 +msgid "Archives" +msgstr "Archives" + +#: controls/templates/controls/controls_detail_content.html:688 +#: controls/templates/controls/controls_detail_mobile.html:727 +msgid "Ajouter un plan de référence" +msgstr "Add a reference plan" + +#: controls/templates/controls/controls_detail_content.html:696 +#: controls/templates/controls/controls_detail_mobile.html:736 +msgid "Ajouter un Plan de Référence" +msgstr "Add a Reference Plan" + +#: controls/templates/controls/controls_detail_content.html:701 +#: controls/templates/controls/controls_detail_mobile.html:741 +msgid "Nom du Plan" +msgstr "Plan Name" + +#: controls/templates/controls/controls_detail_content.html:705 +#: controls/templates/controls/controls_detail_mobile.html:745 +msgid "Sélectionner un Fichier (PDF ou Image)" +msgstr "Select a File (PDF or Image)" + +#: controls/templates/controls/controls_detail_content.html:708 +#: controls/templates/controls/controls_detail_mobile.html:748 +msgid "Uploader" +msgstr "Upload" + +#: controls/templates/controls/controls_detail_content.html:718 +#: controls/templates/controls/controls_detail_mobile.html:758 +msgid "Confirmer la suppression" +msgstr "Confirm deletion" + +#: controls/templates/controls/controls_detail_content.html:722 +#: controls/templates/controls/controls_detail_mobile.html:762 +#: controls/templates/controls/controls_detail_mobile.html:1074 +msgid "Voulez-vous vraiment supprimer cette image ?" +msgstr "Are you sure you want to delete this image?" + +#: controls/templates/controls/controls_detail_content.html:737 +#: controls/templates/controls/controls_detail_content.html:785 +#: controls/templates/controls/controls_detail_mobile.html:864 +msgid "En ligne" +msgstr "Online" + +#: controls/templates/controls/controls_detail_content.html:740 +#: controls/templates/controls/controls_detail_mobile.html:812 +msgid "en attente" +msgstr "pending" + +#: controls/templates/controls/controls_detail_content.html:742 +#: controls/templates/controls/controls_detail_mobile.html:814 +msgid "Forcer la synchronisation" +msgstr "Force synchronization" + +#: controls/templates/controls/controls_detail_content.html:789 +#: controls/templates/controls/controls_detail_mobile.html:877 +msgid "Connexion faible" +msgstr "" + +#: controls/templates/controls/controls_detail_content.html:792 +#: controls/templates/controls/controls_detail_mobile.html:809 +#: controls/templates/controls/controls_detail_mobile.html:881 +msgid "Hors ligne" +msgstr "Offline" + +#: controls/templates/controls/controls_detail_mobile.html:185 +#: controls/templates/controls/remark_form_content.html:88 +msgid "Prendre des photos (mode continu)" +msgstr "Take photos (continuous mode)" + +#: controls/templates/controls/controls_detail_mobile.html:188 +#: controls/templates/controls/remark_form_content.html:91 +msgid "Photos, vidéos, fichiers" +msgstr "Photos, videos, files" + +#: controls/templates/controls/controls_detail_mobile.html:382 +#: controls/templates/controls/controls_detail_mobile.html:390 +msgid "Valider" +msgstr "Validate" + +#: controls/templates/controls/controls_detail_mobile.html:386 +#: controls/templates/controls/controls_detail_mobile.html:394 +msgid "Marquer NOK" +msgstr "Mark as NOK" + +#: controls/templates/controls/controls_detail_mobile.html:655 +msgid "Restaurer la remarque" +msgstr "Restore remark" + +#: controls/templates/controls/controls_detail_mobile.html:778 +#: controls/templates/controls/remarks_form.html:70 +#: controls/templates/controls/remarks_form_mobile.html:70 +msgid "Caméra" +msgstr "Camera" + +#: controls/templates/controls/controls_detail_mobile.html:793 +#: controls/templates/controls/remarks_form.html:85 +#: controls/templates/controls/remarks_form_mobile.html:85 +msgid "Terminer" +msgstr "Finish" + +#: controls/templates/controls/controls_detail_mobile.html:796 +#: controls/templates/controls/remarks_form.html:88 +#: controls/templates/controls/remarks_form_mobile.html:88 +msgid "Capturer" +msgstr "Capture" + +#: controls/templates/controls/controls_detail_mobile.html:961 +msgid "En attente de synchronisation" +msgstr "Pending synchronization" + +#: controls/templates/controls/controls_detail_mobile.html:966 +msgid "Modifier" +msgstr "Edit" + +#: controls/templates/controls/controls_detail_mobile.html:988 +msgid "Créé localement le" +msgstr "Created locally on" + +#: controls/templates/controls/controls_detail_mobile.html:1048 +#: controls/templates/controls/remark_form_content.html:155 +msgid "Mettre à jour" +msgstr "Update" + +#: controls/templates/controls/controls_detail_mobile.html:1056 +msgid "Voulez-vous vraiment supprimer cette remarque ?" +msgstr "Are you sure you want to delete this remark?" + +#: controls/templates/controls/controls_detail_mobile.html:1066 +msgid "Erreur lors de la suppression de la remarque" +msgstr "Error deleting the remark" + +#: controls/templates/controls/controls_detail_mobile.html:1083 +msgid "Erreur lors de la suppression de l\\'image" +msgstr "Error deleting the image" + +#: controls/templates/controls/controls_detail_mobile.html:1099 +msgid "Action non disponible hors ligne" +msgstr "Action not available offline" + +#: controls/templates/controls/controls_form_content.html:180 +msgid "Modifier le contrôle" +msgstr "Edit control" + +#: controls/templates/controls/controls_form_content.html:182 +msgid "Ajouter un nouveau contrôle" +msgstr "Add a new control" + +#: controls/templates/controls/controls_form_content.html:196 +#: controls/templates/controls/controls_form_content.html:205 +msgid "Non modifiable" +msgstr "" + +#: controls/templates/controls/controls_form_content.html:204 +msgid "Généré automatiquement (CTRL-XXXX)" +msgstr "" + +#: controls/templates/controls/controls_form_content.html:211 +msgid "Le code sera généré automatiquement lors de la création." +msgstr "" + +#: controls/templates/controls/controls_form_content.html:220 +msgid "Erreurs :" +msgstr "Errors:" + +#: controls/templates/controls/controls_form_content.html:233 +msgid "Le formulaire contient des erreurs." +msgstr "The form contains errors." + +#: controls/templates/controls/controls_form_content.html:234 +msgid "Veuillez corriger les champs marqués en rouge ci-dessous." +msgstr "Please correct the fields marked in red below." + +#: controls/templates/controls/controls_form_content.html:271 +msgid "Facultatif" +msgstr "Optional" + +#: controls/templates/controls/controls_form_content.html:277 +msgid "Rechercher une intervention..." +msgstr "Search for an intervention..." + +#: controls/templates/controls/controls_form_content.html:374 +msgid "Position géographique" +msgstr "Geographic position" + +#: controls/templates/controls/controls_form_content.html:380 +msgid "Rechercher une adresse..." +msgstr "Search for an address..." + +#: controls/templates/controls/controls_form_content.html:384 +#: controls/templates/controls/controls_form_content.html:580 +#: controls/templates/controls/controls_form_content.html:586 +msgid "Ma position" +msgstr "My location" + +#: controls/templates/controls/controls_form_content.html:387 +msgid "Effacer" +msgstr "Clear" + +#: controls/templates/controls/controls_form_content.html:558 +msgid "Aucun résultat trouvé pour cette adresse" +msgstr "No results found for this address" + +#: controls/templates/controls/controls_form_content.html:563 +msgid "Erreur lors de la recherche d\\'adresse" +msgstr "Error during address search" + +#: controls/templates/controls/controls_form_content.html:573 +msgid "Localisation..." +msgstr "Location..." + +#: controls/templates/controls/controls_form_content.html:584 +msgid "" +"Impossible d\\'accéder à votre position. Assurez-vous que vous avez autorisé " +"l\\'accès à la localisation." +msgstr "" +"Unable to access your location. Make sure you have allowed location access." + +#: controls/templates/controls/controls_form_content.html:595 +msgid "La géolocalisation n\\'est pas disponible dans votre navigateur" +msgstr "Geolocation is not available in your browser" + +#: controls/templates/controls/controls_form_content.html:770 +msgid "Aucun résultat" +msgstr "No results" + +#: controls/templates/controls/controls_form_content.html:824 +msgid "Retirer" +msgstr "Remove" + +#: controls/templates/controls/controls_form_mobile.html:20 +msgid "Modifier le Contrôle" +msgstr "Edit Control" + +#: controls/templates/controls/controls_form_mobile.html:20 +msgid "Nouveau Contrôle" +msgstr "New Control" + +#: controls/templates/controls/controls_index.html:13 +msgid "Nouveau contrôle" +msgstr "New control" + +#: controls/templates/controls/controls_index.html:33 +msgid "Code" +msgstr "Code" + +#: controls/templates/controls/controls_index.html:37 +msgid "Mise à jour" +msgstr "Update" + +#: controls/templates/controls/controls_index_mobile.html:19 +msgid "Ajouter un contrôle" +msgstr "Add a control" + +#: controls/templates/controls/controls_install_mobile.html:9 +msgid "Installer l'application pour les contrôles" +msgstr "Install the application for controls" + +#: controls/templates/controls/controls_install_mobile.html:18 +msgid "StreetUp - Contrôles" +msgstr "StreetUp - Controls" + +#: controls/templates/controls/controls_install_mobile.html:19 +msgid "" +"Pour une meilleure expérience, installez l'application sur votre appareil." +msgstr "For a better experience, install the application on your device." + +#: controls/templates/controls/controls_install_mobile.html:21 +msgid "Installer l'application" +msgstr "Install application" + +#: controls/templates/controls/controls_install_mobile.html:24 +msgid "Désolé, votre navigateur ne supporte pas l'installation d'applications." +msgstr "Sorry, your browser does not support installing applications." + +#: controls/templates/controls/offline.html:8 +#, fuzzy +#| msgid "Modifier le Contrôle" +msgid "Hors ligne — Contrôles" +msgstr "Edit Control" + +#: controls/templates/controls/offline.html:23 +msgid "Vous êtes hors ligne" +msgstr "" + +#: controls/templates/controls/offline.html:25 +msgid "Cette page n'est pas disponible sans connexion réseau." +msgstr "" + +#: controls/templates/controls/offline.html:33 +msgid "Reprendre le contrôle en cours" +msgstr "" + +#: controls/templates/controls/offline.html:38 +msgid "Liste des contrôles (depuis le cache)" +msgstr "" + +#: controls/templates/controls/offline.html:44 +msgid "" +"Les pages déjà visitées lors de votre dernière connexion sont disponibles " +"hors ligne." +msgstr "" + +#: controls/templates/controls/remark_form_content.html:29 +msgid "Historique des suivis" +msgstr "" + +#: controls/templates/controls/remark_form_content.html:67 +msgid "Description / Commentaire de suivi" +msgstr "" + +#: controls/templates/controls/remark_form_content.html:76 +msgid "Enregistrer comme un nouveau suivi dans l'historique" +msgstr "" + +#: controls/templates/controls/remark_form_content.html:79 +msgid "" +"Décochez uniquement si vous souhaitez corriger une faute ou modifier la " +"saisie précédente sans ajouter de nouvelle étape à l'historique." +msgstr "" + +#: controls/templates/controls/remarks_form.html:60 +#: controls/templates/controls/remarks_form_mobile.html:50 +#: controls/templates/controls/remarks_form_mobile.html:58 +#: controls/views_reports_pdf.py:703 +msgid "Remarque" +msgstr "Remark" + +#: controls/templates/controls/remarks_form_mobile.html:52 +msgid "Nouvelle Remarque" +msgstr "New Remark" + +#: controls/views.py:352 +msgid "Le projet spécifié n'existe pas." +msgstr "The specified project does not exist." + +#: controls/views.py:421 +msgid "L'intervention spécifiée n'existe pas." +msgstr "The specified intervention does not exist." + +#: controls/views.py:667 controls/views.py:942 +msgid "Remarque sauvegardée avec succès !" +msgstr "Remark saved successfully!" + +#: controls/views.py:672 +msgid "Erreur lors de la sauvegarde de la remarque." +msgstr "Error saving the remark." + +#: controls/views.py:732 +msgid "Plan ajouté avec succès !" +msgstr "Plan added successfully!" + +#: controls/views.py:735 +msgid "Erreur lors de la sauvegarde du plan." +msgstr "Error saving the plan." + +#: controls/views.py:884 controls/views.py:1007 controls/views.py:1043 +#: controls/views.py:1055 controls/views.py:1071 +msgid "Vous n'avez pas le droit de modifier cette remarque." +msgstr "You do not have permission to modify this remark." + +#: controls/views.py:1018 +#, python-format +msgid "Statut modifié en %s" +msgstr "" + +#: controls/views.py:1032 +msgid "Remarque introuvable" +msgstr "Remark not found" + +#: controls/views.py:1034 +msgid "Requête invalide" +msgstr "Invalid request" + +#: controls/views.py:1094 +msgid "Vous n'avez pas le droit de modifier ce contrôle." +msgstr "You do not have permission to modify this control." + +#: controls/views_reports_pdf.py:47 +#, python-format +msgid "Erreur de chargement d'image: %(e)s" +msgstr "Image loading error: %(e)s" + +#: controls/views_reports_pdf.py:359 +msgid "Rapport de suivi" +msgstr "Follow-up report" + +#: controls/views_reports_pdf.py:367 +msgid "Page" +msgstr "Page" + +#: controls/views_reports_pdf.py:409 +msgid "Description : " +msgstr "Description: " + +#: controls/views_reports_pdf.py:504 +#, fuzzy, python-format +#| msgid "Erreur de chargement d'image: %(e)s" +msgid "Erreur lors du chargement ou du dessin sur le plan %(name)s: %(e)s" +msgstr "Image loading error: %(e)s" + +#: controls/views_reports_pdf.py:742 +msgid "Inconnu" +msgstr "Unknown" + +#: controls/views_reports_pdf.py:837 +msgid "PDF généré et sauvegardé avec succès." +msgstr "PDF generated and saved successfully." + +#: controls/views_reports_pdf.py:844 +msgid "Erreur lors de la génération du PDF" +msgstr "Error generating the PDF" + +#~ msgid "Prendre une photo" +#~ msgstr "Take a photo" diff --git a/streetup/controls/locale/en/LC_MESSAGES/djangojs.mo b/streetup/controls/locale/en/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..1a7ccce Binary files /dev/null and b/streetup/controls/locale/en/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/controls/locale/en/LC_MESSAGES/djangojs.po b/streetup/controls/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..5c282c1 --- /dev/null +++ b/streetup/controls/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,146 @@ +# English translations for the Controls module (JavaScript). +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: 2025-12-04 13:00+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: English \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: controls/static/controls/controls_detail.js:797 +msgid "Rapport généré avec succès !" +msgstr "Report generated successfully!" + +#: controls/static/controls/controls_detail.js:814 +msgid "Généré le" +msgstr "Generated on" + +#: controls/static/controls/controls_detail.js:815 +msgid "Auteur" +msgstr "Author" + +#: controls/static/controls/controls_detail.js:820 +msgid "Télécharger" +msgstr "Download" + +#: controls/static/controls/controls_detail.js:824 +msgid "Partager" +msgstr "Share" + +#: controls/static/controls/controls_detail.js:841 +msgid "Erreur lors de la génération du rapport." +msgstr "Error generating report." + +#: controls/static/controls/controls_detail.js:846 +#: controls/static/controls/controls_detail.js:1040 +#: controls/static/controls/controls_detail.js:1056 +msgid "Une erreur est survenue." +msgstr "An error occurred." + +#: controls/static/controls/controls_detail.js:870 +msgid "Erreur lors de l'archivage." +msgstr "Error during archiving." + +#: controls/static/controls/controls_detail.js:901 +#: controls/static/controls/controls_detail.js:976 +#: controls/static/controls/controls_detail.js:1055 +msgid "Erreur :" +msgstr "Error:" + +#: controls/static/controls/controls_detail.js:902 +#: controls/static/controls/controls_detail.js:977 +msgid "Une erreur est survenue lors de l'archivage." +msgstr "An error occurred during archiving." + +#: controls/static/controls/controls_detail.js:918 +msgid "Erreur lors de la restauration de la remarque." +msgstr "Error restoring the remark." + +#: controls/static/controls/controls_detail.js:996 +msgid "Partager ce PDF" +msgstr "Share this PDF" + +#: controls/static/controls/controls_detail.js:997 +msgid "Voici un rapport PDF que je souhaite partager avec vous." +msgstr "Here is a PDF report I would like to share with you." + +#: controls/static/controls/controls_detail.js:1000 +msgid "Le partage de fichiers n'est pas supporté sur ce navigateur." +msgstr "File sharing is not supported on this browser." + +#: controls/static/controls/controls_detail.js:1003 +msgid "Erreur lors du partage du PDF:" +msgstr "Error sharing PDF:" + +#: controls/static/controls/controls_detail.js:1629 +#: controls/static/controls/remarks_form.js:118 +msgid "La caméra avancée n'est pas supportée sur ce navigateur." +msgstr "Advanced camera is not supported on this browser." + +#: controls/static/controls/controls_detail.js:1634 +#: controls/static/controls/remarks_form.js:122 +msgid "Initialisation de la caméra…" +msgstr "Initializing camera…" + +#: controls/static/controls/controls_detail.js:1650 +#: controls/static/controls/remarks_form.js:160 +#: controls/static/controls/remarks_form.js:188 +msgid "Caméra prête. Appuyez sur Capturer pour prendre une photo." +msgstr "Camera ready. Press Capture to take a photo." + +#: controls/static/controls/controls_detail.js:1654 +#: controls/static/controls/remarks_form.js:224 +msgid "Impossible d'accéder à la caméra. Veuillez vérifier les autorisations." +msgstr "Unable to access the camera. Please check permissions." + +#: controls/static/controls/controls_detail.js:1655 +#: controls/static/controls/remarks_form.js:228 +msgid "Impossible d'accéder à la caméra." +msgstr "Unable to access the camera." + +#: controls/static/controls/controls_detail.js:1677 +#: controls/static/controls/remarks_form.js:250 +msgid "La vidéo n'est pas encore prête." +msgstr "The video is not ready yet." + +#: controls/static/controls/controls_detail.js:1689 +#: controls/static/controls/remarks_form.js:262 +msgid "Erreur lors de la capture." +msgstr "Error during capture." + +#: controls/static/controls/controls_detail.js:1698 +#: controls/static/controls/remarks_form.js:271 +msgid "Photo capturée avec succès." +msgstr "Photo captured successfully." + +#: controls/static/controls/remarks_form.js:50 +msgid "" +"L'enregistrement a pris trop de temps. Vérifiez votre connexion et réessayez." +msgstr "" + +#: controls/static/controls/remarks_form.js:209 +msgid "" +"Accès à la caméra refusé. Pour l'autoriser :\n" +"• Android : Réglages → Applications → [Navigateur] → Autorisations → Caméra\n" +"• iPhone : Réglages → Safari → Caméra → Autoriser" +msgstr "" + +#: controls/static/controls/remarks_form.js:216 +msgid "" +"L'accès à la caméra a été refusé. Veuillez appuyer sur 'Autoriser' lors de " +"la prochaine demande, ou vérifiez les autorisations dans les réglages de " +"votre navigateur." +msgstr "" + +#: controls/static/controls/remarks_form.js:222 +msgid "Aucune caméra disponible sur cet appareil." +msgstr "" diff --git a/streetup/controls/locale/fr/LC_MESSAGES/django.mo b/streetup/controls/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..fdc372b Binary files /dev/null and b/streetup/controls/locale/fr/LC_MESSAGES/django.mo differ diff --git a/streetup/controls/locale/fr/LC_MESSAGES/django.po b/streetup/controls/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..6407654 --- /dev/null +++ b/streetup/controls/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,1054 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: streetup/controls/forms.py:39 streetup/controls/models.py:53 +#: streetup/controls/views_reports_pdf.py:414 +msgid "Localisation" +msgstr "Localisation" + +#: streetup/controls/forms.py:39 streetup/controls/forms.py:40 +#: streetup/controls/forms.py:41 streetup/controls/forms.py:42 +#: streetup/controls/forms.py:43 streetup/controls/forms.py:44 +#: streetup/controls/templates/controls/controls_form_content.html:374 +msgid "facultatif" +msgstr "facultatif" + +#: streetup/controls/forms.py:40 streetup/controls/models.py:49 +#: streetup/controls/templates/controls/controls_detail_content.html:49 +msgid "Catégories" +msgstr "Catégories" + +#: streetup/controls/forms.py:41 +#: streetup/controls/templates/controls/controls_form_content.html:271 +msgid "Interventions liées" +msgstr "Interventions liées" + +#: streetup/controls/forms.py:42 streetup/controls/models.py:50 +#: streetup/controls/templates/controls/controls_detail_content.html:60 +#: streetup/controls/templates/controls/controls_detail_mobile.html:95 +#: streetup/controls/templates/controls/controls_index_mobile.html:40 +msgid "Projet" +msgstr "Projet" + +#: streetup/controls/forms.py:43 +msgid "Phase du projet" +msgstr "Phase du projet" + +#: streetup/controls/forms.py:44 streetup/controls/models.py:45 +#: streetup/controls/templates/controls/controls_detail_content.html:89 +#: streetup/controls/templates/controls/controls_detail_content.html:132 +#: streetup/controls/templates/controls/controls_detail_mobile.html:126 +#: streetup/controls/templates/controls/controls_detail_mobile.html:172 +#: streetup/controls/templates/controls/controls_index.html:34 +msgid "Description" +msgstr "Description" + +#: streetup/controls/forms.py:155 +msgid "Entrez les notes de début ..." +msgstr "Entrez les notes de début ..." + +#: streetup/controls/forms.py:160 +msgid "Entrez les notes de fin ..." +msgstr "Entrez les notes de fin ..." + +#: streetup/controls/forms.py:172 +msgid "Chercher un poste (autocomplete)..." +msgstr "Chercher un poste (autocomplete)..." + +#: streetup/controls/forms.py:189 +msgid "Description si pas de marché" +msgstr "Description si pas de marché" + +#: streetup/controls/forms.py:231 +msgid "Sélectionner un marché" +msgstr "Sélectionner un marché" + +#: streetup/controls/models.py:21 +msgid "En cours" +msgstr "En cours" + +#: streetup/controls/models.py:22 +msgid "Information" +msgstr "Information" + +#: streetup/controls/models.py:23 +msgid "A traiter" +msgstr "A traiter" + +#: streetup/controls/models.py:24 streetup/controls/models.py:229 +msgid "Traité" +msgstr "Traité" + +#: streetup/controls/models.py:25 streetup/controls/models.py:175 +msgid "Validé" +msgstr "Validé" + +#: streetup/controls/models.py:26 streetup/controls/models.py:177 +#: streetup/controls/models.py:231 +msgid "Annulé" +msgstr "Annulé" + +#: streetup/controls/models.py:30 +#: streetup/controls/templates/controls/controls_detail.html:33 +#: streetup/controls/templates/controls/controls_detail_content.html:21 +#: streetup/controls/views_reports_pdf.py:402 +msgid "Contrôle" +msgstr "Contrôle" + +#: streetup/controls/models.py:31 +msgid "Rapport de chantier" +msgstr "Rapport de chantier" + +#: streetup/controls/models.py:32 +msgid "Constat" +msgstr "Constat" + +#: streetup/controls/models.py:33 +msgid "Rapport de réunion" +msgstr "Rapport de réunion" + +#: streetup/controls/models.py:37 +msgid "Privé" +msgstr "Privé" + +#: streetup/controls/models.py:38 +msgid "Interne" +msgstr "Interne" + +#: streetup/controls/models.py:39 +msgid "Interne et externe" +msgstr "Interne et externe" + +#: streetup/controls/models.py:44 +#: streetup/controls/templates/controls/controls_form_content.html:191 +msgid "Code du contrôle" +msgstr "Code du contrôle" + +#: streetup/controls/models.py:46 +#: streetup/controls/templates/controls/controls_detail_content.html:33 +#: streetup/controls/templates/controls/controls_detail_mobile.html:84 +#: streetup/controls/views_reports_pdf.py:704 +msgid "Statut" +msgstr "Statut" + +#: streetup/controls/models.py:47 +msgid "Type de contrôle" +msgstr "Type de contrôle" + +#: streetup/controls/models.py:48 +#: streetup/controls/templates/controls/controls_detail_content.html:41 +msgid "Thématiques" +msgstr "Thématiques" + +#: streetup/controls/models.py:51 +#: streetup/controls/templates/controls/controls_detail_content.html:68 +#: streetup/controls/templates/controls/controls_detail_mobile.html:103 +msgid "Phase" +msgstr "Phase" + +#: streetup/controls/models.py:52 +msgid "Interventions" +msgstr "Interventions" + +#: streetup/controls/models.py:54 +msgid "Géométrie" +msgstr "Géométrie" + +#: streetup/controls/models.py:55 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: streetup/controls/models.py:56 +msgid "Longitude" +msgstr "Longitude" + +#: streetup/controls/models.py:57 +msgid "Latitude" +msgstr "Latitude" + +#: streetup/controls/models.py:61 +msgid "Visibilité" +msgstr "Visibilité" + +#: streetup/controls/models.py:62 +msgid "Edition partagée" +msgstr "Edition partagée" + +#: streetup/controls/models.py:136 +msgid "Plan as-built" +msgstr "Plan as-built" + +#: streetup/controls/models.py:137 +msgid "Fiches techniques" +msgstr "Fiches techniques" + +#: streetup/controls/models.py:174 +msgid "Actif" +msgstr "Actif" + +#: streetup/controls/models.py:176 +msgid "Archivé" +msgstr "Archivé" + +#: streetup/controls/models.py:227 +#: streetup/controls/templates/controls/controls_detail_content.html:125 +#: streetup/controls/templates/controls/controls_detail_mobile.html:163 +#: streetup/controls/templates/controls/controls_detail_mobile.html:956 +msgid "Info" +msgstr "Info" + +#: streetup/controls/models.py:228 +#: streetup/controls/templates/controls/controls_detail_content.html:127 +#: streetup/controls/templates/controls/controls_detail_mobile.html:165 +#: streetup/controls/templates/controls/controls_detail_mobile.html:956 +msgid "NOK" +msgstr "NOK" + +#: streetup/controls/models.py:230 +#: streetup/controls/templates/controls/controls_detail_content.html:126 +#: streetup/controls/templates/controls/controls_detail_mobile.html:164 +#: streetup/controls/templates/controls/controls_detail_mobile.html:955 +msgid "OK" +msgstr "OK" + +#: streetup/controls/models.py:258 +#, python-format +msgid "Remarque %(num)s pour %(control)s" +msgstr "Remarque %(num)s pour %(control)s" + +#: streetup/controls/permissions.py:56 +msgid "Vous n'êtes pas autorisé à accéder à ce contrôle." +msgstr "Vous n'êtes pas autorisé à accéder à ce contrôle." + +#: streetup/controls/permissions.py:73 +msgid "Vous n'êtes pas autorisé à ajouter un contrôle." +msgstr "Vous n'êtes pas autorisé à ajouter un contrôle." + +#: streetup/controls/permissions.py:108 +msgid "Vous n'êtes pas autorisé à modifier ce contrôle." +msgstr "Vous n'êtes pas autorisé à modifier ce contrôle." + +#: streetup/controls/templates/controls/change_template.html:10 +#: streetup/controls/templates/controls/change_template_mobile.html:21 +msgid "Modifier le template de rapport" +msgstr "Modifier le template de rapport" + +#: streetup/controls/templates/controls/change_template.html:18 +#: streetup/controls/templates/controls/change_template_mobile.html:29 +msgid "Notes de début" +msgstr "Notes de début" + +#: streetup/controls/templates/controls/change_template.html:24 +#: streetup/controls/templates/controls/change_template_mobile.html:35 +msgid "Notes de fin" +msgstr "Notes de fin" + +#: streetup/controls/templates/controls/change_template.html:30 +#: streetup/controls/templates/controls/change_template_mobile.html:41 +#: streetup/controls/templates/controls/controls_detail_content.html:725 +#: streetup/controls/templates/controls/controls_detail_mobile.html:765 +#: streetup/controls/templates/controls/controls_form_content.html:407 +#: streetup/controls/templates/controls/remark_form_content.html:161 +#: streetup/controls/templates/controls/remark_form_content.html:163 +msgid "Annuler" +msgstr "Annuler" + +#: streetup/controls/templates/controls/change_template.html:31 +#: streetup/controls/templates/controls/change_template_mobile.html:42 +#: streetup/controls/templates/controls/controls_form_content.html:403 +#: streetup/controls/templates/controls/remark_form_content.html:155 +msgid "Enregistrer" +msgstr "Enregistrer" + +#: streetup/controls/templates/controls/control_report.html:10 +#: streetup/controls/templates/controls/control_report.html:39 +msgid "Rapport de Contrôle" +msgstr "Rapport de Contrôle" + +#: streetup/controls/templates/controls/control_report.html:41 +msgid "Numéro du rapport" +msgstr "Numéro du rapport" + +#: streetup/controls/templates/controls/control_report.html:42 +#: streetup/controls/views_reports_pdf.py:415 +#: streetup/controls/views_reports_pdf.py:702 +msgid "Date" +msgstr "Date" + +#: streetup/controls/templates/controls/control_report.html:48 +msgid "Statut du Projet" +msgstr "Statut du Projet" + +#: streetup/controls/templates/controls/control_report.html:53 +msgid "Photos d'Avancement" +msgstr "Photos d'Avancement" + +#: streetup/controls/templates/controls/control_report.html:62 +msgid "Observations" +msgstr "Observations" + +#: streetup/controls/templates/controls/control_report.html:77 +msgid "Rapport généré par" +msgstr "Rapport généré par" + +#: streetup/controls/templates/controls/control_report.html:80 +msgid "Télécharger le PDF" +msgstr "Télécharger le PDF" + +#: streetup/controls/templates/controls/controls_base_mobile.html:12 +#: streetup/controls/templates/controls/controls_base_mobile.html:63 +#: streetup/controls/templates/controls/controls_index.html:19 +#: streetup/controls/templates/controls/controls_index_mobile.html:13 +msgid "Contrôles" +msgstr "Contrôles" + +#: streetup/controls/templates/controls/controls_base_mobile.html:66 +#: streetup/controls/templates/controls/controls_projects_mobile.html:17 +msgid "Projets" +msgstr "Projets" + +#: streetup/controls/templates/controls/controls_detail_content.html:13 +#: streetup/controls/templates/controls/controls_detail_content.html:719 +#: streetup/controls/templates/controls/controls_detail_mobile.html:68 +#: streetup/controls/templates/controls/controls_detail_mobile.html:136 +#: streetup/controls/templates/controls/controls_detail_mobile.html:734 +#: streetup/controls/templates/controls/controls_detail_mobile.html:759 +#: streetup/controls/templates/controls/controls_detail_mobile.html:780 +#: streetup/controls/templates/controls/remarks_form.html:72 +#: streetup/controls/templates/controls/remarks_form_mobile.html:72 +msgid "Fermer" +msgstr "Fermer" + +#: streetup/controls/templates/controls/controls_detail_content.html:26 +#: streetup/controls/templates/controls/controls_detail_mobile.html:78 +msgid "Modifier le template" +msgstr "Modifier le template" + +#: streetup/controls/templates/controls/controls_detail_content.html:37 +#: streetup/controls/templates/controls/controls_detail_mobile.html:88 +#: streetup/controls/templates/controls/controls_index.html:36 +#: streetup/controls/templates/controls/controls_index_mobile.html:33 +msgid "Type" +msgstr "Type" + +#: streetup/controls/templates/controls/controls_detail_content.html:45 +#: streetup/controls/templates/controls/controls_detail_content.html:54 +msgid "Aucune" +msgstr "Aucune" + +#: streetup/controls/templates/controls/controls_detail_content.html:74 +#: streetup/controls/templates/controls/controls_detail_mobile.html:109 +msgid "Intervention" +msgstr "Intervention" + +#: streetup/controls/templates/controls/controls_detail_content.html:80 +#: streetup/controls/templates/controls/controls_detail_mobile.html:117 +msgid "Créé le" +msgstr "Créé le" + +#: streetup/controls/templates/controls/controls_detail_content.html:81 +#: streetup/controls/templates/controls/controls_detail_mobile.html:118 +#: streetup/controls/templates/controls/controls_index_mobile.html:37 +msgid "Mis à jour le" +msgstr "Mis à jour le" + +#: streetup/controls/templates/controls/controls_detail_content.html:85 +#: streetup/controls/templates/controls/controls_detail_content.html:567 +#: streetup/controls/templates/controls/controls_detail_mobile.html:122 +#: streetup/controls/templates/controls/controls_detail_mobile.html:611 +#: streetup/controls/templates/controls/controls_index.html:35 +msgid "Auteur" +msgstr "Auteur" + +#: streetup/controls/templates/controls/controls_detail_content.html:92 +#: streetup/controls/templates/controls/controls_detail_mobile.html:114 +#: streetup/controls/templates/controls/controls_index_mobile.html:36 +msgid "Lieu" +msgstr "Lieu" + +#: streetup/controls/templates/controls/controls_detail_content.html:108 +#: streetup/controls/templates/controls/controls_detail_mobile.html:145 +#: streetup/controls/templates/controls/remark_form_content.html:12 +msgid "Carte" +msgstr "Carte" + +#: streetup/controls/templates/controls/controls_detail_content.html:114 +#: streetup/controls/templates/controls/controls_detail_content.html:316 +#: streetup/controls/templates/controls/controls_detail_mobile.html:151 +#: streetup/controls/templates/controls/controls_detail_mobile.html:362 +msgid "Ajouter une remarque" +msgstr "Ajouter une remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:135 +#: streetup/controls/templates/controls/controls_detail_mobile.html:175 +msgid "Veuillez remplir la description." +msgstr "Veuillez remplir la description." + +#: streetup/controls/templates/controls/controls_detail_content.html:142 +#: streetup/controls/templates/controls/controls_detail_mobile.html:182 +#: streetup/controls/templates/controls/remark_form_content.html:85 +msgid "Images" +msgstr "Images" + +#: streetup/controls/templates/controls/controls_detail_content.html:145 +#: streetup/controls/templates/controls/controls_detail_mobile.html:194 +msgid "Veuillez ajouter au moins une image." +msgstr "Veuillez ajouter au moins une image." + +#: streetup/controls/templates/controls/controls_detail_content.html:154 +#: streetup/controls/templates/controls/controls_detail_mobile.html:203 +msgid "Tags" +msgstr "Tags" + +#: streetup/controls/templates/controls/controls_detail_content.html:170 +#: streetup/controls/templates/controls/controls_detail_mobile.html:224 +#: streetup/controls/templates/controls/remark_form_content.html:117 +msgid "Quantités liées à la remarque" +msgstr "Quantités liées à la remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:172 +#: streetup/controls/templates/controls/controls_detail_mobile.html:226 +#: streetup/controls/templates/controls/remark_form_content.html:119 +msgid "Ajouter des quantités" +msgstr "Ajouter des quantités" + +#: streetup/controls/templates/controls/controls_detail_content.html:178 +#: streetup/controls/templates/controls/controls_detail_mobile.html:232 +#: streetup/controls/templates/controls/remark_form_content.html:125 +msgid "Aucune quantité pour l'instant." +msgstr "Aucune quantité pour l'instant." + +#: streetup/controls/templates/controls/controls_detail_content.html:207 +#: streetup/controls/templates/controls/controls_detail_mobile.html:255 +msgid "Ajouter" +msgstr "Ajouter" + +#: streetup/controls/templates/controls/controls_detail_content.html:218 +#: streetup/controls/templates/controls/controls_detail_mobile.html:266 +#: streetup/controls/templates/controls/remark_form_content.html:171 +msgid "Quantités de la remarque" +msgstr "Quantités de la remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:233 +#: streetup/controls/templates/controls/controls_detail_mobile.html:281 +#: streetup/controls/templates/controls/remark_form_content.html:186 +msgid "Ligne" +msgstr "Ligne" + +#: streetup/controls/templates/controls/controls_detail_content.html:235 +#: streetup/controls/templates/controls/controls_detail_content.html:419 +#: streetup/controls/templates/controls/controls_detail_content.html:456 +#: streetup/controls/templates/controls/controls_detail_content.html:657 +#: streetup/controls/templates/controls/controls_detail_content.html:726 +#: streetup/controls/templates/controls/controls_detail_mobile.html:283 +#: streetup/controls/templates/controls/controls_detail_mobile.html:465 +#: streetup/controls/templates/controls/controls_detail_mobile.html:502 +#: streetup/controls/templates/controls/controls_detail_mobile.html:697 +#: streetup/controls/templates/controls/controls_detail_mobile.html:766 +#: streetup/controls/templates/controls/controls_detail_mobile.html:969 +#: streetup/controls/templates/controls/controls_detail_mobile.html:998 +#: streetup/controls/templates/controls/remark_form_content.html:188 +msgid "Supprimer" +msgstr "Supprimer" + +#: streetup/controls/templates/controls/controls_detail_content.html:241 +#: streetup/controls/templates/controls/controls_detail_mobile.html:289 +#: streetup/controls/templates/controls/remark_form_content.html:194 +msgid "Marché (optionnel)" +msgstr "Marché (optionnel)" + +#: streetup/controls/templates/controls/controls_detail_content.html:247 +#: streetup/controls/templates/controls/controls_detail_mobile.html:295 +#: streetup/controls/templates/controls/remark_form_content.html:200 +msgid "Poste (autocomplete si un marché est choisi)" +msgstr "Poste (autocomplete si un marché est choisi)" + +#: streetup/controls/templates/controls/controls_detail_content.html:255 +#: streetup/controls/templates/controls/controls_detail_mobile.html:303 +#: streetup/controls/templates/controls/remark_form_content.html:208 +msgid "Description (si pas de marché/poste)" +msgstr "Description (si pas de marché/poste)" + +#: streetup/controls/templates/controls/controls_detail_content.html:262 +#: streetup/controls/templates/controls/controls_detail_mobile.html:310 +#: streetup/controls/templates/controls/remark_form_content.html:215 +msgid "Quantité (1)" +msgstr "Quantité (1)" + +#: streetup/controls/templates/controls/controls_detail_content.html:266 +#: streetup/controls/templates/controls/controls_detail_mobile.html:314 +#: streetup/controls/templates/controls/remark_form_content.html:219 +msgid "Quantité (2)" +msgstr "Quantité (2)" + +#: streetup/controls/templates/controls/controls_detail_content.html:270 +#: streetup/controls/templates/controls/controls_detail_mobile.html:318 +#: streetup/controls/templates/controls/remark_form_content.html:223 +msgid "Quantité (3)" +msgstr "Quantité (3)" + +#: streetup/controls/templates/controls/controls_detail_content.html:277 +#: streetup/controls/templates/controls/controls_detail_mobile.html:325 +#: streetup/controls/templates/controls/remark_form_content.html:230 +msgid "Résultat" +msgstr "Résultat" + +#: streetup/controls/templates/controls/controls_detail_content.html:293 +#: streetup/controls/templates/controls/controls_detail_mobile.html:341 +#: streetup/controls/templates/controls/remark_form_content.html:246 +msgid "Ajouter une ligne" +msgstr "Ajouter une ligne" + +#: streetup/controls/templates/controls/controls_detail_content.html:296 +#: streetup/controls/templates/controls/controls_detail_mobile.html:344 +#: streetup/controls/templates/controls/remark_form_content.html:249 +msgid "Terminer et revenir" +msgstr "Terminer et revenir" + +#: streetup/controls/templates/controls/controls_detail_content.html:323 +#: streetup/controls/templates/controls/controls_detail_mobile.html:368 +msgid "Remarques" +msgstr "Remarques" + +#: streetup/controls/templates/controls/controls_detail_content.html:354 +#: streetup/controls/templates/controls/controls_detail_mobile.html:402 +msgid "Éditer la remarque" +msgstr "Éditer la remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:358 +#: streetup/controls/templates/controls/controls_detail_mobile.html:406 +msgid "Archiver la remarque" +msgstr "Archiver la remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:383 +#: streetup/controls/templates/controls/controls_detail_content.html:639 +#: streetup/controls/templates/controls/controls_detail_mobile.html:429 +#: streetup/controls/templates/controls/controls_detail_mobile.html:679 +msgid "Mise à jour par" +msgstr "Mise à jour par" + +#: streetup/controls/templates/controls/controls_detail_content.html:384 +#: streetup/controls/templates/controls/controls_detail_content.html:640 +#: streetup/controls/templates/controls/controls_detail_mobile.html:430 +#: streetup/controls/templates/controls/controls_detail_mobile.html:680 +msgid "le" +msgstr "le" + +#: streetup/controls/templates/controls/controls_detail_content.html:389 +#: streetup/controls/templates/controls/controls_detail_mobile.html:435 +#, fuzzy +#| msgid "Quantités de la remarque" +msgid "Suivis de la remarque" +msgstr "Quantités de la remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:399 +#: streetup/controls/templates/controls/controls_detail_content.html:436 +#: streetup/controls/templates/controls/controls_detail_mobile.html:445 +#: streetup/controls/templates/controls/controls_detail_mobile.html:482 +#: streetup/controls/templates/controls/remark_form_content.html:39 +msgid "par" +msgstr "" + +#: streetup/controls/templates/controls/controls_detail_content.html:412 +#: streetup/controls/templates/controls/controls_detail_content.html:449 +#: streetup/controls/templates/controls/controls_detail_content.html:650 +#: streetup/controls/templates/controls/controls_detail_mobile.html:458 +#: streetup/controls/templates/controls/controls_detail_mobile.html:495 +#: streetup/controls/templates/controls/controls_detail_mobile.html:690 +msgid "Image de la remarque" +msgstr "Image de la remarque" + +#: streetup/controls/templates/controls/controls_detail_content.html:483 +#: streetup/controls/templates/controls/controls_detail_mobile.html:530 +msgid "Quantités" +msgstr "Quantités" + +#: streetup/controls/templates/controls/controls_detail_content.html:489 +#: streetup/controls/templates/controls/controls_detail_mobile.html:536 +msgid "Poste / Description" +msgstr "Poste / Description" + +#: streetup/controls/templates/controls/controls_detail_content.html:490 +#: streetup/controls/templates/controls/controls_detail_mobile.html:537 +msgid "Total" +msgstr "Total" + +#: streetup/controls/templates/controls/controls_detail_content.html:533 +#: streetup/controls/templates/controls/controls_detail_mobile.html:579 +msgid "Aucune remarque enregistrée." +msgstr "Aucune remarque enregistrée." + +#: streetup/controls/templates/controls/controls_detail_content.html:549 +#: streetup/controls/templates/controls/controls_detail_mobile.html:594 +msgid "Produire un rapport PDF" +msgstr "Produire un rapport PDF" + +#: streetup/controls/templates/controls/controls_detail_content.html:559 +#: streetup/controls/templates/controls/controls_detail_mobile.html:603 +msgid "Rapports" +msgstr "Rapports" + +#: streetup/controls/templates/controls/controls_detail_content.html:566 +#: streetup/controls/templates/controls/controls_detail_mobile.html:610 +msgid "Généré le" +msgstr "Généré le" + +#: streetup/controls/templates/controls/controls_detail_content.html:573 +#: streetup/controls/templates/controls/controls_detail_mobile.html:617 +msgid "Télécharger" +msgstr "Télécharger" + +#: streetup/controls/templates/controls/controls_detail_content.html:576 +#: streetup/controls/templates/controls/controls_detail_mobile.html:620 +msgid "Partager" +msgstr "Partager" + +#: streetup/controls/templates/controls/controls_detail_content.html:583 +#: streetup/controls/templates/controls/controls_detail_mobile.html:627 +msgid "Aucun rapport sauvegardé." +msgstr "Aucun rapport sauvegardé." + +#: streetup/controls/templates/controls/controls_detail_content.html:591 +#: streetup/controls/templates/controls/controls_detail_mobile.html:635 +msgid "Template" +msgstr "Template" + +#: streetup/controls/templates/controls/controls_detail_content.html:593 +#: streetup/controls/templates/controls/controls_detail_mobile.html:637 +msgid "Éditer le template" +msgstr "Éditer le template" + +#: streetup/controls/templates/controls/controls_detail_content.html:603 +#: streetup/controls/templates/controls/controls_detail_mobile.html:647 +msgid "Archives" +msgstr "Archives" + +#: streetup/controls/templates/controls/controls_detail_content.html:688 +#: streetup/controls/templates/controls/controls_detail_mobile.html:727 +msgid "Ajouter un plan de référence" +msgstr "Ajouter un plan de référence" + +#: streetup/controls/templates/controls/controls_detail_content.html:696 +#: streetup/controls/templates/controls/controls_detail_mobile.html:736 +msgid "Ajouter un Plan de Référence" +msgstr "Ajouter un Plan de Référence" + +#: streetup/controls/templates/controls/controls_detail_content.html:701 +#: streetup/controls/templates/controls/controls_detail_mobile.html:741 +msgid "Nom du Plan" +msgstr "Nom du Plan" + +#: streetup/controls/templates/controls/controls_detail_content.html:705 +#: streetup/controls/templates/controls/controls_detail_mobile.html:745 +msgid "Sélectionner un Fichier (PDF ou Image)" +msgstr "Sélectionner un Fichier (PDF ou Image)" + +#: streetup/controls/templates/controls/controls_detail_content.html:708 +#: streetup/controls/templates/controls/controls_detail_mobile.html:748 +msgid "Uploader" +msgstr "Uploader" + +#: streetup/controls/templates/controls/controls_detail_content.html:718 +#: streetup/controls/templates/controls/controls_detail_mobile.html:758 +msgid "Confirmer la suppression" +msgstr "Confirmer la suppression" + +#: streetup/controls/templates/controls/controls_detail_content.html:722 +#: streetup/controls/templates/controls/controls_detail_mobile.html:762 +#: streetup/controls/templates/controls/controls_detail_mobile.html:1074 +msgid "Voulez-vous vraiment supprimer cette image ?" +msgstr "Voulez-vous vraiment supprimer cette image ?" + +#: streetup/controls/templates/controls/controls_detail_content.html:737 +#: streetup/controls/templates/controls/controls_detail_content.html:785 +#: streetup/controls/templates/controls/controls_detail_mobile.html:864 +msgid "En ligne" +msgstr "En ligne" + +#: streetup/controls/templates/controls/controls_detail_content.html:740 +#: streetup/controls/templates/controls/controls_detail_mobile.html:812 +msgid "en attente" +msgstr "en attente" + +#: streetup/controls/templates/controls/controls_detail_content.html:742 +#: streetup/controls/templates/controls/controls_detail_mobile.html:814 +msgid "Forcer la synchronisation" +msgstr "Forcer la synchronisation" + +#: streetup/controls/templates/controls/controls_detail_content.html:789 +#: streetup/controls/templates/controls/controls_detail_mobile.html:877 +msgid "Connexion faible" +msgstr "Connexion faible" + +#: streetup/controls/templates/controls/controls_detail_content.html:792 +#: streetup/controls/templates/controls/controls_detail_mobile.html:809 +#: streetup/controls/templates/controls/controls_detail_mobile.html:881 +msgid "Hors ligne" +msgstr "Hors ligne" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:185 +#: streetup/controls/templates/controls/remark_form_content.html:88 +msgid "Prendre des photos (mode continu)" +msgstr "Prendre des photos (mode continu)" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:188 +#: streetup/controls/templates/controls/remark_form_content.html:91 +msgid "Photos, vidéos, fichiers" +msgstr "Photos, vidéos, fichiers" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:382 +#: streetup/controls/templates/controls/controls_detail_mobile.html:390 +msgid "Valider" +msgstr "Valider" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:386 +#: streetup/controls/templates/controls/controls_detail_mobile.html:394 +msgid "Marquer NOK" +msgstr "Marquer NOK" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:655 +msgid "Restaurer la remarque" +msgstr "Restaurer la remarque" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:778 +#: streetup/controls/templates/controls/remarks_form.html:70 +#: streetup/controls/templates/controls/remarks_form_mobile.html:70 +msgid "Caméra" +msgstr "Caméra" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:793 +#: streetup/controls/templates/controls/remarks_form.html:85 +#: streetup/controls/templates/controls/remarks_form_mobile.html:85 +msgid "Terminer" +msgstr "Terminer" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:796 +#: streetup/controls/templates/controls/remarks_form.html:88 +#: streetup/controls/templates/controls/remarks_form_mobile.html:88 +msgid "Capturer" +msgstr "Capturer" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:961 +msgid "En attente de synchronisation" +msgstr "En attente de synchronisation" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:966 +msgid "Modifier" +msgstr "Modifier" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:988 +msgid "Créé localement le" +msgstr "Créé localement le" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1048 +#: streetup/controls/templates/controls/remark_form_content.html:155 +msgid "Mettre à jour" +msgstr "Mettre à jour" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1056 +msgid "Voulez-vous vraiment supprimer cette remarque ?" +msgstr "Voulez-vous vraiment supprimer cette remarque ?" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1066 +msgid "Erreur lors de la suppression de la remarque" +msgstr "Erreur lors de la suppression de la remarque" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1083 +msgid "Erreur lors de la suppression de l\\'image" +msgstr "Erreur lors de la suppression de l\\'image" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1099 +msgid "Action non disponible hors ligne" +msgstr "Action non disponible hors ligne" + +#: streetup/controls/templates/controls/controls_form_content.html:180 +msgid "Modifier le contrôle" +msgstr "Modifier le contrôle" + +#: streetup/controls/templates/controls/controls_form_content.html:182 +msgid "Ajouter un nouveau contrôle" +msgstr "Ajouter un nouveau contrôle" + +#: streetup/controls/templates/controls/controls_form_content.html:196 +#: streetup/controls/templates/controls/controls_form_content.html:205 +msgid "Non modifiable" +msgstr "Non modifiable" + +#: streetup/controls/templates/controls/controls_form_content.html:204 +msgid "Généré automatiquement (CTRL-XXXX)" +msgstr "Généré automatiquement (CTRL-XXXX)" + +#: streetup/controls/templates/controls/controls_form_content.html:211 +msgid "Le code sera généré automatiquement lors de la création." +msgstr "Le code sera généré automatiquement lors de la création." + +#: streetup/controls/templates/controls/controls_form_content.html:220 +msgid "Erreurs :" +msgstr "Erreurs :" + +#: streetup/controls/templates/controls/controls_form_content.html:233 +msgid "Le formulaire contient des erreurs." +msgstr "Le formulaire contient des erreurs." + +#: streetup/controls/templates/controls/controls_form_content.html:234 +msgid "Veuillez corriger les champs marqués en rouge ci-dessous." +msgstr "Veuillez corriger les champs marqués en rouge ci-dessous." + +#: streetup/controls/templates/controls/controls_form_content.html:271 +msgid "Facultatif" +msgstr "Facultatif" + +#: streetup/controls/templates/controls/controls_form_content.html:277 +msgid "Rechercher une intervention..." +msgstr "Rechercher une intervention..." + +#: streetup/controls/templates/controls/controls_form_content.html:374 +msgid "Position géographique" +msgstr "Position géographique" + +#: streetup/controls/templates/controls/controls_form_content.html:380 +msgid "Rechercher une adresse..." +msgstr "Rechercher une adresse..." + +#: streetup/controls/templates/controls/controls_form_content.html:384 +#: streetup/controls/templates/controls/controls_form_content.html:580 +#: streetup/controls/templates/controls/controls_form_content.html:586 +msgid "Ma position" +msgstr "Ma position" + +#: streetup/controls/templates/controls/controls_form_content.html:387 +msgid "Effacer" +msgstr "Effacer" + +#: streetup/controls/templates/controls/controls_form_content.html:558 +msgid "Aucun résultat trouvé pour cette adresse" +msgstr "Aucun résultat trouvé pour cette adresse" + +#: streetup/controls/templates/controls/controls_form_content.html:563 +msgid "Erreur lors de la recherche d\\'adresse" +msgstr "Erreur lors de la recherche d\\'adresse" + +#: streetup/controls/templates/controls/controls_form_content.html:573 +msgid "Localisation..." +msgstr "Localisation..." + +#: streetup/controls/templates/controls/controls_form_content.html:584 +msgid "" +"Impossible d\\'accéder à votre position. Assurez-vous que vous avez autorisé " +"l\\'accès à la localisation." +msgstr "" +"Impossible d\\'accéder à votre position. Assurez-vous que vous avez autorisé " +"l\\'accès à la localisation." + +#: streetup/controls/templates/controls/controls_form_content.html:595 +msgid "La géolocalisation n\\'est pas disponible dans votre navigateur" +msgstr "La géolocalisation n\\'est pas disponible dans votre navigateur" + +#: streetup/controls/templates/controls/controls_form_content.html:770 +msgid "Aucun résultat" +msgstr "Aucun résultat" + +#: streetup/controls/templates/controls/controls_form_content.html:824 +msgid "Retirer" +msgstr "Retirer" + +#: streetup/controls/templates/controls/controls_form_mobile.html:20 +msgid "Modifier le Contrôle" +msgstr "Modifier le Contrôle" + +#: streetup/controls/templates/controls/controls_form_mobile.html:20 +msgid "Nouveau Contrôle" +msgstr "Nouveau Contrôle" + +#: streetup/controls/templates/controls/controls_index.html:13 +msgid "Nouveau contrôle" +msgstr "Nouveau contrôle" + +#: streetup/controls/templates/controls/controls_index.html:33 +msgid "Code" +msgstr "Code" + +#: streetup/controls/templates/controls/controls_index.html:37 +msgid "Mise à jour" +msgstr "Mise à jour" + +#: streetup/controls/templates/controls/controls_index_mobile.html:19 +msgid "Ajouter un contrôle" +msgstr "Ajouter un contrôle" + +#: streetup/controls/templates/controls/controls_install_mobile.html:9 +msgid "Installer l'application pour les contrôles" +msgstr "Installer l'application pour les contrôles" + +#: streetup/controls/templates/controls/controls_install_mobile.html:18 +msgid "StreetUp - Contrôles" +msgstr "StreetUp - Contrôles" + +#: streetup/controls/templates/controls/controls_install_mobile.html:19 +msgid "" +"Pour une meilleure expérience, installez l'application sur votre appareil." +msgstr "" +"Pour une meilleure expérience, installez l'application sur votre appareil." + +#: streetup/controls/templates/controls/controls_install_mobile.html:21 +msgid "Installer l'application" +msgstr "Installer l'application" + +#: streetup/controls/templates/controls/controls_install_mobile.html:24 +msgid "Désolé, votre navigateur ne supporte pas l'installation d'applications." +msgstr "" +"Désolé, votre navigateur ne supporte pas l'installation d'applications." + +#: streetup/controls/templates/controls/offline.html:8 +msgid "Hors ligne — Contrôles" +msgstr "Hors ligne — Contrôles" + +#: streetup/controls/templates/controls/offline.html:23 +msgid "Vous êtes hors ligne" +msgstr "Vous êtes hors ligne" + +#: streetup/controls/templates/controls/offline.html:25 +msgid "Cette page n'est pas disponible sans connexion réseau." +msgstr "Cette page n'est pas disponible sans connexion réseau." + +#: streetup/controls/templates/controls/offline.html:33 +msgid "Reprendre le contrôle en cours" +msgstr "Reprendre le contrôle en cours" + +#: streetup/controls/templates/controls/offline.html:38 +msgid "Liste des contrôles (depuis le cache)" +msgstr "Liste des contrôles (depuis le cache)" + +#: streetup/controls/templates/controls/offline.html:44 +msgid "" +"Les pages déjà visitées lors de votre dernière connexion sont disponibles " +"hors ligne." +msgstr "" +"Les pages déjà visitées lors de votre dernière connexion sont disponibles " +"hors ligne." + +#: streetup/controls/templates/controls/remark_form_content.html:29 +msgid "Historique des suivis" +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:67 +msgid "Description / Commentaire de suivi" +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:76 +msgid "Enregistrer comme un nouveau suivi dans l'historique" +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:79 +msgid "" +"Décochez uniquement si vous souhaitez corriger une faute ou modifier la " +"saisie précédente sans ajouter de nouvelle étape à l'historique." +msgstr "" + +#: streetup/controls/templates/controls/remarks_form.html:60 +#: streetup/controls/templates/controls/remarks_form_mobile.html:50 +#: streetup/controls/templates/controls/remarks_form_mobile.html:58 +#: streetup/controls/views_reports_pdf.py:703 +msgid "Remarque" +msgstr "Remarque" + +#: streetup/controls/templates/controls/remarks_form_mobile.html:52 +msgid "Nouvelle Remarque" +msgstr "Nouvelle Remarque" + +#: streetup/controls/views.py:352 +msgid "Le projet spécifié n'existe pas." +msgstr "Le projet spécifié n'existe pas." + +#: streetup/controls/views.py:421 +msgid "L'intervention spécifiée n'existe pas." +msgstr "L'intervention spécifiée n'existe pas." + +#: streetup/controls/views.py:667 streetup/controls/views.py:942 +msgid "Remarque sauvegardée avec succès !" +msgstr "Remarque sauvegardée avec succès !" + +#: streetup/controls/views.py:672 +msgid "Erreur lors de la sauvegarde de la remarque." +msgstr "Erreur lors de la sauvegarde de la remarque." + +#: streetup/controls/views.py:732 +msgid "Plan ajouté avec succès !" +msgstr "Plan ajouté avec succès !" + +#: streetup/controls/views.py:735 +msgid "Erreur lors de la sauvegarde du plan." +msgstr "Erreur lors de la sauvegarde du plan." + +#: streetup/controls/views.py:884 streetup/controls/views.py:1007 +#: streetup/controls/views.py:1043 streetup/controls/views.py:1055 +#: streetup/controls/views.py:1071 +msgid "Vous n'avez pas le droit de modifier cette remarque." +msgstr "Vous n'avez pas le droit de modifier cette remarque." + +#: streetup/controls/views.py:1018 +#, python-format +msgid "Statut modifié en %s" +msgstr "" + +#: streetup/controls/views.py:1032 +msgid "Remarque introuvable" +msgstr "Remarque introuvable" + +#: streetup/controls/views.py:1034 +msgid "Requête invalide" +msgstr "Requête invalide" + +#: streetup/controls/views.py:1094 +msgid "Vous n'avez pas le droit de modifier ce contrôle." +msgstr "Vous n'avez pas le droit de modifier ce contrôle." + +#: streetup/controls/views_reports_pdf.py:47 +#, python-format +msgid "Erreur de chargement d'image: %(e)s" +msgstr "Erreur de chargement d'image: %(e)s" + +#: streetup/controls/views_reports_pdf.py:359 +msgid "Rapport de suivi" +msgstr "Rapport de suivi" + +#: streetup/controls/views_reports_pdf.py:367 +msgid "Page" +msgstr "Page" + +#: streetup/controls/views_reports_pdf.py:409 +msgid "Description : " +msgstr "Description : " + +#: streetup/controls/views_reports_pdf.py:504 +#, fuzzy, python-format +#| msgid "Erreur de chargement d'image: %(e)s" +msgid "Erreur lors du chargement ou du dessin sur le plan %(name)s: %(e)s" +msgstr "Erreur de chargement d'image: %(e)s" + +#: streetup/controls/views_reports_pdf.py:742 +msgid "Inconnu" +msgstr "Inconnu" + +#: streetup/controls/views_reports_pdf.py:837 +msgid "PDF généré et sauvegardé avec succès." +msgstr "PDF généré et sauvegardé avec succès." + +#: streetup/controls/views_reports_pdf.py:844 +msgid "Erreur lors de la génération du PDF" +msgstr "Erreur lors de la génération du PDF" diff --git a/streetup/controls/locale/fr/LC_MESSAGES/djangojs.mo b/streetup/controls/locale/fr/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..9195806 Binary files /dev/null and b/streetup/controls/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/controls/locale/fr/LC_MESSAGES/djangojs.po b/streetup/controls/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..311856b --- /dev/null +++ b/streetup/controls/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,146 @@ +# Traductions françaises pour le module Controls (JavaScript). +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: 2025-12-04 13:00+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: controls/static/controls/controls_detail.js:797 +msgid "Rapport généré avec succès !" +msgstr "Rapport généré avec succès !" + +#: controls/static/controls/controls_detail.js:814 +msgid "Généré le" +msgstr "Généré le" + +#: controls/static/controls/controls_detail.js:815 +msgid "Auteur" +msgstr "Auteur" + +#: controls/static/controls/controls_detail.js:820 +msgid "Télécharger" +msgstr "Télécharger" + +#: controls/static/controls/controls_detail.js:824 +msgid "Partager" +msgstr "Partager" + +#: controls/static/controls/controls_detail.js:841 +msgid "Erreur lors de la génération du rapport." +msgstr "Erreur lors de la génération du rapport." + +#: controls/static/controls/controls_detail.js:846 +#: controls/static/controls/controls_detail.js:1040 +#: controls/static/controls/controls_detail.js:1056 +msgid "Une erreur est survenue." +msgstr "Une erreur est survenue." + +#: controls/static/controls/controls_detail.js:870 +msgid "Erreur lors de l'archivage." +msgstr "Erreur lors de l'archivage." + +#: controls/static/controls/controls_detail.js:901 +#: controls/static/controls/controls_detail.js:976 +#: controls/static/controls/controls_detail.js:1055 +msgid "Erreur :" +msgstr "Erreur :" + +#: controls/static/controls/controls_detail.js:902 +#: controls/static/controls/controls_detail.js:977 +msgid "Une erreur est survenue lors de l'archivage." +msgstr "Une erreur est survenue lors de l'archivage." + +#: controls/static/controls/controls_detail.js:918 +msgid "Erreur lors de la restauration de la remarque." +msgstr "Erreur lors de la restauration de la remarque." + +#: controls/static/controls/controls_detail.js:996 +msgid "Partager ce PDF" +msgstr "Partager ce PDF" + +#: controls/static/controls/controls_detail.js:997 +msgid "Voici un rapport PDF que je souhaite partager avec vous." +msgstr "Voici un rapport PDF que je souhaite partager avec vous." + +#: controls/static/controls/controls_detail.js:1000 +msgid "Le partage de fichiers n'est pas supporté sur ce navigateur." +msgstr "Le partage de fichiers n'est pas supporté sur ce navigateur." + +#: controls/static/controls/controls_detail.js:1003 +msgid "Erreur lors du partage du PDF:" +msgstr "Erreur lors du partage du PDF:" + +#: controls/static/controls/controls_detail.js:1629 +#: controls/static/controls/remarks_form.js:118 +msgid "La caméra avancée n'est pas supportée sur ce navigateur." +msgstr "La caméra avancée n'est pas supportée sur ce navigateur." + +#: controls/static/controls/controls_detail.js:1634 +#: controls/static/controls/remarks_form.js:122 +msgid "Initialisation de la caméra…" +msgstr "Initialisation de la caméra…" + +#: controls/static/controls/controls_detail.js:1650 +#: controls/static/controls/remarks_form.js:160 +#: controls/static/controls/remarks_form.js:188 +msgid "Caméra prête. Appuyez sur Capturer pour prendre une photo." +msgstr "Caméra prête. Appuyez sur Capturer pour prendre une photo." + +#: controls/static/controls/controls_detail.js:1654 +#: controls/static/controls/remarks_form.js:224 +msgid "Impossible d'accéder à la caméra. Veuillez vérifier les autorisations." +msgstr "Impossible d'accéder à la caméra. Veuillez vérifier les autorisations." + +#: controls/static/controls/controls_detail.js:1655 +#: controls/static/controls/remarks_form.js:228 +msgid "Impossible d'accéder à la caméra." +msgstr "Impossible d'accéder à la caméra." + +#: controls/static/controls/controls_detail.js:1677 +#: controls/static/controls/remarks_form.js:250 +msgid "La vidéo n'est pas encore prête." +msgstr "La vidéo n'est pas encore prête." + +#: controls/static/controls/controls_detail.js:1689 +#: controls/static/controls/remarks_form.js:262 +msgid "Erreur lors de la capture." +msgstr "Erreur lors de la capture." + +#: controls/static/controls/controls_detail.js:1698 +#: controls/static/controls/remarks_form.js:271 +msgid "Photo capturée avec succès." +msgstr "Photo capturée avec succès." + +#: controls/static/controls/remarks_form.js:50 +msgid "" +"L'enregistrement a pris trop de temps. Vérifiez votre connexion et réessayez." +msgstr "L'enregistrement a pris trop de temps. Vérifiez votre connexion et réessayez." + +#: controls/static/controls/remarks_form.js:209 +msgid "" +"Accès à la caméra refusé. Pour l'autoriser :\n" +"• Android : Réglages → Applications → [Navigateur] → Autorisations → Caméra\n" +"• iPhone : Réglages → Safari → Caméra → Autoriser" +msgstr "Accès à la caméra refusé. Pour l'autoriser :\n• Android : Réglages → Applications → [Navigateur] → Autorisations → Caméra\n• iPhone : Réglages → Safari → Caméra → Autoriser" + +#: controls/static/controls/remarks_form.js:216 +msgid "" +"L'accès à la caméra a été refusé. Veuillez appuyer sur 'Autoriser' lors de " +"la prochaine demande, ou vérifiez les autorisations dans les réglages de " +"votre navigateur." +msgstr "L'accès à la caméra a été refusé. Veuillez appuyer sur 'Autoriser' lors de la prochaine demande, ou vérifiez les autorisations dans les réglages de votre navigateur." + +#: controls/static/controls/remarks_form.js:222 +msgid "Aucune caméra disponible sur cet appareil." +msgstr "Aucune caméra disponible sur cet appareil." diff --git a/streetup/controls/locale/nl/LC_MESSAGES/django.mo b/streetup/controls/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..33072e7 Binary files /dev/null and b/streetup/controls/locale/nl/LC_MESSAGES/django.mo differ diff --git a/streetup/controls/locale/nl/LC_MESSAGES/django.po b/streetup/controls/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..8b18a23 --- /dev/null +++ b/streetup/controls/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,1080 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-10 11:25+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: streetup/controls/forms.py:39 streetup/controls/models.py:53 +#: streetup/controls/views_reports_pdf.py:414 +msgid "Localisation" +msgstr "Locatie" + +#: streetup/controls/forms.py:39 streetup/controls/forms.py:40 +#: streetup/controls/forms.py:41 streetup/controls/forms.py:42 +#: streetup/controls/forms.py:43 streetup/controls/forms.py:44 +#: streetup/controls/templates/controls/controls_form_content.html:374 +msgid "facultatif" +msgstr "facultatief" + +#: streetup/controls/forms.py:40 streetup/controls/models.py:49 +#: streetup/controls/templates/controls/controls_detail_content.html:49 +msgid "Catégories" +msgstr "Categorieën" + +#: streetup/controls/forms.py:41 +#: streetup/controls/templates/controls/controls_form_content.html:271 +msgid "Interventions liées" +msgstr "Gekoppelde interventies" + +#: streetup/controls/forms.py:42 streetup/controls/models.py:50 +#: streetup/controls/templates/controls/controls_detail_content.html:60 +#: streetup/controls/templates/controls/controls_detail_mobile.html:95 +#: streetup/controls/templates/controls/controls_index_mobile.html:40 +msgid "Projet" +msgstr "Project" + +#: streetup/controls/forms.py:43 +msgid "Phase du projet" +msgstr "Projectfase" + +#: streetup/controls/forms.py:44 streetup/controls/models.py:45 +#: streetup/controls/templates/controls/controls_detail_content.html:89 +#: streetup/controls/templates/controls/controls_detail_content.html:132 +#: streetup/controls/templates/controls/controls_detail_mobile.html:126 +#: streetup/controls/templates/controls/controls_detail_mobile.html:172 +#: streetup/controls/templates/controls/controls_index.html:34 +msgid "Description" +msgstr "Beschrijving" + +#: streetup/controls/forms.py:155 +msgid "Entrez les notes de début ..." +msgstr "Voer de beginnotities in ..." + +#: streetup/controls/forms.py:160 +msgid "Entrez les notes de fin ..." +msgstr "Voer de eindnotities in ..." + +#: streetup/controls/forms.py:172 +msgid "Chercher un poste (autocomplete)..." +msgstr "Zoek een post (autocomplete)..." + +#: streetup/controls/forms.py:189 +msgid "Description si pas de marché" +msgstr "Beschrijving indien geen bestek" + +#: streetup/controls/forms.py:231 +msgid "Sélectionner un marché" +msgstr "Bestand selecteren" + +#: streetup/controls/models.py:21 +msgid "En cours" +msgstr "In behandeling" + +#: streetup/controls/models.py:22 +msgid "Information" +msgstr "Informatie" + +#: streetup/controls/models.py:23 +msgid "A traiter" +msgstr "Te behandelen" + +#: streetup/controls/models.py:24 streetup/controls/models.py:229 +msgid "Traité" +msgstr "Behandeld" + +#: streetup/controls/models.py:25 streetup/controls/models.py:175 +msgid "Validé" +msgstr "Goedgekeurd" + +#: streetup/controls/models.py:26 streetup/controls/models.py:177 +#: streetup/controls/models.py:231 +msgid "Annulé" +msgstr "Geannuleerd" + +#: streetup/controls/models.py:30 +#: streetup/controls/templates/controls/controls_detail.html:33 +#: streetup/controls/templates/controls/controls_detail_content.html:21 +#: streetup/controls/views_reports_pdf.py:402 +msgid "Contrôle" +msgstr "Controle" + +#: streetup/controls/models.py:31 +msgid "Rapport de chantier" +msgstr "Werfverslag" + +#: streetup/controls/models.py:32 +msgid "Constat" +msgstr "Vaststelling" + +#: streetup/controls/models.py:33 +msgid "Rapport de réunion" +msgstr "Vergaderverslag" + +#: streetup/controls/models.py:37 +msgid "Privé" +msgstr "Privé" + +#: streetup/controls/models.py:38 +msgid "Interne" +msgstr "Intern" + +#: streetup/controls/models.py:39 +msgid "Interne et externe" +msgstr "Intern en extern" + +#: streetup/controls/models.py:44 +#: streetup/controls/templates/controls/controls_form_content.html:191 +msgid "Code du contrôle" +msgstr "Controlecode" + +#: streetup/controls/models.py:46 +#: streetup/controls/templates/controls/controls_detail_content.html:33 +#: streetup/controls/templates/controls/controls_detail_mobile.html:84 +#: streetup/controls/views_reports_pdf.py:704 +msgid "Statut" +msgstr "Status" + +#: streetup/controls/models.py:47 +msgid "Type de contrôle" +msgstr "Type controle" + +#: streetup/controls/models.py:48 +#: streetup/controls/templates/controls/controls_detail_content.html:41 +msgid "Thématiques" +msgstr "Thema's" + +#: streetup/controls/models.py:51 +#: streetup/controls/templates/controls/controls_detail_content.html:68 +#: streetup/controls/templates/controls/controls_detail_mobile.html:103 +msgid "Phase" +msgstr "Fase" + +#: streetup/controls/models.py:52 +msgid "Interventions" +msgstr "Interventies" + +#: streetup/controls/models.py:54 +msgid "Géométrie" +msgstr "Geometrie" + +#: streetup/controls/models.py:55 +msgid "GeoJSON" +msgstr "GeoJSON" + +#: streetup/controls/models.py:56 +msgid "Longitude" +msgstr "Lengtegraad" + +#: streetup/controls/models.py:57 +msgid "Latitude" +msgstr "Breedtegraad" + +#: streetup/controls/models.py:61 +msgid "Visibilité" +msgstr "Zichtbaarheid" + +#: streetup/controls/models.py:62 +msgid "Edition partagée" +msgstr "Gedeelde editie" + +#: streetup/controls/models.py:136 +msgid "Plan as-built" +msgstr "As-builtplan" + +#: streetup/controls/models.py:137 +msgid "Fiches techniques" +msgstr "Technische fiches" + +#: streetup/controls/models.py:174 +msgid "Actif" +msgstr "Actief" + +#: streetup/controls/models.py:176 +msgid "Archivé" +msgstr "Gearchiveerd" + +#: streetup/controls/models.py:227 +#: streetup/controls/templates/controls/controls_detail_content.html:125 +#: streetup/controls/templates/controls/controls_detail_mobile.html:163 +#: streetup/controls/templates/controls/controls_detail_mobile.html:956 +msgid "Info" +msgstr "Info" + +#: streetup/controls/models.py:228 +#: streetup/controls/templates/controls/controls_detail_content.html:127 +#: streetup/controls/templates/controls/controls_detail_mobile.html:165 +#: streetup/controls/templates/controls/controls_detail_mobile.html:956 +msgid "NOK" +msgstr "NOK" + +#: streetup/controls/models.py:230 +#: streetup/controls/templates/controls/controls_detail_content.html:126 +#: streetup/controls/templates/controls/controls_detail_mobile.html:164 +#: streetup/controls/templates/controls/controls_detail_mobile.html:955 +msgid "OK" +msgstr "OK" + +#: streetup/controls/models.py:258 +#, python-format +msgid "Remarque %(num)s pour %(control)s" +msgstr "Opmerking %(num)s voor %(control)s" + +#: streetup/controls/permissions.py:56 +msgid "Vous n'êtes pas autorisé à accéder à ce contrôle." +msgstr "U bent niet gemachtigd om deze controle te raadplegen." + +#: streetup/controls/permissions.py:73 +msgid "Vous n'êtes pas autorisé à ajouter un contrôle." +msgstr "U bent niet gemachtigd om een controle toe te voegen." + +#: streetup/controls/permissions.py:108 +msgid "Vous n'êtes pas autorisé à modifier ce contrôle." +msgstr "U bent niet gemachtigd om deze controle te wijzigen." + +#: streetup/controls/templates/controls/change_template.html:10 +#: streetup/controls/templates/controls/change_template_mobile.html:21 +msgid "Modifier le template de rapport" +msgstr "Rapportsjabloon bewerken" + +#: streetup/controls/templates/controls/change_template.html:18 +#: streetup/controls/templates/controls/change_template_mobile.html:29 +msgid "Notes de début" +msgstr "Beginnotities" + +#: streetup/controls/templates/controls/change_template.html:24 +#: streetup/controls/templates/controls/change_template_mobile.html:35 +msgid "Notes de fin" +msgstr "Eindnotities" + +#: streetup/controls/templates/controls/change_template.html:30 +#: streetup/controls/templates/controls/change_template_mobile.html:41 +#: streetup/controls/templates/controls/controls_detail_content.html:725 +#: streetup/controls/templates/controls/controls_detail_mobile.html:765 +#: streetup/controls/templates/controls/controls_form_content.html:407 +#: streetup/controls/templates/controls/remark_form_content.html:161 +#: streetup/controls/templates/controls/remark_form_content.html:163 +msgid "Annuler" +msgstr "Annuleren" + +#: streetup/controls/templates/controls/change_template.html:31 +#: streetup/controls/templates/controls/change_template_mobile.html:42 +#: streetup/controls/templates/controls/controls_form_content.html:403 +#: streetup/controls/templates/controls/remark_form_content.html:155 +msgid "Enregistrer" +msgstr "Opslaan" + +#: streetup/controls/templates/controls/control_report.html:10 +#: streetup/controls/templates/controls/control_report.html:39 +msgid "Rapport de Contrôle" +msgstr "Controlerapport" + +#: streetup/controls/templates/controls/control_report.html:41 +msgid "Numéro du rapport" +msgstr "Rapportnummer" + +#: streetup/controls/templates/controls/control_report.html:42 +#: streetup/controls/views_reports_pdf.py:415 +#: streetup/controls/views_reports_pdf.py:702 +msgid "Date" +msgstr "Datum" + +#: streetup/controls/templates/controls/control_report.html:48 +msgid "Statut du Projet" +msgstr "Projectstatus" + +#: streetup/controls/templates/controls/control_report.html:53 +msgid "Photos d'Avancement" +msgstr "Voortgangsfoto's" + +#: streetup/controls/templates/controls/control_report.html:62 +msgid "Observations" +msgstr "Opmerkingen" + +#: streetup/controls/templates/controls/control_report.html:77 +msgid "Rapport généré par" +msgstr "Rapport gegenereerd door" + +#: streetup/controls/templates/controls/control_report.html:80 +msgid "Télécharger le PDF" +msgstr "PDF downloaden" + +#: streetup/controls/templates/controls/controls_base_mobile.html:12 +#: streetup/controls/templates/controls/controls_base_mobile.html:63 +#: streetup/controls/templates/controls/controls_index.html:19 +#: streetup/controls/templates/controls/controls_index_mobile.html:13 +msgid "Contrôles" +msgstr "Controles" + +#: streetup/controls/templates/controls/controls_base_mobile.html:66 +#: streetup/controls/templates/controls/controls_projects_mobile.html:17 +msgid "Projets" +msgstr "Projecten" + +#: streetup/controls/templates/controls/controls_detail_content.html:13 +#: streetup/controls/templates/controls/controls_detail_content.html:719 +#: streetup/controls/templates/controls/controls_detail_mobile.html:68 +#: streetup/controls/templates/controls/controls_detail_mobile.html:136 +#: streetup/controls/templates/controls/controls_detail_mobile.html:734 +#: streetup/controls/templates/controls/controls_detail_mobile.html:759 +#: streetup/controls/templates/controls/controls_detail_mobile.html:780 +#: streetup/controls/templates/controls/remarks_form.html:72 +#: streetup/controls/templates/controls/remarks_form_mobile.html:72 +msgid "Fermer" +msgstr "Sluiten" + +#: streetup/controls/templates/controls/controls_detail_content.html:26 +#: streetup/controls/templates/controls/controls_detail_mobile.html:78 +msgid "Modifier le template" +msgstr "Sjabloon bewerken" + +#: streetup/controls/templates/controls/controls_detail_content.html:37 +#: streetup/controls/templates/controls/controls_detail_mobile.html:88 +#: streetup/controls/templates/controls/controls_index.html:36 +#: streetup/controls/templates/controls/controls_index_mobile.html:33 +msgid "Type" +msgstr "Type" + +#: streetup/controls/templates/controls/controls_detail_content.html:45 +#: streetup/controls/templates/controls/controls_detail_content.html:54 +msgid "Aucune" +msgstr "Geen" + +#: streetup/controls/templates/controls/controls_detail_content.html:74 +#: streetup/controls/templates/controls/controls_detail_mobile.html:109 +msgid "Intervention" +msgstr "Interventie" + +#: streetup/controls/templates/controls/controls_detail_content.html:80 +#: streetup/controls/templates/controls/controls_detail_mobile.html:117 +msgid "Créé le" +msgstr "Aangemaakt op" + +#: streetup/controls/templates/controls/controls_detail_content.html:81 +#: streetup/controls/templates/controls/controls_detail_mobile.html:118 +#: streetup/controls/templates/controls/controls_index_mobile.html:37 +msgid "Mis à jour le" +msgstr "Bijgewerkt op" + +#: streetup/controls/templates/controls/controls_detail_content.html:85 +#: streetup/controls/templates/controls/controls_detail_content.html:567 +#: streetup/controls/templates/controls/controls_detail_mobile.html:122 +#: streetup/controls/templates/controls/controls_detail_mobile.html:611 +#: streetup/controls/templates/controls/controls_index.html:35 +msgid "Auteur" +msgstr "Auteur" + +#: streetup/controls/templates/controls/controls_detail_content.html:92 +#: streetup/controls/templates/controls/controls_detail_mobile.html:114 +#: streetup/controls/templates/controls/controls_index_mobile.html:36 +msgid "Lieu" +msgstr "Locatie" + +#: streetup/controls/templates/controls/controls_detail_content.html:108 +#: streetup/controls/templates/controls/controls_detail_mobile.html:145 +#: streetup/controls/templates/controls/remark_form_content.html:12 +msgid "Carte" +msgstr "Kaart" + +#: streetup/controls/templates/controls/controls_detail_content.html:114 +#: streetup/controls/templates/controls/controls_detail_content.html:316 +#: streetup/controls/templates/controls/controls_detail_mobile.html:151 +#: streetup/controls/templates/controls/controls_detail_mobile.html:362 +msgid "Ajouter une remarque" +msgstr "Opmerking toevoegen" + +#: streetup/controls/templates/controls/controls_detail_content.html:135 +#: streetup/controls/templates/controls/controls_detail_mobile.html:175 +msgid "Veuillez remplir la description." +msgstr "Gelieve de beschrijving in te vullen." + +#: streetup/controls/templates/controls/controls_detail_content.html:142 +#: streetup/controls/templates/controls/controls_detail_mobile.html:182 +#: streetup/controls/templates/controls/remark_form_content.html:85 +msgid "Images" +msgstr "Afbeeldingen" + +#: streetup/controls/templates/controls/controls_detail_content.html:145 +#: streetup/controls/templates/controls/controls_detail_mobile.html:194 +msgid "Veuillez ajouter au moins une image." +msgstr "Voeg minstens één afbeelding toe." + +#: streetup/controls/templates/controls/controls_detail_content.html:154 +#: streetup/controls/templates/controls/controls_detail_mobile.html:203 +msgid "Tags" +msgstr "Labels" + +#: streetup/controls/templates/controls/controls_detail_content.html:170 +#: streetup/controls/templates/controls/controls_detail_mobile.html:224 +#: streetup/controls/templates/controls/remark_form_content.html:117 +msgid "Quantités liées à la remarque" +msgstr "Hoeveelheden gekoppeld aan de opmerking" + +#: streetup/controls/templates/controls/controls_detail_content.html:172 +#: streetup/controls/templates/controls/controls_detail_mobile.html:226 +#: streetup/controls/templates/controls/remark_form_content.html:119 +msgid "Ajouter des quantités" +msgstr "Hoeveelheden toevoegen" + +#: streetup/controls/templates/controls/controls_detail_content.html:178 +#: streetup/controls/templates/controls/controls_detail_mobile.html:232 +#: streetup/controls/templates/controls/remark_form_content.html:125 +msgid "Aucune quantité pour l'instant." +msgstr "Nog geen hoeveelheid." + +#: streetup/controls/templates/controls/controls_detail_content.html:207 +#: streetup/controls/templates/controls/controls_detail_mobile.html:255 +msgid "Ajouter" +msgstr "Toevoegen" + +#: streetup/controls/templates/controls/controls_detail_content.html:218 +#: streetup/controls/templates/controls/controls_detail_mobile.html:266 +#: streetup/controls/templates/controls/remark_form_content.html:171 +msgid "Quantités de la remarque" +msgstr "Hoeveelheden van de opmerking" + +#: streetup/controls/templates/controls/controls_detail_content.html:233 +#: streetup/controls/templates/controls/controls_detail_mobile.html:281 +#: streetup/controls/templates/controls/remark_form_content.html:186 +msgid "Ligne" +msgstr "Lijn" + +#: streetup/controls/templates/controls/controls_detail_content.html:235 +#: streetup/controls/templates/controls/controls_detail_content.html:419 +#: streetup/controls/templates/controls/controls_detail_content.html:456 +#: streetup/controls/templates/controls/controls_detail_content.html:657 +#: streetup/controls/templates/controls/controls_detail_content.html:726 +#: streetup/controls/templates/controls/controls_detail_mobile.html:283 +#: streetup/controls/templates/controls/controls_detail_mobile.html:465 +#: streetup/controls/templates/controls/controls_detail_mobile.html:502 +#: streetup/controls/templates/controls/controls_detail_mobile.html:697 +#: streetup/controls/templates/controls/controls_detail_mobile.html:766 +#: streetup/controls/templates/controls/controls_detail_mobile.html:969 +#: streetup/controls/templates/controls/controls_detail_mobile.html:998 +#: streetup/controls/templates/controls/remark_form_content.html:188 +msgid "Supprimer" +msgstr "Verwijderen" + +#: streetup/controls/templates/controls/controls_detail_content.html:241 +#: streetup/controls/templates/controls/controls_detail_mobile.html:289 +#: streetup/controls/templates/controls/remark_form_content.html:194 +msgid "Marché (optionnel)" +msgstr "Bestek (optioneel)" + +#: streetup/controls/templates/controls/controls_detail_content.html:247 +#: streetup/controls/templates/controls/controls_detail_mobile.html:295 +#: streetup/controls/templates/controls/remark_form_content.html:200 +msgid "Poste (autocomplete si un marché est choisi)" +msgstr "Post (autocomplete als een bestek is gekozen)" + +#: streetup/controls/templates/controls/controls_detail_content.html:255 +#: streetup/controls/templates/controls/controls_detail_mobile.html:303 +#: streetup/controls/templates/controls/remark_form_content.html:208 +msgid "Description (si pas de marché/poste)" +msgstr "Beschrijving (indien geen bestek/post)" + +#: streetup/controls/templates/controls/controls_detail_content.html:262 +#: streetup/controls/templates/controls/controls_detail_mobile.html:310 +#: streetup/controls/templates/controls/remark_form_content.html:215 +msgid "Quantité (1)" +msgstr "Hoeveelheid (1)" + +#: streetup/controls/templates/controls/controls_detail_content.html:266 +#: streetup/controls/templates/controls/controls_detail_mobile.html:314 +#: streetup/controls/templates/controls/remark_form_content.html:219 +msgid "Quantité (2)" +msgstr "Hoeveelheid (2)" + +#: streetup/controls/templates/controls/controls_detail_content.html:270 +#: streetup/controls/templates/controls/controls_detail_mobile.html:318 +#: streetup/controls/templates/controls/remark_form_content.html:223 +msgid "Quantité (3)" +msgstr "Hoeveelheid (3)" + +#: streetup/controls/templates/controls/controls_detail_content.html:277 +#: streetup/controls/templates/controls/controls_detail_mobile.html:325 +#: streetup/controls/templates/controls/remark_form_content.html:230 +msgid "Résultat" +msgstr "Resultaat" + +#: streetup/controls/templates/controls/controls_detail_content.html:293 +#: streetup/controls/templates/controls/controls_detail_mobile.html:341 +#: streetup/controls/templates/controls/remark_form_content.html:246 +msgid "Ajouter une ligne" +msgstr "Regel toevoegen" + +#: streetup/controls/templates/controls/controls_detail_content.html:296 +#: streetup/controls/templates/controls/controls_detail_mobile.html:344 +#: streetup/controls/templates/controls/remark_form_content.html:249 +msgid "Terminer et revenir" +msgstr "Afmaken en terugbrengen" + +#: streetup/controls/templates/controls/controls_detail_content.html:323 +#: streetup/controls/templates/controls/controls_detail_mobile.html:368 +msgid "Remarques" +msgstr "Opmerkingen" + +#: streetup/controls/templates/controls/controls_detail_content.html:354 +#: streetup/controls/templates/controls/controls_detail_mobile.html:402 +msgid "Éditer la remarque" +msgstr "Opmerking bewerken" + +#: streetup/controls/templates/controls/controls_detail_content.html:358 +#: streetup/controls/templates/controls/controls_detail_mobile.html:406 +msgid "Archiver la remarque" +msgstr "Opmerking archiveren" + +#: streetup/controls/templates/controls/controls_detail_content.html:383 +#: streetup/controls/templates/controls/controls_detail_content.html:639 +#: streetup/controls/templates/controls/controls_detail_mobile.html:429 +#: streetup/controls/templates/controls/controls_detail_mobile.html:679 +msgid "Mise à jour par" +msgstr "Bijgewerkt door" + +#: streetup/controls/templates/controls/controls_detail_content.html:384 +#: streetup/controls/templates/controls/controls_detail_content.html:640 +#: streetup/controls/templates/controls/controls_detail_mobile.html:430 +#: streetup/controls/templates/controls/controls_detail_mobile.html:680 +msgid "le" +msgstr "op" + +#: streetup/controls/templates/controls/controls_detail_content.html:389 +#: streetup/controls/templates/controls/controls_detail_mobile.html:435 +#, fuzzy +#| msgid "Quantités de la remarque" +msgid "Suivis de la remarque" +msgstr "Hoeveelheden van de opmerking" + +#: streetup/controls/templates/controls/controls_detail_content.html:399 +#: streetup/controls/templates/controls/controls_detail_content.html:436 +#: streetup/controls/templates/controls/controls_detail_mobile.html:445 +#: streetup/controls/templates/controls/controls_detail_mobile.html:482 +#: streetup/controls/templates/controls/remark_form_content.html:39 +msgid "par" +msgstr "" + +#: streetup/controls/templates/controls/controls_detail_content.html:412 +#: streetup/controls/templates/controls/controls_detail_content.html:449 +#: streetup/controls/templates/controls/controls_detail_content.html:650 +#: streetup/controls/templates/controls/controls_detail_mobile.html:458 +#: streetup/controls/templates/controls/controls_detail_mobile.html:495 +#: streetup/controls/templates/controls/controls_detail_mobile.html:690 +msgid "Image de la remarque" +msgstr "Afbeelding van de opmerking" + +#: streetup/controls/templates/controls/controls_detail_content.html:483 +#: streetup/controls/templates/controls/controls_detail_mobile.html:530 +msgid "Quantités" +msgstr "Hoeveelheden" + +#: streetup/controls/templates/controls/controls_detail_content.html:489 +#: streetup/controls/templates/controls/controls_detail_mobile.html:536 +msgid "Poste / Description" +msgstr "Post / Beschrijving" + +#: streetup/controls/templates/controls/controls_detail_content.html:490 +#: streetup/controls/templates/controls/controls_detail_mobile.html:537 +msgid "Total" +msgstr "Totaal" + +#: streetup/controls/templates/controls/controls_detail_content.html:533 +#: streetup/controls/templates/controls/controls_detail_mobile.html:579 +msgid "Aucune remarque enregistrée." +msgstr "Geen opmerkingen geregistreerd." + +#: streetup/controls/templates/controls/controls_detail_content.html:549 +#: streetup/controls/templates/controls/controls_detail_mobile.html:594 +msgid "Produire un rapport PDF" +msgstr "Een PDF-rapport aanmaken" + +#: streetup/controls/templates/controls/controls_detail_content.html:559 +#: streetup/controls/templates/controls/controls_detail_mobile.html:603 +msgid "Rapports" +msgstr "Rapporten" + +#: streetup/controls/templates/controls/controls_detail_content.html:566 +#: streetup/controls/templates/controls/controls_detail_mobile.html:610 +msgid "Généré le" +msgstr "Gegenereerd op" + +#: streetup/controls/templates/controls/controls_detail_content.html:573 +#: streetup/controls/templates/controls/controls_detail_mobile.html:617 +msgid "Télécharger" +msgstr "Downloaden" + +#: streetup/controls/templates/controls/controls_detail_content.html:576 +#: streetup/controls/templates/controls/controls_detail_mobile.html:620 +msgid "Partager" +msgstr "Delen" + +#: streetup/controls/templates/controls/controls_detail_content.html:583 +#: streetup/controls/templates/controls/controls_detail_mobile.html:627 +msgid "Aucun rapport sauvegardé." +msgstr "Geen rapport opgeslagen." + +#: streetup/controls/templates/controls/controls_detail_content.html:591 +#: streetup/controls/templates/controls/controls_detail_mobile.html:635 +msgid "Template" +msgstr "Sjabloon" + +#: streetup/controls/templates/controls/controls_detail_content.html:593 +#: streetup/controls/templates/controls/controls_detail_mobile.html:637 +msgid "Éditer le template" +msgstr "Sjabloon bewerken" + +#: streetup/controls/templates/controls/controls_detail_content.html:603 +#: streetup/controls/templates/controls/controls_detail_mobile.html:647 +msgid "Archives" +msgstr "Archieven" + +#: streetup/controls/templates/controls/controls_detail_content.html:688 +#: streetup/controls/templates/controls/controls_detail_mobile.html:727 +msgid "Ajouter un plan de référence" +msgstr "Referentieplan toevoegen" + +#: streetup/controls/templates/controls/controls_detail_content.html:696 +#: streetup/controls/templates/controls/controls_detail_mobile.html:736 +msgid "Ajouter un Plan de Référence" +msgstr "Een referentieplan toevoegen" + +#: streetup/controls/templates/controls/controls_detail_content.html:701 +#: streetup/controls/templates/controls/controls_detail_mobile.html:741 +msgid "Nom du Plan" +msgstr "Naam van het plan" + +#: streetup/controls/templates/controls/controls_detail_content.html:705 +#: streetup/controls/templates/controls/controls_detail_mobile.html:745 +msgid "Sélectionner un Fichier (PDF ou Image)" +msgstr "Bestand selecteren (PDF of afbeelding)" + +#: streetup/controls/templates/controls/controls_detail_content.html:708 +#: streetup/controls/templates/controls/controls_detail_mobile.html:748 +msgid "Uploader" +msgstr "Uploaden" + +#: streetup/controls/templates/controls/controls_detail_content.html:718 +#: streetup/controls/templates/controls/controls_detail_mobile.html:758 +msgid "Confirmer la suppression" +msgstr "Verwijdering bevestigen" + +#: streetup/controls/templates/controls/controls_detail_content.html:722 +#: streetup/controls/templates/controls/controls_detail_mobile.html:762 +#: streetup/controls/templates/controls/controls_detail_mobile.html:1074 +msgid "Voulez-vous vraiment supprimer cette image ?" +msgstr "Wilt u deze afbeelding echt verwijderen?" + +#: streetup/controls/templates/controls/controls_detail_content.html:737 +#: streetup/controls/templates/controls/controls_detail_content.html:785 +#: streetup/controls/templates/controls/controls_detail_mobile.html:864 +msgid "En ligne" +msgstr "Online" + +#: streetup/controls/templates/controls/controls_detail_content.html:740 +#: streetup/controls/templates/controls/controls_detail_mobile.html:812 +msgid "en attente" +msgstr "in behandeling" + +#: streetup/controls/templates/controls/controls_detail_content.html:742 +#: streetup/controls/templates/controls/controls_detail_mobile.html:814 +msgid "Forcer la synchronisation" +msgstr "Synchronisatie forceren" + +#: streetup/controls/templates/controls/controls_detail_content.html:789 +#: streetup/controls/templates/controls/controls_detail_mobile.html:877 +msgid "Connexion faible" +msgstr "" + +#: streetup/controls/templates/controls/controls_detail_content.html:792 +#: streetup/controls/templates/controls/controls_detail_mobile.html:809 +#: streetup/controls/templates/controls/controls_detail_mobile.html:881 +msgid "Hors ligne" +msgstr "Offline" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:185 +#: streetup/controls/templates/controls/remark_form_content.html:88 +msgid "Prendre des photos (mode continu)" +msgstr "Foto's nemen (continue modus)" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:188 +#: streetup/controls/templates/controls/remark_form_content.html:91 +msgid "Photos, vidéos, fichiers" +msgstr "Foto's, video's, bestanden" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:382 +#: streetup/controls/templates/controls/controls_detail_mobile.html:390 +msgid "Valider" +msgstr "Valideren" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:386 +#: streetup/controls/templates/controls/controls_detail_mobile.html:394 +msgid "Marquer NOK" +msgstr "Als NOK markeren" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:655 +msgid "Restaurer la remarque" +msgstr "Opmerking herstellen" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:778 +#: streetup/controls/templates/controls/remarks_form.html:70 +#: streetup/controls/templates/controls/remarks_form_mobile.html:70 +msgid "Caméra" +msgstr "Camera" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:793 +#: streetup/controls/templates/controls/remarks_form.html:85 +#: streetup/controls/templates/controls/remarks_form_mobile.html:85 +msgid "Terminer" +msgstr "Voltooien" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:796 +#: streetup/controls/templates/controls/remarks_form.html:88 +#: streetup/controls/templates/controls/remarks_form_mobile.html:88 +msgid "Capturer" +msgstr "Vastleggen" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:961 +msgid "En attente de synchronisation" +msgstr "Wachten op synchronisatie" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:966 +msgid "Modifier" +msgstr "Bewerken" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:988 +msgid "Créé localement le" +msgstr "Lokaal aangemaakt op" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1048 +#: streetup/controls/templates/controls/remark_form_content.html:155 +msgid "Mettre à jour" +msgstr "Bijwerken" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1056 +msgid "Voulez-vous vraiment supprimer cette remarque ?" +msgstr "Wilt u deze opmerking echt verwijderen?" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1066 +msgid "Erreur lors de la suppression de la remarque" +msgstr "Fout bij het verwijderen van de opmerking" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1083 +msgid "Erreur lors de la suppression de l\\'image" +msgstr "Fout bij het verwijderen van de afbeelding" + +#: streetup/controls/templates/controls/controls_detail_mobile.html:1099 +msgid "Action non disponible hors ligne" +msgstr "Actie niet beschikbaar offline" + +#: streetup/controls/templates/controls/controls_form_content.html:180 +msgid "Modifier le contrôle" +msgstr "Controle bewerken" + +#: streetup/controls/templates/controls/controls_form_content.html:182 +msgid "Ajouter un nouveau contrôle" +msgstr "Nieuwe controle toevoegen" + +#: streetup/controls/templates/controls/controls_form_content.html:196 +#: streetup/controls/templates/controls/controls_form_content.html:205 +msgid "Non modifiable" +msgstr "" + +#: streetup/controls/templates/controls/controls_form_content.html:204 +msgid "Généré automatiquement (CTRL-XXXX)" +msgstr "" + +#: streetup/controls/templates/controls/controls_form_content.html:211 +msgid "Le code sera généré automatiquement lors de la création." +msgstr "" + +#: streetup/controls/templates/controls/controls_form_content.html:220 +msgid "Erreurs :" +msgstr "Fouten:" + +#: streetup/controls/templates/controls/controls_form_content.html:233 +msgid "Le formulaire contient des erreurs." +msgstr "Het formulier bevat fouten." + +#: streetup/controls/templates/controls/controls_form_content.html:234 +msgid "Veuillez corriger les champs marqués en rouge ci-dessous." +msgstr "Gelieve de rood gemarkeerde velden hieronder te corrigeren." + +#: streetup/controls/templates/controls/controls_form_content.html:271 +msgid "Facultatif" +msgstr "Facultatief" + +#: streetup/controls/templates/controls/controls_form_content.html:277 +msgid "Rechercher une intervention..." +msgstr "Interventie zoeken..." + +#: streetup/controls/templates/controls/controls_form_content.html:374 +msgid "Position géographique" +msgstr "Geografische positie" + +#: streetup/controls/templates/controls/controls_form_content.html:380 +msgid "Rechercher une adresse..." +msgstr "Een adres zoeken..." + +#: streetup/controls/templates/controls/controls_form_content.html:384 +#: streetup/controls/templates/controls/controls_form_content.html:580 +#: streetup/controls/templates/controls/controls_form_content.html:586 +msgid "Ma position" +msgstr "Mijn locatie" + +#: streetup/controls/templates/controls/controls_form_content.html:387 +msgid "Effacer" +msgstr "Wissen" + +#: streetup/controls/templates/controls/controls_form_content.html:558 +msgid "Aucun résultat trouvé pour cette adresse" +msgstr "Geen resultaat gevonden voor dit adres" + +#: streetup/controls/templates/controls/controls_form_content.html:563 +msgid "Erreur lors de la recherche d\\'adresse" +msgstr "Fout bij het zoeken naar adres" + +#: streetup/controls/templates/controls/controls_form_content.html:573 +msgid "Localisation..." +msgstr "Locatie..." + +#: streetup/controls/templates/controls/controls_form_content.html:584 +msgid "" +"Impossible d\\'accéder à votre position. Assurez-vous que vous avez autorisé " +"l\\'accès à la localisation." +msgstr "" +"Kan uw locatie niet bepalen. Zorg ervoor dat u toegang tot de locatie heeft " +"toegestaan." + +#: streetup/controls/templates/controls/controls_form_content.html:595 +msgid "La géolocalisation n\\'est pas disponible dans votre navigateur" +msgstr "Geolocatie is niet beschikbaar in uw browser" + +#: streetup/controls/templates/controls/controls_form_content.html:770 +msgid "Aucun résultat" +msgstr "Geen resultaten" + +#: streetup/controls/templates/controls/controls_form_content.html:824 +msgid "Retirer" +msgstr "Verwijderen" + +#: streetup/controls/templates/controls/controls_form_mobile.html:20 +msgid "Modifier le Contrôle" +msgstr "Controle bewerken" + +#: streetup/controls/templates/controls/controls_form_mobile.html:20 +msgid "Nouveau Contrôle" +msgstr "Nieuwe controle" + +#: streetup/controls/templates/controls/controls_index.html:13 +msgid "Nouveau contrôle" +msgstr "Nieuwe controle" + +#: streetup/controls/templates/controls/controls_index.html:33 +msgid "Code" +msgstr "Code" + +#: streetup/controls/templates/controls/controls_index.html:37 +msgid "Mise à jour" +msgstr "Bijgewerkt" + +#: streetup/controls/templates/controls/controls_index_mobile.html:19 +msgid "Ajouter un contrôle" +msgstr "Controle toevoegen" + +#: streetup/controls/templates/controls/controls_install_mobile.html:9 +msgid "Installer l'application pour les contrôles" +msgstr "Installeer de app voor controles" + +#: streetup/controls/templates/controls/controls_install_mobile.html:18 +msgid "StreetUp - Contrôles" +msgstr "StreetUp - Controles" + +#: streetup/controls/templates/controls/controls_install_mobile.html:19 +msgid "" +"Pour une meilleure expérience, installez l'application sur votre appareil." +msgstr "Voor een betere ervaring installeert u de app op uw toestel." + +#: streetup/controls/templates/controls/controls_install_mobile.html:21 +msgid "Installer l'application" +msgstr "App installeren" + +#: streetup/controls/templates/controls/controls_install_mobile.html:24 +msgid "Désolé, votre navigateur ne supporte pas l'installation d'applications." +msgstr "Sorry, uw browser ondersteunt het installeren van apps niet." + +#: streetup/controls/templates/controls/offline.html:8 +#, fuzzy +#| msgid "Modifier le Contrôle" +msgid "Hors ligne — Contrôles" +msgstr "Controle bewerken" + +#: streetup/controls/templates/controls/offline.html:23 +msgid "Vous êtes hors ligne" +msgstr "" + +#: streetup/controls/templates/controls/offline.html:25 +msgid "Cette page n'est pas disponible sans connexion réseau." +msgstr "" + +#: streetup/controls/templates/controls/offline.html:33 +msgid "Reprendre le contrôle en cours" +msgstr "" + +#: streetup/controls/templates/controls/offline.html:38 +msgid "Liste des contrôles (depuis le cache)" +msgstr "" + +#: streetup/controls/templates/controls/offline.html:44 +msgid "" +"Les pages déjà visitées lors de votre dernière connexion sont disponibles " +"hors ligne." +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:29 +msgid "Historique des suivis" +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:67 +msgid "Description / Commentaire de suivi" +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:76 +msgid "Enregistrer comme un nouveau suivi dans l'historique" +msgstr "" + +#: streetup/controls/templates/controls/remark_form_content.html:79 +msgid "" +"Décochez uniquement si vous souhaitez corriger une faute ou modifier la " +"saisie précédente sans ajouter de nouvelle étape à l'historique." +msgstr "" + +#: streetup/controls/templates/controls/remarks_form.html:60 +#: streetup/controls/templates/controls/remarks_form_mobile.html:50 +#: streetup/controls/templates/controls/remarks_form_mobile.html:58 +#: streetup/controls/views_reports_pdf.py:703 +msgid "Remarque" +msgstr "Opmerking" + +#: streetup/controls/templates/controls/remarks_form_mobile.html:52 +msgid "Nouvelle Remarque" +msgstr "Nieuwe opmerking" + +#: streetup/controls/views.py:352 +msgid "Le projet spécifié n'existe pas." +msgstr "Het opgegeven project bestaat niet." + +#: streetup/controls/views.py:421 +msgid "L'intervention spécifiée n'existe pas." +msgstr "De opgegeven interventie bestaat niet." + +#: streetup/controls/views.py:667 streetup/controls/views.py:942 +msgid "Remarque sauvegardée avec succès !" +msgstr "Opmerking met succes opgeslagen!" + +#: streetup/controls/views.py:672 +msgid "Erreur lors de la sauvegarde de la remarque." +msgstr "Fout bij het opslaan van de opmerking." + +#: streetup/controls/views.py:732 +msgid "Plan ajouté avec succès !" +msgstr "Plan succesvol toegevoegd!" + +#: streetup/controls/views.py:735 +msgid "Erreur lors de la sauvegarde du plan." +msgstr "Fout bij het opslaan van het plan." + +#: streetup/controls/views.py:884 streetup/controls/views.py:1007 +#: streetup/controls/views.py:1043 streetup/controls/views.py:1055 +#: streetup/controls/views.py:1071 +msgid "Vous n'avez pas le droit de modifier cette remarque." +msgstr "U hebt geen recht om deze opmerking te wijzigen." + +#: streetup/controls/views.py:1018 +#, python-format +msgid "Statut modifié en %s" +msgstr "" + +#: streetup/controls/views.py:1032 +msgid "Remarque introuvable" +msgstr "Opmerking niet gevonden" + +#: streetup/controls/views.py:1034 +msgid "Requête invalide" +msgstr "Ongeldige aanvraag" + +#: streetup/controls/views.py:1094 +msgid "Vous n'avez pas le droit de modifier ce contrôle." +msgstr "U hebt geen recht om deze controle te wijzigen." + +#: streetup/controls/views_reports_pdf.py:47 +#, python-format +msgid "Erreur de chargement d'image: %(e)s" +msgstr "Fout bij het laden van afbeelding: %(e)s" + +#: streetup/controls/views_reports_pdf.py:359 +msgid "Rapport de suivi" +msgstr "Opvolgrapport" + +#: streetup/controls/views_reports_pdf.py:367 +msgid "Page" +msgstr "Pagina" + +#: streetup/controls/views_reports_pdf.py:409 +msgid "Description : " +msgstr "Beschrijving: " + +#: streetup/controls/views_reports_pdf.py:504 +#, fuzzy, python-format +#| msgid "Erreur de chargement d'image: %(e)s" +msgid "Erreur lors du chargement ou du dessin sur le plan %(name)s: %(e)s" +msgstr "Fout bij het laden van afbeelding: %(e)s" + +#: streetup/controls/views_reports_pdf.py:742 +msgid "Inconnu" +msgstr "Onbekend" + +#: streetup/controls/views_reports_pdf.py:837 +msgid "PDF généré et sauvegardé avec succès." +msgstr "PDF succesvol gegenereerd en opgeslagen." + +#: streetup/controls/views_reports_pdf.py:844 +msgid "Erreur lors de la génération du PDF" +msgstr "Fout bij het genereren van de PDF" + +#~ msgid "Prendre une photo" +#~ msgstr "Een foto nemen" + +#~ msgid "Retour" +#~ msgstr "Terug" + +#~ msgid "Saisi le" +#~ msgstr "Ingevoerd op" + +#~ msgid "Marché" +#~ msgstr "Bestek" + +#~ msgid "Supprimer la quantité" +#~ msgstr "Hoeveelheid verwijderen" + +#~ msgid "Ajouter la quantité" +#~ msgstr "Hoeveelheid toevoegen" + +#~ msgid "Quantité ajoutée avec succès." +#~ msgstr "Hoeveelheid succesvol toegevoegd." + +#~ msgid "Quantité supprimée." +#~ msgstr "Hoeveelheid verwijderd." + +#~ msgid "Quantité introuvable ou déjà supprimée." +#~ msgstr "Hoeveelheid niet gevonden of al verwijderd." diff --git a/streetup/controls/locale/nl/LC_MESSAGES/djangojs.mo b/streetup/controls/locale/nl/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000..b108f7b Binary files /dev/null and b/streetup/controls/locale/nl/LC_MESSAGES/djangojs.mo differ diff --git a/streetup/controls/locale/nl/LC_MESSAGES/djangojs.po b/streetup/controls/locale/nl/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..cecaa6b --- /dev/null +++ b/streetup/controls/locale/nl/LC_MESSAGES/djangojs.po @@ -0,0 +1,146 @@ +# Nederlandse vertalingen voor de Controls module (JavaScript). +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-23 14:00+0200\n" +"PO-Revision-Date: 2025-12-04 13:00+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Dutch \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: controls/static/controls/controls_detail.js:797 +msgid "Rapport généré avec succès !" +msgstr "Rapport succesvol gegenereerd!" + +#: controls/static/controls/controls_detail.js:814 +msgid "Généré le" +msgstr "Gegenereerd op" + +#: controls/static/controls/controls_detail.js:815 +msgid "Auteur" +msgstr "Auteur" + +#: controls/static/controls/controls_detail.js:820 +msgid "Télécharger" +msgstr "Downloaden" + +#: controls/static/controls/controls_detail.js:824 +msgid "Partager" +msgstr "Delen" + +#: controls/static/controls/controls_detail.js:841 +msgid "Erreur lors de la génération du rapport." +msgstr "Fout bij het genereren van het rapport." + +#: controls/static/controls/controls_detail.js:846 +#: controls/static/controls/controls_detail.js:1040 +#: controls/static/controls/controls_detail.js:1056 +msgid "Une erreur est survenue." +msgstr "Er is een fout opgetreden." + +#: controls/static/controls/controls_detail.js:870 +msgid "Erreur lors de l'archivage." +msgstr "Fout bij het archiveren." + +#: controls/static/controls/controls_detail.js:901 +#: controls/static/controls/controls_detail.js:976 +#: controls/static/controls/controls_detail.js:1055 +msgid "Erreur :" +msgstr "Fout:" + +#: controls/static/controls/controls_detail.js:902 +#: controls/static/controls/controls_detail.js:977 +msgid "Une erreur est survenue lors de l'archivage." +msgstr "Er is een fout opgetreden bij het archiveren." + +#: controls/static/controls/controls_detail.js:918 +msgid "Erreur lors de la restauration de la remarque." +msgstr "Fout bij het herstellen van de opmerking." + +#: controls/static/controls/controls_detail.js:996 +msgid "Partager ce PDF" +msgstr "Deze PDF delen" + +#: controls/static/controls/controls_detail.js:997 +msgid "Voici un rapport PDF que je souhaite partager avec vous." +msgstr "Hier is een PDF-rapport dat ik met u wil delen." + +#: controls/static/controls/controls_detail.js:1000 +msgid "Le partage de fichiers n'est pas supporté sur ce navigateur." +msgstr "Bestandsdeling wordt niet ondersteund in deze browser." + +#: controls/static/controls/controls_detail.js:1003 +msgid "Erreur lors du partage du PDF:" +msgstr "Fout bij het delen van de PDF:" + +#: controls/static/controls/controls_detail.js:1629 +#: controls/static/controls/remarks_form.js:118 +msgid "La caméra avancée n'est pas supportée sur ce navigateur." +msgstr "De geavanceerde camera wordt niet ondersteund in deze browser." + +#: controls/static/controls/controls_detail.js:1634 +#: controls/static/controls/remarks_form.js:122 +msgid "Initialisation de la caméra…" +msgstr "Initialiseren van de camera…" + +#: controls/static/controls/controls_detail.js:1650 +#: controls/static/controls/remarks_form.js:160 +#: controls/static/controls/remarks_form.js:188 +msgid "Caméra prête. Appuyez sur Capturer pour prendre une photo." +msgstr "Camera klaar. Druk op Vastleggen om een foto te maken." + +#: controls/static/controls/controls_detail.js:1654 +#: controls/static/controls/remarks_form.js:224 +msgid "Impossible d'accéder à la caméra. Veuillez vérifier les autorisations." +msgstr "Kan geen toegang krijgen tot de camera. Controleer de machtigingen." + +#: controls/static/controls/controls_detail.js:1655 +#: controls/static/controls/remarks_form.js:228 +msgid "Impossible d'accéder à la caméra." +msgstr "Kan geen toegang krijgen tot de camera." + +#: controls/static/controls/controls_detail.js:1677 +#: controls/static/controls/remarks_form.js:250 +msgid "La vidéo n'est pas encore prête." +msgstr "De video is nog niet klaar." + +#: controls/static/controls/controls_detail.js:1689 +#: controls/static/controls/remarks_form.js:262 +msgid "Erreur lors de la capture." +msgstr "Fout bij het vastleggen." + +#: controls/static/controls/controls_detail.js:1698 +#: controls/static/controls/remarks_form.js:271 +msgid "Photo capturée avec succès." +msgstr "Foto succesvol vastgelegd." + +#: controls/static/controls/remarks_form.js:50 +msgid "" +"L'enregistrement a pris trop de temps. Vérifiez votre connexion et réessayez." +msgstr "" + +#: controls/static/controls/remarks_form.js:209 +msgid "" +"Accès à la caméra refusé. Pour l'autoriser :\n" +"• Android : Réglages → Applications → [Navigateur] → Autorisations → Caméra\n" +"• iPhone : Réglages → Safari → Caméra → Autoriser" +msgstr "" + +#: controls/static/controls/remarks_form.js:216 +msgid "" +"L'accès à la caméra a été refusé. Veuillez appuyer sur 'Autoriser' lors de " +"la prochaine demande, ou vérifiez les autorisations dans les réglages de " +"votre navigateur." +msgstr "" + +#: controls/static/controls/remarks_form.js:222 +msgid "Aucune caméra disponible sur cet appareil." +msgstr "" diff --git a/streetup/controls/migrations/0001_initial.py b/streetup/controls/migrations/0001_initial.py new file mode 100644 index 0000000..31c43bc --- /dev/null +++ b/streetup/controls/migrations/0001_initial.py @@ -0,0 +1,194 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import common.storages +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('assets', '0002_initial'), + ('common', '0002_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ControlCheck', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('ok', 'OK'), ('nok', 'NOK'), ('na', 'N/A'), ('nv', 'Not verified')], default='nv')), + ], + ), + migrations.CreateModel( + name='ControlQuantity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(blank=True, max_length=255, null=True)), + ('q1', models.DecimalField(decimal_places=2, max_digits=12)), + ('q2', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('q3', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('computed_qty', models.DecimalField(decimal_places=2, default=0, editable=False, max_digits=14)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['created_at', 'id'], + }, + ), + migrations.CreateModel( + name='ControlReferencePlan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('plan', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='control_plans/', verbose_name='Plan')), + ('uploaded_at', models.DateTimeField(auto_now=True)), + ('max_x', models.FloatField(blank=True, null=True)), + ('max_y', models.FloatField(blank=True, null=True)), + ], + options={ + 'ordering': ['control', 'name'], + }, + ), + migrations.CreateModel( + name='ControlReport', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('active', 'Actif'), ('validated', 'Validé'), ('archived', 'Archivé'), ('canceled', 'Annulé')], default='active')), + ('document', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='control_reports/', verbose_name='Document')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='ControlReportTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Template standard', max_length=255)), + ('orientation', models.CharField(choices=[('portrait', 'Portrait'), ('landscape', 'Paysage')], default='portrait', max_length=10)), + ('header_notes', models.TextField(blank=True, null=True)), + ('footer_notes', models.TextField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='DocumentCheck', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('doc_type', models.CharField(choices=[('as_built', 'Plan as-built'), ('techical_sheets', 'Fiches techniques')], default='as_built')), + ('status', models.CharField(choices=[('ok', 'OK'), ('nok', 'NOK'), ('na', 'N/A'), ('nv', 'Not verified')], default='nv')), + ], + ), + migrations.CreateModel( + name='Remark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField()), + ('internal', models.BooleanField(default=False)), + ('status', models.CharField(choices=[('information', 'Info'), ('to_be_processed', 'NOK'), ('processed', 'Traité'), ('validated', 'OK'), ('canceled', 'Annulé')], default='to_be_processed')), + ('archived', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('remark_number', models.PositiveIntegerField(blank=True, editable=False, null=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('x', models.FloatField(blank=True, null=True)), + ('y', models.FloatField(blank=True, null=True)), + ], + options={ + 'ordering': ['control', 'remark_number'], + }, + ), + migrations.CreateModel( + name='RemarkImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to='controls/remarks/', verbose_name='Fichier')), + ('thumbnail', models.ImageField(blank=True, max_length=255, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='controls/remarks/')), + ], + options={ + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='RemarkTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cat_code', models.CharField(max_length=100)), + ('name_fr', models.CharField(max_length=255, verbose_name='Name FR')), + ('name_nl', models.CharField(max_length=255, verbose_name='Name NL')), + ('thematic', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.thematic')), + ], + options={ + 'ordering': ['cat_code'], + }, + ), + migrations.CreateModel( + name='CheckList', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('checklist_code', models.CharField(max_length=100)), + ('name_fr', models.CharField(max_length=255, verbose_name='Name FR')), + ('name_nl', models.CharField(max_length=255, verbose_name='Name NL')), + ('thematic', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.thematic')), + ], + options={ + 'ordering': ['checklist_code'], + }, + ), + migrations.CreateModel( + name='CheckListControlCheck', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists_checks', to='controls.category')), + ('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checks', to='controls.checklist')), + ], + ), + migrations.CreateModel( + name='CheckListDocumentCheck', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('doc_type', models.CharField(choices=[('as_built', 'Plan as-built'), ('techical_sheets', 'Fiches techniques')], default='as_built')), + ('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='document_checks', to='controls.checklist')), + ], + ), + migrations.CreateModel( + name='Control', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('control_code', models.CharField(max_length=100, unique=True, verbose_name='Code du contrôle')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description')), + ('status', models.CharField(choices=[('in_progress', 'En cours'), ('information', 'Information'), ('to_be_processed', 'A traiter'), ('processed', 'Traité'), ('validated', 'Validé'), ('canceled', 'Annulé')], default='in_progress', verbose_name='Statut')), + ('control_type', models.CharField(choices=[('control', 'Contrôle'), ('site_report', 'Rapport de chantier'), ('statement', 'Constat'), ('meeting_report', 'Rapport de réunion')], default='control', verbose_name='Type de contrôle')), + ('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Localisation')), + ('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812, verbose_name='Géométrie')), + ('geojson', models.JSONField(blank=True, null=True, verbose_name='GeoJSON')), + ('lon', models.FloatField(blank=True, null=True, verbose_name='Longitude')), + ('lat', models.FloatField(blank=True, null=True, verbose_name='Latitude')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('visibility', models.CharField(choices=[('private', 'Privé'), ('intern', 'Interne'), ('all', 'Interne et externe')], default='intern', verbose_name='Visibilité')), + ('shared_edition', models.BooleanField(default=False, verbose_name='Edition partagée')), + ('categories', models.ManyToManyField(blank=True, related_name='controls', to='assets.assetcategory', verbose_name='Catégories')), + ('control_author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='controls', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-updated_at'], + }, + ), + ] diff --git a/streetup/controls/migrations/0002_initial.py b/streetup/controls/migrations/0002_initial.py new file mode 100644 index 0000000..57558a7 --- /dev/null +++ b/streetup/controls/migrations/0002_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('controls', '0001_initial'), + ('interventions', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='control', + name='interventions', + field=models.ManyToManyField(blank=True, related_name='related_controls', to='interventions.intervention', verbose_name='Interventions'), + ), + ] diff --git a/streetup/controls/migrations/0003_initial.py b/streetup/controls/migrations/0003_initial.py new file mode 100644 index 0000000..e0c9969 --- /dev/null +++ b/streetup/controls/migrations/0003_initial.py @@ -0,0 +1,137 @@ +# Generated by Django 5.2.8 on 2026-05-21 13:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('common', '0003_initial'), + ('contracts', '0002_initial'), + ('controls', '0002_initial'), + ('documents', '0001_initial'), + ('projects', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='control', + name='phase', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='controls', to='projects.projectphase', verbose_name='Phase'), + ), + migrations.AddField( + model_name='control', + name='project', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='controls', to='projects.project', verbose_name='Projet'), + ), + migrations.AddField( + model_name='control', + name='thematics', + field=models.ManyToManyField(blank=True, related_name='controls', to='common.thematic', verbose_name='Thématiques'), + ), + migrations.AddField( + model_name='controlcheck', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checks', to='controls.category'), + ), + migrations.AddField( + model_name='controlcheck', + name='control', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checks', to='controls.control'), + ), + migrations.AddField( + model_name='controlquantity', + name='contract', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='control_quantities', to='contracts.contract'), + ), + migrations.AddField( + model_name='controlquantity', + name='contract_post', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='control_quantities', to='contracts.contractpost'), + ), + migrations.AddField( + model_name='controlreferenceplan', + name='control', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reference_plans', to='controls.control'), + ), + migrations.AddField( + model_name='controlreferenceplan', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='controlreport', + name='control', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports', to='controls.control'), + ), + migrations.AddField( + model_name='controlreport', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='controlreporttemplate', + name='control', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='controls.control'), + ), + migrations.AddField( + model_name='controlreporttemplate', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='controls_templates', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='documentcheck', + name='control', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='document_checks', to='controls.control'), + ), + migrations.AddField( + model_name='remark', + name='control', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='controls.control'), + ), + migrations.AddField( + model_name='remark', + name='reference_plan', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='remarks', to='controls.controlreferenceplan'), + ), + migrations.AddField( + model_name='remark', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='controls_remarks', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='controlquantity', + name='remark', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quantities', to='controls.remark'), + ), + migrations.AddField( + model_name='remarkimage', + name='managed_document', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument'), + ), + migrations.AddField( + model_name='remarkimage', + name='remark', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='controls.remark'), + ), + migrations.AddField( + model_name='remarkimage', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par'), + ), + migrations.AddField( + model_name='remarktag', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remark_tags', to='controls.category'), + ), + migrations.AddField( + model_name='remarktag', + name='remark', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remark_tags', to='controls.remark'), + ), + ] diff --git a/streetup/controls/migrations/0004_remarkhistory_remarkimage_history_entry.py b/streetup/controls/migrations/0004_remarkhistory_remarkimage_history_entry.py new file mode 100644 index 0000000..5a28344 --- /dev/null +++ b/streetup/controls/migrations/0004_remarkhistory_remarkimage_history_entry.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.8 on 2026-06-23 13:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('controls', '0003_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='RemarkHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True)), + ('status', models.CharField(choices=[('information', 'Info'), ('to_be_processed', 'NOK'), ('processed', 'Traité'), ('validated', 'OK'), ('canceled', 'Annulé')], default='to_be_processed')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('remark', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='controls.remark')), + ], + options={ + 'ordering': ['created_at'], + }, + ), + migrations.AddField( + model_name='remarkimage', + name='history_entry', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='controls.remarkhistory'), + ), + ] diff --git a/streetup/controls/migrations/__init__.py b/streetup/controls/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streetup/controls/models.py b/streetup/controls/models.py new file mode 100644 index 0000000..baacfa2 --- /dev/null +++ b/streetup/controls/models.py @@ -0,0 +1,364 @@ +from django.contrib.gis.db import models +from django.utils.translation import get_language, gettext_lazy as _ +from django.db.models import Max +from django.conf import settings +from django.urls import reverse + +from PIL import Image +from decimal import Decimal, ROUND_HALF_UP + +from common.models import Thematic, Document +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() +from projects.models import Project, ProjectPhase +from interventions.models import Intervention +from assets.models import AssetCategory +from contracts.models import Contract, ContractPost + + +CONTROL_STATUS_CHOICES = { + 'in_progress': _('En cours'), + 'information': _('Information'), + 'to_be_processed': _('A traiter'), + 'processed': _('Traité'), + 'validated': _('Validé'), + 'canceled': _('Annulé'), +} + +CONTROL_TYPE_CHOICES = { + 'control': _('Contrôle'), + 'site_report': _('Rapport de chantier'), + 'statement': _('Constat'), + 'meeting_report': _('Rapport de réunion'), +} + +VISIBILITY_CHOICES = { + 'private': _('Privé'), + 'intern': _('Interne'), + 'all': _('Interne et externe'), +} + + +class Control(models.Model): + control_code = models.CharField(max_length=100, unique=True, verbose_name=_("Code du contrôle")) + description = models.TextField(null=True, blank=True, verbose_name=_("Description")) + status = models.CharField(choices=CONTROL_STATUS_CHOICES, default='in_progress', verbose_name=_("Statut")) + control_type = models.CharField(choices=CONTROL_TYPE_CHOICES, default='control', verbose_name=_("Type de contrôle")) + thematics = models.ManyToManyField(Thematic, related_name='controls', blank=True, verbose_name=_("Thématiques")) + categories = models.ManyToManyField(AssetCategory, related_name='controls', blank=True, verbose_name=_("Catégories")) + project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True, related_name='controls', verbose_name=_("Projet")) + phase = models.ForeignKey(ProjectPhase, null=True, on_delete=models.CASCADE, blank=True, related_name='controls', verbose_name=_("Phase")) + interventions = models.ManyToManyField(Intervention, related_name='related_controls', blank=True, verbose_name=_("Interventions")) + location = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Localisation")) + geom = models.PointField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie")) + geojson = models.JSONField(null=True, blank=True, verbose_name=_("GeoJSON")) + lon = models.FloatField(null=True, blank=True, verbose_name=_("Longitude")) + lat = models.FloatField(null=True, blank=True, verbose_name=_("Latitude")) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + control_author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="controls", null=True, blank=True) + visibility = models.CharField(choices=VISIBILITY_CHOICES, default='intern', verbose_name=_("Visibilité")) + shared_edition = models.BooleanField(default=False, verbose_name=_("Edition partagée")) + + def __str__(self): + return self.control_code + + def get_absolute_url(self): + """ + Returns the URL to access a specific project instance. + """ + return reverse('controls:controls_detail', args=[str(self.id)]) + + def save(self, *args, **kwargs): + """ + Sauvegarde le Control et génère automatiquement la géométrie à partir des coordonnées lon/lat. + Le code de contrôle est généré automatiquement (CTRL-XXXX) si non défini. + """ + # Auto-générer le code de contrôle si non défini + if not self.control_code: + existing_codes = Control.objects.filter( + control_code__regex=r'^CTRL-\d+$' + ).values_list('control_code', flat=True) + max_num = 0 + for code in existing_codes: + try: + num = int(code.split('-')[1]) + if num > max_num: + max_num = num + except (IndexError, ValueError): + pass + self.control_code = f"CTRL-{max_num + 1:04d}" + + # Générer la géométrie si lon et lat sont fournis + if self.lon is not None and self.lat is not None: + from django.contrib.gis.geos import Point + self.geom = Point(self.lon, self.lat, srid=4326).transform(3812, clone=True) + + super().save(*args, **kwargs) + + class Meta: + ordering = ["-updated_at"] + + +class Category(models.Model): + thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True) + cat_code = models.CharField(max_length=100) + name_fr = models.CharField(max_length=255, verbose_name="Name FR") + name_nl = models.CharField(max_length=255, verbose_name="Name NL") + + def get_name(self): + lang = get_language() + return getattr(self, f"name_{lang}", self.name_fr) # Fallback to name_fr + + def __str__(self): + return f"{self.get_name()}" + + class Meta: + ordering = ["cat_code"] + + + +CONTROL_CHECK_STATUS_CHOICES = { + 'ok': 'OK', + 'nok': 'NOK', + 'na': 'N/A', + 'nv': 'Not verified', +} + +class ControlCheck(models.Model): + control = models.ForeignKey(Control, on_delete=models.CASCADE, related_name="checks") + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="checks") + status = models.CharField(choices=CONTROL_CHECK_STATUS_CHOICES, default='nv') + + +DOC_TYPE_CHOICES = { + 'as_built': _('Plan as-built'), + 'techical_sheets': _('Fiches techniques'), +} + +class DocumentCheck(models.Model): + control = models.ForeignKey(Control, on_delete=models.CASCADE, related_name="document_checks") + doc_type = models.CharField(choices=DOC_TYPE_CHOICES, default='as_built') + status = models.CharField(choices=CONTROL_CHECK_STATUS_CHOICES, default='nv') + + +class CheckList(models.Model): + thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True) + checklist_code = models.CharField(max_length=100) + name_fr = models.CharField(max_length=255, verbose_name="Name FR") + name_nl = models.CharField(max_length=255, verbose_name="Name NL") + + def get_name(self): + lang = get_language() + return getattr(self, f"name_{lang}", self.name_fr) # Fallback to name_fr + + def __str__(self): + return f"{self.get_name()}" + + class Meta: + ordering = ["checklist_code"] + + +class CheckListControlCheck(models.Model): + checklist = models.ForeignKey(CheckList, on_delete=models.CASCADE, related_name="checks") + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="checklists_checks") + +class CheckListDocumentCheck(models.Model): + checklist = models.ForeignKey(CheckList, on_delete=models.CASCADE, related_name="document_checks") + doc_type = models.CharField(choices=DOC_TYPE_CHOICES, default='as_built') + + + +REPORT_STATUS_CHOICES = { + 'active': _('Actif'), + 'validated': _('Validé'), + 'archived': _('Archivé'), + 'canceled': _('Annulé'), +} + +class ControlReport(models.Model): + control = models.ForeignKey(Control, on_delete=models.CASCADE, related_name="reports") + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + status = models.CharField(choices=REPORT_STATUS_CHOICES, default='active') + document = models.FileField(upload_to="control_reports/", storage=private_storage, verbose_name="Document", max_length=255) + + class Meta: + ordering = ["-created_at"] + + +class ControlReferencePlan(models.Model): + control = models.ForeignKey(Control, on_delete=models.CASCADE, related_name="reference_plans") + name = models.CharField(max_length=255) + plan = models.FileField(upload_to="control_plans/", storage=private_storage, verbose_name="Plan", max_length=255) + uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + uploaded_at = models.DateTimeField(auto_now=True) + max_x = models.FloatField(null=True, blank=True) + max_y = models.FloatField(null=True, blank=True) + + def save(self, *args, **kwargs): + # Ouvrir l'image pour récupérer ses dimensions + if self.plan: # Vérifier si une image est bien attachée + with Image.open(self.plan) as img: + width, height = img.size + + # Calculer max_x et max_y en fonction des dimensions + if width > height: + # Largeur > hauteur : max_x = 1, max_y est ajusté + self.max_x = 1.0 + self.max_y = height / width + else: + # Hauteur > largeur : max_y = 1, max_x est ajusté + self.max_y = 1.0 + self.max_x = width / height + + # Appeler la méthode save() de la classe parente + super().save(*args, **kwargs) + + class Meta: + ordering = ["control", "name"] + + + + + +REMARK_STATUS_CHOICES = { + 'information': _('Info'), + 'to_be_processed': _('NOK'), + 'processed': _('Traité'), + 'validated': _('OK'), + 'canceled': _('Annulé'), +} + +class Remark(models.Model): + control = models.ForeignKey(Control, on_delete=models.CASCADE, related_name="remarks") + description = models.TextField() + internal = models.BooleanField(default=False) + status = models.CharField(choices=REMARK_STATUS_CHOICES, default='to_be_processed') + archived = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="controls_remarks", null=True, blank=True) + remark_number = models.PositiveIntegerField(null=True, blank=True, editable=False) + reference_plan = models.ForeignKey(ControlReferencePlan, null=True, blank=True, on_delete=models.SET_NULL, related_name="remarks") + lon = models.FloatField(null=True,blank=True) + lat = models.FloatField(null=True,blank=True) + x = models.FloatField(null=True,blank=True) # Coordonnée x relative au plan + y = models.FloatField(null=True,blank=True) # Coordonnée y relative au plan + + def save(self, *args, **kwargs): + if self.remark_number is None: + last_number = Remark.objects.filter(control=self.control).aggregate(Max("remark_number"))["remark_number__max"] + self.remark_number = 1 if last_number is None else last_number + 1 + + super().save(*args, **kwargs) + + def __str__(self): + return _("Remarque %(num)s pour %(control)s") % { + "num": self.remark_number, + "control": self.control, + } + + class Meta: + ordering = ["control", "remark_number"] + + +class RemarkHistory(models.Model): + remark = models.ForeignKey(Remark, on_delete=models.CASCADE, related_name="history") + description = models.TextField(blank=True, null=True) + status = models.CharField(choices=REMARK_STATUS_CHOICES, default='to_be_processed') + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + + class Meta: + ordering = ["created_at"] + + def __str__(self): + return f"History entry for Remark {self.remark.remark_number} ({self.created_at.strftime('%d/%m/%Y')})" + + +class RemarkImage(Document): + remark = models.ForeignKey(Remark, on_delete=models.CASCADE, related_name="images") + history_entry = models.ForeignKey(RemarkHistory, on_delete=models.CASCADE, related_name="images", null=True, blank=True) + file = models.FileField(upload_to="controls/remarks/", storage=private_storage, verbose_name="Fichier", max_length=255) + thumbnail = models.ImageField(upload_to="controls/remarks/", storage=private_storage, null=True, blank=True, max_length=255) + + class Meta: + ordering = ["id"] + +class RemarkTag(models.Model): + remark = models.ForeignKey(Remark, on_delete=models.CASCADE, related_name="remark_tags") + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="remark_tags") + + +class ControlReportTemplate(models.Model): + control = models.ForeignKey(Control, on_delete=models.CASCADE, related_name="templates") + name = models.CharField(max_length=255, default="Template standard") + orientation = models.CharField(max_length=10, choices=[("portrait", "Portrait"), ("landscape", "Paysage")], default="portrait") + header_notes = models.TextField(null=True, blank=True) + footer_notes = models.TextField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="controls_templates", null=True, blank=True) + + + + +class ControlQuantity(models.Model): + """Ligne de quantité ajoutée dans une remarque, potentiellement liée à un marché/poste.""" + remark = models.ForeignKey(Remark, on_delete=models.CASCADE, related_name="quantities") + + # Références marché (facultatives) + contract = models.ForeignKey(Contract, on_delete=models.SET_NULL, null=True, blank=True, related_name="control_quantities") + contract_post = models.ForeignKey(ContractPost, on_delete=models.SET_NULL, null=True, blank=True, related_name="control_quantities") + + # Description libre si pas de marché sélectionné (ou pour override) + description = models.CharField(max_length=255, blank=True, null=True) + + # Décomposition de la quantité + q1 = models.DecimalField(max_digits=12, decimal_places=2) + q2 = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) # ex: 3 m + q3 = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) # ex: 1 m² + + # Résultat calculé + computed_qty = models.DecimalField(max_digits=14, decimal_places=2, editable=False, default=0) + + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["created_at", "id"] + + def __str__(self): + label = self.get_label() + return f"{label} — {self.computed_qty}".strip() + + def get_label(self) -> str: + """Libellé prioritaire: poste de marché > description libre.""" + if self.contract_post: + # tu peux ajuster selon tes champs (code + description) + return f"{self.contract_post.code} — {self.contract_post.description_fr[:60]}" + return self.description or "" + + def _q(self, x): + return Decimal(x) if x is not None else None + + def compute(self): + qty = Decimal("0") + + q1 = self.q1 + q2 = self.q2 + q3 = self.q3 + + if q1 is not None and q2 is not None and q3 is not None: + qty = (q1 * q2 * q3) + elif q1 is not None and q2 is not None: + qty = (q1 * q2) + else: + qty = q1 + + qty = qty.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + return qty + + def save(self, *args, **kwargs): + self.computed_qty = self.compute() + super().save(*args, **kwargs) \ No newline at end of file diff --git a/streetup/controls/permissions.py b/streetup/controls/permissions.py new file mode 100644 index 0000000..11ce6b4 --- /dev/null +++ b/streetup/controls/permissions.py @@ -0,0 +1,144 @@ +from functools import wraps +from django.http import HttpResponseForbidden +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext as _ + +from controls.models import Control +from projects.models import ProjectUserAccess +from common.models import UserThematics + +def user_can_view_control(user, control): + if hasattr(user, "config") and user.config.roles.filter(name__in=['admin', 'top_manager']).exists(): + return True + + if control.control_author == user: + return True + + if hasattr(control, "visibility") and control.visibility == 'private': + return user == control.control_author + + if hasattr(control, "visibility") and control.visibility == 'intern' and ((not hasattr(user, "config")) or user.config.is_intern == False): + return False + + if control.project: + has_project_access = ProjectUserAccess.objects.filter(project=control.project, user=user, can_view=True).exists() + if has_project_access: + return True + # Fallback thématique : les contrôles privés sont déjà gérés plus haut ; + # les non-internes sont bloqués à l'étape visibility=='intern' ci-dessus. + # Un interne ayant accès à l'une des thématiques du contrôle peut donc le voir. + if control.thematics.exists() and hasattr(user, "config") and user.config.is_intern: + return UserThematics.objects.filter( + user_config=user.config, + thematic__in=control.thematics.all(), + can_view_interventions=True + ).exists() + return False + + if control.thematics.exists() and hasattr(user, "config") and user.config.is_intern: + return UserThematics.objects.filter( + user_config=user.config, + thematic__in=control.thematics.all(), + can_view_interventions=True + ).exists() + + return False + + +def control_view_required(view_func): + @wraps(view_func) + def _wrapped_view(request, control_id, *args, **kwargs): + control = get_object_or_404(Control, id=control_id) + + if user_can_view_control(request.user, control): + return view_func(request, control_id, *args, **kwargs) + + return HttpResponseForbidden(_("Vous n'êtes pas autorisé à accéder à ce contrôle.")) + + return _wrapped_view + + +def user_can_add_control(user): + if hasattr(user, "config") and user.config.roles.filter(name__in=['admin', 'manager', 'controller']).exists(): + return True + + return False + +def control_add_required(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + if user_can_add_control(request.user): + return view_func(request, *args, **kwargs) + + return HttpResponseForbidden(_("Vous n'êtes pas autorisé à ajouter un contrôle.")) + + return _wrapped_view + + + +def user_can_change_control(user, control): + if not hasattr(user, "config"): + return False + + if user.config.roles.filter(name__in=["admin"]).exists(): + return True + + if not control.shared_edition: + return user == control.control_author + + # 1. L'utilisateur doit avoir un rôle suffisant + has_permission_role = ( + hasattr(user, "config") and + user.config.roles.filter(name__in=["manager", "controller"]).exists() + ) + + # 2. L'utilisateur doit pouvoir voir ce contrôle + can_view = user_can_view_control(user, control) + + return has_permission_role and can_view + +def control_change_required(view_func): + @wraps(view_func) + def _wrapped_view(request, control_id, *args, **kwargs): + control = get_object_or_404(Control, id=control_id) + + if user_can_change_control(request.user, control): + return view_func(request, control_id, *args, **kwargs) + + return HttpResponseForbidden(_("Vous n'êtes pas autorisé à modifier ce contrôle.")) + + return _wrapped_view + + +# ============================================================================ +# Guards pour les fichiers privés (private_media) +# ============================================================================ + +def guard_control_report_document(obj, user): + """ + Guard pour ControlReport.document. + L'utilisateur doit pouvoir voir le contrôle associé. + """ + if not user.is_authenticated: + return False + return user_can_view_control(user, obj.control) + + +def guard_control_reference_plan(obj, user): + """ + Guard pour ControlReferencePlan.plan. + L'utilisateur doit pouvoir voir le contrôle associé. + """ + if not user.is_authenticated: + return False + return user_can_view_control(user, obj.control) + + +def guard_remark_image_file(obj, user): + """ + Guard pour RemarkImage.file et thumbnail. + L'utilisateur doit pouvoir voir le contrôle associé à la remarque. + """ + if not user.is_authenticated: + return False + return user_can_view_control(user, obj.remark.control) \ No newline at end of file diff --git a/streetup/controls/signals.py b/streetup/controls/signals.py new file mode 100644 index 0000000..9bccaab --- /dev/null +++ b/streetup/controls/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Control, ControlReportTemplate + +@receiver(post_save, sender=Control) +def create_default_control_report_template(sender, instance, created, **kwargs): + if created: + ControlReportTemplate.objects.create( + control=instance, + name="Template standard", + orientation="portrait", + updated_by=instance.control_author if hasattr(instance, "control_author") else None + ) diff --git a/streetup/controls/static/controls/controls_detail.css b/streetup/controls/static/controls/controls_detail.css new file mode 100644 index 0000000..e38d927 --- /dev/null +++ b/streetup/controls/static/controls/controls_detail.css @@ -0,0 +1,178 @@ +body { + padding-top: 70px; /* Ajuste cette valeur selon la hauteur de ta navbar */ +} + +.remark-images { + display: flex; + flex-wrap: wrap; + gap: 10px; /* Espacement entre les images */ +} + +.remark-image-wrapper { + position: relative; + display: inline-block; +} + +.remark-images img { + width: 100px; /* Largeur fixe */ + height: 100px; /* Hauteur fixe */ + object-fit: cover; /* Garde le ratio et remplit */ + border-radius: 8px; /* Coins arrondis */ + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); /* Ombre légère */ +} + +.remark-images a { + display: inline-block; /* Permet de cliquer sur toute la zone */ + border-radius: 8px; + overflow: hidden; /* Pour éviter les débordements */ +} + +.remark-image-delete { + position: absolute; + top: 4px; + right: 4px; + padding: 0.125rem 0.35rem; + z-index: 5; +} + +#remark-form-container { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: white; + z-index: 1050; + display: none; /* Caché par défaut */ + flex-direction: column; +} + +#quantity-form-container { + position: fixed; + top:0; left:0; width:100%; height:100dvh; + background: #fff; + z-index:1050; + + display:none; + justify-content:center; /* horizontal */ + padding: 16px; /* marge écran mobile */ + overflow:auto; /* scroll si contenu > viewport */ +} + +#add-quantity-btn { scroll-margin-top: 80px; } + +#map { + height: 45vh; /* 45% de l'écran pour la carte */ +} + +/* ✅ Positionner le conteneur des boutons sur la carte */ +#layer-controls { + position: absolute; + top: 20px; /* Distance du bas */ + left: 20px; /* Distance de la gauche */ + z-index: 1000; /* Assure que c'est au-dessus de la carte */ +} + +/* ✅ Améliorer le style des boutons */ +#layer-controls button { + display: inline; + margin: 5px 0; + width: 100px; + text-align: center; + overflow: hidden; /* ✅ Empêcher le débordement */ + white-space: nowrap; /* ✅ Empêcher le retour à la ligne */ + text-overflow: ellipsis; /* ✅ Tronquer le texte si nécessaire */ + font-size: 14px; /* ✅ Taille de police par défaut */ + border: 1px solid #007bff; /* ✅ Bordure bleue */ +} + + + +.form-content { + flex-grow: 1; /* Formulaire prend l'espace restant */ + padding: 20px; + overflow-y: auto; +} + +.close-btn { + position: absolute; + top: 15px; right: 20px; + z-index:10001; +} + +.remark-number { + display: inline-flex; + justify-content: center; + align-items: center; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: rgb(169, 179, 191); + color: white; + font-weight: bold; + font-size: 14px; + text-align: center; +} + +.badge { + min-width: 120px; /* Assure une largeur uniforme pour le statut */ + text-align: center; +} + +.d-flex.gap-2 button { + width: 40px; /* Assure que les boutons ont la même taille */ +} + + + +#upload-plan-form-container { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: white; + z-index: 1050; + display: none; /* Caché par défaut */ + flex-direction: column; +} + +.message { + margin-top: 10px; + padding: 10px; + border-radius: 5px; +} +.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} +.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.status-badge { + min-width: 80px; +} + +.stick-to-bottom { + margin-top: auto; + } + + +/***** Action bar mobile *****/ +:root { + --action-bar-h: 220px; /* hauteur par défaut (2 gros boutons) */ +} + +.action-bar { + padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + box-shadow: 0 -6px 16px rgba(0,0,0,.08); + z-index: 1060; +} + +.content-scroll{ + padding-bottom: calc(var(--action-bar-h) + env(safe-area-inset-bottom, 0px)) !important; +} + +#qty-editor, .form-content{ + height: 100dvh; /* mieux que 100vh sur mobile */ + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/streetup/controls/static/controls/controls_detail.js b/streetup/controls/static/controls/controls_detail.js new file mode 100644 index 0000000..a812b06 --- /dev/null +++ b/streetup/controls/static/controls/controls_detail.js @@ -0,0 +1,1837 @@ +// Vérifier si offline-manager.js est chargé + +let map, marker, geolocateControl; +var current_reference_plan = "map"; + +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} +const csrftoken = getCookie('csrftoken'); + +let deleteImageModalInstance = null; +let deleteImageModalElement = null; +let pendingDeleteImage = null; +let deleteImageConfirmButton = null; + + +// ✅ Définition des couleurs selon le statut +const statusColors = { + "information": "#0dcaf0 ", // Bleu (Information) + "to_be_processed": "#dc3545 ", // Jaune (À traiter) + "processed": "#0d6efd ", // Bleu (Traité) + "validated": "#198754 ", // Vert (Validé) + "canceled": "#6c757d" // Gris (Annulé) +}; + + +map = new maplibregl.Map({ + container: 'map', // ID de l'élément HTML + style: window.MAP_STYLE_LIGHT, + center: [4.3572, 50.8477], + zoom: 11, + minzoom: 4, + maxzoom: 24 +}); + +// Ajouter un marqueur au clic +map.on('click', function (e) { + let coords = e.lngLat; + + if (marker) { + marker.setLngLat([coords.lng, coords.lat]); + } else { + marker = new maplibregl.Marker({ draggable: true }) + .setLngLat([coords.lng, coords.lat]) + .addTo(map); + } + + // Mettre à jour les champs cachés avec la localisation + document.getElementById("reference_plan").value = current_reference_plan; + document.getElementById("coord_x").value = coords.lng; + document.getElementById("coord_y").value = coords.lat; + if (current_reference_plan === "map") { + document.getElementById("longitude").value = coords.lng; + document.getElementById("latitude").value = coords.lat; + } +}); + +// Rendre le marqueur draggable et mettre à jour les coordonnées +marker?.on('dragend', function () { + let newCoords = marker.getLngLat(); + document.getElementById("reference_plan").value = current_reference_plan; + document.getElementById("coord_x").value = newCoords.lng; + document.getElementById("coord_y").value = newCoords.lat; + if (current_reference_plan === "map") { + document.getElementById("longitude").value = newCoords.lng; + document.getElementById("latitude").value = newCoords.lat; + } +}); + +map.on("load", function () { + map.addSource("white-image", { + type: "image", + url: '/static/common/solid_white.png', + coordinates: [ + [-2, 2], + [2, 2], + [2, -2], + [-2, -2] + ] + }); + map.addLayer({ + id: 'white-layer', + 'type': 'raster', + 'source': 'white-image', + 'paint': { + 'raster-fade-duration': 0 + } + }); + + + + plansData.forEach((plan, index) => { + const sourceId = `plan-${plan.id}`; // ID unique pour la source + const layerId = `plan-${plan.id}-layer`; // ID unique pour la couche + + // Ajouter la source de l'image + map.addSource(sourceId, { + type: 'image', + url: plan.url, // URL de l'image du plan + coordinates: [ + [plan.min_x, plan.max_y], // Coin supérieur gauche + [plan.max_x, plan.max_y], // Coin supérieur droit + [plan.max_x, plan.min_y], // Coin inférieur droit + [plan.min_x, plan.min_y], // Coin inférieur gauche + ], + }); + + // Ajouter la couche raster pour afficher l'image + map.addLayer({ + id: layerId, + type: 'raster', + source: sourceId, + paint: { + 'raster-fade-duration': 0, + }, + layout: { + visibility: 'none', // Masquer la couche par défaut + } + }); + }); + + + // Ajouter une source GeoJSON pour les remarques + map.addSource('remarks', { + type: 'geojson', + data: remarksData // Utiliser les données GeoJSON des remarques + }); + + // Ajouter une couche pour afficher les remarques + map.addLayer({ + id: 'remarks-layer', + type: 'circle', + source: 'remarks', + paint: { + 'circle-radius': 10, + 'circle-color': [ + 'match', ['get', 'status'], + 'information', statusColors["information"], + 'to_be_processed', statusColors["to_be_processed"], + 'processed', statusColors["processed"], + 'validated', statusColors["validated"], + 'canceled', statusColors["canceled"], + "#000000" // Couleur par défaut (Bleu) + ], + 'circle-stroke-width': 2, // Bordure pour mieux distinguer les couleurs + 'circle-stroke-color': '#ffffff' // Bordure blanche + }, + layout: { + 'circle-sort-key': ['get', 'remark_number'] // ✅ Trie les cercles du plus ancien au plus récent + } + }); + + // ✅ Ajout d'une couche `symbol` pour afficher le `remark_number` + map.addLayer({ + id: 'remarks-labels', + type: 'symbol', + source: 'remarks', + layout: { + 'text-field': ['get', 'remark_number'], // Texte affiché = numéro de la remarque + 'text-size': 10, // Taille du texte + 'text-font': ['Noto Sans Bold'], // Police du texte + //'text-allow-overlap': true, // Empêche le masquage du texte + //'text-ignore-placement': true, // Ignore le placement automatique pour forcer l'affichage + 'symbol-sort-key': ['-', ['get', 'remark_number']] // ✅ Trie les numéros du plus ancien au plus récent + }, + paint: { + 'text-color': '#ffffff', // Texte blanc + 'text-halo-color': '#000000', // Contour noir pour lisibilité + 'text-halo-width': 1 // Épaisseur du contour + } + }); + + // Initialiser le contrôle Geolocate + geolocateControl = new maplibregl.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true + }, + trackUserLocation: true + }); + + // Ajouter le contrôle Geolocate si current_reference_plan est 'map' + if (current_reference_plan === 'map') { + map.addControl(geolocateControl, 'bottom-right'); + } + + + // Si des plans existent, afficher le premier plan par défaut + if (plansData.length > 0) { + showPlan(plansData[0].id); + } +}); + + +// Fonction pour afficher un plan et zoomer dessus +function showPlan(planId) { + const sourceId = `plan-${planId}`; + const layerId = `plan-${planId}-layer`; + + // Masquer toutes les couches de plans + plansData.forEach((_, index) => { + const currentLayerId = `plan-${plansData[index].id}-layer`; + map.setLayoutProperty(currentLayerId, 'visibility', 'none'); + }); + + // Afficher la couche sélectionnée + map.setLayoutProperty(layerId, 'visibility', 'visible'); + + // Zoomer sur les coordonnées du plan sélectionné + const source = map.getSource(sourceId); + if (source && source.coordinates) { + const bounds = new maplibregl.LngLatBounds(); + source.coordinates.forEach(coord => bounds.extend(coord)); + map.fitBounds(bounds, { padding: 0, duration: 1 }); + } + + // Filtrer les remarques en fonction du plan sélectionné + const filteredRemarks = { + type: 'FeatureCollection', + features: remarksData.features.filter(feature => feature.properties.reference_plan === planId) + }; + map.getSource('remarks').setData(filteredRemarks); + + current_reference_plan = planId; + + // Désactiver le contrôle Geolocate si current_reference_plan n'est pas 'map' + if (current_reference_plan !== 'map' && map.hasControl(geolocateControl)) { + map.removeControl(geolocateControl); + } +} + + +function resetView() { + // Masquer toutes les couches + plansData.forEach((_, index) => { + const currentLayerId = `plan-${plansData[index].id}-layer`; + map.setLayoutProperty(currentLayerId, 'visibility', 'none'); + }); + + // remove filters based on previous selected plan + map.getSource('remarks').setData(remarksData); + + // Réinitialiser la vue de la carte + map.setCenter([4.3572, 50.8477]); + map.setZoom(11); + + current_reference_plan = "map"; + + // Réactiver le contrôle Geolocate si current_reference_plan est 'map' + if (current_reference_plan === 'map' && !map.hasControl(geolocateControl)) { + map.addControl(geolocateControl, 'bottom-right'); + } +} + + + +function showRemarkForm() { + // Réinitialiser le formulaire si on n'est pas en mode édition + const form = document.getElementById("remark-form"); + if (!form.dataset.editingRemarkId) { + form.reset(); + + // Réinitialiser les badges de statut + document.querySelectorAll(".status-badge").forEach(badge => { + badge.classList.remove("bg-primary", "bg-info", "bg-success", "bg-danger"); + badge.classList.add("bg-secondary"); + }); + + // Réinitialiser le texte du bouton + const buttonText = document.getElementById('submit-remark-button-text'); + if (buttonText && buttonText.textContent.includes('Mettre à jour')) { + buttonText.textContent = buttonText.textContent.replace('Mettre à jour', 'Ajouter'); + } + + // Réinitialiser la prévisualisation des photos + const previewContainer = document.getElementById('preview'); + if (previewContainer) { + previewContainer.innerHTML = ''; + } + const photoInputsContainer = document.getElementById('photo-inputs'); + if (photoInputsContainer) { + photoInputsContainer.innerHTML = ''; + } + } + + document.getElementById("remark-form-container").style.display = "flex"; + map.resize(); +} + +function hideRemarkForm() { + const form = document.getElementById("remark-form"); + + // Supprimer l'attribut d'édition + if (form.dataset.editingRemarkId) { + delete form.dataset.editingRemarkId; + } + + // Réinitialiser le texte du bouton + const buttonText = document.getElementById('submit-remark-button-text'); + if (buttonText && buttonText.textContent.includes('Mettre à jour')) { + buttonText.textContent = buttonText.textContent.replace('Mettre à jour', 'Ajouter'); + } + + document.getElementById("remark-form-container").style.display = "none"; +} + +function showQuantityForm() { + document.getElementById("quantity-form-container").style.display = "flex"; + map.resize(); +} + +function hideQuantityForm() { + document.getElementById("quantity-form-container").style.display = "none"; +} + +function showUploadPlanForm() { + document.getElementById("upload-plan-form-container").style.display = "flex"; +} +function hideUploadPlanForm() { + document.getElementById("upload-plan-form-container").style.display = "none"; +} + + +document.querySelector("#remark-form").addEventListener("submit", async (e) => { + const form = e.currentTarget; + const submitButton = e.submitter; + + const maxWidth = 4096; + const maxHeight = 4096; + + const fileInput = form.querySelector('input[type="file"][multiple]'); + const files = fileInput.files; + + e.preventDefault(); // on stoppe la soumission classique + + const newFiles = await Promise.all( + Array.from(files).map(file => resizeImage(file, maxWidth, maxHeight)) + ); + + // Remplacer les fichiers dans un nouvel input + const dataTransfer = new DataTransfer(); + newFiles.forEach(file => dataTransfer.items.add(file)); + + // Créer un clone pour remplacer l’input original + const newInput = fileInput.cloneNode(); + newInput.files = dataTransfer.files; + + fileInput.replaceWith(newInput); + + + let valid = true; // Variable pour savoir si le formulaire est valide + + let description = document.querySelector("#id_description"); + let descriptionError = document.querySelector("#descriptionError"); + if (!description.value.trim()) { + description.classList.add("is-invalid"); + descriptionError.style.display = "block"; + valid = false; + } else { + description.classList.remove("is-invalid"); + descriptionError.style.display = "none"; + } + + if (valid) { + const button = document.getElementById("submit-remark-button"); + const spinner = document.getElementById("spinner-remark"); + const buttonText = document.getElementById("submit-remark-button-text"); + spinner.classList.remove("d-none"); + buttonText.style.display = "none"; + + // Vérifier si on est hors ligne (ou connexion trop faible) et si l'offline manager est disponible + const hasOfflineManager = typeof offlineManager !== 'undefined' && + offlineManager !== null && + offlineManager.isReady === true; + const isOffline = hasOfflineManager ? !offlineManager.isOnline : !navigator.onLine; + + if (isOffline && hasOfflineManager) { + try { + // Vérifier si on est en mode édition d'une remarque offline + const editingRemarkId = form.dataset.editingRemarkId; + + // Récupérer l'ID du contrôle depuis l'URL + const pathParts = window.location.pathname.split('/'); + const controlId = pathParts[pathParts.indexOf('controls') + 1]; + + // Collecter les données du formulaire + const formData = { + controlId: controlId, + description: document.querySelector("#id_description").value, + status: document.querySelector("#selectedStatusInput").value, + latitude: document.querySelector("#latitude")?.value || '', + longitude: document.querySelector("#longitude")?.value || '', + coord_x: document.querySelector("#coord_x")?.value || '', + coord_y: document.querySelector("#coord_y")?.value || '', + reference_plan: document.querySelector("#reference_plan")?.value || 'map' + }; + + // Collecter les photos depuis le nouvel input + const photos = []; + if (newInput.files && newInput.files.length > 0) { + photos.push(...Array.from(newInput.files)); + } + + // Collecter aussi les photos depuis les inputs cachés créés par la caméra + const photoInputs = document.querySelectorAll('#photo-inputs input[type="file"]'); + photoInputs.forEach(input => { + if (input.files && input.files.length > 0) { + photos.push(...Array.from(input.files)); + } + }); + + if (editingRemarkId) { + // Mode édition : mettre à jour la remarque existante + await offlineManager.updateRemarkOffline(parseInt(editingRemarkId), formData, photos); + delete form.dataset.editingRemarkId; + } else { + // Mode création : sauvegarder une nouvelle remarque + await offlineManager.saveRemarkOffline(formData, photos); + } + + // Cacher l'overlay de chargement global (version mobile) + const loadingOverlay = document.getElementById('loading-overlay'); + if (loadingOverlay) { + loadingOverlay.classList.remove('active'); + loadingOverlay.setAttribute('aria-hidden', 'true'); + } + + // Réinitialiser le formulaire + form.reset(); + + // Réinitialiser le badge de statut + document.querySelectorAll(".status-badge").forEach(badge => { + badge.classList.remove("bg-info", "bg-success", "bg-danger", "bg-primary"); + badge.classList.add("bg-secondary"); + }); + + // Réinitialiser la prévisualisation des photos + const previewContainer = document.getElementById('preview'); + if (previewContainer) { + previewContainer.innerHTML = ''; + } + const photoInputsContainer = document.getElementById('photo-inputs'); + if (photoInputsContainer) { + photoInputsContainer.innerHTML = ''; + } + + // Cacher le formulaire + if (typeof hideRemarkForm === 'function') { + hideRemarkForm(); + } + + // Réinitialiser le bouton + spinner.classList.add("d-none"); + buttonText.style.display = ""; + buttonText.textContent = buttonText.textContent.includes('Mettre à jour') ? + buttonText.textContent.replace('Mettre à jour', 'Ajouter') : + buttonText.textContent; + button.disabled = false; + + // Mettre à jour le badge de synchronisation et l'affichage des remarques + if (typeof updatePendingBadge === 'function') { + await updatePendingBadge(); + } + if (typeof displayPendingRemarks === 'function') { + await displayPendingRemarks(); + } + + } catch (error) { + // Message d'erreur plus détaillé + let errorMsg = "Erreur lors de la sauvegarde hors ligne."; + if (error.message) { + errorMsg += " Détails: " + error.message; + } + + if (typeof gettext === 'function') { + alert(gettext(errorMsg)); + } else { + alert(errorMsg); + } + + // Réinitialiser le bouton en cas d'erreur + spinner.classList.add("d-none"); + buttonText.style.display = ""; + button.disabled = false; + } + } else if (isOffline && !hasOfflineManager) { + // Mode offline mais offline manager pas prêt + + alert("Vous êtes hors ligne et le système de sauvegarde locale n'est pas encore prêt. Veuillez attendre quelques secondes et réessayer."); + + // Réinitialiser le bouton + spinner.classList.add("d-none"); + buttonText.style.display = ""; + button.disabled = false; + } else { + + // Ajoute un léger délai avant de désactiver le bouton pour permettre la soumission + setTimeout(() => { + button.disabled = true; + }, 50); + + + // ✅ Ajoute un input hidden pour simuler le bouton submit_remark + if (submitButton && submitButton.name) { + const hidden = document.createElement('input'); + hidden.type = 'hidden'; + hidden.name = submitButton.name; + hidden.value = submitButton.value || ''; + form.appendChild(hidden); + } + + // Soumettre le formulaire normalement + form.submit(); + } + } + +}); + + + +function resizeImage(file, maxWidth, maxHeight) { + + return new Promise((resolve) => { + const img = new Image(); + const reader = new FileReader(); + + reader.onload = (e) => { + img.src = e.target.result; + }; + + img.onload = () => { + let { width, height } = img; + const scale = Math.min(maxWidth / width, maxHeight / height, 1); + + const canvas = document.createElement('canvas'); + canvas.width = width * scale; + canvas.height = height * scale; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + const newFile = new File([blob], file.name, { type: file.type }); + resolve(newFile); + }, file.type, 0.8); // qualité compression + }; + + reader.readAsDataURL(file); + + }); +} + + +document.addEventListener("DOMContentLoaded", function() { + let selectedCategories = new Set(); + let inputField = document.getElementById("selectedCategoriesInput"); + + document.querySelectorAll(".category-badge").forEach(badge => { + badge.addEventListener("click", function() { + let categoryId = this.getAttribute("data-id"); + + if (selectedCategories.has(categoryId)) { + selectedCategories.delete(categoryId); + this.classList.remove("bg-primary"); + this.classList.add("bg-secondary"); + } else { + selectedCategories.add(categoryId); + this.classList.remove("bg-secondary"); + this.classList.add("bg-primary"); + } + + inputField.value = Array.from(selectedCategories).join(","); + }); + }); + + + let selectedStatusInput = document.getElementById("selectedStatusInput"); + + document.querySelectorAll(".status-badge").forEach(badge => { + badge.addEventListener("click", function() { + // Réinitialiser tous les badges en gris + document.querySelectorAll(".status-badge").forEach(b => { + b.classList.remove("bg-info", "bg-success", "bg-danger"); + b.classList.add("bg-secondary"); + }); + + // Appliquer la bonne couleur au badge sélectionné + let status = this.getAttribute("data-value"); + if (status === "information") { + this.classList.add("bg-info"); + } else if (status === "validated") { + this.classList.add("bg-success"); + } else if (status === "to_be_processed") { + this.classList.add("bg-danger"); + } + + // Mettre à jour la valeur du champ caché + selectedStatusInput.value = status; + }); + }); + + attachShareHandlers(); + + deleteImageModalElement = document.getElementById('deleteImageModal'); + if (deleteImageModalElement) { + deleteImageConfirmButton = document.getElementById('confirmDeleteImage'); + + if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + deleteImageModalInstance = bootstrap.Modal.getOrCreateInstance(deleteImageModalElement); + } + + if (deleteImageConfirmButton) { + deleteImageConfirmButton.addEventListener('click', function() { + if (!pendingDeleteImage) { + return; + } + + deleteImageConfirmButton.disabled = true; + + const deletionPromise = deleteRemarkImage(pendingDeleteImage.imageId, pendingDeleteImage.triggerButton); + + if (deletionPromise && typeof deletionPromise.then === 'function') { + deletionPromise + .then(success => { + if (success && deleteImageModalInstance) { + deleteImageModalInstance.hide(); + } + }) + .finally(() => { + deleteImageConfirmButton.disabled = false; + }); + } else { + deleteImageConfirmButton.disabled = false; + } + }); + } + + deleteImageModalElement.addEventListener('hidden.bs.modal', function() { + pendingDeleteImage = null; + if (deleteImageConfirmButton) { + deleteImageConfirmButton.disabled = false; + } + }); + } +}); + + +document.addEventListener('click', function(event) { + const deleteButton = event.target.closest('.remark-image-delete'); + if (!deleteButton) { + return; + } + + event.preventDefault(); + + const imageId = deleteButton.dataset.imageId; + if (!imageId) { + return; + } + + if (!deleteImageModalInstance && deleteImageConfirmButton && deleteImageModalElement && typeof bootstrap !== 'undefined' && bootstrap.Modal) { + deleteImageModalInstance = bootstrap.Modal.getOrCreateInstance(deleteImageModalElement); + } + + if (deleteImageModalInstance && deleteImageConfirmButton) { + pendingDeleteImage = { + imageId: imageId, + triggerButton: deleteButton + }; + deleteImageModalInstance.show(); + } else { + let confirmationMessage = 'Voulez-vous vraiment supprimer cette image ?'; + + if (deleteImageModalElement) { + const modalBody = deleteImageModalElement.querySelector('.modal-body'); + if (modalBody && modalBody.textContent.trim()) { + confirmationMessage = modalBody.textContent.trim(); + } + } + + if (window.confirm(confirmationMessage)) { + deleteRemarkImage(imageId, deleteButton); + } + } +}); + + + + +function updateRemarkStatus(remarkId, newStatus) { + fetch(`/controls/update_remark_status/${remarkId}/${newStatus}/`, { + method: "POST", + headers: { "X-CSRFToken": csrftoken }, + mode: 'same-origin' + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // ✅ Mise à jour du badge de statut + let statusBadge = document.getElementById(`status-${remarkId}`); + statusBadge.innerHTML = ` ${data.new_status_label}`; + statusBadge.className = `badge ${data.new_status_class}`; + + // ✅ Mise à jour des boutons (Supprimer les anciens et ajouter les bons) + let buttonsContainer = document.getElementById(`buttons-${remarkId}`); + buttonsContainer.innerHTML = ""; + + if (data.new_status === "information") { + buttonsContainer.innerHTML = ` + + `; + } else if (data.new_status === "to_be_processed" || data.new_status === "processed") { + buttonsContainer.innerHTML = ` + `; + } else if (data.new_status === "validated") { + buttonsContainer.innerHTML = ` + `; + } + + // ✅ Mise à jour de la couleur sur la carte + updateRemarkColorOnMap(remarkId, data.new_status); + + // ✅ Mise à jour de la date et de l’utilisateur + document.getElementById(`updated-by-${remarkId}`).innerText = data.updated_by; + document.getElementById(`updated-at-${remarkId}`).innerText = data.updated_at; + } else { + alert("Erreur: " + data.error); + } + }) + .catch(error => console.error("Erreur AJAX:", error)); +} + +function markOk(remarkId) { + updateRemarkStatus(remarkId, "validated"); +} + +function markNok(remarkId) { + updateRemarkStatus(remarkId, "to_be_processed"); +} + +function updateRemarkColorOnMap(remarkId, newStatus) { + if (!map.getSource('remarks')) return; + + remarksData.features.forEach(feature => { + if (feature.properties.id === remarkId) { + feature.properties.status = newStatus; + } + }); + + map.getSource('remarks').setData(remarksData); +} + +if (document.getElementById("generate-report-btn")) { + document.getElementById("generate-report-btn").addEventListener("click", function () { + let button = this; + let url = button.getAttribute("data-url"); + let spinner = document.getElementById("spinner-pdf"); + let buttonText = document.getElementById("button-text-pdf"); + let statusMessage = document.getElementById("status-message-pdf"); + + // Afficher le spinner et désactiver le bouton + spinner.classList.remove("d-none"); + buttonText.style.display = "none"; + button.disabled = true; + + // Envoyer la requête AJAX + fetch(url, { + method: "GET", + headers: { + "X-Requested-With": "XMLHttpRequest" + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + statusMessage.innerHTML = ` +
          + 📄 ${gettext('Rapport généré avec succès !')} +
          `; + + + // Formate la date (si elle est dans data.created_at sous forme ISO) + const date = new Date(data.created_at); + const formattedDate = date.toLocaleString('fr-FR', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit' + }); + + // Crée le nouvel élément de rapport + const li = document.createElement('li'); + li.className = "list-group-item d-flex justify-content-between align-items-start"; + li.innerHTML = ` +
          + + 📅 ${gettext('Généré le')} : ${formattedDate}
          + 🖊 ${gettext('Auteur')} : ${data.created_by} +
          +
          +
          + + 📥 ${gettext('Télécharger')} + +
          + +
          + `; + + // Insère l'élément au début de la liste + const list = document.getElementById('card-reports'); + const line = document.createElement('div'); + line.innerHTML = '
          '; + list.prepend(line); + list.prepend(li); + + // Réattache le listener de partage + attachShareHandlers(); + + } + else { + statusMessage.innerHTML = `
          ❌ ${gettext('Erreur lors de la génération du rapport.')}
          `; + } + }) + .catch(error => { + console.error("Erreur:", error); + statusMessage.innerHTML = `
          ⚠ ${gettext('Une erreur est survenue.')}
          `; + }) + .finally(() => { + // Cacher le spinner et réactiver le bouton + spinner.classList.add("d-none"); + buttonText.style.display = "inline"; + button.disabled = false; + }); + }); +} + + +function archive_remark(remarkId) { + // 1. Appel AJAX + fetch(`/controls/archive_remark/${remarkId}/`, { + method: 'POST', + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ archived: true }) + }) + .then(response => { + if (!response.ok) { + throw new Error(gettext("Erreur lors de l'archivage.")); + } + return response.json(); + }) + .then(data => { + // 2. Déplacer la remarque dans le DOM + const remarkElement = document.getElementById(`remark-${remarkId}`); + const archiveContainer = document.getElementById("archives-list"); + + if (remarkElement && archiveContainer) { + const separator = document.createElement('div'); + separator.className = "shadow border-3 border-bottom mt-3 mb-4"; + archiveContainer.appendChild(separator); + + archiveContainer.appendChild(remarkElement); + + const buttonsContainer = document.getElementById(`buttons-${remarkId}`); + if (buttonsContainer) { + buttonsContainer.className = "d-flex flex-column gap-2 position-absolute top-0 end-0 p-2 mb-1"; + buttonsContainer.innerHTML = ` + + `; + } + } + + // (Optionnel) Modifier son apparence pour montrer qu'elle est archivée + remarkElement.classList.add("bg-light", "text-muted"); + }) + .catch(error => { + console.error(gettext("Erreur :"), error); + alert(gettext("Une erreur est survenue lors de l'archivage.")); + }); +} + +function restore_remark(remarkId) { + // 1. Appel AJAX + fetch(`/controls/restore_remark/${remarkId}/`, { + method: 'POST', + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ archived: true }) + }) + .then(response => { + if (!response.ok) { + throw new Error(gettext("Erreur lors de la restauration de la remarque.")); + } + return response.json(); + }) + .then(data => { + // 2. Déplacer la remarque dans le DOM + const remarkElement = document.getElementById(`remark-${remarkId}`); + const archiveContainer = document.getElementById("remarks-list"); + + if (remarkElement && archiveContainer) { + const separator = document.createElement('div'); + separator.className = "shadow border-3 border-bottom mt-3 mb-4"; + archiveContainer.appendChild(separator); + + archiveContainer.appendChild(remarkElement); + + const buttonsContainer = document.getElementById(`buttons-${remarkId}`); + if (buttonsContainer) { + buttonsContainer.className = "d-flex flex-column gap-2 position-absolute top-0 end-0 p-2 mb-1"; + let buttonsHTML = ""; + + if (data.status === "information") { + buttonsHTML = ` + + + `; + } else if (["to_be_processed", "processed"].includes(data.status)) { + buttonsHTML = ` + + `; + } else if (data.status === "validated") { + buttonsHTML = ` + + `; + } + + // Bouton archive (toujours présent) + buttonsHTML += ` + + `; + + buttonsContainer.innerHTML = buttonsHTML; + } + } + + remarkElement.classList.remove("bg-light", "text-muted"); + }) + .catch(error => { + console.error(gettext("Erreur :"), error); + alert(gettext("Une erreur est survenue lors de l'archivage.")); + }); +} + + +// Partage du rapport PDF +async function sharePDF(pdfUrl) { + try { + // Téléchargez le PDF en tant que fichier Blob + const response = await fetch(pdfUrl); + const blob = await response.blob(); + + // Créez un fichier à partir du Blob + const file = new File([blob], 'Rapport.pdf', { type: 'application/pdf' }); + + // Utilisez l'API Web Share pour partager le fichier + if (navigator.share) { + await navigator.share({ + files: [file], + title: gettext('Partager ce PDF'), + text: gettext('Voici un rapport PDF que je souhaite partager avec vous.'), + }); + } else { + alert(gettext("Le partage de fichiers n'est pas supporté sur ce navigateur.")); + } + } catch (error) { + console.error(gettext('Erreur lors du partage du PDF:'), error); + } +} + + +function attachShareHandlers() { + document.querySelectorAll('.share-button').forEach(button => { + button.addEventListener('click', async () => { + + const container = button.closest('div'); + const downloadLink = container.querySelector('a[download]'); + const pdfUrl = downloadLink ? downloadLink.href : null; + + if (!pdfUrl) return; + sharePDF(pdfUrl); + + }); + }); +} + + +function deleteRemarkImage(imageId, triggerButton) { + return fetch(`/controls/remark_images/${imageId}/delete/`, { + method: 'POST', + headers: { + 'X-CSRFToken': csrftoken + } + }) + .then(async response => { + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; + }) + .then(({ ok, data }) => { + if (!ok || !data.success) { + if (data && data.error) { + alert(data.error); + } else { + alert(gettext('Une erreur est survenue.')); + } + return false; + } + + if (triggerButton) { + const wrapper = triggerButton.closest('.remark-image-wrapper'); + if (wrapper) { + wrapper.remove(); + } + } + + return true; + }) + .catch(error => { + console.error(gettext('Erreur :'), error); + alert(gettext('Une erreur est survenue.')); + return false; + }); +} + + +function showControlEditSpinner(event) { + + document.getElementById("control-edit-icon").classList.add("d-none"); + document.getElementById("control-edit-spinner").classList.remove("d-none"); + + // Optionnel : désactiver le lien temporairement pour éviter double clic + document.getElementById("control-edit-btn").classList.add("disabled"); + + // Laisser le lien faire sa redirection (ne pas faire event.preventDefault()) +} + +// Reset l’état du bouton au chargement de la page +window.addEventListener("pageshow", function(event) { + // Ce test est utile surtout pour les back/forward navigation + if (event.persisted) { + document.getElementById("control-edit-icon").classList.remove("d-none"); + document.getElementById("control-edit-spinner").classList.add("d-none"); + document.getElementById("control-edit-btn").classList.remove("disabled"); + } +}); + + + + + + +// (function() { +// const $contract = document.getElementById("quantity-contract-select"); +// const $label = document.getElementById("id_contract_post_label"); +// const $hiddenId = document.getElementById("id_contract_post_id"); +// const $sugg = document.getElementById("qty-post-suggestions"); + +// // Live preview quantité +// function dec(v){ return v ? parseFloat(v.replace(",", ".")) : null; } +// function updatePreview(){ +// const q1 = dec(document.getElementById("id_q1").value); +// const q2 = dec(document.getElementById("id_q2").value); +// const q3 = dec(document.getElementById("id_q3").value); + +// let qty = 0; + +// if (!isNaN(q1) && q1 !== null && !isNaN(q2) && q2 !== null && !isNaN(q3) && q3 !== null) { +// qty = q1 * q2 * q3; +// } else if (!isNaN(q1) && q1 !== null && !isNaN(q2) && q2 !== null) { +// qty = q1 * q2; +// } else if (!isNaN(q1) && q1 !== null) { +// qty = q1; +// } + +// const shown = isFinite(qty) ? qty.toFixed(2) : ""; +// document.getElementById("qty-preview").value = shown; +// } + +// ["id_q1","id_q2","id_q3"].forEach(id=>{ +// const el = document.getElementById(id); +// if (el) el.addEventListener("input", updatePreview); +// }); +// updatePreview(); + +// // Autocomplete poste +// function clearSuggestions(){ $sugg.innerHTML = ""; $sugg.style.display = "none"; } +// function showSuggestions(items){ +// $sugg.innerHTML = ""; +// items.forEach(it=>{ +// const a = document.createElement("a"); +// const label = `${it.order_number} - ${it.code} - ${it.description}`; +// a.href = "#"; +// a.className = "list-group-item list-group-item-action"; +// a.textContent = label; +// a.addEventListener("click", (e)=>{ +// e.preventDefault(); +// $label.value = `${it.code} - ${it.description}`; +// $hiddenId.value = it.id; +// clearSuggestions(); +// }); +// $sugg.appendChild(a); +// }); +// $sugg.style.display = items.length ? "block" : "none"; +// } + +// let debounceTimer=null; +// $label && $label.addEventListener("input", function(){ +// $hiddenId.value = ""; // on reset l’ID tant que l’utilisateur modifie le libellé +// clearTimeout(debounceTimer); +// debounceTimer = setTimeout(()=>{ +// const q = ($label.value || "").trim(); +// const contractId = $contract ? $contract.value : ""; +// if (!contractId || q.length < 1){ clearSuggestions(); return; } + +// const url = autocomplete_contract_posts_url + "?contract_id=" + encodeURIComponent(contractId) + "&q=" + encodeURIComponent(q); +// fetch(url, {credentials: "same-origin"}) +// .then(r=>r.json()) +// .then(data=> showSuggestions(data || [])) +// .catch(()=> clearSuggestions()); +// }, 200); +// }); + +// // Si on change de marché, on vide le poste choisi +// $contract && $contract.addEventListener("change", function(){ +// $label.value = ""; +// $hiddenId.value = ""; +// clearSuggestions(); +// }); + +// // Fermer le menu si on clique dehors +// document.addEventListener("click", function(e){ +// if (!$sugg.contains(e.target) && e.target !== $label) clearSuggestions(); +// }); +// })(); + + + + + + + +(function() { + const remarkForm = document.getElementById("remark-form"); + + function getTotalFormsInput() { + // on le (re)cherche à la demande pour éviter null + const el = remarkForm?.querySelector('input[name="qty-TOTAL_FORMS"]') + || document.querySelector('input[name="qty-TOTAL_FORMS"]'); + return el || null; + } + + function ensureMgmtFormOrFail() { + const total = getTotalFormsInput(); + if (!total) { + console.error('[qty] Management form manquant: input[name="qty-TOTAL_FORMS"] introuvable.'); + alert("Erreur: management form du formset absent. Vérifie le prefix='qty' et l’inclusion de {{ qty_formset.management_form }} dans #remark-form."); + return false; + } + return true; + } + + // --- Bottom sheet + const editor = document.getElementById("qty-editor"); + const openBtn = document.getElementById("open-qty-editor"); + const closeBtn = document.getElementById("close-qty-editor"); + const applyBtn = document.getElementById("apply-qty-and-close"); + + // --- Hidden real formset + const hiddenWrap = document.getElementById("qty-hidden-forms"); + const totalFormsInput = remarkForm.querySelector('input[name="qty-TOTAL_FORMS"]'); + const emptyHiddenTpl = document.getElementById("qty-empty-form-template").innerHTML; + + // --- Editor rows + const editorRows = document.getElementById("qty-editor-rows"); + const editorRowTpl = document.getElementById("qty-editor-row-template").innerHTML; + + // --- Summary + const summaryList = document.getElementById("qty-summary-list"); + const summaryEmpty = document.getElementById("qty-summary-empty"); + + function showEditor() { editor.style.display = "block"; buildEditorFromHidden(); } + function hideEditor() { editor.style.display = "none"; } + + openBtn.addEventListener("click", showEditor); + closeBtn.addEventListener("click", hideEditor); + applyBtn.addEventListener("click", () => { + // rien à sauver ici : les inputs sont déjà “les vrais”. + rebuildSummary(); + hideEditor(); + }); + + function buildEditorFromHidden() { + editorRows.innerHTML = ""; + const hiddenForms = hiddenWrap.querySelectorAll(".qty-form"); + hiddenForms.forEach((hf, idx) => { + const html = editorRowTpl.replaceAll("__idx__", idx).replaceAll("__num__", (idx + 1)); + const temp = document.createElement("div"); + temp.innerHTML = html.trim(); + const card = temp.firstElementChild; + card.dataset.formIndex = idx; + + mountMirrorField(hf, card, "contract"); + mountMirrorField(hf, card, "contract_post_label"); + mountMirrorField(hf, card, "contract_post_id"); + mountMirrorField(hf, card, "description"); + mountMirrorField(hf, card, "q1"); + mountMirrorField(hf, card, "q2"); + mountMirrorField(hf, card, "q3"); + mountMirrorField(hf, card, "DELETE"); + + bindRow(card); + bindAutocompleteForRow(card); + editorRows.appendChild(card); + }); + } + + function mountMirrorField(hiddenFormEl, card, fieldName) { + const original = hiddenFormEl.querySelector(`[name$="-${fieldName}"]`); + const slot = card.querySelector(`.field-${fieldName}`); + if (!original || !slot) return; + + // Cloner pour l'UI, sans "name" pour éviter d'être soumis 2x + const mirror = original.cloneNode(true); + mirror.removeAttribute("name"); + mirror.id = (original.id || `${original.name}-mirror`) + "-mirror"; + // Style mobile + if (/^(INPUT|TEXTAREA|SELECT)$/.test(mirror.tagName)) { + mirror.classList.add("form-control"); + if (mirror.type === "number") mirror.setAttribute("inputmode","decimal"); + } + if (/^(INPUT|TEXTAREA)$/.test(mirror.tagName)) { + mirror.setAttribute("autocomplete", "off"); + mirror.setAttribute("autocorrect", "off"); // iOS + mirror.setAttribute("autocapitalize", "none"); + mirror.setAttribute("spellcheck", "false"); + } + slot.appendChild(mirror); + + // Copier la valeur initiale + if (mirror.tagName === "SELECT") { + mirror.value = original.value; + } else if (mirror.type === "checkbox" || mirror.type === "radio") { + mirror.checked = original.checked; + } else { + mirror.value = original.value; + } + + // Sync mirror -> original + const syncToOriginal = () => { + if (mirror.tagName === "SELECT") { + original.value = mirror.value; + } else if (mirror.type === "checkbox" || mirror.type === "radio") { + original.checked = mirror.checked; + } else { + original.value = mirror.value; + } + // Déclenche un event "change" au cas où + original.dispatchEvent(new Event("change", { bubbles: true })); + }; + + // Sync original -> mirror (utile si tu modifies côté serveur/re-rendu, ou autre JS) + const syncToMirror = () => { + if (mirror.tagName === "SELECT") { + mirror.value = original.value; + } else if (mirror.type === "checkbox" || mirror.type === "radio") { + mirror.checked = original.checked; + } else { + mirror.value = original.value; + } + }; + + mirror.addEventListener("input", syncToOriginal); + mirror.addEventListener("change", syncToOriginal); + original.addEventListener("change", syncToMirror); + + // Pour l’autocomplete “poste”, branche tes handlers sur "mirror" (et dans syncToOriginal, la valeur est copié vers l’original) + } + + function bindRow(card) { + const q1 = card.querySelector('[id$="-q1-mirror"]'); + const q2 = card.querySelector('[id$="-q2-mirror"]'); + const q3 = card.querySelector('[id$="-q3-mirror"]'); + const preview = card.querySelector('.qty-preview'); + + function recalc() { + const val = computeQty(q1?.value, q2?.value, q3?.value); + if (preview) preview.value = fmtQty(val); + } + [q1, q2, q3].forEach(inp => inp && inp.addEventListener("input", recalc)); + recalc(); + + const removeBtn = card.querySelector(".editor-remove-row"); + removeBtn?.addEventListener("click", () => { + const delMirror = card.querySelector('[id$="-DELETE-mirror"]'); + if (delMirror) { + delMirror.checked = true; + delMirror.dispatchEvent(new Event("change", { bubbles: true })); // propager vers l’original + } + card.style.display = "none"; + renumberRows(); + rebuildSummary(); // refresh la liste + }); + } + + + function removeHiddenFormAt(idx) { + const forms = Array.from(hiddenWrap.querySelectorAll(".qty-form")); + const target = forms[idx]; + if (target) { + target.remove(); + // réindexer les names - crucial si on retire physiquement des lignes + reindexFormset(); + } + } + + function renumberRows() { + const cards = editorRows.querySelectorAll('.card[data-form-index]'); + let n = 1; + cards.forEach(c => { + if (c.style.display !== "none") { + const rn = c.querySelector(".row-number"); + if (rn) rn.textContent = n++; + } + }); + } + + // Ajout de ligne + document.getElementById("add-qty-row").addEventListener("click", () => { + if (!ensureMgmtFormOrFail()) return; + + const totalEl = getTotalFormsInput(); + const idx = parseInt(totalEl.value, 10); + + // 1) créer HIDDEN original + const tmp = document.createElement("div"); + tmp.innerHTML = emptyHiddenTpl.trim(); + const newHidden = tmp.firstElementChild; + newHidden.innerHTML = newHidden.innerHTML.replaceAll(/__prefix__/g, idx); + newHidden.dataset.formIndex = idx; + newHidden.classList.add("qty-form"); // <-- important : la classe existe + hiddenWrap.appendChild(newHidden); + totalEl.value = idx + 1; + + // 2) carte visible + const html = editorRowTpl.replaceAll("__idx__", idx).replaceAll("__num__", (idx + 1)); + const t = document.createElement("div"); + t.innerHTML = html.trim(); + const card = t.firstElementChild; + editorRows.appendChild(card); + + // 3) monter les mirrors (clones visibles synchronisés) + mountMirrorField(newHidden, card, "contract"); + mountMirrorField(newHidden, card, "contract_post_label"); + mountMirrorField(newHidden, card, "contract_post_id"); + mountMirrorField(newHidden, card, "description"); + mountMirrorField(newHidden, card, "q1"); + mountMirrorField(newHidden, card, "q2"); + mountMirrorField(newHidden, card, "q3"); + mountMirrorField(newHidden, card, "DELETE"); + + bindRow(card); + bindAutocompleteForRow(card); + renumberRows(); + }); + + // Recrée le récapitulatif à partir des hidden forms + function rebuildSummary() { + summaryList.innerHTML = ""; + let count = 0; + + const forms = hiddenWrap.querySelectorAll(".qty-form"); // originaux + forms.forEach(hf => { + const isDeleted = hf.querySelector('input[name$="-DELETE"]')?.checked; + if (isDeleted) return; + + const q1 = hf.querySelector('[name$="-q1"]')?.value; + const q2 = hf.querySelector('[name$="-q2"]')?.value; + const q3 = hf.querySelector('[name$="-q3"]')?.value; + const total = computeQty(q1, q2, q3); + + const desc = hf.querySelector('[name$="-description"]')?.value || ""; + const postLabel = hf.querySelector('[name$="-contract_post_label"]')?.value || ""; + const contractSel = hf.querySelector('[name$="-contract"]'); + const contractTxt = contractSel && contractSel.options[contractSel.selectedIndex]?.text || ""; + + const dims = [ + (q1 ? String(q1) : null), + (q2 ? String(q2) : null), + (q3 ? String(q3) : null), + ].filter(Boolean).join(" × "); + + const item = document.createElement("div"); + item.className = "list-group-item d-flex flex-column"; + const line1 = postLabel + ? `
          ${postLabel}
          ` + : (desc ? `
          ${desc}
          ` : `
          —
          `); + const line2 = `
          ${contractTxt || '—'}${dims ? ` · ${dims}` : ''}
          `; + const line3 = `
          ${fmtQty(total)}
          `; + item.innerHTML = `${line1}${line2}${line3}`; + + summaryList.appendChild(item); + count++; + }); + + summaryEmpty.style.display = (count === 0) ? "" : "none"; + } + + + + // Si on retire physiquement des lignes, il faut réindexer tous les names qty--field + function reindexFormset() { + const forms = Array.from(hiddenWrap.querySelectorAll(".qty-form")); + forms.forEach((hf, i) => { + hf.dataset.formIndex = i; + hf.querySelectorAll("[name]").forEach(inp => { + inp.name = inp.name.replace(/qty-\d+-/,'qty-' + i + '-'); + if (inp.id) inp.id = inp.id.replace(/qty-\d+-/,'qty-' + i + '-'); + }); + }); + totalFormsInput.value = forms.length; + } + + // Init: construire un récap si des lignes existent déjà (ex. erreurs de validation) + rebuildSummary(); +})(); + + + + +function toDec(v) { + if (v === undefined || v === null) return null; + const s = String(v).trim().replace(',', '.'); + if (s === '') return null; + const n = Number(s); + return Number.isFinite(n) ? n : null; +} + +function computeQty(q1, q2, q3) { + const a = toDec(q1), b = toDec(q2), c = toDec(q3); + let res = 0; + if (a !== null && b !== null && c !== null) res = a * b * c; + else if (a !== null && b !== null) res = a * b; + else res = a || 0; + // arrondi half-up à 2 décimales + return Math.round(res * 100) / 100; +} + +function fmtQty(n) { + return (Number.isFinite(n) ? n : 0).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + + + + + +function bindAutocompleteForRow(card) { + // Champs de cette ligne (MIRRORS) + const $contract = card.querySelector('.field-contract select'); + const $label = card.querySelector('[id$="-contract_post_label-mirror"]'); + const $hiddenId = card.querySelector('[id$="-contract_post_id-mirror"]'); + + // Zone suggestions dédiée à la ligne + const idx = card.dataset.formIndex; + const $sugg = card.querySelector('#qty-post-suggestions-' + idx) + || card.querySelector('.qty-post-suggestions'); // fallback si tu préfères une classe + + if (!$label || !$hiddenId || !$sugg) return; + + function clearSuggestions(){ + $sugg.innerHTML = ""; + $sugg.classList.add("d-none"); + $sugg.style.display = "none"; + } + function showSuggestions(items){ + $sugg.innerHTML = ""; + items.forEach(it => { + const a = document.createElement("a"); + const label = `${it.order_number} - ${it.code} - ${it.description}`; + a.href = "#"; + a.className = "list-group-item list-group-item-action"; + a.textContent = label; + a.addEventListener("click", (e) => { + e.preventDefault(); + // 1) Remplir les MIRRORS + $label.value = `${it.order_number} - ${it.code} - ${it.description}`; + $hiddenId.value = it.id; + // 2) Propager aux originaux via événements (mountMirrorField écoute input/change) + $label.dispatchEvent(new Event("input", { bubbles: true })); + $hiddenId.dispatchEvent(new Event("change", { bubbles: true })); + clearSuggestions(); + }); + $sugg.appendChild(a); + }); + $sugg.classList.toggle("d-none", items.length === 0); + $sugg.style.display = items.length ? "block" : "none"; + } + + // Debounce + let debounceTimer=null; + $label.addEventListener("input", function(e){ + // On reset l’ID tant que l’utilisateur édite le libellé + // Ne reset que sur frappe utilisateur + if (e.isTrusted) { + $hiddenId.value = ""; + $hiddenId.dispatchEvent(new Event("change", { bubbles: true })); + } + + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + const q = ($label.value || "").trim(); + const contractId = $contract ? $contract.value : ""; + if (!contractId || q.length < 1){ clearSuggestions(); return; } + + const url = autocomplete_contract_posts_url + + "?contract_id=" + encodeURIComponent(contractId) + + "&q=" + encodeURIComponent(q); + + fetch(url, { credentials: "same-origin" }) + .then(r => r.ok ? r.json() : []) + .then(data => showSuggestions(data || [])) + .catch(() => clearSuggestions()); + }, 200); + }); + + // Si on change de marché, on vide le poste choisi + $contract?.addEventListener("change", function(){ + $label.value = ""; + $label.dispatchEvent(new Event("input", { bubbles: true })); // effacera aussi l’ID via le handler ci-dessus + clearSuggestions(); + }); + + // Fermer le menu si on clique en dehors + document.addEventListener("click", function(e){ + if (!$sugg.contains(e.target) && e.target !== $label) clearSuggestions(); + }); +} + + +// ======================================== +// Gestion du modal caméra (mode continu) +// ======================================== + +(function() { + // Éléments du DOM + const openCameraModalBtn = document.getElementById('open-camera-modal-btn'); + const cameraModalEl = document.getElementById('cameraModal'); + const cameraPreview = document.getElementById('cameraPreview'); + const cameraCanvas = document.getElementById('cameraCanvas'); + const cameraFlashOverlay = document.getElementById('cameraFlashOverlay'); + const cameraStatus = document.getElementById('cameraStatus'); + const cameraCaptureBtn = document.getElementById('cameraCaptureBtn'); + const cameraStopBtn = document.getElementById('cameraStopBtn'); + const selectFilesBtn = document.getElementById('select-files-btn'); + const filesInput = document.getElementById('id_images'); + const previewContainer = document.getElementById('preview'); + const photoInputsContainer = document.getElementById('photo-inputs'); + + if (!cameraModalEl) return; // Pas de modal caméra, on sort + + let cameraModal = null; + let cameraStream = null; + let photoInputIndex = 0; + const isCameraSupported = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); + + // Initialiser le modal Bootstrap + if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + cameraModal = bootstrap.Modal.getOrCreateInstance(cameraModalEl); + } + + function triggerCameraFlash() { + if (!cameraFlashOverlay) return; + cameraFlashOverlay.style.opacity = '1'; + setTimeout(() => { + cameraFlashOverlay.style.opacity = '0'; + }, 150); + } + + function updateCameraStatus(message, type = 'info') { + if (!cameraStatus) return; + cameraStatus.textContent = message || ''; + cameraStatus.className = 'small text-center mb-2'; + if (type === 'error') { + cameraStatus.classList.add('text-danger'); + } else if (type === 'success') { + cameraStatus.classList.add('text-success'); + } else { + cameraStatus.classList.add('text-muted'); + } + } + + async function startCameraStream() { + if (!isCameraSupported) { + alert(gettext("La caméra avancée n'est pas supportée sur ce navigateur.")); + return false; + } + + try { + updateCameraStatus(gettext('Initialisation de la caméra…'), 'info'); + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'environment', + width: { ideal: 1920 }, + height: { ideal: 1080 } + }, + audio: false, + }); + + if ('srcObject' in cameraPreview) { + cameraPreview.srcObject = cameraStream; + } else { + cameraPreview.src = window.URL.createObjectURL(cameraStream); + } + cameraPreview.play(); + updateCameraStatus(gettext('Caméra prête. Appuyez sur Capturer pour prendre une photo.'), 'info'); + return true; + } catch (err) { + console.error('Erreur getUserMedia:', err); + alert(gettext("Impossible d'accéder à la caméra. Veuillez vérifier les autorisations.")); + updateCameraStatus(gettext("Impossible d'accéder à la caméra."), 'error'); + return false; + } + } + + function stopCameraStream() { + if (cameraStream) { + cameraStream.getTracks().forEach(track => track.stop()); + cameraStream = null; + } + if (cameraPreview) { + cameraPreview.srcObject = null; + } + updateCameraStatus('', 'info'); + } + + function captureFrame() { + if (!cameraPreview || !cameraCanvas) return; + + const videoWidth = cameraPreview.videoWidth; + const videoHeight = cameraPreview.videoHeight; + if (!videoWidth || !videoHeight) { + updateCameraStatus(gettext("La vidéo n'est pas encore prête."), 'error'); + return; + } + + cameraCanvas.width = videoWidth; + cameraCanvas.height = videoHeight; + + const ctx = cameraCanvas.getContext('2d'); + ctx.drawImage(cameraPreview, 0, 0, videoWidth, videoHeight); + + cameraCanvas.toBlob(function(blob) { + if (!blob) { + updateCameraStatus(gettext("Erreur lors de la capture."), 'error'); + return; + } + + const filename = `remark_photo_${Date.now()}.jpg`; + const file = new File([blob], filename, { type: 'image/jpeg' }); + + // Ajouter au formulaire + addFileToPreview(file); + updateCameraStatus(gettext('Photo capturée avec succès.'), 'success'); + }, 'image/jpeg', 0.9); + } + + function addFileToPreview(file) { + const currentIndex = photoInputIndex++; + + // Créer un input file caché pour ce fichier + const singleInput = document.createElement('input'); + singleInput.type = 'file'; + singleInput.name = 'images'; + singleInput.className = 'd-none'; + singleInput.dataset.index = currentIndex; + + // Injecter le fichier via DataTransfer + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + singleInput.files = dataTransfer.files; + + if (photoInputsContainer) { + photoInputsContainer.appendChild(singleInput); + } + + // Créer l'aperçu visuel + const wrapper = document.createElement('div'); + wrapper.className = 'preview-wrapper'; + wrapper.dataset.inputIndex = currentIndex; + + const reader = new FileReader(); + reader.onload = function(e) { + const img = document.createElement('img'); + img.src = e.target.result; + img.className = 'preview-image'; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.innerHTML = '×'; + btn.className = 'remove-btn'; + btn.onclick = function() { + singleInput.remove(); + wrapper.remove(); + }; + + wrapper.appendChild(img); + wrapper.appendChild(btn); + if (previewContainer) { + previewContainer.appendChild(wrapper); + } + }; + reader.readAsDataURL(file); + } + + // Event: Ouvrir le modal caméra + if (openCameraModalBtn && cameraModal) { + openCameraModalBtn.addEventListener('click', async function() { + const ok = await startCameraStream(); + if (!ok) return; + cameraModal.show(); + }); + } + + // Event: Fermeture du modal + if (cameraModalEl) { + cameraModalEl.addEventListener('hidden.bs.modal', function() { + stopCameraStream(); + }); + } + + // Event: Bouton Terminer + if (cameraStopBtn) { + cameraStopBtn.addEventListener('click', function() { + if (cameraModal) { + cameraModal.hide(); + } + }); + } + + // Event: Bouton Capturer + if (cameraCaptureBtn) { + cameraCaptureBtn.addEventListener('click', function() { + triggerCameraFlash(); + captureFrame(); + }); + } + + // Event: Sélection de fichiers classique + if (selectFilesBtn && filesInput) { + selectFilesBtn.addEventListener('click', function() { + filesInput.click(); + }); + + filesInput.addEventListener('change', function() { + const files = Array.from(this.files); + files.forEach(file => addFileToPreview(file)); + this.value = ''; // Reset pour permettre de resélectionner + }); + } +})(); + +// Fonction pour afficher une image dans une modal +function showImageModal(imageUrl) { + // Créer une modal pour afficher l'image en plein écran + let modal = document.getElementById('image-preview-modal'); + + if (!modal) { + // Créer la modal si elle n'existe pas + modal = document.createElement('div'); + modal.id = 'image-preview-modal'; + modal.className = 'modal fade'; + modal.tabIndex = -1; + modal.setAttribute('aria-hidden', 'true'); + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + } + + // Mettre à jour l'image + const img = modal.querySelector('#modal-preview-image'); + if (img) { + img.src = imageUrl; + } + + // Afficher la modal + if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + const modalInstance = new bootstrap.Modal(modal); + modalInstance.show(); + } +} \ No newline at end of file diff --git a/streetup/controls/static/controls/controls_index_mobile.css b/streetup/controls/static/controls/controls_index_mobile.css new file mode 100644 index 0000000..5a8bddc --- /dev/null +++ b/streetup/controls/static/controls/controls_index_mobile.css @@ -0,0 +1,64 @@ + +#map { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: white; + z-index: 999; /* Sous le contenu principal */ + opacity: 0; /* Invisible par défaut */ + pointer-events: none; /* Désactive les interactions */ + transition: opacity 0.3s ease; /* Animation de transition */ +} +#map.visible { + opacity: 1; /* Rend la carte visible */ + pointer-events: auto; /* Active les interactions */ + z-index: 1001; /* Au-dessus du contenu principal */ +} +.map-toggle { + position: absolute; + top: 10px; + right: 10px; + z-index: 1002; /* Au-dessus de tout */ + cursor: pointer; +} +.map-toggle i { + font-size: 24px; +} +.pointer { + cursor: pointer; +} +.control-item { + display: flex; + align-items: center; + padding: 10px; + border-bottom: 1px solid #eee; +} +.control-item .status-icon { + margin-right: 10px; + font-size: 20px; +} +.control-item .content { + flex-grow: 1; +} +.control-item .title { + font-weight: bold; + color: #007bff; + text-decoration: underline !important; +} +.control-item .title:hover { + text-decoration: underline; +} +.control-item .description { + font-size: 14px; + color: #666; + display: -webkit-box; + line-clamp: 2; /* Limite à 2 lignes */ + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} +.status-in_progress { color: #28a745; } /* Vert pour actif */ +.status-on_pause { color: #ffc107; } /* Jaune pour en pause */ +.status-archived { color: #6c757d; } /* Gris pour archivé */ \ No newline at end of file diff --git a/streetup/controls/static/controls/controls_index_mobile.js b/streetup/controls/static/controls/controls_index_mobile.js new file mode 100644 index 0000000..dc05294 --- /dev/null +++ b/streetup/controls/static/controls/controls_index_mobile.js @@ -0,0 +1,49 @@ +/* + +//Init map +console.log("init map"); +map = new maplibregl.Map({ + container: 'map', + style: window.MAP_STYLE_LIGHT, + center: [4.3572, 50.8477], + zoom: 11, + minzoom: 0, + maxzoom: 24 +}); + +// Ajout des marqueurs pour les projets +controls.forEach(control => { + const popupContent = ` +
          + ${control.name} +

          ${control.description}

          +
          + `; + + // Create the marker + const marker = new maplibregl.Marker({ className: 'pointer'}) + .setLngLat(control.coordinates) // Use the coordinates from the Django view + .setPopup(new maplibregl.Popup().setHTML(popupContent)) + .addTo(map); +}); + + +// Gestion de l'affichage de la carte +const openMapButton = document.getElementById('openMap'); +const closeMapButton = document.getElementById('closeMap'); +const mapContainer = document.getElementById('map'); + +openMapButton.addEventListener('click', () => { + mapContainer.classList.add('visible'); // Rend la carte visible + closeMapButton.style.display = 'block'; + openMapButton.style.display = 'none'; +}); + +closeMapButton.addEventListener('click', () => { + mapContainer.classList.remove('visible'); // Cache la carte + closeMapButton.style.display = 'none'; + openMapButton.style.display = 'block'; +}); + + */ + diff --git a/streetup/controls/static/controls/controls_projects_mobile.css b/streetup/controls/static/controls/controls_projects_mobile.css new file mode 100644 index 0000000..c48fbad --- /dev/null +++ b/streetup/controls/static/controls/controls_projects_mobile.css @@ -0,0 +1,64 @@ + +#map { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: white; + z-index: 999; /* Sous le contenu principal */ + opacity: 0; /* Invisible par défaut */ + pointer-events: none; /* Désactive les interactions */ + transition: opacity 0.3s ease; /* Animation de transition */ +} +#map.visible { + opacity: 1; /* Rend la carte visible */ + pointer-events: auto; /* Active les interactions */ + z-index: 1001; /* Au-dessus du contenu principal */ +} +.map-toggle { + position: absolute; + top: 10px; + right: 10px; + z-index: 1002; /* Au-dessus de tout */ + cursor: pointer; +} +.map-toggle i { + font-size: 24px; +} +.pointer { + cursor: pointer; +} +.project-item { + display: flex; + align-items: center; + padding: 10px; + border-bottom: 1px solid #eee; +} +.project-item .status-icon { + margin-right: 10px; + font-size: 20px; +} +.project-item .content { + flex-grow: 1; +} +.project-item .title { + font-weight: bold; + color: #007bff; + /* text-decoration: none; */ +} +.project-item .title:hover { + text-decoration: underline; +} +.project-item .description { + font-size: 14px; + color: #666; + display: -webkit-box; + line-clamp: 2; /* Limite à 2 lignes */ + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} +.status-in_progress { color: #28a745; } /* Vert pour actif */ +.status-on_pause { color: #ffc107; } /* Jaune pour en pause */ +.status-archived { color: #6c757d; } /* Gris pour archivé */ \ No newline at end of file diff --git a/streetup/controls/static/controls/controls_projects_mobile.js b/streetup/controls/static/controls/controls_projects_mobile.js new file mode 100644 index 0000000..aa642af --- /dev/null +++ b/streetup/controls/static/controls/controls_projects_mobile.js @@ -0,0 +1,47 @@ + + +//Init map +console.log("init map"); +map = new maplibregl.Map({ + container: 'map', + style: window.MAP_STYLE_LIGHT, + center: [4.3572, 50.8477], + zoom: 11, + minzoom: 0, + maxzoom: 24 +}); + +// Ajout des marqueurs pour les projets +projects.forEach(project => { + const popupContent = ` +
          + ${project.name} +

          ${project.description}

          +
          + `; + + // Create the marker + const marker = new maplibregl.Marker({ className: 'pointer'}) + .setLngLat(project.coordinates) // Use the coordinates from the Django view + .setPopup(new maplibregl.Popup().setHTML(popupContent)) + .addTo(map); +}); + + +// Gestion de l'affichage de la carte +const openMapButton = document.getElementById('openMap'); +const closeMapButton = document.getElementById('closeMap'); +const mapContainer = document.getElementById('map'); + +openMapButton.addEventListener('click', () => { + mapContainer.classList.add('visible'); // Rend la carte visible + closeMapButton.style.display = 'block'; + openMapButton.style.display = 'none'; +}); + +closeMapButton.addEventListener('click', () => { + mapContainer.classList.remove('visible'); // Cache la carte + closeMapButton.style.display = 'none'; + openMapButton.style.display = 'block'; +}); + diff --git a/streetup/controls/static/controls/icon-192x192.png b/streetup/controls/static/controls/icon-192x192.png new file mode 100644 index 0000000..dcaab7b Binary files /dev/null and b/streetup/controls/static/controls/icon-192x192.png differ diff --git a/streetup/controls/static/controls/icon-512x512.png b/streetup/controls/static/controls/icon-512x512.png new file mode 100644 index 0000000..1c97ca7 Binary files /dev/null and b/streetup/controls/static/controls/icon-512x512.png differ diff --git a/streetup/controls/static/controls/offline-manager.js b/streetup/controls/static/controls/offline-manager.js new file mode 100644 index 0000000..9632e58 --- /dev/null +++ b/streetup/controls/static/controls/offline-manager.js @@ -0,0 +1,672 @@ +// Gestion du stockage offline pour les remarques + +class OfflineManager { + constructor() { + this.dbName = 'StreetUpOffline'; + this.dbVersion = 2; // Incrémenté pour forcer la création des stores + this.db = null; + this.isOnline = navigator.onLine; + this.isReady = false; + + // Seuils de qualité réseau en-dessous desquels le mode hors ligne est activé. + // Équivalent 4G faible : effectiveType != '4g', downlink < 0.5 Mbps ou rtt > 500 ms. + this.connectionThreshold = { + minEffectiveType: '4g', // '3g' et inférieurs déclenchent le mode offline + minDownlink: 0.5, // Mbps + maxRtt: 500 // ms + }; + + this.setupEventListeners(); + this._evaluateConnectionQuality(); + } + + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.dbVersion); + + request.onerror = () => { + reject(request.error); + }; + + request.onsuccess = () => { + this.db = request.result; + + // Vérifier que les stores existent + const hasRemarks = this.db.objectStoreNames.contains('pendingRemarks'); + const hasPhotos = this.db.objectStoreNames.contains('pendingPhotos'); + + if (!hasRemarks || !hasPhotos) { + this.db.close(); + + // Supprimer et recréer + const deleteRequest = indexedDB.deleteDatabase(this.dbName); + deleteRequest.onsuccess = () => { + // Réessayer + this.init().then(resolve).catch(reject); + }; + deleteRequest.onerror = () => reject(deleteRequest.error); + return; + } + + this.isReady = true; + resolve(this.db); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + // Store pour les remarques en attente + if (!db.objectStoreNames.contains('pendingRemarks')) { + const store = db.createObjectStore('pendingRemarks', { + keyPath: 'id', + autoIncrement: true + }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + store.createIndex('controlId', 'controlId', { unique: false }); + } + + // Store pour les photos (blob) + if (!db.objectStoreNames.contains('pendingPhotos')) { + db.createObjectStore('pendingPhotos', { + keyPath: 'id', + autoIncrement: true + }); + } + }; + }); + } + + setupEventListeners() { + // Écouter les changements de connectivité réseau (binaire) + window.addEventListener('online', () => { + this._evaluateConnectionQuality(); + if (this.isOnline) { + this.showNotification('Connexion rétablie', 'success'); + this.syncPendingData(); + } else { + this.showNotification('Connexion trop faible – mode hors ligne maintenu', 'warning'); + } + }); + + window.addEventListener('offline', () => { + this.isOnline = false; + this.showNotification('Mode hors ligne activé', 'warning'); + }); + + // Network Information API : détecter les changements de qualité réseau + const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + if (conn) { + conn.addEventListener('change', () => { + const wasOnline = this.isOnline; + this._evaluateConnectionQuality(); + if (!wasOnline && this.isOnline) { + this.showNotification('Connexion suffisante – retour en ligne', 'success'); + this.syncPendingData(); + } else if (wasOnline && !this.isOnline) { + this.showNotification('Connexion trop faible – mode hors ligne activé', 'warning'); + } + }); + } + } + + /** + * Retourne true si la connexion actuelle est en-dessous du seuil configuré. + * Utilise l'API Network Information si disponible, sinon retourne false (pas de dégradation). + */ + _isConnectionPoor() { + const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + if (!conn) return false; // API non disponible : conserver le comportement par défaut + + const poorTypes = ['slow-2g', '2g', '3g']; + if (poorTypes.includes(conn.effectiveType)) return true; + if (conn.rtt > 0 && conn.rtt > this.connectionThreshold.maxRtt) return true; + if (conn.downlink > 0 && conn.downlink < this.connectionThreshold.minDownlink) return true; + + return false; + } + + /** + * Évalue la qualité de connexion et met à jour isOnline en conséquence. + * Dispatch un événement CustomEvent pour notifier les templates. + */ + _evaluateConnectionQuality() { + if (!navigator.onLine) { + this.isOnline = false; + return; + } + + const isPoor = this._isConnectionPoor(); + const wasOnline = this.isOnline; + this.isOnline = !isPoor; + + if (!wasOnline && this.isOnline) { + window.dispatchEvent(new CustomEvent('effectiveOnline')); + } else if (wasOnline && !this.isOnline) { + window.dispatchEvent(new CustomEvent('effectiveOffline', { detail: { reason: 'poor_connection' } })); + } + } + + // Sauvegarder une remarque localement + async saveRemarkOffline(remarkData, photos = []) { + if (!this.db || !this.isReady) { + throw new Error('IndexedDB non initialisé'); + } + + // IMPORTANT : Convertir les photos en blobs AVANT d'ouvrir la transaction + // pour éviter que la transaction expire + const photoRecords = []; + for (const photo of photos) { + const photoBlob = await this.fileToBlob(photo); + photoRecords.push({ + filename: photo.name, + blob: photoBlob, + type: photo.type + }); + } + + // Maintenant ouvrir la transaction et faire toutes les opérations de manière synchrone + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite'); + const remarksStore = transaction.objectStore('pendingRemarks'); + const photosStore = transaction.objectStore('pendingPhotos'); + + const remarkRecord = { + controlId: remarkData.controlId, + description: remarkData.description, + status: remarkData.status, + latitude: remarkData.latitude, + longitude: remarkData.longitude, + coord_x: remarkData.coord_x, + coord_y: remarkData.coord_y, + reference_plan: remarkData.reference_plan, + timestamp: Date.now(), + photoIds: [] + }; + + // Gérer les erreurs de transaction + transaction.onerror = () => { + reject(transaction.error); + }; + + transaction.oncomplete = () => { + this.showNotification( + `Remarque sauvegardée localement. Elle sera synchronisée dès le retour du réseau.`, + 'info' + ); + resolve(remarkId); + }; + + // Sauvegarder les photos de manière synchrone + let photosAdded = 0; + let remarkId = null; + + if (photoRecords.length > 0) { + photoRecords.forEach((photoRecord) => { + const photoRequest = photosStore.add(photoRecord); + photoRequest.onsuccess = () => { + remarkRecord.photoIds.push(photoRequest.result); + photosAdded++; + + // Une fois toutes les photos ajoutées, ajouter la remarque + if (photosAdded === photoRecords.length) { + const remarkRequest = remarksStore.add(remarkRecord); + remarkRequest.onsuccess = () => { + remarkId = remarkRequest.result; + }; + remarkRequest.onerror = () => reject(remarkRequest.error); + } + }; + photoRequest.onerror = () => reject(photoRequest.error); + }); + } else { + // Pas de photos, ajouter directement la remarque + const remarkRequest = remarksStore.add(remarkRecord); + remarkRequest.onsuccess = () => { + remarkId = remarkRequest.result; + }; + remarkRequest.onerror = () => reject(remarkRequest.error); + } + }); + } + + async fileToBlob(file) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => resolve(new Blob([e.target.result], { type: file.type })); + reader.readAsArrayBuffer(file); + }); + } + + addRecord(store, record) { + return new Promise((resolve, reject) => { + const request = store.add(record); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + // Synchroniser les données en attente + async syncPendingData() { + if (!this.isOnline) { + return; + } + + const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readonly'); + const remarksStore = transaction.objectStore('pendingRemarks'); + + const remarks = await this.getAllRecords(remarksStore); + + if (remarks.length === 0) { + return; + } + + this.showNotification( + `Synchronisation de ${remarks.length} remarque(s) en cours...`, + 'info' + ); + + let syncedCount = 0; + let failedCount = 0; + + for (const remark of remarks) { + try { + await this.uploadRemark(remark); + await this.deleteRemark(remark.id); + syncedCount++; + } catch (error) { + failedCount++; + } + } + + if (syncedCount > 0) { + this.showNotification( + `${syncedCount} remarque(s) synchronisée(s) avec succès`, + 'success' + ); + + // Recharger la page pour afficher les nouvelles remarques + setTimeout(() => { + window.location.reload(); + }, 1500); + } + + if (failedCount > 0) { + this.showNotification( + `${failedCount} remarque(s) n'ont pas pu être synchronisées`, + 'error' + ); + } + } + + async getAllRecords(store) { + return new Promise((resolve, reject) => { + const request = store.getAll(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + async uploadRemark(remark) { + const photosStore = this.db.transaction('pendingPhotos', 'readonly') + .objectStore('pendingPhotos'); + + // Récupérer les photos + const photos = []; + for (const photoId of remark.photoIds) { + const photo = await this.getRecord(photosStore, photoId); + if (photo) { + photos.push(new File([photo.blob], photo.filename, { type: photo.type })); + } + } + + // Créer FormData + const formData = new FormData(); + + // IMPORTANT: Ajouter le paramètre submit_remark pour que le backend traite la requête + formData.append('submit_remark', 'true'); + + // Champ obligatoire : control ID + formData.append('control', remark.controlId); + + formData.append('description', remark.description); + formData.append('status', remark.status); + formData.append('latitude', remark.latitude || ''); + formData.append('longitude', remark.longitude || ''); + formData.append('coord_x', remark.coord_x || ''); + formData.append('coord_y', remark.coord_y || ''); + formData.append('reference_plan', remark.reference_plan || ''); + formData.append('categories', ''); // Catégories vides par défaut + + // Ajouter les champs du management form pour le formset (quantity) + // Django s'attend à ces champs même si le formset est vide + formData.append('qty-TOTAL_FORMS', '0'); + formData.append('qty-INITIAL_FORMS', '0'); + formData.append('qty-MIN_NUM_FORMS', '0'); + formData.append('qty-MAX_NUM_FORMS', '1000'); + + photos.forEach(photo => { + formData.append('images', photo); + }); + + // Envoyer au serveur + const csrftoken = this.getCookie('csrftoken'); + const currentUrl = window.location.pathname; + + const response = await fetch(currentUrl, { + method: 'POST', + headers: { + 'X-CSRFToken': csrftoken, + }, + body: formData + }); + + if (!response.ok) { + throw new Error(`Erreur HTTP: ${response.status} - ${response.statusText}`); + } + + // Supprimer les photos associées + const photosTransaction = this.db.transaction('pendingPhotos', 'readwrite'); + const photosWriteStore = photosTransaction.objectStore('pendingPhotos'); + for (const photoId of remark.photoIds) { + await this.deleteRecord(photosWriteStore, photoId); + } + + return response; + } + + async getRecord(store, id) { + return new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + async deleteRemark(remarkId) { + const transaction = this.db.transaction('pendingRemarks', 'readwrite'); + const store = transaction.objectStore('pendingRemarks'); + return this.deleteRecord(store, remarkId); + } + + deleteRecord(store, id) { + return new Promise((resolve, reject) => { + const request = store.delete(id); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + showNotification(message, type = 'info') { + // Créer un toast Bootstrap + const bgClass = type === 'success' ? 'success' : + type === 'error' ? 'danger' : + type === 'warning' ? 'warning' : 'info'; + + const toastHtml = ` + + `; + + // Ajouter au container de toasts + let container = document.getElementById('toast-container'); + if (!container) { + container = this.createToastContainer(); + } + + container.insertAdjacentHTML('beforeend', toastHtml); + const toastEl = container.lastElementChild; + + if (typeof bootstrap !== 'undefined' && bootstrap.Toast) { + const toast = new bootstrap.Toast(toastEl, { delay: 5000 }); + toast.show(); + + toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove()); + } + } + + createToastContainer() { + const container = document.createElement('div'); + container.id = 'toast-container'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '9999'; + document.body.appendChild(container); + return container; + } + + // Obtenir le nombre de remarques en attente + async getPendingCount() { + if (!this.db) return 0; + const transaction = this.db.transaction('pendingRemarks', 'readonly'); + const store = transaction.objectStore('pendingRemarks'); + return new Promise((resolve, reject) => { + const request = store.count(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + // Obtenir toutes les remarques en attente pour un contrôle + async getPendingRemarks(controlId) { + if (!this.db) return []; + const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readonly'); + const remarksStore = transaction.objectStore('pendingRemarks'); + const photosStore = transaction.objectStore('pendingPhotos'); + + const allRemarks = await this.getAllRecords(remarksStore); + const controlRemarks = allRemarks.filter(r => r.controlId == controlId); + + // Charger les photos pour chaque remarque + for (const remark of controlRemarks) { + remark.photos = []; + for (const photoId of remark.photoIds || []) { + const photo = await this.getRecord(photosStore, photoId); + if (photo) { + // Créer une URL pour afficher la photo + remark.photos.push({ + id: photoId, + url: URL.createObjectURL(photo.blob), + filename: photo.filename + }); + } + } + } + + return controlRemarks; + } + + // Mettre à jour une remarque offline + async updateRemarkOffline(remarkId, remarkData, photos = []) { + if (!this.db || !this.isReady) { + throw new Error('IndexedDB non initialisé'); + } + + // Convertir les nouvelles photos en blobs + const photoRecords = []; + for (const photo of photos) { + const photoBlob = await this.fileToBlob(photo); + photoRecords.push({ + filename: photo.name, + blob: photoBlob, + type: photo.type + }); + } + + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite'); + const remarksStore = transaction.objectStore('pendingRemarks'); + const photosStore = transaction.objectStore('pendingPhotos'); + + transaction.onerror = () => reject(transaction.error); + transaction.oncomplete = () => { + this.showNotification('Remarque mise à jour localement', 'info'); + resolve(); + }; + + // Récupérer la remarque existante + const getRequest = remarksStore.get(remarkId); + getRequest.onsuccess = () => { + const existingRemark = getRequest.result; + if (!existingRemark) { + reject(new Error('Remarque non trouvée')); + return; + } + + // Mettre à jour les données + existingRemark.description = remarkData.description; + existingRemark.status = remarkData.status; + existingRemark.latitude = remarkData.latitude; + existingRemark.longitude = remarkData.longitude; + existingRemark.coord_x = remarkData.coord_x; + existingRemark.coord_y = remarkData.coord_y; + existingRemark.reference_plan = remarkData.reference_plan; + + // Ajouter les nouvelles photos + let photosAdded = 0; + if (photoRecords.length > 0) { + photoRecords.forEach((photoRecord) => { + const photoRequest = photosStore.add(photoRecord); + photoRequest.onsuccess = () => { + existingRemark.photoIds.push(photoRequest.result); + photosAdded++; + + if (photosAdded === photoRecords.length) { + remarksStore.put(existingRemark); + } + }; + }); + } else { + remarksStore.put(existingRemark); + } + }; + getRequest.onerror = () => reject(getRequest.error); + }); + } + + // Supprimer une remarque offline + async deleteRemarkOffline(remarkId) { + if (!this.db || !this.isReady) { + throw new Error('IndexedDB non initialisé'); + } + + const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite'); + const remarksStore = transaction.objectStore('pendingRemarks'); + const photosStore = transaction.objectStore('pendingPhotos'); + + return new Promise((resolve, reject) => { + // Récupérer la remarque pour obtenir les photoIds + const getRequest = remarksStore.get(remarkId); + getRequest.onsuccess = async () => { + const remark = getRequest.result; + if (remark && remark.photoIds) { + // Supprimer les photos associées + for (const photoId of remark.photoIds) { + await this.deleteRecord(photosStore, photoId); + } + } + + // Supprimer la remarque + const deleteRequest = remarksStore.delete(remarkId); + deleteRequest.onsuccess = () => { + this.showNotification('Remarque supprimée localement', 'info'); + resolve(); + }; + deleteRequest.onerror = () => reject(deleteRequest.error); + }; + getRequest.onerror = () => reject(getRequest.error); + }); + } + + // Supprimer une photo d'une remarque offline + async deletePhotoOffline(remarkId, photoId) { + if (!this.db || !this.isReady) { + throw new Error('IndexedDB non initialisé'); + } + + const transaction = this.db.transaction(['pendingRemarks', 'pendingPhotos'], 'readwrite'); + const remarksStore = transaction.objectStore('pendingRemarks'); + const photosStore = transaction.objectStore('pendingPhotos'); + + return new Promise((resolve, reject) => { + const getRequest = remarksStore.get(remarkId); + getRequest.onsuccess = () => { + const remark = getRequest.result; + if (!remark) { + reject(new Error('Remarque non trouvée')); + return; + } + + // Retirer l'ID de la photo + remark.photoIds = remark.photoIds.filter(id => id !== photoId); + + // Mettre à jour la remarque + const putRequest = remarksStore.put(remark); + putRequest.onsuccess = () => { + // Supprimer la photo + photosStore.delete(photoId); + resolve(); + }; + putRequest.onerror = () => reject(putRequest.error); + }; + getRequest.onerror = () => reject(getRequest.error); + }); + } +} + +// Instance globale +let offlineManager = null; +let offlineManagerReady = false; + +// Initialiser immédiatement, pas dans DOMContentLoaded +(async function initOfflineManager() { + try { + offlineManager = new OfflineManager(); + + // Attendre que la DB soit initialisée + await offlineManager.init(); + + offlineManagerReady = true; + + // Tenter une synchronisation au chargement si en ligne + if (navigator.onLine) { + setTimeout(() => { + if (offlineManager) { + offlineManager.syncPendingData(); + } + }, 1000); + } + + // Mettre à jour le badge après initialisation + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + if (typeof updatePendingBadge === 'function') { + updatePendingBadge(); + } + }); + } else { + if (typeof updatePendingBadge === 'function') { + updatePendingBadge(); + } + } + } catch (error) { + console.error('Erreur initialisation offline manager:', error); + } +})(); diff --git a/streetup/controls/static/controls/remarks_form.js b/streetup/controls/static/controls/remarks_form.js new file mode 100644 index 0000000..f5ce151 --- /dev/null +++ b/streetup/controls/static/controls/remarks_form.js @@ -0,0 +1,1124 @@ +// Fonction gettext de fallback si Django i18n n'est pas chargé +if (typeof gettext === 'undefined') { + window.gettext = function(msgid) { + return msgid; + }; +} + +$(document).ready(function() { + + let selectedStatusInput = document.getElementById("selectedStatusInput"); + + document.querySelectorAll(".status-badge").forEach(badge => { + badge.addEventListener("click", function() { + // Réinitialiser tous les badges en gris + document.querySelectorAll(".status-badge").forEach(b => { + b.classList.remove("bg-info", "bg-success", "bg-danger"); + b.classList.add("bg-secondary"); + }); + + // Appliquer la bonne couleur au badge sélectionné + let status = this.getAttribute("data-value"); + if (status === "information") { + this.classList.add("bg-info"); + } else if (status === "validated") { + this.classList.add("bg-success"); + } else if (status === "to_be_processed") { + this.classList.add("bg-danger"); + } + + // Mettre à jour la valeur du champ caché + selectedStatusInput.value = status; + }); + }); + + // Afficher le spinner uniquement si le formulaire est valide + // (le submit event ne se déclenche qu'après validation HTML5) + const _remarkForm = document.getElementById("remark-form"); + if (_remarkForm) { + _remarkForm.addEventListener("submit", function() { + const spinner = document.getElementById("spinner-remark"); + const btn = document.getElementById("submit-remark-button"); + if (spinner) spinner.classList.remove("d-none"); + if (btn) btn.disabled = true; + + // Sécurité : si la page ne navigue pas dans les 20s (ex. erreur réseau PWA), + // on ré-active le bouton et on affiche un message d'erreur + const safetyTimer = setTimeout(function() { + if (spinner) spinner.classList.add("d-none"); + if (btn) btn.disabled = false; + alert(gettext("L'enregistrement a pris trop de temps. Vérifiez votre connexion et réessayez.")); + }, 20000); + + // Si la page navigue bien, annuler le timer (via pagehide/visibilitychange) + const cancelTimer = function() { clearTimeout(safetyTimer); }; + window.addEventListener("pagehide", cancelTimer, { once: true }); + window.addEventListener("beforeunload", cancelTimer, { once: true }); + }); + } + + // ======================================== + // Gestion du modal caméra (mode continu) + // ======================================== + + // Attendre un petit délai pour s'assurer que tout le DOM est chargé + setTimeout(function() { + (function() { + // Éléments du DOM + const openCameraModalBtn = document.getElementById('open-camera-modal-btn'); + const cameraModalEl = document.getElementById('cameraModal'); + const cameraPreview = document.getElementById('cameraPreview'); + const cameraCanvas = document.getElementById('cameraCanvas'); + const cameraFlashOverlay = document.getElementById('cameraFlashOverlay'); + const cameraStatus = document.getElementById('cameraStatus'); + const cameraCaptureBtn = document.getElementById('cameraCaptureBtn'); + const cameraStopBtn = document.getElementById('cameraStopBtn'); + const selectFilesBtn = document.getElementById('select-files-btn'); + const filesInput = document.getElementById('id_images'); + const previewContainer = document.getElementById('preview'); + const photoInputsContainer = document.getElementById('photo-inputs'); + + if (!cameraModalEl) { + return; // Pas de modal caméra, on sort + } + + let cameraModal = null; + let cameraStream = null; + let photoInputIndex = 0; + const isCameraSupported = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); + + // Initialiser le modal Bootstrap + if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { + cameraModal = bootstrap.Modal.getOrCreateInstance(cameraModalEl); + } + + function triggerCameraFlash() { + if (!cameraFlashOverlay) return; + cameraFlashOverlay.style.opacity = '1'; + setTimeout(() => { + cameraFlashOverlay.style.opacity = '0'; + }, 150); + } + + function updateCameraStatus(message, type = 'info') { + if (!cameraStatus) return; + cameraStatus.textContent = message || ''; + cameraStatus.className = 'small text-center mb-2'; + if (type === 'error') { + cameraStatus.classList.add('text-danger'); + } else if (type === 'success') { + cameraStatus.classList.add('text-success'); + } else { + cameraStatus.classList.add('text-muted'); + } + } + + async function startCameraStream() { + if (!isCameraSupported) { + alert(gettext("La caméra avancée n'est pas supportée sur ce navigateur.")); + return false; + } + + updateCameraStatus(gettext('Initialisation de la caméra…'), 'info'); + + // getUserMedia est appelé en premier, directement dans le gestionnaire de + // clic, pour conserver la chaîne de "user gesture" sur iOS Safari. + // Tout await supplémentaire avant cet appel risque de faire perdre ce contexte + // et empêcher iOS d'afficher la boîte de dialogue de permission. + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'environment', + width: { ideal: 1920 }, + height: { ideal: 1080 } + }, + audio: false, + }); + + if ('srcObject' in cameraPreview) { + cameraPreview.srcObject = cameraStream; + } else { + cameraPreview.src = window.URL.createObjectURL(cameraStream); + } + + // Attendre que la vidéo soit réellement prête avant de signaler le succès. + // Sur iOS Safari, sans cela, la prévisualisation peut rester noire. + await new Promise(function(resolve) { + const onReady = function() { + cameraPreview.removeEventListener('loadedmetadata', onReady); + cameraPreview.removeEventListener('canplay', onReady); + resolve(); + }; + if (cameraPreview.readyState >= 1) { + resolve(); // métadonnées déjà disponibles + } else { + cameraPreview.addEventListener('loadedmetadata', onReady, { once: true }); + cameraPreview.addEventListener('canplay', onReady, { once: true }); + } + }); + await cameraPreview.play(); + updateCameraStatus(gettext('Caméra prête. Appuyez sur Capturer pour prendre une photo.'), 'info'); + return true; + + } catch (err) { + console.error('Erreur getUserMedia:', err.name, err); + + if (err.name === 'OverconstrainedError' || err.name === 'ConstraintNotSatisfiedError') { + // Contraintes trop strictes (résolution) : réessayer sans résolution imposée + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' }, + audio: false, + }); + if ('srcObject' in cameraPreview) { + cameraPreview.srcObject = cameraStream; + } else { + cameraPreview.src = window.URL.createObjectURL(cameraStream); + } + await new Promise(function(resolve) { + const onReady = function() { resolve(); }; + if (cameraPreview.readyState >= 1) { + resolve(); + } else { + cameraPreview.addEventListener('loadedmetadata', onReady, { once: true }); + cameraPreview.addEventListener('canplay', onReady, { once: true }); + } + }); + await cameraPreview.play(); + updateCameraStatus(gettext('Caméra prête. Appuyez sur Capturer pour prendre une photo.'), 'info'); + return true; + } catch (err2) { + console.error('Erreur getUserMedia (fallback):', err2.name, err2); + } + } + + // Analyser l'erreur APRÈS l'échec (ne pas await avant getUserMedia) + let permState = null; + if (navigator.permissions && navigator.permissions.query) { + try { + const ps = await navigator.permissions.query({ name: 'camera' }); + permState = ps.state; // 'granted' | 'denied' | 'prompt' + } catch (e) { /* Permissions API non supportée (iOS Safari) */ } + } + + let alertMsg; + if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { + if (permState === 'denied') { + // Permission explicitement refusée et mémorisée → guider vers les réglages + alertMsg = gettext( + "Accès à la caméra refusé. Pour l'autoriser :\n" + + "• Android : Réglages → Applications → [Navigateur] → Autorisations → Caméra\n" + + "• iPhone : Réglages → Safari → Caméra → Autoriser" + ); + } else { + // Refus ponctuel ou iOS (Permissions API non dispo) → inviter à réessayer + alertMsg = gettext( + "L'accès à la caméra a été refusé. Veuillez appuyer sur 'Autoriser' " + + "lors de la prochaine demande, ou vérifiez les autorisations dans " + + "les réglages de votre navigateur." + ); + } + } else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') { + alertMsg = gettext("Aucune caméra disponible sur cet appareil."); + } else { + alertMsg = gettext("Impossible d'accéder à la caméra. Veuillez vérifier les autorisations."); + } + + alert(alertMsg); + updateCameraStatus(gettext("Impossible d'accéder à la caméra."), 'error'); + return false; + } + } + + function stopCameraStream() { + if (cameraStream) { + cameraStream.getTracks().forEach(track => track.stop()); + cameraStream = null; + } + if (cameraPreview) { + cameraPreview.srcObject = null; + } + updateCameraStatus('', 'info'); + } + + function captureFrame() { + if (!cameraPreview || !cameraCanvas) return; + + const videoWidth = cameraPreview.videoWidth; + const videoHeight = cameraPreview.videoHeight; + if (!videoWidth || !videoHeight) { + updateCameraStatus(gettext("La vidéo n'est pas encore prête."), 'error'); + return; + } + + cameraCanvas.width = videoWidth; + cameraCanvas.height = videoHeight; + + const ctx = cameraCanvas.getContext('2d'); + ctx.drawImage(cameraPreview, 0, 0, videoWidth, videoHeight); + + cameraCanvas.toBlob(function(blob) { + if (!blob) { + updateCameraStatus(gettext("Erreur lors de la capture."), 'error'); + return; + } + + const filename = `remark_photo_${Date.now()}.jpg`; + const file = new File([blob], filename, { type: 'image/jpeg' }); + + // Ajouter au formulaire + addFileToPreview(file); + updateCameraStatus(gettext('Photo capturée avec succès.'), 'success'); + }, 'image/jpeg', 0.9); + } + + function addFileToPreview(file) { + const currentIndex = photoInputIndex++; + + // Créer un input file caché pour ce fichier + const singleInput = document.createElement('input'); + singleInput.type = 'file'; + singleInput.name = 'images'; + singleInput.className = 'd-none'; + singleInput.dataset.index = currentIndex; + + // Injecter le fichier via DataTransfer + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + singleInput.files = dataTransfer.files; + + if (photoInputsContainer) { + photoInputsContainer.appendChild(singleInput); + } + + // Créer l'aperçu visuel + const wrapper = document.createElement('div'); + wrapper.className = 'preview-wrapper position-relative'; + wrapper.dataset.inputIndex = currentIndex; + wrapper.style.width = '80px'; + wrapper.style.height = '80px'; + + const reader = new FileReader(); + reader.onload = function(e) { + const img = document.createElement('img'); + img.src = e.target.result; + img.className = 'preview-image rounded'; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.innerHTML = '×'; + btn.className = 'remove-btn btn btn-danger btn-sm position-absolute'; + btn.style.top = '-8px'; + btn.style.right = '-8px'; + btn.style.width = '24px'; + btn.style.height = '24px'; + btn.style.padding = '0'; + btn.style.lineHeight = '1'; + btn.style.borderRadius = '50%'; + btn.onclick = function() { + singleInput.remove(); + wrapper.remove(); + }; + + wrapper.appendChild(img); + wrapper.appendChild(btn); + if (previewContainer) { + previewContainer.appendChild(wrapper); + } + }; + reader.readAsDataURL(file); + } + + // Event: Ouvrir le modal caméra + if (openCameraModalBtn && cameraModal) { + openCameraModalBtn.addEventListener('click', async function() { + // 1. Obtenir le flux caméra dans le contexte du geste utilisateur. + // getUserMedia doit rester ici (pas dans shown.bs.modal) pour iOS. + const ok = await startCameraStream(); + if (!ok) return; + + // 2. Sur iOS Safari PWA, attacher un flux à une